Spaces:
Sleeping
Sleeping
File size: 7,150 Bytes
f927995 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | """
Engine 2 and Engine 3 API endpoints.
Exposes propagation, community, criticality, action-planning, and diagnostics
used by the frontend operations surfaces.
"""
import logging
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from ..config import settings
from ..deps import (
get_gemini_analyst,
get_gemini_flash_client,
get_graph_service,
get_retriever,
get_shock_propagator,
)
from ..services.criticality import compute_node_criticality
from ..services.gemini_analyst import GeminiAnalyst
from ..services.graph_service import GraphService
from ..services.shock_propagation import ShockPropagator
logger = logging.getLogger("shockmap.engine_api")
router = APIRouter(prefix="/api/v1", tags=["engine2", "propagation"])
PROPAGATION_FORMULA = "R_i = PR_i(shock) * (1 - S_i) * exp(-B_i / tau) * C_community"
class PropagationRequest(BaseModel):
province: str = Field(..., description="Province to simulate shock from")
sector: str = Field(default="pharma", description="Sector context")
class ActionPlanRequest(BaseModel):
region: str = Field(..., description="Shocked region (province)")
shock_type: str = Field(
default="factory_shutdown",
description="Type: export_ban | factory_shutdown | env_compliance | logistics_block | price_spike",
)
sector: str = Field(default="pharma")
def _engine_data_mode(gemini_available: bool, retriever_enabled: bool) -> str:
if settings.DEMO_MODE and (gemini_available or retriever_enabled):
return "hybrid_demo_live"
if settings.DEMO_MODE:
return "demo"
return "live"
@router.get("/propagation/{province}")
async def get_propagation(
province: str,
sector: str = Query(default="pharma"),
graph_service: GraphService = Depends(get_graph_service),
propagator: ShockPropagator = Depends(get_shock_propagator),
):
"""
Returns PageRank-based shock propagation from a given province.
"""
node = graph_service.get_node(province)
if not node:
all_provinces = graph_service.nodes_by_type("province")
match = next(
(item for item in all_provinces if item and item["name"].lower() == province.lower()),
None,
)
if match:
province = match["id"]
else:
available = [item["name"] for item in all_provinces if item]
raise HTTPException(
status_code=404,
detail=f"Province '{province}' not found. Available: {', '.join(available[:10])}",
)
trace = propagator.get_propagation_trace(province, sector)
combined_risks = graph_service.compute_combined_risk(province, sector)
return {
"province": province,
"sector": sector,
"method": "personalized_pagerank",
"formula": PROPAGATION_FORMULA,
"affected_nodes": combined_risks,
"propagation_trace": trace,
"total_affected": len(combined_risks),
}
@router.get("/communities")
async def get_communities(
graph_service: GraphService = Depends(get_graph_service),
):
"""
Returns Louvain-detected supply chain communities.
"""
return graph_service.get_communities()
@router.get("/communities/{node_id}")
async def get_node_community(
node_id: str,
graph_service: GraphService = Depends(get_graph_service),
):
"""
Returns the community membership for a specific node.
"""
community_id = graph_service.get_node_community(node_id)
if community_id is None:
raise HTTPException(status_code=404, detail=f"Node '{node_id}' not found")
communities = graph_service.get_communities()
community = next(
(item for item in communities.get("communities", []) if item["id"] == community_id),
None,
)
return {
"node_id": node_id,
"community_id": community_id,
"community": community,
}
@router.get("/criticality/{node_id}")
async def get_criticality(
node_id: str,
graph_service: GraphService = Depends(get_graph_service),
):
"""
Returns the multi-factor criticality breakdown for a node.
"""
result = compute_node_criticality(graph_service, node_id)
if result is None:
raise HTTPException(status_code=404, detail=f"Node '{node_id}' not found")
return result
@router.post("/action-plan")
async def generate_action_plan(
request: ActionPlanRequest,
analyst: GeminiAnalyst = Depends(get_gemini_analyst),
):
"""
Engine 3: Generates a grounded action plan for a detected shock.
"""
return await analyst.generate_action_plan(
shocked_region=request.region,
shock_type=request.shock_type,
sector=request.sector,
)
@router.get("/engines")
async def get_engine_status(
graph_service: GraphService = Depends(get_graph_service),
propagator: ShockPropagator = Depends(get_shock_propagator),
gemini_client=Depends(get_gemini_flash_client),
retriever=Depends(get_retriever),
):
"""
Returns the status of all three engines for diagnostics and UI badges.
"""
communities = graph_service.get_communities()
gemini_available = bool(gemini_client.is_available())
retriever_enabled = bool(getattr(retriever, "_enabled", False))
data_mode = _engine_data_mode(gemini_available, retriever_enabled)
return {
"engine_1": {
"name": "Signal Intelligence",
"method": "Gemini Flash structured extraction",
"status": "active",
"data_mode": data_mode,
"gemini_flash_available": gemini_available,
"response_strategy": "keyword pre-filter -> Flash JSON -> deterministic fallback",
"description": "Extracts structured supply-shock entities from GDELT and supporting feeds.",
},
"engine_2": {
"name": "Shock Propagation",
"method": "Personalized PageRank + Louvain Communities",
"status": "active",
"graph_nodes": graph_service.graph.number_of_nodes(),
"graph_edges": graph_service.graph.number_of_edges(),
"community_detection_enabled": communities.get("community_detection_enabled", True),
"community_detection_method": communities.get("community_detection_method", "louvain"),
"communities_detected": communities.get("total_communities", 0),
"gnn_enabled": settings.ENABLE_GNN,
"gnn_available": propagator.is_available(),
"active_mode": propagator.mode(),
"formula": PROPAGATION_FORMULA,
},
"engine_3": {
"name": "Action Intelligence",
"method": "Grounded RAG + Gemini Flash",
"status": "active",
"data_mode": data_mode,
"gemini_flash_available": gemini_available,
"retriever_enabled": retriever_enabled,
"response_strategy": "retrieval merge -> Flash JSON -> scenario-backed fallback",
"description": "Generates operator-ready actions with citations and deterministic degradation.",
},
}
|