Spaces:
Sleeping
Sleeping
| """ | |
| Explainability layer: generates human-readable explanations for policy actions. | |
| Links mutations to research evidence and identifies trade-offs. | |
| """ | |
| import json | |
| from typing import List, Dict, Any, Optional | |
| # from langchain_groq import ChatGroq | |
| from langchain_google_genai import ChatGoogleGenerativeAI | |
| import config | |
| class ExplanationGenerator: | |
| """Generates narrative explanations for policy mutations.""" | |
| def __init__(self, policy: Dict[str, Any], research_chunks: List[str]): | |
| """ | |
| Initialize generator. | |
| Args: | |
| policy: Policy dict with mutations | |
| research_chunks: List of research excerpts supporting the policy | |
| """ | |
| self.policy = policy | |
| self.research = research_chunks | |
| # self.llm = ChatGroq( | |
| # model=config.LLM_MODEL, | |
| # temperature=0.5, | |
| # api_key=config.GROQ_API_KEY | |
| # ) | |
| self.llm = ChatGoogleGenerativeAI( | |
| model="gemini-2.5-flash", | |
| temperature=0.5, | |
| google_api_key=config.GEMINI_API_KEY | |
| ) | |
| def generate_full_explanation(self) -> Dict[str, Any]: | |
| """ | |
| Generate complete explainability output for a policy. | |
| Returns: | |
| Dict with intro narrative, per-mutation explanations, trade-offs | |
| """ | |
| explanations = { | |
| 'policy_id': self.policy.get('policy_id'), | |
| 'policy_name': self.policy.get('name'), | |
| 'narrative_intro': self._generate_intro(), | |
| 'mutations': [], | |
| 'overall_narrative': self._generate_overall_narrative() | |
| } | |
| # Generate explanation for each mutation | |
| for mutation in self.policy.get('mutations', []): | |
| exp = self._explain_mutation(mutation) | |
| explanations['mutations'].append(exp) | |
| return explanations | |
| def _generate_intro(self) -> str: | |
| """Generate opening narrative for the policy.""" | |
| prompt = f""" | |
| Write a compelling 2-3 sentence introduction to this climate policy intervention. | |
| Focus on why it matters for public health and urban sustainability. | |
| Policy Name: {self.policy.get('name')} | |
| Number of Actions: {len(self.policy.get('mutations', []))} | |
| Expected CO₂ Reduction: {self.policy.get('estimated_impacts', {}).get('co2_reduction_pct', 0)}% | |
| Expected AQI Improvement: {self.policy.get('estimated_impacts', {}).get('aqi_improvement_pct', 0)}% | |
| Keep it concise, impactful, and accessible to policymakers. | |
| """ | |
| try: | |
| response = self.llm.invoke(prompt) | |
| return response.content | |
| except Exception as e: | |
| print(f"Error generating intro: {e}") | |
| return f"Policy: {self.policy.get('name')} aims to reduce emissions and improve air quality." | |
| def _generate_overall_narrative(self) -> str: | |
| """Generate overall policy narrative.""" | |
| prompt = f""" | |
| Summarize this climate policy in 3-4 sentences suitable for a policy brief. | |
| Explain WHAT the policy does, WHY it's effective, and WHO benefits. | |
| Policy: {self.policy.get('name')} | |
| Description: {self.policy.get('description', '')} | |
| Trade-offs: {json.dumps(self.policy.get('trade_offs', []))} | |
| Be concise and professional. Avoid jargon. | |
| """ | |
| try: | |
| response = self.llm.invoke(prompt) | |
| return response.content | |
| except Exception as e: | |
| print(f"Error generating overall narrative: {e}") | |
| return "Policy designed to reduce urban emissions through targeted sector interventions." | |
| def _explain_mutation(self, mutation: Dict[str, Any]) -> Dict[str, Any]: | |
| """ | |
| Generate explanation for a single mutation. | |
| Args: | |
| mutation: Single mutation dict | |
| Returns: | |
| Dict with narrative, research backing, affected stakeholders | |
| """ | |
| # Generate narrative explaining the mutation | |
| narrative = self._generate_mutation_narrative(mutation) | |
| # Extract relevant research quotes | |
| relevant_quotes = self._extract_relevant_quotes(mutation) | |
| # Identify stakeholders affected | |
| stakeholders = self._identify_stakeholders(mutation) | |
| return { | |
| 'mutation': { | |
| 'type': mutation.get('type'), | |
| 'target': mutation.get('node_id') or f"{mutation.get('source')} → {mutation.get('target')}", | |
| 'reason': mutation.get('reason', '') | |
| }, | |
| 'narrative': narrative, | |
| 'supporting_research': relevant_quotes, | |
| 'affected_stakeholders': stakeholders | |
| } | |
| def _generate_mutation_narrative(self, mutation: Dict[str, Any]) -> str: | |
| """Generate 2-3 sentence explanation of why this mutation is applied.""" | |
| mutation_type = mutation.get('type') | |
| if mutation_type == 'disable_node': | |
| target = mutation.get('node_id', 'sector') | |
| prompt = f""" | |
| Explain why disabling the {target} sector is effective for reducing emissions. | |
| Write 2-3 sentences suitable for a policy document. | |
| Focus on: causal mechanism, expected benefits, and evidence base. | |
| Research evidence available: | |
| {chr(10).join(self.research[:2])} | |
| """ | |
| elif mutation_type == 'reduce_edge_weight': | |
| source = mutation.get('source', 'source') | |
| target = mutation.get('target', 'target') | |
| new_weight = mutation.get('new_weight', 0.5) | |
| reduction = ((1 - new_weight) * 100) if new_weight else 0 | |
| prompt = f""" | |
| Explain why reducing the {source} → {target} relationship (by ~{reduction:.0f}%) helps reduce emissions. | |
| This represents implementing technology or policy to reduce the causal influence. | |
| Write 2-3 sentences. Include: HOW (technology/policy), WHY (mechanism), IMPACT (benefits). | |
| Research evidence: | |
| {chr(10).join(self.research[:2])} | |
| """ | |
| elif mutation_type == 'increase_edge_weight': | |
| source = mutation.get('source', 'source') | |
| target = mutation.get('target', 'target') | |
| new_weight = mutation.get('new_weight', 0.7) | |
| increase = ((new_weight - 1) * 100) if new_weight > 1 else 0 | |
| prompt = f""" | |
| Explain why strengthening the {source} → {target} relationship helps achieve climate goals. | |
| This might represent policy incentives or investment in beneficial activities. | |
| Write 2-3 sentences explaining the mechanism and benefits. | |
| Research evidence: | |
| {chr(10).join(self.research[:2])} | |
| """ | |
| else: | |
| return "This policy action adjusts system dynamics to improve environmental outcomes." | |
| try: | |
| response = self.llm.invoke(prompt) | |
| return response.content | |
| except Exception as e: | |
| print(f"Error generating mutation narrative: {e}") | |
| return mutation.get('reason', 'Policy action targeting emissions reduction.') | |
| def _extract_relevant_quotes(self, mutation: Dict[str, Any]) -> List[str]: | |
| """ | |
| Find research quotes most relevant to this mutation. | |
| Simple keyword matching; can be enhanced with semantic search. | |
| """ | |
| quotes = [] | |
| keywords = [] | |
| # Determine relevant keywords based on mutation | |
| if mutation.get('type') == 'disable_node': | |
| keywords = [mutation.get('node_id', '')] | |
| else: | |
| keywords = [ | |
| mutation.get('source', ''), | |
| mutation.get('target', '') | |
| ] | |
| # Search for quotes containing keywords | |
| for chunk in self.research: | |
| for keyword in keywords: | |
| if keyword.lower() in chunk.lower(): | |
| if chunk not in quotes: | |
| quotes.append(chunk[:150] + "..." if len(chunk) > 150 else chunk) | |
| break | |
| # Return first 2 relevant quotes | |
| return quotes[:2] if quotes else [ | |
| "Research demonstrates effectiveness of targeted emission control policies.", | |
| "Evidence supports multi-sector approach to urban air quality improvement." | |
| ] | |
| def _identify_stakeholders(self, mutation: Dict[str, Any]) -> List[Dict[str, str]]: | |
| """ | |
| Identify sectors/groups affected by this mutation. | |
| """ | |
| stakeholders = [] | |
| mutation_type = mutation.get('type') | |
| # Map nodes/sectors to stakeholders | |
| sector_to_stakeholders = { | |
| 'transport': [ | |
| {'group': 'Transport operators', 'impact': 'operational changes'}, | |
| {'group': 'Auto manufacturers', 'impact': 'R&D investment'}, | |
| {'group': 'Urban commuters', 'impact': 'improved air quality'} | |
| ], | |
| 'energy': [ | |
| {'group': 'Power utilities', 'impact': 'generation mix shift'}, | |
| {'group': 'Coal industry', 'impact': 'transition costs'}, | |
| {'group': 'Solar/wind companies', 'impact': 'growth opportunities'}, | |
| {'group': 'Citizens', 'impact': 'cleaner air'} | |
| ], | |
| 'industries': [ | |
| {'group': 'Manufacturers', 'impact': 'efficiency improvements'}, | |
| {'group': 'Workers', 'impact': 'job transitions'}, | |
| {'group': 'Consumers', 'impact': 'potential cost changes'} | |
| ], | |
| 'infrastructure': [ | |
| {'group': 'Construction sector', 'impact': 'green building standards'}, | |
| {'group': 'Urban planners', 'impact': 'planning considerations'}, | |
| {'group': 'Real estate', 'impact': 'sustainable development'} | |
| ] | |
| } | |
| # Extract stakeholders based on affected node/sector | |
| if mutation_type == 'disable_node': | |
| target = mutation.get('node_id', '') | |
| else: | |
| target = mutation.get('source', '') or mutation.get('target', '') | |
| for key, groups in sector_to_stakeholders.items(): | |
| if key in target.lower(): | |
| stakeholders = groups | |
| break | |
| return stakeholders if stakeholders else [ | |
| {'group': 'Urban residents', 'impact': 'health and quality of life'}, | |
| {'group': 'Industry stakeholders', 'impact': 'economic adaptation'} | |
| ] | |
| # ============================================================================ | |
| # STANDALONE HELPER FUNCTIONS | |
| # ============================================================================ | |
| def generate_policy_explanation( | |
| policy: Dict[str, Any], | |
| research_chunks: List[str] | |
| ) -> Dict[str, Any]: | |
| """ | |
| Convenience function to generate explanation for a policy. | |
| Args: | |
| policy: Policy dict | |
| research_chunks: List of research excerpts | |
| Returns: | |
| Full explanation dict | |
| """ | |
| generator = ExplanationGenerator(policy, research_chunks) | |
| return generator.generate_full_explanation() | |