Spaces:
Running
Running
Saibalaji Namburi
feat(k8s): deploy api to kind multinode cluster and verify with adversarial red-teaming simulator
5553f71 | """ | |
| CustomerCore API — Server-Sent Events (SSE) Stream Router (Phase 13) | |
| Wires the Supabase persistent repository layer into the streaming endpoints. | |
| Replaces the in-memory triage_store with TicketRepository. | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import json | |
| from datetime import datetime | |
| from fastapi import APIRouter, Depends, HTTPException | |
| from fastapi.responses import StreamingResponse | |
| from src.api.auth import AuthenticatedTenant, verify_token | |
| from src.api.models import TriageStatus | |
| from src.db.repository import TicketRepository | |
| router = APIRouter(prefix="/api/v1/triage", tags=["Streaming"]) | |
| # How often to check for status updates (seconds) | |
| _POLL_INTERVAL = 0.5 | |
| # Maximum wait time before giving up and closing the stream | |
| _MAX_WAIT_SECONDS = 120 | |
| async def stream_triage( | |
| ticket_id: str, | |
| caller: AuthenticatedTenant = Depends(verify_token), | |
| ) -> StreamingResponse: | |
| """ | |
| Stream real-time triage progress via Server-Sent Events from Supabase. | |
| """ | |
| repo = TicketRepository(caller.tenant_id) | |
| record = await repo.get(ticket_id) | |
| if not record: | |
| raise HTTPException(status_code=404, detail=f"Ticket {ticket_id} not found.") | |
| async def event_generator(): | |
| """ | |
| Async generator that polls the database and yields SSE events. | |
| """ | |
| elapsed = 0.0 | |
| last_status = None | |
| # Send initial connection confirmation event | |
| yield _sse_event({ | |
| "type": "connected", | |
| "ticket_id": ticket_id, | |
| "timestamp": datetime.utcnow().isoformat(), | |
| }) | |
| while elapsed < _MAX_WAIT_SECONDS: | |
| await asyncio.sleep(_POLL_INTERVAL) | |
| elapsed += _POLL_INTERVAL | |
| rec = await repo.get(ticket_id) | |
| current_status = TriageStatus(rec["status"]) if rec else None | |
| # Only emit an event if status changed (avoids flooding the client) | |
| if current_status != last_status: | |
| last_status = current_status | |
| if current_status == TriageStatus.PROCESSING: | |
| yield _sse_event({ | |
| "type": "status", | |
| "status": "processing", | |
| "message": "AI agents are analyzing your ticket...", | |
| "elapsed_ms": int(elapsed * 1000), | |
| }) | |
| elif current_status == TriageStatus.HITL: | |
| yield _sse_event({ | |
| "type": "hitl", | |
| "status": "hitl", | |
| "message": "Ticket flagged for human review.", | |
| "hitl_reason": rec.get("hitl_reason") if rec else None, | |
| "elapsed_ms": int(elapsed * 1000), | |
| }) | |
| yield _sse_event({"type": "done"}) | |
| return | |
| elif current_status == TriageStatus.COMPLETE: | |
| from src.api.routers.triage import row_to_response | |
| res_obj = row_to_response(rec) if rec else None | |
| yield _sse_event({ | |
| "type": "complete", | |
| "status": "complete", | |
| "category": res_obj.category if res_obj else None, | |
| "priority": res_obj.priority.value if res_obj and res_obj.priority else None, | |
| "confidence": res_obj.confidence if res_obj else 0.85, | |
| "churn_risk": res_obj.churn_risk if res_obj else "low", | |
| "hitl_required": res_obj.hitl_required if res_obj else False, | |
| "processing_ms": res_obj.processing_ms if res_obj else None, | |
| "elapsed_ms": int(elapsed * 1000), | |
| }) | |
| yield _sse_event({"type": "done"}) | |
| return | |
| elif current_status == TriageStatus.FAILED: | |
| yield _sse_event({ | |
| "type": "error", | |
| "status": "failed", | |
| "error": rec.get("error_message", "Unknown error") if rec else "Unknown error", | |
| "elapsed_ms": int(elapsed * 1000), | |
| }) | |
| yield _sse_event({"type": "done"}) | |
| return | |
| # Send a keep-alive comment every 10 seconds (prevents proxy timeouts) | |
| if elapsed % 10 < _POLL_INTERVAL: | |
| yield ": keep-alive\n\n" | |
| # Timeout — stream ran too long | |
| yield _sse_event({ | |
| "type": "timeout", | |
| "message": f"Stream timed out after {_MAX_WAIT_SECONDS}s. " | |
| f"Poll GET /api/v1/triage/{ticket_id} for result.", | |
| }) | |
| return StreamingResponse( | |
| event_generator(), | |
| media_type="text/event-stream", | |
| headers={ | |
| "Cache-Control": "no-cache", | |
| "X-Accel-Buffering": "no", # Disable nginx buffering (critical for SSE) | |
| "Connection": "keep-alive", | |
| }, | |
| ) | |
| def _sse_event(data: dict) -> str: | |
| """Format a dictionary as a W3C-compliant SSE event string.""" | |
| return f"data: {json.dumps(data)}\n\n" | |