Skip to content

Commit 3d7ad1d

Browse files
committed
feat: Add API endpoints for event replay, listing, validation, and export.
1 parent 7c5663e commit 3d7ad1d

1 file changed

Lines changed: 194 additions & 0 deletions

File tree

api/replay_endpoints.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# api/replay_endpoints.py
2+
from fastapi import APIRouter, HTTPException, BackgroundTasks
3+
from pydantic import BaseModel
4+
from typing import List, Optional
5+
from pathlib import Path
6+
7+
from src.replay.replay_engine import ArtifactReplayEngine
8+
9+
router = APIRouter(prefix="/replay", tags=["replay"])
10+
11+
# Global replay engine
12+
replay_engine = None
13+
14+
class ReplayRequest(BaseModel):
15+
event_id: str
16+
recalculate: bool = True
17+
18+
class BatchReplayRequest(BaseModel):
19+
event_ids: Optional[List[str]] = None
20+
recalculate: bool = True
21+
22+
class ExportRequest(BaseModel):
23+
event_id: str
24+
output_dir: str = "./exports"
25+
26+
@router.on_event("startup")
27+
async def startup_event():
28+
"""Initialize replay engine on startup"""
29+
global replay_engine
30+
artifacts_path = Path("./artifacts")
31+
artifacts_path.mkdir(exist_ok=True)
32+
replay_engine = ArtifactReplayEngine(artifacts_path)
33+
34+
@router.get("/events")
35+
async def list_events(limit: int = 20):
36+
"""List available events for replay"""
37+
if not replay_engine:
38+
raise HTTPException(status_code=500, detail="Replay engine not initialized")
39+
40+
event_dirs = replay_engine.find_event_directories()
41+
events = []
42+
43+
for event_dir in event_dirs[:limit]:
44+
artifacts = replay_engine.load_event_artifacts(event_dir)
45+
if artifacts:
46+
events.append({
47+
"event_id": event_dir.name,
48+
"incident_preview": artifacts['incident_text'][:100] + "..." if len(artifacts['incident_text']) > 100 else artifacts['incident_text'],
49+
"agents": list(artifacts['evidence_extractions'].keys()),
50+
"artifact_count": len(artifacts.get('evidence_extractions', {})) + len(artifacts.get('agent_claims', {}))
51+
})
52+
53+
return {
54+
"total_events": len(event_dirs),
55+
"events_shown": min(limit, len(event_dirs)),
56+
"events": events
57+
}
58+
59+
@router.post("/replay")
60+
async def replay_event(request: ReplayRequest):
61+
"""Replay a specific event"""
62+
if not replay_engine:
63+
raise HTTPException(status_code=500, detail="Replay engine not initialized")
64+
65+
try:
66+
result = replay_engine.replay_event(request.event_id, request.recalculate)
67+
68+
# Convert to serializable format
69+
result_serializable = {
70+
"event_id": result["event_id"],
71+
"status": result["status"],
72+
"decision": result["convergence_metrics"].decision,
73+
"residual_disagreement": result["convergence_metrics"].residual_disagreement,
74+
"deterministic_check": result.get("deterministic_check", "N/A")
75+
}
76+
77+
if "comparison_with_original" in result:
78+
result_serializable["comparison"] = result["comparison_with_original"]
79+
80+
return result_serializable
81+
82+
except ValueError as e:
83+
raise HTTPException(status_code=404, detail=str(e))
84+
except Exception as e:
85+
raise HTTPException(status_code=500, detail=str(e))
86+
87+
@router.post("/batch-replay")
88+
async def batch_replay(request: BatchReplayRequest, background_tasks: BackgroundTasks):
89+
"""Batch replay multiple events"""
90+
if not replay_engine:
91+
raise HTTPException(status_code=500, detail="Replay engine not initialized")
92+
93+
try:
94+
result = replay_engine.batch_replay(request.event_ids, request.recalculate)
95+
return result
96+
except Exception as e:
97+
raise HTTPException(status_code=500, detail=str(e))
98+
99+
@router.get("/validate/{event_id}")
100+
async def validate_event(event_id: str):
101+
"""Validate artifact contracts for an event"""
102+
if not replay_engine:
103+
raise HTTPException(status_code=500, detail="Replay engine not initialized")
104+
105+
try:
106+
result = replay_engine.validate_artifact_contracts(event_id)
107+
return result
108+
except Exception as e:
109+
raise HTTPException(status_code=500, detail=str(e))
110+
111+
@router.get("/batch-validate")
112+
async def batch_validate():
113+
"""Batch validate all events"""
114+
if not replay_engine:
115+
raise HTTPException(status_code=500, detail="Replay engine not initialized")
116+
117+
try:
118+
event_dirs = replay_engine.find_event_directories()
119+
results = []
120+
121+
for event_dir in event_dirs[:50]: # Limit to 50 for performance
122+
result = replay_engine.validate_artifact_contracts(event_dir.name)
123+
results.append({
124+
"event_id": event_dir.name,
125+
"valid": result["valid"],
126+
"missing_artifacts": result.get("missing_artifacts", []),
127+
"invalid_artifacts": result.get("invalid_artifacts", [])
128+
})
129+
130+
return {
131+
"total_events": len(event_dirs),
132+
"validated": len(results),
133+
"results": results
134+
}
135+
except Exception as e:
136+
raise HTTPException(status_code=500, detail=str(e))
137+
138+
@router.post("/export")
139+
async def export_event(request: ExportRequest):
140+
"""Export event for reproducibility"""
141+
if not replay_engine:
142+
raise HTTPException(status_code=500, detail="Replay engine not initialized")
143+
144+
try:
145+
export_dir = replay_engine.export_event_for_reproducibility(
146+
request.event_id,
147+
Path(request.output_dir)
148+
)
149+
150+
# Read manifest
151+
manifest_path = export_dir / "manifest.json"
152+
if manifest_path.exists():
153+
with open(manifest_path, 'r') as f:
154+
manifest = json.load(f)
155+
156+
return {
157+
"event_id": request.event_id,
158+
"export_directory": str(export_dir),
159+
"manifest": manifest
160+
}
161+
162+
except ValueError as e:
163+
raise HTTPException(status_code=404, detail=str(e))
164+
except Exception as e:
165+
raise HTTPException(status_code=500, detail=str(e))
166+
167+
@router.get("/deterministic-check/{event_id}")
168+
async def deterministic_check(event_id: str):
169+
"""Check if replay produces identical results"""
170+
if not replay_engine:
171+
raise HTTPException(status_code=500, detail="Replay engine not initialized")
172+
173+
try:
174+
# First replay
175+
result1 = replay_engine.replay_event(event_id, recalculate=True)
176+
177+
# Second replay (should be identical)
178+
result2 = replay_engine.replay_event(event_id, recalculate=True)
179+
180+
comparison = replay_engine._compare_convergence_results(
181+
result1["convergence_metrics"],
182+
result2["convergence_metrics"]
183+
)
184+
185+
return {
186+
"event_id": event_id,
187+
"deterministic": comparison["identical"],
188+
"decision_match": comparison["decision_match"],
189+
"confidence_difference": comparison["confidence_diff"],
190+
"differences": comparison["differences"]
191+
}
192+
193+
except Exception as e:
194+
raise HTTPException(status_code=500, detail=str(e))

0 commit comments

Comments
 (0)