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