Spaces:
Sleeping
Sleeping
Commit
Β·
f844095
1
Parent(s):
ba4e1dd
Restore scenario analysis feature - core project functionality
Browse filesβ
Restored Files:
- scenario_analysis_service.py - Main scenario analysis engine
- policy_chart_generator.py - Policy visualization charts
- policy_impact_simulator.py - Policy impact simulation
- policy_simulator_api.py - API endpoints for policy simulation
- policy_chat_interface.py - Interactive policy chat
π§ Fixed Imports:
- Restored scenario_service import in rag_service.py
- Added ScenarioAnalysisService to app.py
- Fixed module not found errors
π― Features Available:
- Government scenario analysis
- Policy impact visualization
- Interactive policy simulations
- Chart generation for policy data
- app.py +8 -0
- policy_chart_generator.py +231 -0
- policy_chat_interface.py +668 -0
- policy_impact_simulator.py +475 -0
- policy_simulator_api.py +285 -0
- scenario_analysis_service.py +602 -0
app.py
CHANGED
|
@@ -11,6 +11,7 @@ from hybrid_llm_service import HybridLLMService
|
|
| 11 |
from voice_service import VoiceService
|
| 12 |
from rag_service import search_documents
|
| 13 |
from lancedb_service import LanceDBService
|
|
|
|
| 14 |
import config
|
| 15 |
from dotenv import load_dotenv
|
| 16 |
|
|
@@ -68,6 +69,7 @@ app.add_middleware(
|
|
| 68 |
llm_service = None
|
| 69 |
voice_service = None
|
| 70 |
lancedb_service = None
|
|
|
|
| 71 |
|
| 72 |
def get_llm_service():
|
| 73 |
global llm_service
|
|
@@ -87,6 +89,12 @@ def get_lancedb_service():
|
|
| 87 |
lancedb_service = LanceDBService()
|
| 88 |
return lancedb_service
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
# Health check endpoint
|
| 91 |
@app.get("/health")
|
| 92 |
async def health_check():
|
|
|
|
| 11 |
from voice_service import VoiceService
|
| 12 |
from rag_service import search_documents
|
| 13 |
from lancedb_service import LanceDBService
|
| 14 |
+
from scenario_analysis_service import ScenarioAnalysisService
|
| 15 |
import config
|
| 16 |
from dotenv import load_dotenv
|
| 17 |
|
|
|
|
| 69 |
llm_service = None
|
| 70 |
voice_service = None
|
| 71 |
lancedb_service = None
|
| 72 |
+
scenario_service = None
|
| 73 |
|
| 74 |
def get_llm_service():
|
| 75 |
global llm_service
|
|
|
|
| 89 |
lancedb_service = LanceDBService()
|
| 90 |
return lancedb_service
|
| 91 |
|
| 92 |
+
def get_scenario_service():
|
| 93 |
+
global scenario_service
|
| 94 |
+
if scenario_service is None:
|
| 95 |
+
scenario_service = ScenarioAnalysisService()
|
| 96 |
+
return scenario_service
|
| 97 |
+
|
| 98 |
# Health check endpoint
|
| 99 |
@app.get("/health")
|
| 100 |
async def health_check():
|
policy_chart_generator.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Chart Generation for Policy Impact Simulator
|
| 3 |
+
Creates visual charts and graphs for policy analysis results
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import matplotlib.pyplot as plt
|
| 7 |
+
import numpy as np
|
| 8 |
+
from typing import Dict, Any, List
|
| 9 |
+
import os
|
| 10 |
+
import base64
|
| 11 |
+
import io
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
|
| 14 |
+
class PolicyChartGenerator:
|
| 15 |
+
"""Generates charts and graphs for policy impact analysis"""
|
| 16 |
+
|
| 17 |
+
def __init__(self):
|
| 18 |
+
# Set matplotlib to use non-interactive backend
|
| 19 |
+
plt.switch_backend('Agg')
|
| 20 |
+
|
| 21 |
+
def generate_scenario_comparison_chart(self, variants: Dict[str, Any], title: str = "Policy Scenario Analysis") -> str:
|
| 22 |
+
"""Generate a bar chart comparing scenario variants"""
|
| 23 |
+
|
| 24 |
+
# Extract scenario data
|
| 25 |
+
scenario_names = []
|
| 26 |
+
costs = []
|
| 27 |
+
colors = []
|
| 28 |
+
|
| 29 |
+
color_map = {
|
| 30 |
+
'best_case': '#28a745', # Green
|
| 31 |
+
'base_case': '#007bff', # Blue
|
| 32 |
+
'worst_case': '#dc3545' # Red
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
for scenario_type, data in variants.items():
|
| 36 |
+
if isinstance(data, dict) and 'total_cost' in data:
|
| 37 |
+
scenario_names.append(scenario_type.replace('_', ' ').title())
|
| 38 |
+
costs.append(data['total_cost'])
|
| 39 |
+
colors.append(color_map.get(scenario_type, '#6c757d'))
|
| 40 |
+
|
| 41 |
+
# Create chart
|
| 42 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
| 43 |
+
bars = ax.bar(scenario_names, costs, color=colors, alpha=0.8)
|
| 44 |
+
|
| 45 |
+
# Customize chart
|
| 46 |
+
ax.set_title(title, fontsize=16, fontweight='bold', pad=20)
|
| 47 |
+
ax.set_ylabel('Total Cost (βΉ Crores)', fontsize=12)
|
| 48 |
+
ax.set_xlabel('Scenario', fontsize=12)
|
| 49 |
+
|
| 50 |
+
# Add value labels on bars
|
| 51 |
+
for bar, cost in zip(bars, costs):
|
| 52 |
+
height = bar.get_height()
|
| 53 |
+
ax.text(bar.get_x() + bar.get_width()/2., height + max(costs) * 0.01,
|
| 54 |
+
f'βΉ{cost:,.0f}', ha='center', va='bottom', fontweight='bold')
|
| 55 |
+
|
| 56 |
+
# Format y-axis
|
| 57 |
+
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'βΉ{x:,.0f}'))
|
| 58 |
+
|
| 59 |
+
# Add grid
|
| 60 |
+
ax.grid(True, alpha=0.3, axis='y')
|
| 61 |
+
ax.set_axisbelow(True)
|
| 62 |
+
|
| 63 |
+
# Tight layout
|
| 64 |
+
plt.tight_layout()
|
| 65 |
+
|
| 66 |
+
# Convert to base64 string
|
| 67 |
+
return self._fig_to_base64(fig)
|
| 68 |
+
|
| 69 |
+
def generate_yearly_breakdown_chart(self, yearly_data: List[Dict], title: str = "Yearly Impact Breakdown") -> str:
|
| 70 |
+
"""Generate a line chart showing yearly breakdown"""
|
| 71 |
+
|
| 72 |
+
years = []
|
| 73 |
+
impacts = []
|
| 74 |
+
beneficiaries = []
|
| 75 |
+
|
| 76 |
+
for year_data in yearly_data:
|
| 77 |
+
years.append(f"Year {year_data.get('year', 0)}")
|
| 78 |
+
impacts.append(year_data.get('impact', 0))
|
| 79 |
+
beneficiaries.append(year_data.get('affected_beneficiaries', 0))
|
| 80 |
+
|
| 81 |
+
# Create dual-axis chart
|
| 82 |
+
fig, ax1 = plt.subplots(figsize=(12, 6))
|
| 83 |
+
|
| 84 |
+
# Plot impact on primary axis
|
| 85 |
+
color1 = '#007bff'
|
| 86 |
+
ax1.set_xlabel('Years', fontsize=12)
|
| 87 |
+
ax1.set_ylabel('Impact (βΉ Crores)', fontsize=12, color=color1)
|
| 88 |
+
line1 = ax1.plot(years, impacts, color=color1, marker='o', linewidth=3, markersize=8, label='Financial Impact')
|
| 89 |
+
ax1.tick_params(axis='y', labelcolor=color1)
|
| 90 |
+
ax1.grid(True, alpha=0.3)
|
| 91 |
+
|
| 92 |
+
# Create secondary axis for beneficiaries
|
| 93 |
+
ax2 = ax1.twinx()
|
| 94 |
+
color2 = '#28a745'
|
| 95 |
+
ax2.set_ylabel('Beneficiaries', fontsize=12, color=color2)
|
| 96 |
+
line2 = ax2.plot(years, beneficiaries, color=color2, marker='s', linewidth=3, markersize=8, linestyle='--', label='Affected Population')
|
| 97 |
+
ax2.tick_params(axis='y', labelcolor=color2)
|
| 98 |
+
|
| 99 |
+
# Add title
|
| 100 |
+
ax1.set_title(title, fontsize=16, fontweight='bold', pad=20)
|
| 101 |
+
|
| 102 |
+
# Add value labels
|
| 103 |
+
for i, (year, impact, beneficiary) in enumerate(zip(years, impacts, beneficiaries)):
|
| 104 |
+
ax1.annotate(f'βΉ{impact:.1f}', (i, impact), textcoords="offset points", xytext=(0,10), ha='center', fontweight='bold')
|
| 105 |
+
ax2.annotate(f'{beneficiary:,}', (i, beneficiary), textcoords="offset points", xytext=(0,-15), ha='center', fontweight='bold', color=color2)
|
| 106 |
+
|
| 107 |
+
# Add legend
|
| 108 |
+
lines1, labels1 = ax1.get_legend_handles_labels()
|
| 109 |
+
lines2, labels2 = ax2.get_legend_handles_labels()
|
| 110 |
+
ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')
|
| 111 |
+
|
| 112 |
+
plt.tight_layout()
|
| 113 |
+
return self._fig_to_base64(fig)
|
| 114 |
+
|
| 115 |
+
def generate_ascii_chart(self, variants: Dict[str, Any], width: int = 50) -> str:
|
| 116 |
+
"""Generate ASCII chart for text-based display"""
|
| 117 |
+
|
| 118 |
+
if not variants:
|
| 119 |
+
return "No data available for chart generation"
|
| 120 |
+
|
| 121 |
+
# Extract costs
|
| 122 |
+
costs = []
|
| 123 |
+
names = []
|
| 124 |
+
for scenario_type, data in variants.items():
|
| 125 |
+
if isinstance(data, dict) and 'total_cost' in data:
|
| 126 |
+
costs.append(data['total_cost'])
|
| 127 |
+
names.append(scenario_type.replace('_', ' ').title())
|
| 128 |
+
|
| 129 |
+
if not costs:
|
| 130 |
+
return "No cost data available"
|
| 131 |
+
|
| 132 |
+
max_cost = max(costs) if costs else 1
|
| 133 |
+
|
| 134 |
+
chart = "π Policy Scenario Comparison (βΉ Crores)\n"
|
| 135 |
+
chart += "=" * (width + 20) + "\n"
|
| 136 |
+
|
| 137 |
+
for name, cost in zip(names, costs):
|
| 138 |
+
# Calculate bar length
|
| 139 |
+
bar_length = int((cost / max_cost) * width) if max_cost > 0 else 0
|
| 140 |
+
bar = "β" * bar_length
|
| 141 |
+
|
| 142 |
+
# Add emoji based on scenario type
|
| 143 |
+
emoji = "π’" if "Best" in name else "π΄" if "Worst" in name else "π΅"
|
| 144 |
+
|
| 145 |
+
chart += f"{emoji} {name:12} β{bar:<{width}} βΉ{cost:,.0f}\n"
|
| 146 |
+
|
| 147 |
+
chart += "=" * (width + 20) + "\n"
|
| 148 |
+
return chart
|
| 149 |
+
|
| 150 |
+
def generate_implementation_timeline_chart(self, timeline_data: Dict[str, Any]) -> str:
|
| 151 |
+
"""Generate ASCII timeline chart for implementation phases"""
|
| 152 |
+
|
| 153 |
+
phases = [
|
| 154 |
+
"π Planning Phase (Months 1-3)",
|
| 155 |
+
"βοΈ Legal Review (Months 2-4)",
|
| 156 |
+
"π° Budget Approval (Months 4-6)",
|
| 157 |
+
"π System Updates (Months 6-8)",
|
| 158 |
+
"π’ Communication (Months 8-10)",
|
| 159 |
+
"π Implementation (Months 10-12)"
|
| 160 |
+
]
|
| 161 |
+
|
| 162 |
+
timeline = "ποΈ Implementation Timeline\n"
|
| 163 |
+
timeline += "=" * 60 + "\n"
|
| 164 |
+
|
| 165 |
+
for i, phase in enumerate(phases, 1):
|
| 166 |
+
progress_bar = "β" * (i * 2) + "β" * ((6 - i) * 2)
|
| 167 |
+
timeline += f" {phase}\n"
|
| 168 |
+
timeline += f" [{progress_bar}] {i}/6 phases\n\n"
|
| 169 |
+
|
| 170 |
+
complexity = timeline_data.get('complexity', 'Medium')
|
| 171 |
+
timeline += f"π§ Implementation Complexity: {complexity}\n"
|
| 172 |
+
timeline += f"β±οΈ Estimated Duration: 12 months\n"
|
| 173 |
+
timeline += f"π‘ Key Success Factors: Budget approval, stakeholder buy-in, system readiness\n"
|
| 174 |
+
|
| 175 |
+
return timeline
|
| 176 |
+
|
| 177 |
+
def _fig_to_base64(self, fig) -> str:
|
| 178 |
+
"""Convert matplotlib figure to base64 string"""
|
| 179 |
+
|
| 180 |
+
buffer = io.BytesIO()
|
| 181 |
+
fig.savefig(buffer, format='png', dpi=300, bbox_inches='tight')
|
| 182 |
+
buffer.seek(0)
|
| 183 |
+
|
| 184 |
+
# Convert to base64
|
| 185 |
+
img_base64 = base64.b64encode(buffer.read()).decode('utf-8')
|
| 186 |
+
|
| 187 |
+
# Close figure to free memory
|
| 188 |
+
plt.close(fig)
|
| 189 |
+
|
| 190 |
+
return f"data:image/png;base64,{img_base64}"
|
| 191 |
+
|
| 192 |
+
def generate_comprehensive_report_charts(self, analysis_data: Dict[str, Any]) -> Dict[str, str]:
|
| 193 |
+
"""Generate all charts for a comprehensive policy analysis report"""
|
| 194 |
+
|
| 195 |
+
charts = {}
|
| 196 |
+
|
| 197 |
+
# Scenario comparison chart
|
| 198 |
+
if 'variants' in analysis_data:
|
| 199 |
+
charts['scenario_comparison'] = self.generate_scenario_comparison_chart(
|
| 200 |
+
analysis_data['variants'],
|
| 201 |
+
f"Policy Impact: {analysis_data.get('parameter_name', 'Analysis')}"
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
# ASCII version for text display
|
| 205 |
+
charts['scenario_ascii'] = self.generate_ascii_chart(analysis_data['variants'])
|
| 206 |
+
|
| 207 |
+
# Yearly breakdown chart
|
| 208 |
+
if 'scenario_projections' in analysis_data:
|
| 209 |
+
charts['yearly_breakdown'] = self.generate_yearly_breakdown_chart(
|
| 210 |
+
analysis_data['scenario_projections'],
|
| 211 |
+
"Financial Impact Over Time"
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
# Implementation timeline
|
| 215 |
+
if 'implementation' in analysis_data:
|
| 216 |
+
charts['implementation_timeline'] = self.generate_implementation_timeline_chart(
|
| 217 |
+
analysis_data['implementation']
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
return charts
|
| 221 |
+
|
| 222 |
+
# Standalone functions for easy integration
|
| 223 |
+
def generate_policy_charts(analysis_data: Dict[str, Any]) -> Dict[str, str]:
|
| 224 |
+
"""Generate charts for policy analysis data"""
|
| 225 |
+
generator = PolicyChartGenerator()
|
| 226 |
+
return generator.generate_comprehensive_report_charts(analysis_data)
|
| 227 |
+
|
| 228 |
+
def generate_ascii_scenario_chart(variants: Dict[str, Any]) -> str:
|
| 229 |
+
"""Generate ASCII chart for immediate text display"""
|
| 230 |
+
generator = PolicyChartGenerator()
|
| 231 |
+
return generator.generate_ascii_chart(variants)
|
policy_chat_interface.py
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Policy Impact Simulator Chat Integration
|
| 3 |
+
Provides natural language interface for policy impact simulation
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
import json
|
| 8 |
+
import logging
|
| 9 |
+
from typing import Dict, Any, Optional, List
|
| 10 |
+
from datetime import datetime, timedelta
|
| 11 |
+
from dataclasses import dataclass
|
| 12 |
+
|
| 13 |
+
from policy_impact_simulator import (
|
| 14 |
+
PolicyImpactSimulator,
|
| 15 |
+
PolicyScenario,
|
| 16 |
+
PolicyParameter
|
| 17 |
+
)
|
| 18 |
+
from policy_chart_generator import PolicyChartGenerator
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
@dataclass
|
| 23 |
+
class PolicyQuery:
|
| 24 |
+
"""Parsed policy query from natural language"""
|
| 25 |
+
intent: str
|
| 26 |
+
parameter: Optional[PolicyParameter]
|
| 27 |
+
current_value: Optional[float]
|
| 28 |
+
proposed_value: Optional[float]
|
| 29 |
+
years: int = 5
|
| 30 |
+
raw_query: str = ""
|
| 31 |
+
|
| 32 |
+
class PolicySimulatorChatInterface:
|
| 33 |
+
"""Chat interface for policy impact simulation"""
|
| 34 |
+
|
| 35 |
+
def __init__(self):
|
| 36 |
+
self.simulator = PolicyImpactSimulator()
|
| 37 |
+
self.chart_generator = PolicyChartGenerator()
|
| 38 |
+
self.conversation_context = {}
|
| 39 |
+
|
| 40 |
+
# Enhanced pattern matching for natural language queries
|
| 41 |
+
self.patterns = {
|
| 42 |
+
"simulate_dr": [
|
| 43 |
+
r"simulate.*dearness.*relief.*(\d+).*to.*(\d+)",
|
| 44 |
+
r"dr.*increase.*from.*(\d+).*to.*(\d+)",
|
| 45 |
+
r"what.*if.*dr.*changes.*(\d+).*(\d+)",
|
| 46 |
+
r"impact.*dearness.*(\d+).*percent.*(\d+).*percent",
|
| 47 |
+
r"dr.*analysis.*(\d+).*(\d+)",
|
| 48 |
+
r"dearness.*relief.*(\d+).*(\d+)",
|
| 49 |
+
r"impact.*increasing.*dr.*by.*(\d+)",
|
| 50 |
+
r"show.*impact.*dr.*by.*(\d+)",
|
| 51 |
+
r"dr.*increase.*by.*(\d+).*percent",
|
| 52 |
+
r"increase.*dr.*by.*(\d+)"
|
| 53 |
+
],
|
| 54 |
+
"simulate_pension": [
|
| 55 |
+
r"simulate.*basic.*pension.*(\d+).*to.*(\d+)",
|
| 56 |
+
r"pension.*increase.*from.*(\d+).*to.*(\d+)",
|
| 57 |
+
r"what.*if.*pension.*changes.*(\d+).*(\d+)",
|
| 58 |
+
r"impact.*basic.*pension.*(\d+).*(\d+)",
|
| 59 |
+
r"pension.*boost.*(\d+).*(\d+)",
|
| 60 |
+
r"basic.*pension.*(\d+).*(\d+)",
|
| 61 |
+
r"analyze.*minimum.*pension.*changes",
|
| 62 |
+
r"analyze.*basic.*pension",
|
| 63 |
+
r"minimum.*pension.*analysis",
|
| 64 |
+
r"basic.*pension.*impact"
|
| 65 |
+
],
|
| 66 |
+
"simulate_medical": [
|
| 67 |
+
r"simulate.*medical.*allowance.*(\d+).*to.*(\d+)",
|
| 68 |
+
r"medical.*increase.*from.*(\d+).*to.*(\d+)",
|
| 69 |
+
r"what.*if.*medical.*changes.*(\d+).*(\d+)",
|
| 70 |
+
r"medical.*allowance.*(\d+).*(\d+)"
|
| 71 |
+
],
|
| 72 |
+
"scenario_analysis": [
|
| 73 |
+
r"scenario.*analysis.*pension",
|
| 74 |
+
r"do.*scenario.*analysis",
|
| 75 |
+
r"pension.*scenario.*analysis",
|
| 76 |
+
r"analyze.*pension.*scenarios",
|
| 77 |
+
r"scenario.*analysis.*dr",
|
| 78 |
+
r"pension.*rules.*scenario",
|
| 79 |
+
r"government.*policy.*scenario",
|
| 80 |
+
r"policy.*scenario.*analysis"
|
| 81 |
+
],
|
| 82 |
+
"interactive_form": [
|
| 83 |
+
r"start.*scenario.*analysis",
|
| 84 |
+
r"interactive.*scenario",
|
| 85 |
+
r"step.*by.*step.*analysis",
|
| 86 |
+
r"guided.*analysis",
|
| 87 |
+
r"scenario.*form"
|
| 88 |
+
],
|
| 89 |
+
"general_policy": [
|
| 90 |
+
r"policy.*impact.*simulation",
|
| 91 |
+
r"simulate.*policy.*change",
|
| 92 |
+
r"what.*if.*we.*change",
|
| 93 |
+
r"impact.*analysis.*policy",
|
| 94 |
+
r"policy.*analysis",
|
| 95 |
+
r"government.*policy.*impact"
|
| 96 |
+
],
|
| 97 |
+
"compare_scenarios": [
|
| 98 |
+
r"compare.*scenarios",
|
| 99 |
+
r"which.*is.*better.*policy",
|
| 100 |
+
r"cost.*comparison.*policies",
|
| 101 |
+
r"scenario.*comparison",
|
| 102 |
+
r"best.*case.*worst.*case"
|
| 103 |
+
]
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
def process_policy_query(self, query: str, user_id: str = "default") -> Dict[str, Any]:
|
| 107 |
+
"""
|
| 108 |
+
Process natural language policy query and return simulation results
|
| 109 |
+
"""
|
| 110 |
+
try:
|
| 111 |
+
# Parse the query
|
| 112 |
+
parsed_query = self._parse_policy_query(query)
|
| 113 |
+
|
| 114 |
+
if not parsed_query:
|
| 115 |
+
return self._get_help_response()
|
| 116 |
+
|
| 117 |
+
# Handle different intents
|
| 118 |
+
if parsed_query.intent == "simulate":
|
| 119 |
+
return self._handle_simulation_request(parsed_query, user_id)
|
| 120 |
+
elif parsed_query.intent == "compare":
|
| 121 |
+
return self._handle_comparison_request(parsed_query, user_id)
|
| 122 |
+
elif parsed_query.intent == "help":
|
| 123 |
+
return self._get_help_response()
|
| 124 |
+
elif parsed_query.intent == "scenario_analysis":
|
| 125 |
+
return self._handle_scenario_analysis_request(parsed_query, user_id)
|
| 126 |
+
elif parsed_query.intent == "interactive_form":
|
| 127 |
+
return self._start_interactive_form(user_id)
|
| 128 |
+
else:
|
| 129 |
+
return self._get_clarification_response(query)
|
| 130 |
+
|
| 131 |
+
except Exception as e:
|
| 132 |
+
logger.error(f"Policy query processing error: {e}")
|
| 133 |
+
return {
|
| 134 |
+
"type": "error",
|
| 135 |
+
"message": f"Sorry, I encountered an error processing your policy query: {str(e)}",
|
| 136 |
+
"suggestions": ["Try rephrasing your question", "Use specific numbers and policy parameters"]
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
def _parse_policy_query(self, query: str) -> Optional[PolicyQuery]:
|
| 140 |
+
"""Parse natural language query into structured policy query"""
|
| 141 |
+
query_lower = query.lower()
|
| 142 |
+
|
| 143 |
+
# Check for explicit help requests (more specific to avoid false positives)
|
| 144 |
+
help_patterns = [r"^help", r"how.*do.*i", r"what.*can.*you", r"guide.*me", r"need.*help"]
|
| 145 |
+
if any(re.search(pattern, query_lower) for pattern in help_patterns):
|
| 146 |
+
return PolicyQuery(intent="help", parameter=None, current_value=None, proposed_value=None, raw_query=query)
|
| 147 |
+
|
| 148 |
+
# Check for comparison requests
|
| 149 |
+
if any(word in query_lower for word in ["compare", "vs", "versus", "which is better"]):
|
| 150 |
+
return PolicyQuery(intent="compare", parameter=None, current_value=None, proposed_value=None, raw_query=query)
|
| 151 |
+
|
| 152 |
+
# Try to match simulation patterns
|
| 153 |
+
for intent, patterns in self.patterns.items():
|
| 154 |
+
for pattern in patterns:
|
| 155 |
+
match = re.search(pattern, query_lower)
|
| 156 |
+
if match:
|
| 157 |
+
return self._extract_simulation_params(intent, match, query)
|
| 158 |
+
|
| 159 |
+
# Check for general policy simulation intent
|
| 160 |
+
if any(word in query_lower for word in ["simulate", "impact", "policy", "change", "effect"]):
|
| 161 |
+
return PolicyQuery(intent="simulate", parameter=None, current_value=None, proposed_value=None, raw_query=query)
|
| 162 |
+
|
| 163 |
+
return None
|
| 164 |
+
|
| 165 |
+
def _extract_simulation_params(self, intent: str, match, raw_query: str) -> PolicyQuery:
|
| 166 |
+
"""Extract simulation parameters from regex match"""
|
| 167 |
+
try:
|
| 168 |
+
# Handle new intent types that don't need parameter extraction
|
| 169 |
+
if intent in ["scenario_analysis", "interactive_form"]:
|
| 170 |
+
return PolicyQuery(
|
| 171 |
+
intent=intent,
|
| 172 |
+
parameter=None,
|
| 173 |
+
current_value=None,
|
| 174 |
+
proposed_value=None,
|
| 175 |
+
raw_query=raw_query
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
groups = match.groups()
|
| 179 |
+
|
| 180 |
+
# Map intent to parameter for simulation intents
|
| 181 |
+
parameter_mapping = {
|
| 182 |
+
"simulate_dr": PolicyParameter.DEARNESS_RELIEF,
|
| 183 |
+
"simulate_pension": PolicyParameter.BASIC_PENSION,
|
| 184 |
+
"simulate_medical": PolicyParameter.MEDICAL_ALLOWANCE
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
parameter = parameter_mapping.get(intent, PolicyParameter.DEARNESS_RELIEF)
|
| 188 |
+
|
| 189 |
+
# Handle different pattern types
|
| 190 |
+
if len(groups) == 0:
|
| 191 |
+
# No numbers - provide default analysis scenario
|
| 192 |
+
if parameter == PolicyParameter.DEARNESS_RELIEF:
|
| 193 |
+
current_value = 12.0 # Current DR is 12%
|
| 194 |
+
proposed_value = 18.0 # Standard 6% increase
|
| 195 |
+
elif parameter == PolicyParameter.BASIC_PENSION:
|
| 196 |
+
current_value = 6000.0 # Current basic pension is βΉ6,000
|
| 197 |
+
proposed_value = 8000.0 # Standard βΉ2,000 increase
|
| 198 |
+
elif parameter == PolicyParameter.MEDICAL_ALLOWANCE:
|
| 199 |
+
current_value = 1000.0 # Current medical allowance is βΉ1,000
|
| 200 |
+
proposed_value = 1500.0 # Standard βΉ500 increase
|
| 201 |
+
else:
|
| 202 |
+
current_value = 0.0
|
| 203 |
+
proposed_value = 1.0
|
| 204 |
+
elif len(groups) == 1:
|
| 205 |
+
# Single number - treat as percentage increase
|
| 206 |
+
increase_amount = float(groups[0])
|
| 207 |
+
|
| 208 |
+
# Set current values based on parameter type
|
| 209 |
+
if parameter == PolicyParameter.DEARNESS_RELIEF:
|
| 210 |
+
current_value = 12.0 # Current DR is 12%
|
| 211 |
+
proposed_value = current_value + increase_amount
|
| 212 |
+
elif parameter == PolicyParameter.BASIC_PENSION:
|
| 213 |
+
current_value = 6000.0 # Current basic pension is βΉ6,000
|
| 214 |
+
proposed_value = current_value + increase_amount
|
| 215 |
+
elif parameter == PolicyParameter.MEDICAL_ALLOWANCE:
|
| 216 |
+
current_value = 1000.0 # Current medical allowance is βΉ1,000
|
| 217 |
+
proposed_value = current_value + increase_amount
|
| 218 |
+
else:
|
| 219 |
+
current_value = increase_amount
|
| 220 |
+
proposed_value = increase_amount * 1.5 # Default 50% increase
|
| 221 |
+
else:
|
| 222 |
+
# Two numbers - from X to Y
|
| 223 |
+
current_value = float(groups[0]) if len(groups) > 0 else None
|
| 224 |
+
proposed_value = float(groups[1]) if len(groups) > 1 else None
|
| 225 |
+
|
| 226 |
+
# Extract years if mentioned
|
| 227 |
+
years_match = re.search(r"(\d+).*years?", raw_query.lower())
|
| 228 |
+
years = int(years_match.group(1)) if years_match else 5
|
| 229 |
+
|
| 230 |
+
return PolicyQuery(
|
| 231 |
+
intent="simulate",
|
| 232 |
+
parameter=parameter,
|
| 233 |
+
current_value=current_value,
|
| 234 |
+
proposed_value=proposed_value,
|
| 235 |
+
years=min(years, 10), # Cap at 10 years
|
| 236 |
+
raw_query=raw_query
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
except Exception as e:
|
| 240 |
+
logger.error(f"Parameter extraction error: {e}")
|
| 241 |
+
return PolicyQuery(intent="simulate", parameter=None, current_value=None, proposed_value=None, raw_query=raw_query)
|
| 242 |
+
|
| 243 |
+
def _handle_simulation_request(self, parsed_query: PolicyQuery, user_id: str) -> Dict[str, Any]:
|
| 244 |
+
"""Handle policy simulation request"""
|
| 245 |
+
try:
|
| 246 |
+
# If missing parameters, provide clarification with specific guidance
|
| 247 |
+
if not all([parsed_query.parameter, parsed_query.current_value, parsed_query.proposed_value]):
|
| 248 |
+
return self._get_clarification_response(parsed_query.raw_query)
|
| 249 |
+
|
| 250 |
+
# Create scenario
|
| 251 |
+
scenario = PolicyScenario(
|
| 252 |
+
parameter=parsed_query.parameter,
|
| 253 |
+
current_value=parsed_query.current_value,
|
| 254 |
+
proposed_value=parsed_query.proposed_value,
|
| 255 |
+
effective_date=datetime.now() + timedelta(days=90), # 3 months from now
|
| 256 |
+
affected_population=self._estimate_affected_population(parsed_query.parameter),
|
| 257 |
+
annual_growth_rate=0.03,
|
| 258 |
+
inflation_rate=0.06
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
# Run simulation
|
| 262 |
+
result = self.simulator.simulate_policy_impact(scenario, parsed_query.years, True)
|
| 263 |
+
|
| 264 |
+
# Format for chat response
|
| 265 |
+
return self._format_simulation_response(result, parsed_query)
|
| 266 |
+
|
| 267 |
+
except Exception as e:
|
| 268 |
+
logger.error(f"Simulation request error: {e}")
|
| 269 |
+
return {
|
| 270 |
+
"type": "error",
|
| 271 |
+
"message": f"Simulation failed: {str(e)}",
|
| 272 |
+
"raw_query": parsed_query.raw_query
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
def _estimate_affected_population(self, parameter: PolicyParameter) -> int:
|
| 276 |
+
"""Estimate affected population based on parameter type"""
|
| 277 |
+
population_estimates = {
|
| 278 |
+
PolicyParameter.DEARNESS_RELIEF: 510000, # All pensioners
|
| 279 |
+
PolicyParameter.BASIC_PENSION: 450000, # Basic pension recipients
|
| 280 |
+
PolicyParameter.MEDICAL_ALLOWANCE: 510000, # All pensioners
|
| 281 |
+
PolicyParameter.PENSION_FACTOR: 510000,
|
| 282 |
+
PolicyParameter.MINIMUM_PENSION: 200000 # Lower income pensioners
|
| 283 |
+
}
|
| 284 |
+
return population_estimates.get(parameter, 400000)
|
| 285 |
+
|
| 286 |
+
def _format_simulation_response(self, result: Dict[str, Any], query: PolicyQuery) -> Dict[str, Any]:
|
| 287 |
+
"""Format simulation results for chat display"""
|
| 288 |
+
if "error" in result:
|
| 289 |
+
return {
|
| 290 |
+
"type": "error",
|
| 291 |
+
"message": f"Simulation error: {result['error']}"
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
total_impact = result.get("total_impact", {})
|
| 295 |
+
projections = result.get("scenario_projections", [])
|
| 296 |
+
clause_analysis = result.get("clause_analysis", {})
|
| 297 |
+
|
| 298 |
+
# Create summary message
|
| 299 |
+
summary = f"""
|
| 300 |
+
π― **Policy Impact Simulation Results**
|
| 301 |
+
|
| 302 |
+
**Parameter**: {result.get('parameter_name', 'Unknown')}
|
| 303 |
+
**Change**: {result.get('current_value')} β {result.get('proposed_value')}
|
| 304 |
+
**Effective Date**: {result.get('effective_date', '').split('T')[0]}
|
| 305 |
+
|
| 306 |
+
π **Financial Impact Over {result.get('projection_years')} Years**:
|
| 307 |
+
β’ **Total Additional Cost**: βΉ{total_impact.get('total_additional_cost_crores', 0):.1f} crores
|
| 308 |
+
β’ **Percentage Increase**: {total_impact.get('percentage_increase', 0):.1f}%
|
| 309 |
+
β’ **Annual Average**: βΉ{total_impact.get('annual_average_impact_crores', 0):.1f} crores
|
| 310 |
+
β’ **Cost per Beneficiary**: βΉ{total_impact.get('cost_per_beneficiary_annual', 0):,.0f} per year
|
| 311 |
+
|
| 312 |
+
π **Year-by-Year Breakdown**:
|
| 313 |
+
"""
|
| 314 |
+
|
| 315 |
+
# Add yearly breakdown
|
| 316 |
+
for i, proj in enumerate(projections[:3]): # Show first 3 years
|
| 317 |
+
summary += f"Year {proj.year}: βΉ{proj.impact/10000000:.1f} crores impact ({proj.affected_beneficiaries:,} beneficiaries)\n"
|
| 318 |
+
|
| 319 |
+
if len(projections) > 3:
|
| 320 |
+
summary += f"... and {len(projections)-3} more years\n"
|
| 321 |
+
|
| 322 |
+
# Add clause information
|
| 323 |
+
if clause_analysis:
|
| 324 |
+
clause_diff = clause_analysis.get("clause_diff", {})
|
| 325 |
+
summary += f"""
|
| 326 |
+
βοΈ **Policy Changes**:
|
| 327 |
+
β’ **Change Type**: {clause_diff.get('change_type', 'Unknown').title()}
|
| 328 |
+
β’ **Magnitude**: {clause_diff.get('change_percentage', 0):.1f}% change
|
| 329 |
+
β’ **Affected Clauses**: {len(clause_analysis.get('affected_clauses', []))} clauses modified
|
| 330 |
+
"""
|
| 331 |
+
|
| 332 |
+
# Add scenario variants
|
| 333 |
+
variants = result.get("variants", {})
|
| 334 |
+
charts = []
|
| 335 |
+
|
| 336 |
+
if variants:
|
| 337 |
+
best_case = sum(p.impact for p in variants.get("best_case", [])) / 10000000
|
| 338 |
+
worst_case = sum(p.impact for p in variants.get("worst_case", [])) / 10000000
|
| 339 |
+
base_case = total_impact.get('total_additional_cost_crores', 0)
|
| 340 |
+
|
| 341 |
+
summary += f"""
|
| 342 |
+
π **Scenario Analysis**:
|
| 343 |
+
β’ **Best Case**: βΉ{best_case:.1f} crores
|
| 344 |
+
β’ **Base Case**: βΉ{base_case:.1f} crores
|
| 345 |
+
β’ **Worst Case**: βΉ{worst_case:.1f} crores
|
| 346 |
+
"""
|
| 347 |
+
|
| 348 |
+
# Generate charts
|
| 349 |
+
try:
|
| 350 |
+
# Scenario comparison chart
|
| 351 |
+
scenario_data = {
|
| 352 |
+
'best_case': {'total_cost': best_case},
|
| 353 |
+
'base_case': {'total_cost': base_case},
|
| 354 |
+
'worst_case': {'total_cost': worst_case}
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
chart_title = f"{result.get('parameter_name', 'Policy')} Impact Analysis"
|
| 358 |
+
scenario_chart = self.chart_generator.generate_scenario_comparison_chart(
|
| 359 |
+
scenario_data, chart_title
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
if scenario_chart:
|
| 363 |
+
charts.append({
|
| 364 |
+
"type": "scenario_comparison",
|
| 365 |
+
"title": chart_title,
|
| 366 |
+
"data": scenario_chart
|
| 367 |
+
})
|
| 368 |
+
|
| 369 |
+
# Year-by-year breakdown chart
|
| 370 |
+
if projections:
|
| 371 |
+
# Convert projections to the format expected by chart generator
|
| 372 |
+
yearly_data = []
|
| 373 |
+
for proj in projections:
|
| 374 |
+
yearly_data.append({
|
| 375 |
+
'year': proj.year,
|
| 376 |
+
'impact': proj.impact / 10000000, # Convert to crores
|
| 377 |
+
'beneficiaries': proj.affected_beneficiaries
|
| 378 |
+
})
|
| 379 |
+
|
| 380 |
+
trend_chart = self.chart_generator.generate_yearly_breakdown_chart(
|
| 381 |
+
yearly_data, f"{result.get('parameter_name', 'Policy')} 5-Year Impact"
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
if trend_chart:
|
| 385 |
+
charts.append({
|
| 386 |
+
"type": "yearly_trend",
|
| 387 |
+
"title": "5-Year Financial Impact Trend",
|
| 388 |
+
"data": trend_chart
|
| 389 |
+
})
|
| 390 |
+
|
| 391 |
+
except Exception as e:
|
| 392 |
+
logger.error(f"Chart generation error: {e}")
|
| 393 |
+
|
| 394 |
+
return {
|
| 395 |
+
"type": "policy_simulation",
|
| 396 |
+
"message": summary,
|
| 397 |
+
"simulation_id": result.get("scenario_id"),
|
| 398 |
+
"detailed_results": result,
|
| 399 |
+
"charts": charts,
|
| 400 |
+
"export_options": ["CSV", "PDF", "JSON"],
|
| 401 |
+
"follow_up_suggestions": [
|
| 402 |
+
"Compare with other policy scenarios",
|
| 403 |
+
"Analyze implementation timeline",
|
| 404 |
+
"Export detailed evidence pack",
|
| 405 |
+
"Simulate different effective dates"
|
| 406 |
+
]
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
def _get_interactive_form(self, parsed_query: PolicyQuery) -> Dict[str, Any]:
|
| 410 |
+
"""Provide interactive form for missing parameters"""
|
| 411 |
+
available_parameters = [
|
| 412 |
+
{"id": "dearness_relief", "name": "Dearness Relief (%)", "current": 12.0, "unit": "%"},
|
| 413 |
+
{"id": "basic_pension", "name": "Basic Pension (βΉ)", "current": 6000, "unit": "βΉ"},
|
| 414 |
+
{"id": "medical_allowance", "name": "Medical Allowance (βΉ)", "current": 1000, "unit": "βΉ"},
|
| 415 |
+
{"id": "pension_factor", "name": "Pension Factor", "current": 1.0, "unit": "multiplier"},
|
| 416 |
+
{"id": "minimum_pension", "name": "Minimum Pension (βΉ)", "current": 3500, "unit": "βΉ"}
|
| 417 |
+
]
|
| 418 |
+
|
| 419 |
+
return {
|
| 420 |
+
"type": "interactive_form",
|
| 421 |
+
"message": "I'd be happy to help you simulate policy impact! Please provide the following details:",
|
| 422 |
+
"form_fields": [
|
| 423 |
+
{
|
| 424 |
+
"name": "parameter",
|
| 425 |
+
"label": "Policy Parameter",
|
| 426 |
+
"type": "select",
|
| 427 |
+
"options": available_parameters,
|
| 428 |
+
"required": True
|
| 429 |
+
},
|
| 430 |
+
{
|
| 431 |
+
"name": "current_value",
|
| 432 |
+
"label": "Current Value",
|
| 433 |
+
"type": "number",
|
| 434 |
+
"required": True
|
| 435 |
+
},
|
| 436 |
+
{
|
| 437 |
+
"name": "proposed_value",
|
| 438 |
+
"label": "Proposed Value",
|
| 439 |
+
"type": "number",
|
| 440 |
+
"required": True
|
| 441 |
+
},
|
| 442 |
+
{
|
| 443 |
+
"name": "years",
|
| 444 |
+
"label": "Projection Years",
|
| 445 |
+
"type": "number",
|
| 446 |
+
"default": 5,
|
| 447 |
+
"min": 1,
|
| 448 |
+
"max": 10
|
| 449 |
+
}
|
| 450 |
+
],
|
| 451 |
+
"examples": [
|
| 452 |
+
"Simulate DR increase from 12% to 18% over 5 years",
|
| 453 |
+
"What if basic pension changes from βΉ6000 to βΉ8000?",
|
| 454 |
+
"Impact of medical allowance increase to βΉ2000"
|
| 455 |
+
]
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
def _handle_comparison_request(self, parsed_query: PolicyQuery, user_id: str) -> Dict[str, Any]:
|
| 459 |
+
"""Handle policy comparison request"""
|
| 460 |
+
sample_comparisons = [
|
| 461 |
+
{
|
| 462 |
+
"name": "DR vs Basic Pension Increase",
|
| 463 |
+
"scenarios": [
|
| 464 |
+
{"parameter": "DR", "change": "12% β 18%", "impact": "βΉ85.2 crores"},
|
| 465 |
+
{"parameter": "Basic Pension", "change": "βΉ6000 β βΉ8000", "impact": "βΉ108.0 crores"}
|
| 466 |
+
],
|
| 467 |
+
"recommendation": "DR increase is more cost-effective"
|
| 468 |
+
},
|
| 469 |
+
{
|
| 470 |
+
"name": "Short vs Long Term Impact",
|
| 471 |
+
"scenarios": [
|
| 472 |
+
{"period": "3 years", "total_impact": "βΉ150.5 crores"},
|
| 473 |
+
{"period": "10 years", "total_impact": "βΉ628.3 crores"}
|
| 474 |
+
],
|
| 475 |
+
"recommendation": "Long-term planning essential"
|
| 476 |
+
}
|
| 477 |
+
]
|
| 478 |
+
|
| 479 |
+
return {
|
| 480 |
+
"type": "policy_comparison",
|
| 481 |
+
"message": "Here are some policy comparison examples. Would you like to compare specific scenarios?",
|
| 482 |
+
"comparisons": sample_comparisons,
|
| 483 |
+
"custom_comparison": {
|
| 484 |
+
"description": "I can help you compare up to 5 different policy scenarios",
|
| 485 |
+
"example": "Compare DR increase vs pension boost vs medical allowance increase"
|
| 486 |
+
}
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
def _get_help_response(self) -> Dict[str, Any]:
|
| 490 |
+
"""Provide help information"""
|
| 491 |
+
return {
|
| 492 |
+
"type": "help",
|
| 493 |
+
"message": """
|
| 494 |
+
π― **Policy Impact Simulator Help**
|
| 495 |
+
|
| 496 |
+
I can help you simulate the financial impact of government policy changes. Here's what I can do:
|
| 497 |
+
|
| 498 |
+
**π Simulation Capabilities**:
|
| 499 |
+
β’ Dearness Relief (DR) changes
|
| 500 |
+
β’ Basic pension adjustments
|
| 501 |
+
β’ Medical allowance modifications
|
| 502 |
+
β’ Pension factor changes
|
| 503 |
+
β’ Minimum pension guarantees
|
| 504 |
+
|
| 505 |
+
**π¬ How to Ask**:
|
| 506 |
+
β’ "Simulate DR increase from 12% to 18%"
|
| 507 |
+
β’ "What if basic pension changes from βΉ6000 to βΉ8000?"
|
| 508 |
+
β’ "Impact of medical allowance increase to βΉ2000 over 5 years"
|
| 509 |
+
β’ "Compare DR increase vs pension boost"
|
| 510 |
+
|
| 511 |
+
**π What You Get**:
|
| 512 |
+
β’ Total financial impact over 3-10 years
|
| 513 |
+
β’ Year-by-year breakdown
|
| 514 |
+
β’ Best/base/worst case scenarios
|
| 515 |
+
β’ Affected population estimates
|
| 516 |
+
β’ Policy clause analysis
|
| 517 |
+
β’ Implementation timeline
|
| 518 |
+
β’ Exportable evidence pack
|
| 519 |
+
|
| 520 |
+
**π Quick Examples**:
|
| 521 |
+
Try asking: "Show me the impact of increasing DR by 6%"
|
| 522 |
+
""",
|
| 523 |
+
"quick_actions": [
|
| 524 |
+
{"label": "Sample DR Simulation", "query": "simulate DR from 12 to 18 percent"},
|
| 525 |
+
{"label": "Basic Pension Impact", "query": "what if basic pension increases to 8000"},
|
| 526 |
+
{"label": "Compare Scenarios", "query": "compare policy scenarios"},
|
| 527 |
+
{"label": "View Parameters", "query": "show available policy parameters"}
|
| 528 |
+
]
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
def _get_clarification_response(self, query: str) -> Dict[str, Any]:
|
| 532 |
+
"""Request clarification for unclear queries"""
|
| 533 |
+
|
| 534 |
+
# Provide specific guidance based on the query content
|
| 535 |
+
query_lower = query.lower()
|
| 536 |
+
|
| 537 |
+
if "da" in query_lower or "dearness allowance" in query_lower or "dr" in query_lower:
|
| 538 |
+
specific_message = """π― **DA/DR Impact Analysis**
|
| 539 |
+
|
| 540 |
+
I can help you analyze the Dearness Allowance (DA) impact! To provide accurate calculations, please specify:
|
| 541 |
+
|
| 542 |
+
π **Required Details:**
|
| 543 |
+
β’ **Current DA Rate**: What's the existing DA percentage? (e.g., 12%)
|
| 544 |
+
β’ **Proposed DA Rate**: What should the new DA be? (e.g., 18% for a 6% increase)
|
| 545 |
+
β’ **Base Pension Amount**: What's the basic pension amount? (e.g., βΉ6,000)
|
| 546 |
+
β’ **Analysis Period**: How many years to analyze? (default: 5 years)
|
| 547 |
+
|
| 548 |
+
π‘ **Quick Examples:**
|
| 549 |
+
β’ "Show DA impact from 12% to 18% for pension βΉ6000"
|
| 550 |
+
β’ "Analyze 6% DA increase from current 12% to 18%"
|
| 551 |
+
β’ "DA simulation: current 12%, increase to 18%, basic pension βΉ6000"
|
| 552 |
+
|
| 553 |
+
π **What You'll Get:**
|
| 554 |
+
β’ Financial impact over 5 years with charts
|
| 555 |
+
β’ Cost per beneficiary calculations
|
| 556 |
+
β’ Best/Base/Worst case scenarios
|
| 557 |
+
β’ Implementation timeline and evidence pack"""
|
| 558 |
+
|
| 559 |
+
elif "pension" in query_lower and ("basic" in query_lower or "minimum" in query_lower):
|
| 560 |
+
specific_message = """π― **Basic Pension Impact Analysis**
|
| 561 |
+
|
| 562 |
+
I can help analyze basic pension changes! Please provide:
|
| 563 |
+
|
| 564 |
+
π **Required Details:**
|
| 565 |
+
β’ **Current Basic Pension**: What's the existing amount? (e.g., βΉ6,000)
|
| 566 |
+
β’ **Proposed Basic Pension**: What should the new amount be? (e.g., βΉ8,000)
|
| 567 |
+
β’ **Analysis Period**: How many years to analyze? (default: 5 years)
|
| 568 |
+
|
| 569 |
+
π‘ **Quick Examples:**
|
| 570 |
+
β’ "Show basic pension impact from βΉ6000 to βΉ8000"
|
| 571 |
+
β’ "Analyze pension increase to βΉ8000 over 5 years"
|
| 572 |
+
β’ "Basic pension simulation: current βΉ6000, increase to βΉ8000"
|
| 573 |
+
|
| 574 |
+
π **What You'll Get:**
|
| 575 |
+
β’ Financial impact projections with charts
|
| 576 |
+
β’ Affected population estimates
|
| 577 |
+
β’ Cost analysis and implementation timeline"""
|
| 578 |
+
|
| 579 |
+
else:
|
| 580 |
+
specific_message = """π― **Policy Impact Simulation Help**
|
| 581 |
+
|
| 582 |
+
I understand you want to analyze policy impact! To provide accurate calculations, please specify:
|
| 583 |
+
|
| 584 |
+
π **Choose Your Analysis:**
|
| 585 |
+
β’ **DA/DR Changes**: Dearness Allowance adjustments (e.g., "DA from 12% to 18%")
|
| 586 |
+
β’ **Basic Pension**: Minimum pension amount changes (e.g., "Pension from βΉ6000 to βΉ8000")
|
| 587 |
+
β’ **Medical Allowance**: Healthcare support changes (e.g., "Medical allowance to βΉ1500")
|
| 588 |
+
|
| 589 |
+
π‘ **Format Your Request:**
|
| 590 |
+
Include: Current value β Proposed value β Base amount (if applicable)
|
| 591 |
+
|
| 592 |
+
π **Quick Examples:**
|
| 593 |
+
β’ "Show DA impact from 12% to 18% for pension βΉ6000"
|
| 594 |
+
β’ "Analyze basic pension increase from βΉ6000 to βΉ8000"
|
| 595 |
+
β’ "Medical allowance impact from βΉ1000 to βΉ1500" """
|
| 596 |
+
|
| 597 |
+
return {
|
| 598 |
+
"type": "clarification",
|
| 599 |
+
"message": specific_message,
|
| 600 |
+
"original_query": query,
|
| 601 |
+
"suggestions": [
|
| 602 |
+
"π Use the format: 'Show [policy] impact from [current] to [proposed]'",
|
| 603 |
+
"π Include specific numbers and amounts",
|
| 604 |
+
"β±οΈ Specify time period if different from 5 years"
|
| 605 |
+
],
|
| 606 |
+
"quick_actions": [
|
| 607 |
+
{"text": "π DA Analysis Example", "query": "Show DA impact from 12% to 18% for pension βΉ6000"},
|
| 608 |
+
{"text": "π° Pension Analysis Example", "query": "Show basic pension impact from βΉ6000 to βΉ8000"},
|
| 609 |
+
{"text": "π Start Interactive Form", "query": "start scenario analysis"}
|
| 610 |
+
]
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
def _handle_scenario_analysis_request(self, parsed_query: PolicyQuery, user_id: str) -> Dict[str, Any]:
|
| 614 |
+
"""Handle general scenario analysis requests"""
|
| 615 |
+
return {
|
| 616 |
+
"type": "scenario_analysis_help",
|
| 617 |
+
"message": """π― **Scenario Analysis for Rajasthan Pension Policies**
|
| 618 |
+
|
| 619 |
+
I can help you analyze different scenarios for pension policy changes. Here are the most common analyses:
|
| 620 |
+
|
| 621 |
+
π **Available Scenario Analyses:**
|
| 622 |
+
β’ **Dearness Relief (DR)**: Analyze inflation adjustments (current: 12%)
|
| 623 |
+
β’ **Basic Pension**: Analyze minimum pension changes (current: βΉ6,000)
|
| 624 |
+
β’ **Medical Allowance**: Analyze healthcare support changes (current: βΉ1,000)
|
| 625 |
+
β’ **Pension Factor**: Analyze salary multiplier changes (current: 1.0x)
|
| 626 |
+
|
| 627 |
+
π¬ **How to Request Analysis:**
|
| 628 |
+
β’ "Simulate DR increase from 12% to 18% over 5 years"
|
| 629 |
+
β’ "What if basic pension increases to βΉ8,000?"
|
| 630 |
+
β’ "Compare best and worst case scenarios for medical allowance"
|
| 631 |
+
|
| 632 |
+
π **What You'll Get:**
|
| 633 |
+
β’ Financial impact projections (3-10 years)
|
| 634 |
+
β’ Best/Base/Worst case scenarios
|
| 635 |
+
β’ Affected population estimates
|
| 636 |
+
β’ Implementation timeline and complexity
|
| 637 |
+
β’ Exportable evidence packs
|
| 638 |
+
|
| 639 |
+
π **Try These Examples:**""",
|
| 640 |
+
"quick_actions": [
|
| 641 |
+
{"text": "π Analyze DR Scenarios", "query": "Show DR scenario analysis from 12% to 18%"},
|
| 642 |
+
{"text": "π° Analyze Pension Scenarios", "query": "Show basic pension scenarios from 6000 to 8000"},
|
| 643 |
+
{"text": "π₯ Analyze Medical Allowance", "query": "Show medical allowance scenarios"},
|
| 644 |
+
{"text": "π Start Interactive Form", "query": "start scenario analysis"}
|
| 645 |
+
]
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
def _start_interactive_form(self, user_id: str) -> Dict[str, Any]:
|
| 649 |
+
"""Start interactive scenario analysis form"""
|
| 650 |
+
try:
|
| 651 |
+
from scenario_chat_form import start_scenario_analysis_form
|
| 652 |
+
return start_scenario_analysis_form(user_id)
|
| 653 |
+
except Exception as e:
|
| 654 |
+
logger.error(f"Interactive form start failed: {e}")
|
| 655 |
+
return {
|
| 656 |
+
"type": "error",
|
| 657 |
+
"message": "Sorry, I couldn't start the interactive form. Let me help you with a quick simulation instead.",
|
| 658 |
+
"fallback_form": self._get_clarification_response("")
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
# Usage integration function
|
| 662 |
+
def process_policy_chat_query(query: str, user_id: str = "default") -> Dict[str, Any]:
|
| 663 |
+
"""
|
| 664 |
+
Main function to process policy-related chat queries
|
| 665 |
+
Use this in your main chat system
|
| 666 |
+
"""
|
| 667 |
+
interface = PolicySimulatorChatInterface()
|
| 668 |
+
return interface.process_policy_query(query, user_id)
|
policy_impact_simulator.py
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Policy Impact Simulator Service
|
| 3 |
+
Analyzes policy changes and their financial/social impact over time periods
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import logging
|
| 8 |
+
from typing import Dict, List, Any, Optional, Tuple
|
| 9 |
+
from datetime import datetime, timedelta
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import numpy as np
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
from enum import Enum
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
class PolicyParameter(Enum):
|
| 18 |
+
"""Policy parameters that can be simulated"""
|
| 19 |
+
PENSION_FACTOR = "pension_factor"
|
| 20 |
+
DEARNESS_RELIEF = "dearness_relief"
|
| 21 |
+
BASIC_PENSION = "basic_pension"
|
| 22 |
+
MINIMUM_PENSION = "minimum_pension"
|
| 23 |
+
COMMUTATION_FACTOR = "commutation_factor"
|
| 24 |
+
MEDICAL_ALLOWANCE = "medical_allowance"
|
| 25 |
+
FAMILY_PENSION = "family_pension"
|
| 26 |
+
GRATUITY_LIMIT = "gratuity_limit"
|
| 27 |
+
HRA_PERCENTAGE = "hra_percentage"
|
| 28 |
+
AGE_LIMIT = "retirement_age"
|
| 29 |
+
|
| 30 |
+
class ScenarioType(Enum):
|
| 31 |
+
"""Types of scenarios for impact analysis"""
|
| 32 |
+
BEST_CASE = "best"
|
| 33 |
+
BASE_CASE = "base"
|
| 34 |
+
WORST_CASE = "worst"
|
| 35 |
+
|
| 36 |
+
@dataclass
|
| 37 |
+
class PolicyScenario:
|
| 38 |
+
"""Configuration for a policy scenario"""
|
| 39 |
+
parameter: PolicyParameter
|
| 40 |
+
current_value: float
|
| 41 |
+
proposed_value: float
|
| 42 |
+
effective_date: datetime
|
| 43 |
+
affected_population: int
|
| 44 |
+
annual_growth_rate: float = 0.03 # Default 3% annual growth
|
| 45 |
+
inflation_rate: float = 0.06 # Default 6% inflation
|
| 46 |
+
|
| 47 |
+
@dataclass
|
| 48 |
+
class ImpactProjection:
|
| 49 |
+
"""Financial impact projection"""
|
| 50 |
+
year: int
|
| 51 |
+
baseline_cost: float
|
| 52 |
+
scenario_cost: float
|
| 53 |
+
impact: float
|
| 54 |
+
affected_beneficiaries: int
|
| 55 |
+
notes: str
|
| 56 |
+
|
| 57 |
+
class PolicyImpactSimulator:
|
| 58 |
+
"""Simulates financial and social impact of policy changes"""
|
| 59 |
+
|
| 60 |
+
def __init__(self):
|
| 61 |
+
self.policy_definitions = self._load_policy_definitions()
|
| 62 |
+
self.historical_data = self._load_historical_data()
|
| 63 |
+
|
| 64 |
+
def _load_policy_definitions(self) -> Dict[PolicyParameter, Dict]:
|
| 65 |
+
"""Load policy parameter definitions and constraints"""
|
| 66 |
+
return {
|
| 67 |
+
PolicyParameter.PENSION_FACTOR: {
|
| 68 |
+
"name": "Pension Factor",
|
| 69 |
+
"description": "Multiplier for calculating monthly pension",
|
| 70 |
+
"unit": "multiplier",
|
| 71 |
+
"typical_range": (0.5, 2.0),
|
| 72 |
+
"current_rajasthan": 1.0,
|
| 73 |
+
"impact_type": "direct_benefit"
|
| 74 |
+
},
|
| 75 |
+
PolicyParameter.DEARNESS_RELIEF: {
|
| 76 |
+
"name": "Dearness Relief (DR)",
|
| 77 |
+
"description": "Percentage increase to counter inflation",
|
| 78 |
+
"unit": "percentage",
|
| 79 |
+
"typical_range": (0, 50),
|
| 80 |
+
"current_rajasthan": 12.0,
|
| 81 |
+
"impact_type": "cost_adjustment"
|
| 82 |
+
},
|
| 83 |
+
PolicyParameter.BASIC_PENSION: {
|
| 84 |
+
"name": "Basic Pension Amount",
|
| 85 |
+
"description": "Minimum pension amount per month",
|
| 86 |
+
"unit": "rupees",
|
| 87 |
+
"typical_range": (3000, 15000),
|
| 88 |
+
"current_rajasthan": 6000,
|
| 89 |
+
"impact_type": "direct_benefit"
|
| 90 |
+
},
|
| 91 |
+
PolicyParameter.MINIMUM_PENSION: {
|
| 92 |
+
"name": "Minimum Pension Guarantee",
|
| 93 |
+
"description": "Guaranteed minimum pension amount",
|
| 94 |
+
"unit": "rupees",
|
| 95 |
+
"typical_range": (2000, 10000),
|
| 96 |
+
"current_rajasthan": 3500,
|
| 97 |
+
"impact_type": "safety_net"
|
| 98 |
+
},
|
| 99 |
+
PolicyParameter.MEDICAL_ALLOWANCE: {
|
| 100 |
+
"name": "Medical Allowance",
|
| 101 |
+
"description": "Monthly medical expense allowance",
|
| 102 |
+
"unit": "rupees",
|
| 103 |
+
"typical_range": (500, 5000),
|
| 104 |
+
"current_rajasthan": 1000,
|
| 105 |
+
"impact_type": "healthcare_benefit"
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
def _load_historical_data(self) -> Dict:
|
| 110 |
+
"""Load historical policy implementation data"""
|
| 111 |
+
return {
|
| 112 |
+
"rajasthan_pensioners": {
|
| 113 |
+
"2020": 450000,
|
| 114 |
+
"2021": 465000,
|
| 115 |
+
"2022": 480000,
|
| 116 |
+
"2023": 495000,
|
| 117 |
+
"2024": 510000
|
| 118 |
+
},
|
| 119 |
+
"average_pension": {
|
| 120 |
+
"2020": 8500,
|
| 121 |
+
"2021": 9200,
|
| 122 |
+
"2022": 9800,
|
| 123 |
+
"2023": 10400,
|
| 124 |
+
"2024": 11000
|
| 125 |
+
},
|
| 126 |
+
"budget_allocation": {
|
| 127 |
+
"2020": 3850000000, # 385 crores
|
| 128 |
+
"2021": 4280000000, # 428 crores
|
| 129 |
+
"2022": 4700000000, # 470 crores
|
| 130 |
+
"2023": 5100000000, # 510 crores
|
| 131 |
+
"2024": 5600000000 # 560 crores
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
def simulate_policy_impact(
|
| 136 |
+
self,
|
| 137 |
+
scenario: PolicyScenario,
|
| 138 |
+
years: int = 5,
|
| 139 |
+
include_variants: bool = True
|
| 140 |
+
) -> Dict[str, Any]:
|
| 141 |
+
"""
|
| 142 |
+
Simulate the impact of a policy change over specified years
|
| 143 |
+
|
| 144 |
+
Args:
|
| 145 |
+
scenario: Policy scenario configuration
|
| 146 |
+
years: Number of years to project
|
| 147 |
+
include_variants: Include best/worst case scenarios
|
| 148 |
+
|
| 149 |
+
Returns:
|
| 150 |
+
Complete impact analysis with projections and metadata
|
| 151 |
+
"""
|
| 152 |
+
try:
|
| 153 |
+
# Generate baseline projections
|
| 154 |
+
baseline_projections = self._generate_baseline_projections(scenario, years)
|
| 155 |
+
|
| 156 |
+
# Generate scenario projections
|
| 157 |
+
scenario_projections = self._generate_scenario_projections(scenario, years)
|
| 158 |
+
|
| 159 |
+
# Calculate variants if requested
|
| 160 |
+
variants = {}
|
| 161 |
+
if include_variants:
|
| 162 |
+
variants = self._generate_scenario_variants(scenario, years)
|
| 163 |
+
|
| 164 |
+
# Generate clause differences and timeline
|
| 165 |
+
clause_analysis = self._analyze_clause_changes(scenario)
|
| 166 |
+
|
| 167 |
+
# Create evidence pack
|
| 168 |
+
evidence_pack = self._create_evidence_pack(
|
| 169 |
+
scenario, baseline_projections, scenario_projections, variants
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
return {
|
| 173 |
+
"scenario_id": f"policy_sim_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
|
| 174 |
+
"parameter": scenario.parameter.value,
|
| 175 |
+
"parameter_name": self.policy_definitions[scenario.parameter]["name"],
|
| 176 |
+
"current_value": scenario.current_value,
|
| 177 |
+
"proposed_value": scenario.proposed_value,
|
| 178 |
+
"effective_date": scenario.effective_date.isoformat(),
|
| 179 |
+
"projection_years": years,
|
| 180 |
+
"baseline_projections": baseline_projections,
|
| 181 |
+
"scenario_projections": scenario_projections,
|
| 182 |
+
"variants": variants,
|
| 183 |
+
"total_impact": self._calculate_total_impact(baseline_projections, scenario_projections),
|
| 184 |
+
"clause_analysis": clause_analysis,
|
| 185 |
+
"evidence_pack": evidence_pack,
|
| 186 |
+
"generated_at": datetime.now().isoformat(),
|
| 187 |
+
"assumptions": self._document_assumptions(scenario)
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
except Exception as e:
|
| 191 |
+
logger.error(f"Policy simulation error: {e}")
|
| 192 |
+
return {"error": str(e)}
|
| 193 |
+
|
| 194 |
+
def _generate_baseline_projections(self, scenario: PolicyScenario, years: int) -> List[ImpactProjection]:
|
| 195 |
+
"""Generate baseline (no change) projections"""
|
| 196 |
+
projections = []
|
| 197 |
+
base_population = scenario.affected_population
|
| 198 |
+
current_avg_benefit = self._estimate_current_benefit(scenario)
|
| 199 |
+
|
| 200 |
+
for year in range(1, years + 1):
|
| 201 |
+
# Account for population growth and inflation
|
| 202 |
+
year_population = int(base_population * (1 + scenario.annual_growth_rate) ** year)
|
| 203 |
+
year_benefit = current_avg_benefit * (1 + scenario.inflation_rate) ** year
|
| 204 |
+
|
| 205 |
+
annual_cost = year_population * year_benefit * 12 # Monthly to annual
|
| 206 |
+
|
| 207 |
+
projections.append(ImpactProjection(
|
| 208 |
+
year=year,
|
| 209 |
+
baseline_cost=annual_cost,
|
| 210 |
+
scenario_cost=annual_cost, # Same for baseline
|
| 211 |
+
impact=0,
|
| 212 |
+
affected_beneficiaries=year_population,
|
| 213 |
+
notes=f"Baseline year {year}: No policy change, inflation adjustment only"
|
| 214 |
+
))
|
| 215 |
+
|
| 216 |
+
return projections
|
| 217 |
+
|
| 218 |
+
def _generate_scenario_projections(self, scenario: PolicyScenario, years: int) -> List[ImpactProjection]:
|
| 219 |
+
"""Generate scenario (with change) projections"""
|
| 220 |
+
projections = []
|
| 221 |
+
base_population = scenario.affected_population
|
| 222 |
+
current_benefit = self._estimate_current_benefit(scenario)
|
| 223 |
+
new_benefit = self._calculate_new_benefit(scenario)
|
| 224 |
+
|
| 225 |
+
for year in range(1, years + 1):
|
| 226 |
+
year_population = int(base_population * (1 + scenario.annual_growth_rate) ** year)
|
| 227 |
+
|
| 228 |
+
# Baseline cost (what it would have been)
|
| 229 |
+
baseline_benefit = current_benefit * (1 + scenario.inflation_rate) ** year
|
| 230 |
+
baseline_cost = year_population * baseline_benefit * 12
|
| 231 |
+
|
| 232 |
+
# Scenario cost (with policy change)
|
| 233 |
+
scenario_benefit = new_benefit * (1 + scenario.inflation_rate) ** year
|
| 234 |
+
scenario_cost = year_population * scenario_benefit * 12
|
| 235 |
+
|
| 236 |
+
impact = scenario_cost - baseline_cost
|
| 237 |
+
|
| 238 |
+
projections.append(ImpactProjection(
|
| 239 |
+
year=year,
|
| 240 |
+
baseline_cost=baseline_cost,
|
| 241 |
+
scenario_cost=scenario_cost,
|
| 242 |
+
impact=impact,
|
| 243 |
+
affected_beneficiaries=year_population,
|
| 244 |
+
notes=f"Year {year}: Policy change impact βΉ{impact/10000000:.1f} crores"
|
| 245 |
+
))
|
| 246 |
+
|
| 247 |
+
return projections
|
| 248 |
+
|
| 249 |
+
def _generate_scenario_variants(self, scenario: PolicyScenario, years: int) -> Dict[str, List[ImpactProjection]]:
|
| 250 |
+
"""Generate best/worst case scenario variants"""
|
| 251 |
+
variants = {}
|
| 252 |
+
|
| 253 |
+
# Best case: Lower growth, higher efficiency
|
| 254 |
+
best_scenario = PolicyScenario(
|
| 255 |
+
parameter=scenario.parameter,
|
| 256 |
+
current_value=scenario.current_value,
|
| 257 |
+
proposed_value=scenario.proposed_value,
|
| 258 |
+
effective_date=scenario.effective_date,
|
| 259 |
+
affected_population=int(scenario.affected_population * 0.9), # 10% fewer beneficiaries
|
| 260 |
+
annual_growth_rate=scenario.annual_growth_rate * 0.8, # 20% lower growth
|
| 261 |
+
inflation_rate=scenario.inflation_rate * 0.9 # 10% lower inflation
|
| 262 |
+
)
|
| 263 |
+
variants["best_case"] = self._generate_scenario_projections(best_scenario, years)
|
| 264 |
+
|
| 265 |
+
# Worst case: Higher growth, implementation challenges
|
| 266 |
+
worst_scenario = PolicyScenario(
|
| 267 |
+
parameter=scenario.parameter,
|
| 268 |
+
current_value=scenario.current_value,
|
| 269 |
+
proposed_value=scenario.proposed_value * 1.1, # 10% higher due to implementation costs
|
| 270 |
+
effective_date=scenario.effective_date,
|
| 271 |
+
affected_population=int(scenario.affected_population * 1.2), # 20% more beneficiaries
|
| 272 |
+
annual_growth_rate=scenario.annual_growth_rate * 1.3, # 30% higher growth
|
| 273 |
+
inflation_rate=scenario.inflation_rate * 1.1 # 10% higher inflation
|
| 274 |
+
)
|
| 275 |
+
variants["worst_case"] = self._generate_scenario_projections(worst_scenario, years)
|
| 276 |
+
|
| 277 |
+
return variants
|
| 278 |
+
|
| 279 |
+
def _analyze_clause_changes(self, scenario: PolicyScenario) -> Dict[str, Any]:
|
| 280 |
+
"""Analyze what policy clauses would change"""
|
| 281 |
+
parameter_def = self.policy_definitions[scenario.parameter]
|
| 282 |
+
|
| 283 |
+
return {
|
| 284 |
+
"affected_clauses": self._identify_affected_clauses(scenario.parameter),
|
| 285 |
+
"clause_diff": {
|
| 286 |
+
"before": f"{parameter_def['name']}: {scenario.current_value} {parameter_def['unit']}",
|
| 287 |
+
"after": f"{parameter_def['name']}: {scenario.proposed_value} {parameter_def['unit']}",
|
| 288 |
+
"change_type": "increase" if scenario.proposed_value > scenario.current_value else "decrease",
|
| 289 |
+
"change_magnitude": abs(scenario.proposed_value - scenario.current_value),
|
| 290 |
+
"change_percentage": ((scenario.proposed_value - scenario.current_value) / scenario.current_value) * 100
|
| 291 |
+
},
|
| 292 |
+
"effective_timeline": {
|
| 293 |
+
"announcement_date": (scenario.effective_date - timedelta(days=90)).isoformat(),
|
| 294 |
+
"legislative_process": (scenario.effective_date - timedelta(days=60)).isoformat(),
|
| 295 |
+
"notification_date": (scenario.effective_date - timedelta(days=30)).isoformat(),
|
| 296 |
+
"effective_date": scenario.effective_date.isoformat(),
|
| 297 |
+
"first_payment": (scenario.effective_date + timedelta(days=30)).isoformat()
|
| 298 |
+
},
|
| 299 |
+
"legal_references": self._get_legal_references(scenario.parameter)
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
def _identify_affected_clauses(self, parameter: PolicyParameter) -> List[str]:
|
| 303 |
+
"""Identify which policy clauses would be affected"""
|
| 304 |
+
clause_mapping = {
|
| 305 |
+
PolicyParameter.PENSION_FACTOR: [
|
| 306 |
+
"Rajasthan Civil Services (Pension) Rules, 1996 - Rule 35",
|
| 307 |
+
"Pension calculation formula - Clause 4.2.1",
|
| 308 |
+
"Service weightage provisions - Clause 6.1"
|
| 309 |
+
],
|
| 310 |
+
PolicyParameter.DEARNESS_RELIEF: [
|
| 311 |
+
"Dearness Relief calculation - Rule 42",
|
| 312 |
+
"Inflation adjustment mechanism - Clause 3.4",
|
| 313 |
+
"Automatic revision provisions - Rule 43"
|
| 314 |
+
],
|
| 315 |
+
PolicyParameter.BASIC_PENSION: [
|
| 316 |
+
"Minimum pension guarantee - Rule 28",
|
| 317 |
+
"Basic pension structure - Clause 2.1",
|
| 318 |
+
"Pension floor provisions - Rule 29"
|
| 319 |
+
],
|
| 320 |
+
PolicyParameter.MEDICAL_ALLOWANCE: [
|
| 321 |
+
"Medical benefits - Rule 52",
|
| 322 |
+
"Healthcare allowance - Clause 8.3",
|
| 323 |
+
"Medical reimbursement - Rule 53"
|
| 324 |
+
]
|
| 325 |
+
}
|
| 326 |
+
return clause_mapping.get(parameter, ["General pension provisions"])
|
| 327 |
+
|
| 328 |
+
def _get_legal_references(self, parameter: PolicyParameter) -> List[str]:
|
| 329 |
+
"""Get relevant legal references for the parameter"""
|
| 330 |
+
references = {
|
| 331 |
+
PolicyParameter.PENSION_FACTOR: [
|
| 332 |
+
"Rajasthan Civil Services (Pension) Rules, 1996",
|
| 333 |
+
"Rajasthan Government Resolution No. F.2(5)FD/Rules/96",
|
| 334 |
+
"Central Civil Services (Pension) Rules, 2021 - Reference"
|
| 335 |
+
],
|
| 336 |
+
PolicyParameter.DEARNESS_RELIEF: [
|
| 337 |
+
"Rajasthan Civil Services (Revised Pay) Rules, 2017",
|
| 338 |
+
"Dearness Allowance calculation guidelines",
|
| 339 |
+
"RCS(RP) Rules notification dated 15.08.2017"
|
| 340 |
+
]
|
| 341 |
+
}
|
| 342 |
+
return references.get(parameter, ["Rajasthan Civil Services (Pension) Rules, 1996"])
|
| 343 |
+
|
| 344 |
+
def _estimate_current_benefit(self, scenario: PolicyScenario) -> float:
|
| 345 |
+
"""Estimate current monthly benefit amount"""
|
| 346 |
+
parameter_def = self.policy_definitions[scenario.parameter]
|
| 347 |
+
|
| 348 |
+
if scenario.parameter == PolicyParameter.BASIC_PENSION:
|
| 349 |
+
return scenario.current_value
|
| 350 |
+
elif scenario.parameter == PolicyParameter.PENSION_FACTOR:
|
| 351 |
+
# Average salary * factor
|
| 352 |
+
return 25000 * scenario.current_value # Assumed average salary
|
| 353 |
+
elif scenario.parameter == PolicyParameter.DEARNESS_RELIEF:
|
| 354 |
+
# DR percentage of basic pension
|
| 355 |
+
base_pension = 8000 # Assumed base
|
| 356 |
+
return base_pension * (scenario.current_value / 100)
|
| 357 |
+
else:
|
| 358 |
+
return parameter_def.get("current_rajasthan", 5000)
|
| 359 |
+
|
| 360 |
+
def _calculate_new_benefit(self, scenario: PolicyScenario) -> float:
|
| 361 |
+
"""Calculate new monthly benefit amount after policy change"""
|
| 362 |
+
if scenario.parameter == PolicyParameter.BASIC_PENSION:
|
| 363 |
+
return scenario.proposed_value
|
| 364 |
+
elif scenario.parameter == PolicyParameter.PENSION_FACTOR:
|
| 365 |
+
return 25000 * scenario.proposed_value
|
| 366 |
+
elif scenario.parameter == PolicyParameter.DEARNESS_RELIEF:
|
| 367 |
+
base_pension = 8000
|
| 368 |
+
return base_pension * (scenario.proposed_value / 100)
|
| 369 |
+
else:
|
| 370 |
+
return scenario.proposed_value
|
| 371 |
+
|
| 372 |
+
def _calculate_total_impact(self, baseline: List[ImpactProjection], scenario: List[ImpactProjection]) -> Dict[str, float]:
|
| 373 |
+
"""Calculate total financial impact"""
|
| 374 |
+
total_additional_cost = sum(proj.impact for proj in scenario)
|
| 375 |
+
total_baseline_cost = sum(proj.baseline_cost for proj in baseline)
|
| 376 |
+
|
| 377 |
+
return {
|
| 378 |
+
"total_additional_cost_crores": total_additional_cost / 10000000, # Convert to crores
|
| 379 |
+
"total_baseline_cost_crores": total_baseline_cost / 10000000,
|
| 380 |
+
"percentage_increase": (total_additional_cost / total_baseline_cost) * 100 if total_baseline_cost > 0 else 0,
|
| 381 |
+
"annual_average_impact_crores": (total_additional_cost / len(scenario)) / 10000000,
|
| 382 |
+
"cost_per_beneficiary_annual": total_additional_cost / (scenario[0].affected_beneficiaries * len(scenario))
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
def _create_evidence_pack(self, scenario, baseline, projections, variants) -> Dict[str, Any]:
|
| 386 |
+
"""Create exportable evidence pack"""
|
| 387 |
+
return {
|
| 388 |
+
"summary_stats": {
|
| 389 |
+
"policy_parameter": self.policy_definitions[scenario.parameter]["name"],
|
| 390 |
+
"change_magnitude": f"{scenario.current_value} β {scenario.proposed_value}",
|
| 391 |
+
"affected_population": scenario.affected_population,
|
| 392 |
+
"projection_period": f"{len(projections)} years",
|
| 393 |
+
"total_impact": f"βΉ{sum(p.impact for p in projections)/10000000:.1f} crores"
|
| 394 |
+
},
|
| 395 |
+
"yearly_breakdown": [
|
| 396 |
+
{
|
| 397 |
+
"year": p.year,
|
| 398 |
+
"baseline_crores": p.baseline_cost / 10000000,
|
| 399 |
+
"scenario_crores": p.scenario_cost / 10000000,
|
| 400 |
+
"impact_crores": p.impact / 10000000,
|
| 401 |
+
"beneficiaries": p.affected_beneficiaries
|
| 402 |
+
}
|
| 403 |
+
for p in projections
|
| 404 |
+
],
|
| 405 |
+
"scenario_comparison": {
|
| 406 |
+
"best_case_total": sum(p.impact for p in variants.get("best_case", [])) / 10000000 if variants else 0,
|
| 407 |
+
"base_case_total": sum(p.impact for p in projections) / 10000000,
|
| 408 |
+
"worst_case_total": sum(p.impact for p in variants.get("worst_case", [])) / 10000000 if variants else 0
|
| 409 |
+
},
|
| 410 |
+
"export_formats": {
|
| 411 |
+
"csv_data": "Ready for CSV export",
|
| 412 |
+
"chart_data": "Ready for visualization",
|
| 413 |
+
"pdf_report": "Formatted for official reporting"
|
| 414 |
+
}
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
def _document_assumptions(self, scenario: PolicyScenario) -> Dict[str, Any]:
|
| 418 |
+
"""Document all assumptions used in the simulation"""
|
| 419 |
+
return {
|
| 420 |
+
"demographic_assumptions": {
|
| 421 |
+
"affected_population": scenario.affected_population,
|
| 422 |
+
"annual_growth_rate": f"{scenario.annual_growth_rate*100:.1f}%",
|
| 423 |
+
"population_source": "Rajasthan pension department estimates"
|
| 424 |
+
},
|
| 425 |
+
"economic_assumptions": {
|
| 426 |
+
"inflation_rate": f"{scenario.inflation_rate*100:.1f}%",
|
| 427 |
+
"salary_growth": "Aligned with inflation",
|
| 428 |
+
"implementation_efficiency": "100% (no leakage assumed)"
|
| 429 |
+
},
|
| 430 |
+
"policy_assumptions": {
|
| 431 |
+
"effective_date": scenario.effective_date.strftime("%Y-%m-%d"),
|
| 432 |
+
"implementation_delay": "None assumed",
|
| 433 |
+
"administrative_costs": "Not included in projections",
|
| 434 |
+
"compliance_rate": "100% (full implementation assumed)"
|
| 435 |
+
},
|
| 436 |
+
"data_sources": [
|
| 437 |
+
"Rajasthan Finance Department budget documents",
|
| 438 |
+
"Pension disbursement historical data",
|
| 439 |
+
"National Sample Survey Office reports",
|
| 440 |
+
"Reserve Bank of India inflation projections"
|
| 441 |
+
]
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
# Usage example and helper functions
|
| 445 |
+
def create_sample_scenarios() -> List[PolicyScenario]:
|
| 446 |
+
"""Create sample policy scenarios for testing"""
|
| 447 |
+
return [
|
| 448 |
+
PolicyScenario(
|
| 449 |
+
parameter=PolicyParameter.DEARNESS_RELIEF,
|
| 450 |
+
current_value=12.0,
|
| 451 |
+
proposed_value=18.0,
|
| 452 |
+
effective_date=datetime(2025, 4, 1),
|
| 453 |
+
affected_population=510000,
|
| 454 |
+
annual_growth_rate=0.03,
|
| 455 |
+
inflation_rate=0.06
|
| 456 |
+
),
|
| 457 |
+
PolicyScenario(
|
| 458 |
+
parameter=PolicyParameter.BASIC_PENSION,
|
| 459 |
+
current_value=6000,
|
| 460 |
+
proposed_value=8000,
|
| 461 |
+
effective_date=datetime(2025, 7, 1),
|
| 462 |
+
affected_population=450000,
|
| 463 |
+
annual_growth_rate=0.025,
|
| 464 |
+
inflation_rate=0.055
|
| 465 |
+
),
|
| 466 |
+
PolicyScenario(
|
| 467 |
+
parameter=PolicyParameter.MEDICAL_ALLOWANCE,
|
| 468 |
+
current_value=1000,
|
| 469 |
+
proposed_value=2000,
|
| 470 |
+
effective_date=datetime(2025, 10, 1),
|
| 471 |
+
affected_population=510000,
|
| 472 |
+
annual_growth_rate=0.035,
|
| 473 |
+
inflation_rate=0.065
|
| 474 |
+
)
|
| 475 |
+
]
|
policy_simulator_api.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Policy Impact Simulator API endpoints
|
| 3 |
+
Provides REST API access to policy simulation functionality
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from fastapi import APIRouter, HTTPException, Query
|
| 7 |
+
from pydantic import BaseModel, Field
|
| 8 |
+
from typing import List, Optional, Dict, Any
|
| 9 |
+
from datetime import datetime, date
|
| 10 |
+
from enum import Enum
|
| 11 |
+
|
| 12 |
+
from policy_impact_simulator import (
|
| 13 |
+
PolicyImpactSimulator,
|
| 14 |
+
PolicyScenario,
|
| 15 |
+
PolicyParameter,
|
| 16 |
+
create_sample_scenarios
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
router = APIRouter(prefix="/api/policy-simulator", tags=["policy-simulator"])
|
| 20 |
+
|
| 21 |
+
# Pydantic models for API
|
| 22 |
+
class PolicyParameterEnum(str, Enum):
|
| 23 |
+
pension_factor = "pension_factor"
|
| 24 |
+
dearness_relief = "dearness_relief"
|
| 25 |
+
basic_pension = "basic_pension"
|
| 26 |
+
minimum_pension = "minimum_pension"
|
| 27 |
+
commutation_factor = "commutation_factor"
|
| 28 |
+
medical_allowance = "medical_allowance"
|
| 29 |
+
family_pension = "family_pension"
|
| 30 |
+
gratuity_limit = "gratuity_limit"
|
| 31 |
+
hra_percentage = "hra_percentage"
|
| 32 |
+
retirement_age = "retirement_age"
|
| 33 |
+
|
| 34 |
+
class PolicySimulationRequest(BaseModel):
|
| 35 |
+
parameter: PolicyParameterEnum = Field(..., description="Policy parameter to simulate")
|
| 36 |
+
current_value: float = Field(..., description="Current value of the parameter")
|
| 37 |
+
proposed_value: float = Field(..., description="Proposed new value")
|
| 38 |
+
effective_date: date = Field(..., description="When the policy change takes effect")
|
| 39 |
+
affected_population: int = Field(..., description="Number of people affected")
|
| 40 |
+
annual_growth_rate: float = Field(0.03, description="Annual population growth rate")
|
| 41 |
+
inflation_rate: float = Field(0.06, description="Expected inflation rate")
|
| 42 |
+
projection_years: int = Field(5, description="Number of years to project", ge=1, le=10)
|
| 43 |
+
include_variants: bool = Field(True, description="Include best/worst case scenarios")
|
| 44 |
+
|
| 45 |
+
class QuickSimulationRequest(BaseModel):
|
| 46 |
+
scenario_name: str = Field(..., description="Name of predefined scenario")
|
| 47 |
+
years: int = Field(5, description="Projection years", ge=1, le=10)
|
| 48 |
+
|
| 49 |
+
# Initialize simulator
|
| 50 |
+
simulator = PolicyImpactSimulator()
|
| 51 |
+
|
| 52 |
+
@router.post("/simulate")
|
| 53 |
+
async def simulate_policy_impact(request: PolicySimulationRequest):
|
| 54 |
+
"""
|
| 55 |
+
Simulate the impact of a policy change
|
| 56 |
+
|
| 57 |
+
Provides comprehensive analysis including:
|
| 58 |
+
- Baseline vs scenario projections
|
| 59 |
+
- Best/base/worst case scenarios
|
| 60 |
+
- Clause analysis and timeline
|
| 61 |
+
- Exportable evidence pack
|
| 62 |
+
"""
|
| 63 |
+
try:
|
| 64 |
+
# Convert API request to internal scenario
|
| 65 |
+
scenario = PolicyScenario(
|
| 66 |
+
parameter=PolicyParameter(request.parameter.value),
|
| 67 |
+
current_value=request.current_value,
|
| 68 |
+
proposed_value=request.proposed_value,
|
| 69 |
+
effective_date=datetime.combine(request.effective_date, datetime.min.time()),
|
| 70 |
+
affected_population=request.affected_population,
|
| 71 |
+
annual_growth_rate=request.annual_growth_rate,
|
| 72 |
+
inflation_rate=request.inflation_rate
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
# Run simulation
|
| 76 |
+
result = simulator.simulate_policy_impact(
|
| 77 |
+
scenario=scenario,
|
| 78 |
+
years=request.projection_years,
|
| 79 |
+
include_variants=request.include_variants
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
if "error" in result:
|
| 83 |
+
raise HTTPException(status_code=500, detail=result["error"])
|
| 84 |
+
|
| 85 |
+
return {
|
| 86 |
+
"success": True,
|
| 87 |
+
"simulation_result": result,
|
| 88 |
+
"api_version": "1.0",
|
| 89 |
+
"generated_at": datetime.now().isoformat()
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
except Exception as e:
|
| 93 |
+
raise HTTPException(status_code=500, detail=f"Simulation failed: {str(e)}")
|
| 94 |
+
|
| 95 |
+
@router.post("/quick-simulate")
|
| 96 |
+
async def quick_simulate(request: QuickSimulationRequest):
|
| 97 |
+
"""
|
| 98 |
+
Run a quick simulation using predefined scenarios
|
| 99 |
+
|
| 100 |
+
Available scenarios:
|
| 101 |
+
- dr_increase_6_percent: Increase DR from 12% to 18%
|
| 102 |
+
- basic_pension_boost: Increase basic pension from βΉ6000 to βΉ8000
|
| 103 |
+
- medical_allowance_double: Double medical allowance to βΉ2000
|
| 104 |
+
"""
|
| 105 |
+
try:
|
| 106 |
+
predefined_scenarios = {
|
| 107 |
+
"dr_increase_6_percent": PolicyScenario(
|
| 108 |
+
parameter=PolicyParameter.DEARNESS_RELIEF,
|
| 109 |
+
current_value=12.0,
|
| 110 |
+
proposed_value=18.0,
|
| 111 |
+
effective_date=datetime(2025, 4, 1),
|
| 112 |
+
affected_population=510000
|
| 113 |
+
),
|
| 114 |
+
"basic_pension_boost": PolicyScenario(
|
| 115 |
+
parameter=PolicyParameter.BASIC_PENSION,
|
| 116 |
+
current_value=6000,
|
| 117 |
+
proposed_value=8000,
|
| 118 |
+
effective_date=datetime(2025, 7, 1),
|
| 119 |
+
affected_population=450000
|
| 120 |
+
),
|
| 121 |
+
"medical_allowance_double": PolicyScenario(
|
| 122 |
+
parameter=PolicyParameter.MEDICAL_ALLOWANCE,
|
| 123 |
+
current_value=1000,
|
| 124 |
+
proposed_value=2000,
|
| 125 |
+
effective_date=datetime(2025, 10, 1),
|
| 126 |
+
affected_population=510000
|
| 127 |
+
)
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
if request.scenario_name not in predefined_scenarios:
|
| 131 |
+
raise HTTPException(
|
| 132 |
+
status_code=400,
|
| 133 |
+
detail=f"Unknown scenario. Available: {list(predefined_scenarios.keys())}"
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
scenario = predefined_scenarios[request.scenario_name]
|
| 137 |
+
result = simulator.simulate_policy_impact(scenario, request.years, True)
|
| 138 |
+
|
| 139 |
+
return {
|
| 140 |
+
"success": True,
|
| 141 |
+
"scenario_name": request.scenario_name,
|
| 142 |
+
"simulation_result": result
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
except Exception as e:
|
| 146 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 147 |
+
|
| 148 |
+
@router.get("/parameters")
|
| 149 |
+
async def get_policy_parameters():
|
| 150 |
+
"""Get available policy parameters with their definitions"""
|
| 151 |
+
try:
|
| 152 |
+
parameters = {}
|
| 153 |
+
for param in PolicyParameter:
|
| 154 |
+
param_def = simulator.policy_definitions.get(param, {})
|
| 155 |
+
parameters[param.value] = {
|
| 156 |
+
"name": param_def.get("name", param.value),
|
| 157 |
+
"description": param_def.get("description", ""),
|
| 158 |
+
"unit": param_def.get("unit", ""),
|
| 159 |
+
"typical_range": param_def.get("typical_range", (0, 100)),
|
| 160 |
+
"current_rajasthan": param_def.get("current_rajasthan", 0),
|
| 161 |
+
"impact_type": param_def.get("impact_type", "benefit")
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
return {
|
| 165 |
+
"success": True,
|
| 166 |
+
"parameters": parameters,
|
| 167 |
+
"total_count": len(parameters)
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
except Exception as e:
|
| 171 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 172 |
+
|
| 173 |
+
@router.get("/scenarios/samples")
|
| 174 |
+
async def get_sample_scenarios():
|
| 175 |
+
"""Get sample scenarios for testing and demonstration"""
|
| 176 |
+
try:
|
| 177 |
+
samples = create_sample_scenarios()
|
| 178 |
+
|
| 179 |
+
scenarios_data = []
|
| 180 |
+
for i, scenario in enumerate(samples):
|
| 181 |
+
scenarios_data.append({
|
| 182 |
+
"id": f"sample_{i+1}",
|
| 183 |
+
"parameter": scenario.parameter.value,
|
| 184 |
+
"parameter_name": simulator.policy_definitions[scenario.parameter]["name"],
|
| 185 |
+
"current_value": scenario.current_value,
|
| 186 |
+
"proposed_value": scenario.proposed_value,
|
| 187 |
+
"effective_date": scenario.effective_date.date().isoformat(),
|
| 188 |
+
"affected_population": scenario.affected_population,
|
| 189 |
+
"description": f"Sample scenario: {simulator.policy_definitions[scenario.parameter]['name']} change"
|
| 190 |
+
})
|
| 191 |
+
|
| 192 |
+
return {
|
| 193 |
+
"success": True,
|
| 194 |
+
"sample_scenarios": scenarios_data,
|
| 195 |
+
"usage": "Use these scenarios with /quick-simulate endpoint"
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
except Exception as e:
|
| 199 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 200 |
+
|
| 201 |
+
@router.get("/analysis/{simulation_id}")
|
| 202 |
+
async def get_detailed_analysis(simulation_id: str):
|
| 203 |
+
"""Get detailed analysis for a completed simulation"""
|
| 204 |
+
# This would typically fetch from a database
|
| 205 |
+
# For now, return a placeholder response
|
| 206 |
+
return {
|
| 207 |
+
"success": True,
|
| 208 |
+
"message": "Detailed analysis endpoint - would fetch stored simulation results",
|
| 209 |
+
"simulation_id": simulation_id,
|
| 210 |
+
"note": "In production, this would retrieve complete analysis from database"
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
@router.post("/compare")
|
| 214 |
+
async def compare_scenarios(scenarios: List[PolicySimulationRequest]):
|
| 215 |
+
"""Compare multiple policy scenarios side by side"""
|
| 216 |
+
try:
|
| 217 |
+
if len(scenarios) > 5:
|
| 218 |
+
raise HTTPException(status_code=400, detail="Maximum 5 scenarios can be compared")
|
| 219 |
+
|
| 220 |
+
comparison_results = []
|
| 221 |
+
|
| 222 |
+
for i, request in enumerate(scenarios):
|
| 223 |
+
scenario = PolicyScenario(
|
| 224 |
+
parameter=PolicyParameter(request.parameter.value),
|
| 225 |
+
current_value=request.current_value,
|
| 226 |
+
proposed_value=request.proposed_value,
|
| 227 |
+
effective_date=datetime.combine(request.effective_date, datetime.min.time()),
|
| 228 |
+
affected_population=request.affected_population,
|
| 229 |
+
annual_growth_rate=request.annual_growth_rate,
|
| 230 |
+
inflation_rate=request.inflation_rate
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
result = simulator.simulate_policy_impact(scenario, request.projection_years, False)
|
| 234 |
+
comparison_results.append({
|
| 235 |
+
"scenario_index": i + 1,
|
| 236 |
+
"parameter": request.parameter.value,
|
| 237 |
+
"impact_summary": result.get("total_impact", {}),
|
| 238 |
+
"first_year_impact": result["scenario_projections"][0].impact if result.get("scenario_projections") else 0
|
| 239 |
+
})
|
| 240 |
+
|
| 241 |
+
# Create comparison summary
|
| 242 |
+
total_impacts = [r.get("impact_summary", {}).get("total_additional_cost_crores", 0) for r in comparison_results]
|
| 243 |
+
|
| 244 |
+
return {
|
| 245 |
+
"success": True,
|
| 246 |
+
"comparison_count": len(scenarios),
|
| 247 |
+
"scenarios": comparison_results,
|
| 248 |
+
"summary": {
|
| 249 |
+
"highest_impact": max(total_impacts) if total_impacts else 0,
|
| 250 |
+
"lowest_impact": min(total_impacts) if total_impacts else 0,
|
| 251 |
+
"total_combined_impact": sum(total_impacts),
|
| 252 |
+
"recommendation": "Detailed comparison analysis completed"
|
| 253 |
+
}
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
except Exception as e:
|
| 257 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 258 |
+
|
| 259 |
+
@router.get("/export/{simulation_id}")
|
| 260 |
+
async def export_simulation_data(
|
| 261 |
+
simulation_id: str,
|
| 262 |
+
format: str = Query("json", regex="^(json|csv|pdf)$")
|
| 263 |
+
):
|
| 264 |
+
"""Export simulation results in various formats"""
|
| 265 |
+
try:
|
| 266 |
+
# This would typically fetch the simulation from database
|
| 267 |
+
# For now, return export information
|
| 268 |
+
|
| 269 |
+
export_info = {
|
| 270 |
+
"json": "Complete simulation data in JSON format",
|
| 271 |
+
"csv": "Yearly projections and impact data in CSV format",
|
| 272 |
+
"pdf": "Formatted policy impact report in PDF format"
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
return {
|
| 276 |
+
"success": True,
|
| 277 |
+
"simulation_id": simulation_id,
|
| 278 |
+
"export_format": format,
|
| 279 |
+
"description": export_info.get(format, "Unknown format"),
|
| 280 |
+
"download_url": f"/api/policy-simulator/download/{simulation_id}/{format}",
|
| 281 |
+
"note": "In production, would generate actual downloadable files"
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
except Exception as e:
|
| 285 |
+
raise HTTPException(status_code=500, detail=str(e))
|
scenario_analysis_service.py
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import matplotlib.pyplot as plt
|
| 2 |
+
import seaborn as sns
|
| 3 |
+
import plotly.graph_objects as go
|
| 4 |
+
import plotly.express as px
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import numpy as np
|
| 7 |
+
import networkx as nx
|
| 8 |
+
from datetime import datetime, timedelta
|
| 9 |
+
import base64
|
| 10 |
+
import io
|
| 11 |
+
import json
|
| 12 |
+
import logging
|
| 13 |
+
from typing import Dict, List, Optional, Tuple, Any
|
| 14 |
+
import asyncio
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger("voicebot")
|
| 17 |
+
|
| 18 |
+
class ScenarioAnalysisService:
|
| 19 |
+
def __init__(self):
|
| 20 |
+
"""Initialize the scenario analysis service"""
|
| 21 |
+
# Set matplotlib to use non-interactive backend
|
| 22 |
+
plt.switch_backend('Agg')
|
| 23 |
+
# Set style for better looking plots
|
| 24 |
+
sns.set_style("whitegrid")
|
| 25 |
+
plt.style.use('seaborn-v0_8')
|
| 26 |
+
|
| 27 |
+
async def analyze_government_scenario(self, scenario_data: Dict[str, Any]) -> Dict[str, Any]:
|
| 28 |
+
"""
|
| 29 |
+
Analyze government scenarios and create appropriate visualizations
|
| 30 |
+
"""
|
| 31 |
+
try:
|
| 32 |
+
scenario_type = scenario_data.get("type", "").lower()
|
| 33 |
+
data = scenario_data.get("data", {})
|
| 34 |
+
title = scenario_data.get("title", "Government Scenario Analysis")
|
| 35 |
+
|
| 36 |
+
logger.info(f"π Analyzing scenario: {scenario_type}")
|
| 37 |
+
|
| 38 |
+
# Route to appropriate analysis method based on scenario type
|
| 39 |
+
if scenario_type in ["budget", "financial", "expenditure"]:
|
| 40 |
+
return await self._analyze_budget_scenario(data, title)
|
| 41 |
+
elif scenario_type in ["policy", "implementation", "timeline"]:
|
| 42 |
+
return await self._analyze_policy_scenario(data, title)
|
| 43 |
+
elif scenario_type in ["organization", "hierarchy", "structure"]:
|
| 44 |
+
return await self._analyze_organizational_scenario(data, title)
|
| 45 |
+
elif scenario_type in ["performance", "metrics", "kpi"]:
|
| 46 |
+
return await self._analyze_performance_scenario(data, title)
|
| 47 |
+
elif scenario_type in ["workflow", "process", "flow"]:
|
| 48 |
+
return await self._analyze_workflow_scenario(data, title)
|
| 49 |
+
else:
|
| 50 |
+
return await self._analyze_general_scenario(data, title, scenario_type)
|
| 51 |
+
|
| 52 |
+
except Exception as e:
|
| 53 |
+
logger.error(f"β Error in scenario analysis: {str(e)}")
|
| 54 |
+
return {
|
| 55 |
+
"success": False,
|
| 56 |
+
"error": str(e),
|
| 57 |
+
"analysis": "Failed to analyze scenario"
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
async def _analyze_budget_scenario(self, data: Dict, title: str) -> Dict[str, Any]:
|
| 61 |
+
"""Analyze budget and financial scenarios"""
|
| 62 |
+
try:
|
| 63 |
+
# Create sample data if not provided
|
| 64 |
+
if not data:
|
| 65 |
+
departments = ['Health', 'Education', 'Infrastructure', 'Defense', 'Social Welfare']
|
| 66 |
+
budgets = [2500, 3000, 4000, 5000, 1500]
|
| 67 |
+
data = {"departments": departments, "budgets": budgets}
|
| 68 |
+
|
| 69 |
+
# Create multiple visualizations
|
| 70 |
+
images = []
|
| 71 |
+
analysis_text = []
|
| 72 |
+
|
| 73 |
+
# 1. Pie Chart for Budget Distribution
|
| 74 |
+
fig, ax = plt.subplots(figsize=(10, 8))
|
| 75 |
+
colors = plt.cm.Set3(np.linspace(0, 1, len(data["departments"])))
|
| 76 |
+
wedges, texts, autotexts = ax.pie(
|
| 77 |
+
data["budgets"],
|
| 78 |
+
labels=data["departments"],
|
| 79 |
+
autopct='%1.1f%%',
|
| 80 |
+
colors=colors,
|
| 81 |
+
startangle=90
|
| 82 |
+
)
|
| 83 |
+
ax.set_title(f"{title} - Budget Distribution", fontsize=16, fontweight='bold')
|
| 84 |
+
|
| 85 |
+
# Convert to base64
|
| 86 |
+
buffer = io.BytesIO()
|
| 87 |
+
plt.savefig(buffer, format='png', bbox_inches='tight', dpi=300)
|
| 88 |
+
buffer.seek(0)
|
| 89 |
+
pie_image = base64.b64encode(buffer.getvalue()).decode()
|
| 90 |
+
images.append({"type": "pie_chart", "data": pie_image})
|
| 91 |
+
plt.close()
|
| 92 |
+
|
| 93 |
+
# 2. Bar Chart for Budget Comparison
|
| 94 |
+
fig, ax = plt.subplots(figsize=(12, 8))
|
| 95 |
+
bars = ax.bar(data["departments"], data["budgets"], color=colors)
|
| 96 |
+
ax.set_title(f"{title} - Department-wise Budget Allocation", fontsize=16, fontweight='bold')
|
| 97 |
+
ax.set_xlabel("Departments", fontsize=12)
|
| 98 |
+
ax.set_ylabel("Budget (in Crores)", fontsize=12)
|
| 99 |
+
|
| 100 |
+
# Add value labels on bars
|
| 101 |
+
for bar in bars:
|
| 102 |
+
height = bar.get_height()
|
| 103 |
+
ax.annotate(f'βΉ{height}Cr',
|
| 104 |
+
xy=(bar.get_x() + bar.get_width() / 2, height),
|
| 105 |
+
xytext=(0, 3),
|
| 106 |
+
textcoords="offset points",
|
| 107 |
+
ha='center', va='bottom')
|
| 108 |
+
|
| 109 |
+
plt.xticks(rotation=45)
|
| 110 |
+
buffer = io.BytesIO()
|
| 111 |
+
plt.savefig(buffer, format='png', bbox_inches='tight', dpi=300)
|
| 112 |
+
buffer.seek(0)
|
| 113 |
+
bar_image = base64.b64encode(buffer.getvalue()).decode()
|
| 114 |
+
images.append({"type": "bar_chart", "data": bar_image})
|
| 115 |
+
plt.close()
|
| 116 |
+
|
| 117 |
+
# Generate analysis
|
| 118 |
+
total_budget = sum(data["budgets"])
|
| 119 |
+
max_dept = data["departments"][data["budgets"].index(max(data["budgets"]))]
|
| 120 |
+
min_dept = data["departments"][data["budgets"].index(min(data["budgets"]))]
|
| 121 |
+
|
| 122 |
+
analysis_text = [
|
| 123 |
+
f"π **Budget Analysis Summary:**",
|
| 124 |
+
f"β’ Total Budget: βΉ{total_budget} Crores",
|
| 125 |
+
f"β’ Highest Allocation: {max_dept} (βΉ{max(data['budgets'])} Cr)",
|
| 126 |
+
f"β’ Lowest Allocation: {min_dept} (βΉ{min(data['budgets'])} Cr)",
|
| 127 |
+
f"β’ Average Allocation: βΉ{total_budget/len(data['budgets']):.1f} Crores",
|
| 128 |
+
"",
|
| 129 |
+
f"π‘ **Key Insights:**",
|
| 130 |
+
f"β’ {max_dept} receives {max(data['budgets'])/total_budget*100:.1f}% of total budget",
|
| 131 |
+
f"β’ Budget distribution shows focus on {max_dept} and infrastructure development",
|
| 132 |
+
f"β’ Consider rebalancing if {min_dept} requires more funding"
|
| 133 |
+
]
|
| 134 |
+
|
| 135 |
+
return {
|
| 136 |
+
"success": True,
|
| 137 |
+
"analysis": "\n".join(analysis_text),
|
| 138 |
+
"images": images,
|
| 139 |
+
"scenario_type": "budget",
|
| 140 |
+
"total_budget": total_budget
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
except Exception as e:
|
| 144 |
+
logger.error(f"β Error in budget analysis: {str(e)}")
|
| 145 |
+
raise e
|
| 146 |
+
|
| 147 |
+
async def _analyze_policy_scenario(self, data: Dict, title: str) -> Dict[str, Any]:
|
| 148 |
+
"""Analyze policy implementation scenarios"""
|
| 149 |
+
try:
|
| 150 |
+
# Create timeline visualization
|
| 151 |
+
if not data:
|
| 152 |
+
phases = ['Planning', 'Approval', 'Implementation', 'Monitoring', 'Evaluation']
|
| 153 |
+
durations = [30, 15, 90, 60, 30] # days
|
| 154 |
+
data = {"phases": phases, "durations": durations}
|
| 155 |
+
|
| 156 |
+
# Create Gantt chart-like visualization
|
| 157 |
+
fig, ax = plt.subplots(figsize=(14, 8))
|
| 158 |
+
|
| 159 |
+
# Calculate start dates
|
| 160 |
+
start_date = datetime.now()
|
| 161 |
+
start_dates = []
|
| 162 |
+
current_date = start_date
|
| 163 |
+
|
| 164 |
+
for duration in data["durations"]:
|
| 165 |
+
start_dates.append(current_date)
|
| 166 |
+
current_date += timedelta(days=duration)
|
| 167 |
+
|
| 168 |
+
# Create horizontal bar chart
|
| 169 |
+
colors = plt.cm.viridis(np.linspace(0, 1, len(data["phases"])))
|
| 170 |
+
for i, (phase, duration, start, color) in enumerate(zip(data["phases"], data["durations"], start_dates, colors)):
|
| 171 |
+
ax.barh(i, duration, left=(start - start_date).days, color=color, alpha=0.7)
|
| 172 |
+
ax.text((start - start_date).days + duration/2, i, f'{phase}\n({duration} days)',
|
| 173 |
+
ha='center', va='center', fontweight='bold')
|
| 174 |
+
|
| 175 |
+
ax.set_yticks(range(len(data["phases"])))
|
| 176 |
+
ax.set_yticklabels(data["phases"])
|
| 177 |
+
ax.set_xlabel("Timeline (Days from Start)")
|
| 178 |
+
ax.set_title(f"{title} - Policy Implementation Timeline", fontsize=16, fontweight='bold')
|
| 179 |
+
ax.grid(axis='x', alpha=0.3)
|
| 180 |
+
|
| 181 |
+
buffer = io.BytesIO()
|
| 182 |
+
plt.savefig(buffer, format='png', bbox_inches='tight', dpi=300)
|
| 183 |
+
buffer.seek(0)
|
| 184 |
+
timeline_image = base64.b64encode(buffer.getvalue()).decode()
|
| 185 |
+
plt.close()
|
| 186 |
+
|
| 187 |
+
# Create network diagram for stakeholder relationships
|
| 188 |
+
fig, ax = plt.subplots(figsize=(12, 10))
|
| 189 |
+
G = nx.Graph()
|
| 190 |
+
|
| 191 |
+
# Add nodes (stakeholders)
|
| 192 |
+
stakeholders = ['Ministry', 'State Govt', 'Local Bodies', 'Citizens', 'NGOs', 'Private Sector']
|
| 193 |
+
G.add_nodes_from(stakeholders)
|
| 194 |
+
|
| 195 |
+
# Add edges (relationships)
|
| 196 |
+
relationships = [
|
| 197 |
+
('Ministry', 'State Govt'), ('State Govt', 'Local Bodies'),
|
| 198 |
+
('Local Bodies', 'Citizens'), ('Ministry', 'NGOs'),
|
| 199 |
+
('Private Sector', 'Ministry'), ('NGOs', 'Citizens')
|
| 200 |
+
]
|
| 201 |
+
G.add_edges_from(relationships)
|
| 202 |
+
|
| 203 |
+
# Draw network
|
| 204 |
+
pos = nx.spring_layout(G, k=2, iterations=50)
|
| 205 |
+
nx.draw(G, pos, ax=ax, with_labels=True, node_color='lightblue',
|
| 206 |
+
node_size=3000, font_size=10, font_weight='bold',
|
| 207 |
+
edge_color='gray', width=2)
|
| 208 |
+
|
| 209 |
+
ax.set_title(f"{title} - Stakeholder Network", fontsize=16, fontweight='bold')
|
| 210 |
+
ax.axis('off')
|
| 211 |
+
|
| 212 |
+
buffer = io.BytesIO()
|
| 213 |
+
plt.savefig(buffer, format='png', bbox_inches='tight', dpi=300)
|
| 214 |
+
buffer.seek(0)
|
| 215 |
+
network_image = base64.b64encode(buffer.getvalue()).decode()
|
| 216 |
+
plt.close()
|
| 217 |
+
|
| 218 |
+
images = [
|
| 219 |
+
{"type": "timeline", "data": timeline_image},
|
| 220 |
+
{"type": "network", "data": network_image}
|
| 221 |
+
]
|
| 222 |
+
|
| 223 |
+
total_duration = sum(data["durations"])
|
| 224 |
+
critical_phase = data["phases"][data["durations"].index(max(data["durations"]))]
|
| 225 |
+
|
| 226 |
+
analysis_text = [
|
| 227 |
+
f"π **Policy Implementation Analysis:**",
|
| 228 |
+
f"β’ Total Implementation Time: {total_duration} days",
|
| 229 |
+
f"β’ Critical Phase: {critical_phase} ({max(data['durations'])} days)",
|
| 230 |
+
f"β’ Number of Phases: {len(data['phases'])}",
|
| 231 |
+
"",
|
| 232 |
+
f"π **Stakeholder Network:**",
|
| 233 |
+
f"β’ {len(stakeholders)} key stakeholders identified",
|
| 234 |
+
f"β’ {len(relationships)} critical relationships mapped",
|
| 235 |
+
"",
|
| 236 |
+
f"β οΈ **Risk Factors:**",
|
| 237 |
+
f"β’ {critical_phase} phase requires most attention",
|
| 238 |
+
f"β’ Coordination between stakeholders is crucial",
|
| 239 |
+
f"β’ Monitor progress at each phase transition"
|
| 240 |
+
]
|
| 241 |
+
|
| 242 |
+
return {
|
| 243 |
+
"success": True,
|
| 244 |
+
"analysis": "\n".join(analysis_text),
|
| 245 |
+
"images": images,
|
| 246 |
+
"scenario_type": "policy",
|
| 247 |
+
"total_duration": total_duration
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
except Exception as e:
|
| 251 |
+
logger.error(f"β Error in policy analysis: {str(e)}")
|
| 252 |
+
raise e
|
| 253 |
+
|
| 254 |
+
async def _analyze_organizational_scenario(self, data: Dict, title: str) -> Dict[str, Any]:
|
| 255 |
+
"""Analyze organizational structure scenarios"""
|
| 256 |
+
try:
|
| 257 |
+
# Create organizational hierarchy chart
|
| 258 |
+
fig, ax = plt.subplots(figsize=(14, 10))
|
| 259 |
+
|
| 260 |
+
# Create hierarchical layout
|
| 261 |
+
G = nx.DiGraph()
|
| 262 |
+
|
| 263 |
+
# Sample organizational structure
|
| 264 |
+
if not data:
|
| 265 |
+
hierarchy = {
|
| 266 |
+
'Secretary': ['Joint Secretary 1', 'Joint Secretary 2'],
|
| 267 |
+
'Joint Secretary 1': ['Director 1', 'Director 2'],
|
| 268 |
+
'Joint Secretary 2': ['Director 3', 'Director 4'],
|
| 269 |
+
'Director 1': ['Deputy Director 1', 'Deputy Director 2'],
|
| 270 |
+
'Director 2': ['Deputy Director 3'],
|
| 271 |
+
'Director 3': ['Deputy Director 4', 'Deputy Director 5'],
|
| 272 |
+
'Director 4': ['Deputy Director 6']
|
| 273 |
+
}
|
| 274 |
+
data = {"hierarchy": hierarchy}
|
| 275 |
+
|
| 276 |
+
# Build graph
|
| 277 |
+
for parent, children in data["hierarchy"].items():
|
| 278 |
+
for child in children:
|
| 279 |
+
G.add_edge(parent, child)
|
| 280 |
+
|
| 281 |
+
# Create hierarchical layout
|
| 282 |
+
pos = nx.nx_agraph.graphviz_layout(G, prog='dot') if hasattr(nx, 'nx_agraph') else nx.spring_layout(G)
|
| 283 |
+
|
| 284 |
+
# Draw organizational chart
|
| 285 |
+
nx.draw(G, pos, ax=ax, with_labels=True, node_color='lightcoral',
|
| 286 |
+
node_size=4000, font_size=8, font_weight='bold',
|
| 287 |
+
edge_color='darkgray', arrows=True, arrowsize=20,
|
| 288 |
+
bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8))
|
| 289 |
+
|
| 290 |
+
ax.set_title(f"{title} - Organizational Structure", fontsize=16, fontweight='bold')
|
| 291 |
+
ax.axis('off')
|
| 292 |
+
|
| 293 |
+
buffer = io.BytesIO()
|
| 294 |
+
plt.savefig(buffer, format='png', bbox_inches='tight', dpi=300)
|
| 295 |
+
buffer.seek(0)
|
| 296 |
+
org_image = base64.b64encode(buffer.getvalue()).decode()
|
| 297 |
+
plt.close()
|
| 298 |
+
|
| 299 |
+
images = [{"type": "organization_chart", "data": org_image}]
|
| 300 |
+
|
| 301 |
+
# Calculate organizational metrics
|
| 302 |
+
total_positions = len(G.nodes())
|
| 303 |
+
levels = len(set(nx.shortest_path_length(G, 'Secretary').values())) if 'Secretary' in G.nodes() else 0
|
| 304 |
+
span_of_control = sum(len(children) for children in data["hierarchy"].values()) / len(data["hierarchy"])
|
| 305 |
+
|
| 306 |
+
analysis_text = [
|
| 307 |
+
f"π’ **Organizational Analysis:**",
|
| 308 |
+
f"β’ Total Positions: {total_positions}",
|
| 309 |
+
f"β’ Organizational Levels: {levels}",
|
| 310 |
+
f"β’ Average Span of Control: {span_of_control:.1f}",
|
| 311 |
+
"",
|
| 312 |
+
f"π **Structure Insights:**",
|
| 313 |
+
f"β’ Hierarchical structure with clear reporting lines",
|
| 314 |
+
f"β’ {len(data['hierarchy'])} management positions",
|
| 315 |
+
f"β’ Balanced distribution of responsibilities",
|
| 316 |
+
"",
|
| 317 |
+
f"π‘ **Recommendations:**",
|
| 318 |
+
f"β’ Consider flattening structure if span > 7",
|
| 319 |
+
f"β’ Ensure clear role definitions at each level",
|
| 320 |
+
f"β’ Regular review of reporting relationships"
|
| 321 |
+
]
|
| 322 |
+
|
| 323 |
+
return {
|
| 324 |
+
"success": True,
|
| 325 |
+
"analysis": "\n".join(analysis_text),
|
| 326 |
+
"images": images,
|
| 327 |
+
"scenario_type": "organization",
|
| 328 |
+
"total_positions": total_positions
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
except Exception as e:
|
| 332 |
+
logger.error(f"β Error in organizational analysis: {str(e)}")
|
| 333 |
+
raise e
|
| 334 |
+
|
| 335 |
+
async def _analyze_performance_scenario(self, data: Dict, title: str) -> Dict[str, Any]:
|
| 336 |
+
"""Analyze performance metrics scenarios"""
|
| 337 |
+
try:
|
| 338 |
+
# Create performance dashboard
|
| 339 |
+
if not data:
|
| 340 |
+
metrics = ['Efficiency', 'Quality', 'Timeliness', 'Cost', 'Satisfaction']
|
| 341 |
+
current = [75, 82, 68, 85, 78]
|
| 342 |
+
target = [85, 90, 80, 80, 85]
|
| 343 |
+
data = {"metrics": metrics, "current": current, "target": target}
|
| 344 |
+
|
| 345 |
+
# Create multi-subplot figure
|
| 346 |
+
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
|
| 347 |
+
|
| 348 |
+
# 1. Performance vs Target comparison
|
| 349 |
+
x = np.arange(len(data["metrics"]))
|
| 350 |
+
width = 0.35
|
| 351 |
+
|
| 352 |
+
ax1.bar(x - width/2, data["current"], width, label='Current', color='skyblue', alpha=0.8)
|
| 353 |
+
ax1.bar(x + width/2, data["target"], width, label='Target', color='orange', alpha=0.8)
|
| 354 |
+
ax1.set_xlabel('Metrics')
|
| 355 |
+
ax1.set_ylabel('Score (%)')
|
| 356 |
+
ax1.set_title('Performance vs Target')
|
| 357 |
+
ax1.set_xticks(x)
|
| 358 |
+
ax1.set_xticklabels(data["metrics"], rotation=45)
|
| 359 |
+
ax1.legend()
|
| 360 |
+
ax1.grid(axis='y', alpha=0.3)
|
| 361 |
+
|
| 362 |
+
# 2. Radar chart
|
| 363 |
+
angles = np.linspace(0, 2 * np.pi, len(data["metrics"]), endpoint=False)
|
| 364 |
+
angles = np.concatenate((angles, [angles[0]]))
|
| 365 |
+
|
| 366 |
+
current_scores = data["current"] + [data["current"][0]]
|
| 367 |
+
target_scores = data["target"] + [data["target"][0]]
|
| 368 |
+
|
| 369 |
+
ax2 = plt.subplot(2, 2, 2, projection='polar')
|
| 370 |
+
ax2.plot(angles, current_scores, 'o-', linewidth=2, label='Current', color='blue')
|
| 371 |
+
ax2.fill(angles, current_scores, alpha=0.25, color='blue')
|
| 372 |
+
ax2.plot(angles, target_scores, 'o-', linewidth=2, label='Target', color='red')
|
| 373 |
+
ax2.fill(angles, target_scores, alpha=0.25, color='red')
|
| 374 |
+
ax2.set_xticks(angles[:-1])
|
| 375 |
+
ax2.set_xticklabels(data["metrics"])
|
| 376 |
+
ax2.set_title('Performance Radar')
|
| 377 |
+
ax2.legend()
|
| 378 |
+
|
| 379 |
+
# 3. Gap analysis
|
| 380 |
+
gaps = [target - current for current, target in zip(data["current"], data["target"])]
|
| 381 |
+
colors = ['red' if gap > 0 else 'green' for gap in gaps]
|
| 382 |
+
|
| 383 |
+
ax3.bar(data["metrics"], gaps, color=colors, alpha=0.7)
|
| 384 |
+
ax3.set_xlabel('Metrics')
|
| 385 |
+
ax3.set_ylabel('Gap (Target - Current)')
|
| 386 |
+
ax3.set_title('Performance Gap Analysis')
|
| 387 |
+
ax3.axhline(y=0, color='black', linestyle='-', alpha=0.3)
|
| 388 |
+
plt.setp(ax3.xaxis.get_majorticklabels(), rotation=45)
|
| 389 |
+
|
| 390 |
+
# 4. Performance trend (simulated)
|
| 391 |
+
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
|
| 392 |
+
overall_trend = [70, 72, 75, 76, 78, np.mean(data["current"])]
|
| 393 |
+
|
| 394 |
+
ax4.plot(months, overall_trend, marker='o', linewidth=3, markersize=8, color='green')
|
| 395 |
+
ax4.set_xlabel('Month')
|
| 396 |
+
ax4.set_ylabel('Overall Performance (%)')
|
| 397 |
+
ax4.set_title('Performance Trend')
|
| 398 |
+
ax4.grid(True, alpha=0.3)
|
| 399 |
+
|
| 400 |
+
plt.tight_layout()
|
| 401 |
+
|
| 402 |
+
buffer = io.BytesIO()
|
| 403 |
+
plt.savefig(buffer, format='png', bbox_inches='tight', dpi=300)
|
| 404 |
+
buffer.seek(0)
|
| 405 |
+
performance_image = base64.b64encode(buffer.getvalue()).decode()
|
| 406 |
+
plt.close()
|
| 407 |
+
|
| 408 |
+
images = [{"type": "performance_dashboard", "data": performance_image}]
|
| 409 |
+
|
| 410 |
+
# Calculate performance metrics
|
| 411 |
+
avg_current = np.mean(data["current"])
|
| 412 |
+
avg_target = np.mean(data["target"])
|
| 413 |
+
overall_gap = avg_target - avg_current
|
| 414 |
+
critical_areas = [metric for metric, gap in zip(data["metrics"], gaps) if gap > 10]
|
| 415 |
+
|
| 416 |
+
analysis_text = [
|
| 417 |
+
f"π **Performance Analysis:**",
|
| 418 |
+
f"β’ Current Average Performance: {avg_current:.1f}%",
|
| 419 |
+
f"β’ Target Average Performance: {avg_target:.1f}%",
|
| 420 |
+
f"β’ Overall Performance Gap: {overall_gap:.1f}%",
|
| 421 |
+
"",
|
| 422 |
+
f"π **Key Findings:**",
|
| 423 |
+
f"β’ Best Performing Area: {data['metrics'][data['current'].index(max(data['current']))]}",
|
| 424 |
+
f"β’ Areas Needing Improvement: {', '.join(critical_areas) if critical_areas else 'None critical'}",
|
| 425 |
+
f"β’ Performance is {'on track' if overall_gap < 5 else 'needs attention'}",
|
| 426 |
+
"",
|
| 427 |
+
f"π― **Action Items:**",
|
| 428 |
+
f"β’ Focus on areas with gaps > 10%",
|
| 429 |
+
f"β’ Maintain strong performance in current best areas",
|
| 430 |
+
f"β’ Set monthly improvement targets"
|
| 431 |
+
]
|
| 432 |
+
|
| 433 |
+
return {
|
| 434 |
+
"success": True,
|
| 435 |
+
"analysis": "\n".join(analysis_text),
|
| 436 |
+
"images": images,
|
| 437 |
+
"scenario_type": "performance",
|
| 438 |
+
"overall_gap": overall_gap
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
except Exception as e:
|
| 442 |
+
logger.error(f"β Error in performance analysis: {str(e)}")
|
| 443 |
+
raise e
|
| 444 |
+
|
| 445 |
+
async def _analyze_workflow_scenario(self, data: Dict, title: str) -> Dict[str, Any]:
|
| 446 |
+
"""Analyze workflow and process scenarios"""
|
| 447 |
+
try:
|
| 448 |
+
# Create workflow diagram
|
| 449 |
+
fig, ax = plt.subplots(figsize=(16, 10))
|
| 450 |
+
|
| 451 |
+
# Sample workflow if no data provided
|
| 452 |
+
if not data:
|
| 453 |
+
steps = ['Application', 'Verification', 'Approval', 'Processing', 'Dispatch']
|
| 454 |
+
connections = [('Application', 'Verification'), ('Verification', 'Approval'),
|
| 455 |
+
('Approval', 'Processing'), ('Processing', 'Dispatch')]
|
| 456 |
+
times = [2, 5, 3, 7, 1] # days
|
| 457 |
+
data = {"steps": steps, "connections": connections, "times": times}
|
| 458 |
+
|
| 459 |
+
# Create workflow graph
|
| 460 |
+
G = nx.DiGraph()
|
| 461 |
+
for i, step in enumerate(data["steps"]):
|
| 462 |
+
G.add_node(step, time=data["times"][i])
|
| 463 |
+
|
| 464 |
+
for connection in data["connections"]:
|
| 465 |
+
G.add_edge(connection[0], connection[1])
|
| 466 |
+
|
| 467 |
+
# Layout for workflow
|
| 468 |
+
pos = nx.spring_layout(G, k=3, iterations=50)
|
| 469 |
+
|
| 470 |
+
# Draw workflow
|
| 471 |
+
node_colors = plt.cm.RdYlGn_r(np.array(data["times"])/max(data["times"]))
|
| 472 |
+
nx.draw(G, pos, ax=ax, with_labels=True, node_color=node_colors,
|
| 473 |
+
node_size=4000, font_size=10, font_weight='bold',
|
| 474 |
+
edge_color='darkblue', arrows=True, arrowsize=20,
|
| 475 |
+
bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.9))
|
| 476 |
+
|
| 477 |
+
# Add time labels
|
| 478 |
+
for i, (node, (x, y)) in enumerate(pos.items()):
|
| 479 |
+
ax.text(x, y-0.15, f'{data["times"][i]} days',
|
| 480 |
+
ha='center', va='center', fontsize=8,
|
| 481 |
+
bbox=dict(boxstyle="round,pad=0.2", facecolor="yellow", alpha=0.7))
|
| 482 |
+
|
| 483 |
+
ax.set_title(f"{title} - Workflow Process", fontsize=16, fontweight='bold')
|
| 484 |
+
ax.axis('off')
|
| 485 |
+
|
| 486 |
+
# Add colorbar for time scale
|
| 487 |
+
sm = plt.cm.ScalarMappable(cmap=plt.cm.RdYlGn_r,
|
| 488 |
+
norm=plt.Normalize(vmin=min(data["times"]), vmax=max(data["times"])))
|
| 489 |
+
sm.set_array([])
|
| 490 |
+
cbar = plt.colorbar(sm, ax=ax, shrink=0.8)
|
| 491 |
+
cbar.set_label('Processing Time (days)', rotation=270, labelpad=20)
|
| 492 |
+
|
| 493 |
+
buffer = io.BytesIO()
|
| 494 |
+
plt.savefig(buffer, format='png', bbox_inches='tight', dpi=300)
|
| 495 |
+
buffer.seek(0)
|
| 496 |
+
workflow_image = base64.b64encode(buffer.getvalue()).decode()
|
| 497 |
+
plt.close()
|
| 498 |
+
|
| 499 |
+
images = [{"type": "workflow_diagram", "data": workflow_image}]
|
| 500 |
+
|
| 501 |
+
# Calculate workflow metrics
|
| 502 |
+
total_time = sum(data["times"])
|
| 503 |
+
bottleneck = data["steps"][data["times"].index(max(data["times"]))]
|
| 504 |
+
fastest_step = data["steps"][data["times"].index(min(data["times"]))]
|
| 505 |
+
|
| 506 |
+
analysis_text = [
|
| 507 |
+
f"β‘ **Workflow Analysis:**",
|
| 508 |
+
f"β’ Total Process Time: {total_time} days",
|
| 509 |
+
f"β’ Number of Steps: {len(data['steps'])}",
|
| 510 |
+
f"β’ Average Step Time: {total_time/len(data['steps']):.1f} days",
|
| 511 |
+
"",
|
| 512 |
+
f"π¦ **Process Insights:**",
|
| 513 |
+
f"β’ Bottleneck: {bottleneck} ({max(data['times'])} days)",
|
| 514 |
+
f"β’ Fastest Step: {fastest_step} ({min(data['times'])} days)",
|
| 515 |
+
f"β’ Process Efficiency: {'Good' if total_time < 20 else 'Needs Improvement'}",
|
| 516 |
+
"",
|
| 517 |
+
f"π§ **Optimization Opportunities:**",
|
| 518 |
+
f"β’ Focus on reducing {bottleneck} processing time",
|
| 519 |
+
f"β’ Consider parallel processing where possible",
|
| 520 |
+
f"β’ Implement automation for routine steps"
|
| 521 |
+
]
|
| 522 |
+
|
| 523 |
+
return {
|
| 524 |
+
"success": True,
|
| 525 |
+
"analysis": "\n".join(analysis_text),
|
| 526 |
+
"images": images,
|
| 527 |
+
"scenario_type": "workflow",
|
| 528 |
+
"total_time": total_time
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
except Exception as e:
|
| 532 |
+
logger.error(f"β Error in workflow analysis: {str(e)}")
|
| 533 |
+
raise e
|
| 534 |
+
|
| 535 |
+
async def _analyze_general_scenario(self, data: Dict, title: str, scenario_type: str) -> Dict[str, Any]:
|
| 536 |
+
"""Analyze general scenarios with basic visualizations"""
|
| 537 |
+
try:
|
| 538 |
+
# Create simple visualization
|
| 539 |
+
fig, ax = plt.subplots(figsize=(12, 8))
|
| 540 |
+
|
| 541 |
+
if not data:
|
| 542 |
+
categories = ['Category A', 'Category B', 'Category C', 'Category D']
|
| 543 |
+
values = [25, 35, 20, 20]
|
| 544 |
+
data = {"categories": categories, "values": values}
|
| 545 |
+
|
| 546 |
+
# Create bar chart
|
| 547 |
+
colors = plt.cm.tab10(np.linspace(0, 1, len(data["categories"])))
|
| 548 |
+
bars = ax.bar(data["categories"], data["values"], color=colors, alpha=0.8)
|
| 549 |
+
|
| 550 |
+
ax.set_title(f"{title} - {scenario_type.title()} Analysis", fontsize=16, fontweight='bold')
|
| 551 |
+
ax.set_xlabel("Categories")
|
| 552 |
+
ax.set_ylabel("Values")
|
| 553 |
+
|
| 554 |
+
# Add value labels
|
| 555 |
+
for bar in bars:
|
| 556 |
+
height = bar.get_height()
|
| 557 |
+
ax.annotate(f'{height}',
|
| 558 |
+
xy=(bar.get_x() + bar.get_width() / 2, height),
|
| 559 |
+
xytext=(0, 3),
|
| 560 |
+
textcoords="offset points",
|
| 561 |
+
ha='center', va='bottom')
|
| 562 |
+
|
| 563 |
+
plt.xticks(rotation=45)
|
| 564 |
+
ax.grid(axis='y', alpha=0.3)
|
| 565 |
+
|
| 566 |
+
buffer = io.BytesIO()
|
| 567 |
+
plt.savefig(buffer, format='png', bbox_inches='tight', dpi=300)
|
| 568 |
+
buffer.seek(0)
|
| 569 |
+
general_image = base64.b64encode(buffer.getvalue()).decode()
|
| 570 |
+
plt.close()
|
| 571 |
+
|
| 572 |
+
images = [{"type": "general_analysis", "data": general_image}]
|
| 573 |
+
|
| 574 |
+
total_value = sum(data["values"])
|
| 575 |
+
max_category = data["categories"][data["values"].index(max(data["values"]))]
|
| 576 |
+
|
| 577 |
+
analysis_text = [
|
| 578 |
+
f"π **{scenario_type.title()} Analysis:**",
|
| 579 |
+
f"β’ Total Value: {total_value}",
|
| 580 |
+
f"β’ Highest Category: {max_category}",
|
| 581 |
+
f"β’ Number of Categories: {len(data['categories'])}",
|
| 582 |
+
"",
|
| 583 |
+
f"π **Key Insights:**",
|
| 584 |
+
f"β’ {max_category} shows the highest value",
|
| 585 |
+
f"β’ Distribution across {len(data['categories'])} categories",
|
| 586 |
+
f"β’ Further analysis may be needed based on specific requirements"
|
| 587 |
+
]
|
| 588 |
+
|
| 589 |
+
return {
|
| 590 |
+
"success": True,
|
| 591 |
+
"analysis": "\n".join(analysis_text),
|
| 592 |
+
"images": images,
|
| 593 |
+
"scenario_type": scenario_type,
|
| 594 |
+
"total_value": total_value
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
except Exception as e:
|
| 598 |
+
logger.error(f"β Error in general analysis: {str(e)}")
|
| 599 |
+
raise e
|
| 600 |
+
|
| 601 |
+
# Global instance
|
| 602 |
+
scenario_service = ScenarioAnalysisService()
|