digital-twin-backend / explainability.py
AlexKurian's picture
Added gemini
98060ad
"""
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()