Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions backend/app/api/intelligence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from pathlib import Path
from fastapi import APIRouter, Depends

from app.intelligence.service import SafetyIntelligenceService, provider_from_environment
from app.rag.retriever import Retriever
from app.rag.vector_store import JsonVectorStore
from app.rag.embedder import DeterministicEmbedder
from app.schemas import RiskEngineInput, SafetyIntelligenceResponse

router = APIRouter(
prefix="/intelligence",
tags=["Safety Intelligence"]
)

ROOT = Path(__file__).resolve().parents[3]
STORE_PATH = ROOT / "backend/data/knowledge/vector_store/chunks.json"

def get_intelligence_service() -> SafetyIntelligenceService:
store = JsonVectorStore(STORE_PATH)
embedder = DeterministicEmbedder()
retriever = Retriever(store, embedder)
provider = provider_from_environment()
return SafetyIntelligenceService(retriever, provider)

@router.post("/generate", response_model=SafetyIntelligenceResponse)
def generate_intelligence(
risk: RiskEngineInput,
service: SafetyIntelligenceService = Depends(get_intelligence_service)
) -> SafetyIntelligenceResponse:
"""
Generate an advisory safety intelligence response for a given risk engine input.
"""
return service.generate(risk)
19 changes: 17 additions & 2 deletions backend/app/api/interventions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
from fastapi import APIRouter
from pydantic import BaseModel
import time

router= APIRouter(
prefix="/interventions",
router = APIRouter(
prefix="/api/interventions",
tags=["Interventions"]
)
Comment on lines +5 to 8

class InterventionRequest(BaseModel):
zone_id: str
action_taken: str

@router.get("/")
def get_interventions():
return {
"message": "Coming Soon"
}

@router.post("/")
def trigger_intervention(req: InterventionRequest):
return {
"intervention_id": f"INT-{int(time.time())}",
"status": "success",
"action": req.action_taken,
"zone": req.zone_id
}
275 changes: 275 additions & 0 deletions backend/app/api/simulation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
import os
import sys
import time
from typing import Dict, Any, List
from pathlib import Path
from pydantic import BaseModel
from fastapi import APIRouter
from datetime import datetime

# Setup paths for digital twin
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
sys.path.append(os.path.join(BASE_DIR, "digital twin"))
Comment on lines +10 to +12

try:
from simulator.plant import Plant, SCENARIO_CLASSES
from simulator.config import ScenarioID
except ImportError as e:
print(f"Warning: digital twin could not be imported. {e}")
Plant = None

# Intelligence Integration
from app.api.intelligence import get_intelligence_service
from app.schemas import RiskEngineInput

router = APIRouter(prefix="/api/simulation", tags=["simulation"])

class SimulationController:
def __init__(self):
self.plant = Plant() if Plant else None
self.is_running = True
self.current_scenario = "normal"
self.speed = 1.0
self.last_state = None
self.last_tick_time = time.time()
self.zones_data = {}
self.history = {} # type: Dict[str, List[float]]
self.alerts = []

# Intelligence cache
self.last_ai_update = 0
self.ai_cache = {}

# Instantiate intelligence service once
self.intelligence_service = get_intelligence_service()

if self.plant:
self._set_scenario(self.current_scenario)

def _set_scenario(self, scenario_id: str):
self.current_scenario = scenario_id
self.last_ai_update = 0 # Force immediate AI update on scenario change
self.history = {} # Clear history
self.ai_cache = {} # Clear AI cache
if Plant:
sc_cls = SCENARIO_CLASSES.get(scenario_id, SCENARIO_CLASSES["normal"])
self.plant.reset()
self.plant.set_scenario(sc_cls(duration=10000))

def tick(self):
if not self.is_running or not self.plant:
return

now = time.time()
if now - self.last_tick_time >= (1.0 / self.speed):
row = self.plant.tick()
self._process_row(row)
self.last_tick_time = now

def _process_row(self, row: dict):
self.last_state = row

zone_ids = ["ZONE_A", "ZONE_B", "ZONE_C", "ZONE_D", "ZONE_E", "ZONE_F", "ZONE_G"]

for zid in zone_ids:
if zid not in self.zones_data:
self.zones_data[zid] = {
"risk_score": 0,
"worker_count": 0,
"sensors": {
"hydrocarbon": {"value": 0.0},
"h2s": {"value": 0.0},
"oxygen": {"value": 20.9},
"pressure": {"value": 1.01},
"temperature": {"value": 30.0}
},
"cctv": [],
"workers": [],
"ai_fusion": {}
}
if zid not in self.history:
self.history[zid] = []

# Map some digital twin properties to Zone D for display
base_risk = 5
if self.current_scenario == "gas_leak":
base_risk = 60
elif self.current_scenario == "explosion_risk":
base_risk = 90
elif self.current_scenario == "ventilation_failure":
base_risk = 55
elif self.current_scenario == "pump_failure":
base_risk = 70
elif self.current_scenario == "hot_work_gas_leak":
base_risk = 85
elif self.current_scenario == "confined_space":
base_risk = 65

self.zones_data["ZONE_D"]["risk_score"] = base_risk
self.zones_data["ZONE_D"]["worker_count"] = 3 if self.current_scenario != "normal" else 2

import math
t = time.time()

hc_val = row.get("hc_gas_lel", 0.0)
h2s_val = row.get("h2s_ppm", 0.0)
pressure_val = row.get("pipeline_pressure", 1.01)
temp_val = row.get("pipeline_temperature", 120.0)

# Normalize wild pressure values from digital twin
if pressure_val > 100:
pressure_val = 1.0 + (pressure_val % 2.0)

# Apply scenario-specific realistic boosts
if self.current_scenario in ["gas_leak", "ventilation_failure"]:
hc_val += 25.0 + math.sin(t * 0.5) * 5.0
h2s_val += 50.0 + math.cos(t * 0.3) * 10.0
elif self.current_scenario in ["explosion_risk", "hot_work_gas_leak"]:
pressure_val += 8.0 + math.sin(t * 0.8) * 2.0
hc_val += 10.0 + math.sin(t * 0.2) * 2.0
temp_val += 40.0 + math.cos(t * 0.5) * 5.0
elif self.current_scenario == "confined_space":
h2s_val += 20.0 + math.sin(t * 0.4) * 5.0
pressure_val += 3.0 + math.cos(t * 0.6) * 1.0

hc_val = max(0.0, hc_val)
h2s_val = max(0.0, h2s_val)
pressure_val = max(0.0, pressure_val)

self.zones_data["ZONE_D"]["sensors"]["hydrocarbon"]["value"] = hc_val
self.history["ZONE_D"].append(hc_val)
if len(self.history["ZONE_D"]) > 20:
self.history["ZONE_D"].pop(0)

self.zones_data["ZONE_D"]["sensors"]["h2s"]["value"] = h2s_val
self.zones_data["ZONE_D"]["sensors"]["oxygen"]["value"] = row.get("oxygen_pct", 20.9)
self.zones_data["ZONE_D"]["sensors"]["temperature"]["value"] = temp_val
self.zones_data["ZONE_D"]["sensors"]["pressure"]["value"] = pressure_val

self.alerts = []
if base_risk > 50:
self.alerts.append({
"zone_id": "ZONE_D",
"primary_hazard": self.current_scenario.replace("_", " ").upper(),
"explanation": row.get("event_label", "Anomalous readings detected"),
"alert_id": "ALT-" + str(int(time.time()))
})

sim_ctrl = SimulationController()

class StartRequest(BaseModel):
scenario: str
speed: float = 1.0
Comment on lines +160 to +162

@router.post("/start")
def start_sim(req: StartRequest):
sim_ctrl._set_scenario(req.scenario)
sim_ctrl.speed = req.speed
sim_ctrl.is_running = True
return {"status": "started"}

@router.post("/pause")
def pause_sim():
sim_ctrl.is_running = False
return {"status": "paused"}

@router.post("/resume")
def resume_sim():
sim_ctrl.is_running = True
return {"status": "resumed"}

@router.post("/step")
def step_sim():
sim_ctrl.tick()
return {"status": "stepped"}

@router.get("/state")
def get_state(zone_id: str = "ZONE_D"):
sim_ctrl.tick()

# Intelligence integration - update every 10 seconds if risk is high
now = time.time()
if now - sim_ctrl.last_ai_update > 10:
sim_ctrl.last_ai_update = now
zdata = sim_ctrl.zones_data.get(zone_id, {})

if zdata.get("risk_score", 0) > 40:
risk_mapping = {
"gas_leak": "TOXIC_GAS_EXPOSURE",
"ventilation_failure": "OXYGEN_DEFICIENCY",
"pump_failure": "EQUIPMENT_FAILURE",
"hot_work_gas_leak": "FIRE_EXPLOSION",
"confined_space": "CONFINED_SPACE",
"explosion_risk": "FIRE_EXPLOSION"
}
mapped_risk = risk_mapping.get(sim_ctrl.current_scenario, "UNKNOWN")

factor_mapping = {
"gas_leak": "HIGH_H2S",
"ventilation_failure": "VENTILATION_FAILURE",
"pump_failure": "ABNORMAL_FLOW",
"hot_work_gas_leak": "HOT_WORK_ACTIVE",
"confined_space": "CONFINED_SPACE_ACTIVE",
"explosion_risk": "RISING_LEL"
}
mapped_factor = factor_mapping.get(sim_ctrl.current_scenario, "UNKNOWN")

# Construct input
risk_input = RiskEngineInput(
alert_id="ALT-" + str(int(now)),
timestamp=datetime.fromtimestamp(now),
zone_id=zone_id,
equipment_ids=["DC-100"],
risk_type=mapped_risk,
risk_score=float(zdata["risk_score"]) / 100.0,
severity="HIGH",
predicted_incident=sim_ctrl.current_scenario.replace("_", " ").title() + " Potential Incident",
contributing_factors=[mapped_factor],
sensor_evidence={
"hydrocarbon": zdata["sensors"]["hydrocarbon"]["value"],
"temperature": zdata["sensors"]["temperature"]["value"]
},
estimated_lead_time_minutes=2.0
)

try:
# Call Intelligence Service
response = sim_ctrl.intelligence_service.generate(risk_input)

sim_ctrl.ai_cache[zone_id] = {
"triggered_rules": [a.title for a in response.recommended_actions[:2]],
"primary_hazard": response.executive_summary.split(".")[0],
"explanation": response.risk_explanation,
"confidence": response.intelligence_confidence,
"lead_time_vs_baseline": risk_input.estimated_lead_time_minutes * 60
}
except Exception as e:
sim_ctrl.ai_cache[zone_id] = {
"triggered_rules": ["AI_ERROR"],
"primary_hazard": "Intelligence Unavailable",
"explanation": f"Failed to generate intelligence: {str(e)}",
"confidence": 0.0,
"lead_time_vs_baseline": 0.0
}
else:
sim_ctrl.ai_cache[zone_id] = {
"triggered_rules": [],
"primary_hazard": "NONE / HEALTHY",
"explanation": "Sensors reporting normal operations. No hazard indicators detected.",
"confidence": 1.0,
"lead_time_vs_baseline": 0.0
}

if zone_id in sim_ctrl.zones_data:
sim_ctrl.zones_data[zone_id]["ai_fusion"] = sim_ctrl.ai_cache.get(zone_id, {})

return {
"simulation": {
"scenario": sim_ctrl.current_scenario,
"is_running": sim_ctrl.is_running
},
"zones": {k: {"risk_score": v["risk_score"], "worker_count": v["worker_count"]} for k,v in sim_ctrl.zones_data.items()},
"selected_zone": sim_ctrl.zones_data.get(zone_id, {}),
"history": sim_ctrl.history,
"alerts": sim_ctrl.alerts
}
24 changes: 23 additions & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
import os

from app.api.health import router as health_router
from app.api.sensor import router as sensor_router
Expand All @@ -7,6 +10,8 @@
from app.api.cctv import router as cctv_router
from app.api.alerts import router as alerts_router
from app.api.interventions import router as interventions_router
from app.api.intelligence import router as intelligence_router
from app.api.simulation import router as simulation_router

from app.database.database import engine
from app.database.base import Base
Expand All @@ -17,6 +22,14 @@
version="1.0.0"
)

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Comment on lines +25 to +31

Base.metadata.create_all(bind=engine)

app.include_router(health_router)
Expand All @@ -25,4 +38,13 @@
app.include_router(maintenance_router)
app.include_router(cctv_router)
app.include_router(alerts_router)
app.include_router(interventions_router)
app.include_router(interventions_router)
app.include_router(intelligence_router)
app.include_router(simulation_router)

# Mount frontend
FRONTEND_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "frontend")
if os.path.exists(FRONTEND_DIR):
app.mount("/", StaticFiles(directory=FRONTEND_DIR, html=True), name="frontend")
else:
print(f"Warning: frontend directory not found at {FRONTEND_DIR}")