""" Autopsy API — post-hoc root cause analysis for failed actions. """ from fastapi import APIRouter from backend.analysis.failure_autopsy import FailureAutopsy from backend.analysis.timeline_store import STORE as TIMELINE_STORE from backend.api.replay import SESSION_DB router = APIRouter(prefix="/autopsy", tags=["autopsy"]) # ----------------------------- # RUN AUTOPSY # ----------------------------- @router.get("/{session_id}/{action}") def run_autopsy(session_id: str, action: str): """Run a failure autopsy for `action` against the replay session `session_id`.""" # Try to fetch events for this session from SessionDB events = SESSION_DB.get_replay_stream(session_id) if not events: # Fallback: use the live timeline store journal = TIMELINE_STORE else: # Wrap the session's event list in a tiny shim that quacks like # the TimelineStore API the FailureAutopsy expects. journal = _JournalShim(events) autopsy = FailureAutopsy(journal) return autopsy.report(action) # ----------------------------- # Legacy wrapper for api.py facade # ----------------------------- def autopsy_endpoint(session_id, action=None): """Legacy entrypoint — caller passed only session_id; we look up failures across all known sessions.""" events = SESSION_DB.get_replay_stream(session_id) if not events: return {"error": "session not found"} journal = _JournalShim(events) autopsy = FailureAutopsy(journal) # If no action given, report on the first failure we find if action is None: failures = autopsy.find_failures() if not failures: return {"status": "no_failures_in_session", "session_id": session_id} action = failures[0].get("data", failures[0]).get("action") if not action: return {"status": "no_actionable_failure", "session_id": session_id} return autopsy.report(action) class _JournalShim: """Minimal adapter that lets FailureAutopsy work against a plain list of events (from a replay session) instead of a full TimelineStore.""" def __init__(self, events): self.events = events def find_failures(self): return [ e for e in self.events if e.get("type") in ("failure", "task_update", "pipeline_update") and (e.get("state") == "failed" or e.get("data", {}).get("state") == "failed") ] def trace_action(self, action_name): return [ e for e in self.events if (e.get("action") == action_name) or (e.get("data", {}).get("action") == action_name) ]