causalis-backend / backend /src /provider.py
github-actions[bot]
Deploy from GitHub: a9912df
69f842d
Raw History Blame Contribute Delete
12.5 kB
import json
import os
import warnings
from pathlib import Path
warnings.filterwarnings("ignore", message=".*urllib3 v2 only supports OpenSSL.*")
warnings.filterwarnings("ignore", message=".*urllib3.*")
warnings.filterwarnings("ignore", category=FutureWarning, module="google")
warnings.filterwarnings("ignore", message=".*capture.*")
os.environ["ANONYMIZED_TELEMETRY"] = "False"
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from dotenv import load_dotenv
import google.generativeai as genai
from src.world_model import MaritimeWorldModel
from src.temporal_model import TemporalModel
from src.simulator import DisruptionSimulator
from src.entity_extractor import extract_entities, _detect_severity
from src.prompt_builder import build_prompt
load_dotenv(Path(__file__).resolve().parents[2] / ".env")
app = FastAPI(title="Causalis Streaming Provider")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
_world = None
_temporal = None
_simulator = None
_kb = None
@app.on_event("startup")
async def startup():
print("[provider] FastAPI app started. Heavy models will load on first request.")
def _get_world():
global _world, _temporal, _simulator, _kb
if _world is None:
try:
_world = MaritimeWorldModel()
_temporal = TemporalModel(_world)
_simulator = DisruptionSimulator(_world, _temporal)
except Exception as e:
_world = None
_temporal = None
_simulator = None
raise
try:
from src.rag import MaritimeKnowledgeBase
_kb = MaritimeKnowledgeBase()
except Exception as e:
print(f"[provider] Warning: RAG initialization failed: {e}")
_kb = None
return _world, _temporal, _simulator, _kb
class ChatRequest(BaseModel):
messages: list
stream: bool = True
@app.post("/v1/chat/stream")
async def chat_stream(request: ChatRequest):
if not request.messages:
raise HTTPException(status_code=400, detail="No messages provided")
user_message = request.messages[-1].get("content", "")
print(f"[provider] Incoming message: {user_message[:100]}")
_, _, simulator, kb = _get_world()
entities = extract_entities(user_message)
chokepoints = entities.get("chokepoints", [])
ports = entities.get("ports", [])
severity = entities.get("severity", "partial")
print(
f"[provider] Entities: chokepoints={chokepoints}, ports={ports}, severity={severity}"
)
simulation = None
if chokepoints:
simulation = simulator.run_scenario(chokepoints, severity)
print(
f"[provider] Simulation ran: {simulation is not None}, scenario={simulation.scenario if simulation else None}"
)
else:
print(f"[provider] No chokepoints found β€” using fallback prompt")
rag_results = kb.retrieve(user_message, n_results=3) if kb else []
rag_context = "\n\n".join(r["content"] for r in rag_results) if rag_results else ""
all_entities = chokepoints + ports
if simulation:
prompt = build_prompt(user_message, simulation, rag_context, all_entities)
print(f"[provider] Using SIMULATION prompt (len={len(prompt)})")
else:
entity_instruction = (
f"Relevant spatial entities: {all_entities}\n"
if all_entities
else "Relevant spatial entities: []\n"
)
prompt = (
f"User Question: {user_message}\n\n"
f"Reference Material:\n{rag_context}\n\n"
"## OpenUI Lang Syntax\n"
"You MUST respond using OpenUI Lang. Every response must start with:\n"
"root = Stack([...])\n\n"
"Available components (arguments are POSITIONAL, in the order shown):\n"
"- Stack(children) β€” vertical layout container. children is a list of components.\n"
"- TextBlock(text) β€” narrative text. text is a string.\n"
"- GlobeVersion(version, entities) β€” trigger globe visualization. version is int (use 1), entities is list of strings.\n"
"- ImpactStats(vessels, routes, cost_usd) β€” metrics grid. vessels and routes are ints, cost_usd is int in USD.\n"
'- CarrierTable(carriers) β€” carrier table. carriers is a list of objects with "name" and "exposure" keys.\n'
"- ReroutingCard(route_id, additional_days, additional_cost_usd, vessels_affected) β€” route alternative.\n"
"- PortCongestion(port_id, baseline, forecast, dwell_increase_hours) β€” congestion forecast.\n"
'- CascadeTimeline(timeline) β€” timeline with list of "port" and "hours_to_impact" objects.\n'
f"{entity_instruction}\n"
"Syntax rules:\n"
"1. Always start with: root = Stack([...])\n"
"2. Arguments are POSITIONAL β€” pass values in the order shown above. Do NOT use key=value syntax.\n"
'3. Use GlobeVersion for spatial references: GlobeVersion(1, ["suez_canal"])\n'
'4. Combine components in a Stack: root = Stack([TextBlock("Blockage causes delays"), GlobeVersion(1, ["suez_canal"])])\n'
"5. Never use string concatenation (+) inside component arguments\n\n"
"Answer based on the reference material. If no simulation data is available, "
"explain what you can do and ask the user to specify a chokepoint or route to simulate.\n"
"IMPORTANT: Your ENTIRE response must be valid OpenUI Lang starting with root = Stack([...]). "
"Do NOT output plain text or markdown outside of OpenUI Lang syntax.\n"
)
api_key = os.environ.get("GEMINI_API_KEY", "")
if not api_key:
async def no_key():
yield "[Set GEMINI_API_KEY in .env to enable LLM responses]"
return StreamingResponse(no_key(), media_type="text/event-stream")
genai.configure(api_key=api_key)
model_name = os.environ.get("GEMINI_MODEL", "gemini-flash-latest")
print(f"[provider] Using model: {model_name}")
model = genai.GenerativeModel(model_name)
async def text_stream():
try:
full_response = ""
response = model.generate_content(prompt, stream=True)
for chunk in response:
if chunk.text:
full_response += chunk.text
yield chunk.text
print(
f"[provider] Response length: {len(full_response)}, starts with: {full_response[:80]}"
)
except Exception as e:
print(f"[provider] Error: {e}")
yield f"[Error: {e}]"
return StreamingResponse(text_stream(), media_type="text/event-stream")
@app.get("/health")
async def health():
return {"status": "ok"}
@app.get("/")
async def root():
return {"status": "ok", "service": "causalis"}
@app.get("/v1/spatial/ports")
async def get_ports():
world, _, _, _ = _get_world()
ports = []
for _, port in world._ports.iterrows():
ports.append(
{
"id": port["id"],
"name": port["name"],
"latitude": float(port["latitude"]),
"longitude": float(port["longitude"]),
"country": port["country"],
"type": port["type"],
"annual_teu": int(port["annual_teu"]),
"max_draft_meters": float(port["max_draft_meters"]),
"typical_dwell_hours": int(port["typical_dwell_hours"]),
}
)
return {"ports": ports}
@app.get("/v1/spatial/chokepoints")
async def get_chokepoints():
world, _, _, _ = _get_world()
chokepoints = []
for _, cp in world._chokepoints.iterrows():
centroid = cp.geometry.centroid
chokepoints.append(
{
"id": cp["id"],
"name": cp["name"],
"latitude": centroid.y,
"longitude": centroid.x,
"type": cp["type"],
"daily_vessels": int(cp["daily_vessels"]),
"strategic_importance": int(cp["strategic_importance"]),
}
)
return {"chokepoints": chokepoints}
@app.get("/v1/spatial/routes")
async def get_routes():
world, _, _, _ = _get_world()
routes = []
for _, route in world._routes.iterrows():
routes.append(
{
"id": route["id"],
"name": route["name"],
"origin_port_id": route["origin_port_id"],
"destination_port_id": route["destination_port_id"],
"chokepoints_transited": route["chokepoints_transited"],
"distance_nm": float(route["distance_nm"]),
"transit_days": float(route["transit_days"]),
}
)
return {"routes": routes}
@app.get("/v1/simulate")
async def simulate(chokepoints: str = "", severity: str = "", message: str = ""):
if severity:
detected = severity
elif message:
has_chokepoints = bool(chokepoints)
detected = _detect_severity(message, has_chokepoints)
else:
detected = "partial"
if not chokepoints and message:
entities = extract_entities(message)
chokepoints = ",".join(entities.get("chokepoints", []))
if not chokepoints:
return {
"scenario": {
"chokepoints": [],
"severity": detected,
"affected_vessels": 0,
},
"affected_routes": [],
"carriers": [],
"rerouting": {"alternatives": []},
"port_congestion": [],
"cascade": {"impact_timeline": []},
}
cp_list = [c.strip() for c in chokepoints.split(",") if c.strip()]
world, _, simulator, _ = _get_world()
simulation = simulator.run_scenario(cp_list, detected)
return {
"scenario": simulation.scenario,
"affected_routes": simulation.affected_routes,
"carriers": simulation.carriers,
"rerouting": simulation.rerouting,
"port_congestion": simulation.port_congestion,
"cascade": simulation.cascade,
}
@app.get("/v1/spatial/port/{port_id}")
async def get_port(port_id: str):
world, _, _, _ = _get_world()
port_row = world._ports[world._ports["id"] == port_id]
if port_row.empty:
raise HTTPException(status_code=404, detail=f"Port '{port_id}' not found")
port = port_row.iloc[0]
return {
"id": port["id"],
"name": port["name"],
"latitude": float(port["latitude"]),
"longitude": float(port["longitude"]),
"country": port["country"],
"type": port["type"],
"annual_teu": int(port["annual_teu"]),
"max_draft_meters": float(port["max_draft_meters"]),
"typical_dwell_hours": int(port["typical_dwell_hours"]),
}
@app.get("/v1/spatial/chokepoint/{chokepoint_id}")
async def get_chokepoint(chokepoint_id: str):
world, _, _, _ = _get_world()
cp_row = world._chokepoints[world._chokepoints["id"] == chokepoint_id]
if cp_row.empty:
raise HTTPException(
status_code=404, detail=f"Chokepoint '{chokepoint_id}' not found"
)
cp = cp_row.iloc[0]
centroid = cp.geometry.centroid
return {
"id": cp["id"],
"name": cp["name"],
"latitude": centroid.y,
"longitude": centroid.x,
"type": cp["type"],
"daily_vessels": int(cp["daily_vessels"]),
"strategic_importance": int(cp["strategic_importance"]),
}
@app.get("/v1/spatial/route/{route_id}")
async def get_route(route_id: str):
world, _, _, _ = _get_world()
route_row = world._routes[world._routes["id"] == route_id]
if route_row.empty:
raise HTTPException(status_code=404, detail=f"Route '{route_id}' not found")
route = route_row.iloc[0]
return {
"id": route["id"],
"name": route["name"],
"origin_port_id": route["origin_port_id"],
"destination_port_id": route["destination_port_id"],
"chokepoints_transited": route["chokepoints_transited"],
"distance_nm": float(route["distance_nm"]),
"transit_days": float(route["transit_days"]),
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)