Backend — FastAPI Service¶
REST API¶
| Method | Path | Description |
|---|---|---|
| GET | /health |
Liveness/readiness probe |
| POST | /api/simulate/inject-anomaly |
Demo-only: skips telemetry, hands the crew an already-detected anomaly directly |
| POST | /api/simulate/chaos |
Demo-only: spikes a metric/region in the real synthetic OTel pipeline (the "chaos script") |
| GET | /api/incidents |
List incidents |
| GET | /api/incidents/{incident_id} |
Incident detail + full agent timeline |
| POST | /api/incidents/{incident_id}/approve |
Human approves a pending Responder action |
| POST | /api/incidents/{incident_id}/reject |
Human rejects a pending Responder action |
| GET | /api/incidents/{incident_id}/postmortem |
Fetch generated postmortem markdown |
| GET | /api/agents/status |
Current state of each agent, with the set of incidents it's actively working (idle / running / blocked) |
| GET | /api/dashboards/panel-image |
Proxies the get_panel_image MCP tool result as a PNG |
| GET | /api/analytics/summary |
Cross-incident stats: totals, status breakdown, MTTR, breach counts by metric/region |
| WS | /ws/control-room |
Real-time event stream to the frontend (see below) |
WebSocket event contract¶
Every push uses the AgentEventEnvelope schema (see low-level-design.md):
{
"type": "responder_action_pending",
"incident_id": "1b2e...",
"agent": "responder",
"timestamp": "2026-08-22T19:04:11Z",
"payload": {
"action_type": "cdn_regional_failover",
"risk_level": "high",
"description": "Fail APAC edge traffic over to backup CDN pool."
}
}
Backend entry point¶
# backend/app/main.py (abridged -- see the file for the lifespan/DB-init wiring)
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from app.routers import agents, analytics, dashboards, health, incidents, simulate
from app.services import sentinel_loop
from app.ws.manager import manager
app = FastAPI(title="Premiere Control Room API", lifespan=lifespan)
app.add_middleware(CORSMiddleware, allow_origins=settings.cors_origin_list, allow_methods=["*"], allow_headers=["*"])
app.include_router(health.router)
app.include_router(incidents.router, prefix="/api/incidents", tags=["incidents"])
app.include_router(agents.router, prefix="/api/agents", tags=["agents"])
app.include_router(dashboards.router, prefix="/api/dashboards", tags=["dashboards"])
app.include_router(simulate.router, prefix="/api/simulate", tags=["simulate"])
app.include_router(analytics.router, prefix="/api/analytics", tags=["analytics"])
@app.websocket("/ws/control-room")
async def control_room_socket(ws: WebSocket) -> None:
await manager.connect(ws)
try:
while True:
await ws.receive_text() # heartbeat / no-op from client
except WebSocketDisconnect:
manager.disconnect(ws)
lifespan runs app.db.init_db() on startup (creating tables if needed) and starts/stops app/services/sentinel_loop.py's background polling task -- a no-op when the mock crew is active, since it requires real Grafana/Gemini credentials (see agents.md). GET /api/agents/status is backed by app/services/agent_status.py, an in-memory registry tracking, per agent, the set of incidents it's actively working -- not a single "current incident" -- so overlapping incidents (see low-level-design.md) don't clobber each other's status.
Approval-gate endpoint (forced function calling in practice)¶
# backend/app/routers/incidents.py
from uuid import UUID
from fastapi import APIRouter, HTTPException
from app.orchestrator import orchestrator
router = APIRouter()
@router.post("/{incident_id}/approve")
async def approve_action(incident_id: UUID) -> dict[str, str]:
ok = await orchestrator.resolve_pending_approval(incident_id, approved=True, actor="oncall-engineer")
if not ok:
raise HTTPException(404, "No pending approval for this incident")
return {"status": "approved"}
@router.post("/{incident_id}/reject")
async def reject_action(incident_id: UUID) -> dict[str, str]:
ok = await orchestrator.resolve_pending_approval(incident_id, approved=False, actor="oncall-engineer")
if not ok:
raise HTTPException(404, "No pending approval for this incident")
return {"status": "rejected"}
The Responder agent's tool call for any risk_level == "high" action always routes through request_human_approval, which blocks on an asyncio.Event set by this endpoint — the agent cannot proceed past this point on its own, regardless of what the LLM decides. This is the concrete implementation of the hackathon's "Forced Function Calling" pattern and of the "Studio Head enforcing governance" framing. See agents.md for the tool implementation.