Admin Replay: re-run the pipeline over past calls in a sandbox (#170)
Correlation has only ever been measured through live AI windows: days of wall time per change, and the 09-20→22 window was invalidated outright by unfunded AI accounts (#169). Recordings are kept regardless of AI, so the traffic to measure against already exists. - internal/replay.py: runs a time range of real calls through the live pipeline code in original order, clock pinned per call, into replay_runs/{run_id}/calls|incidents. Modes: audio (re-transcribe), transcripts (re-extract), reuse (correlation only from a prior run's scenes). Simulates the idle-resolve and orphan-recorrelation sweeps on virtual time. No alerts, summaries, vocab, AI-health alerts or pending terms. One run at a time, <=5000 calls, <=7 days. - firestore.py: ContextVar sandbox redirect for calls/incidents. - clock.py: ContextVar-pinnable now(), used on the correlation path. - feature_flags.py: ContextVar flag override so replay runs with live AI off. - upload.py: scene loop extracted to _extract_and_correlate, shared by the live pipeline and replay so replay measures the code that runs live. - resolved_via on every incident resolve, so a real clear can be told from the idle timeout — live and in replay. - routers/replay.py + /admin Replay tab: estimate, start, compare runs, drill into incidents with audio. Reviewed by drb-correlation-review; its leak and fidelity findings are fixed and covered by tests. c2-core: 456 pass. Frontend typecheck not run (no Node on the authoring box). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e79b8bc37d
commit
aff3f16d32
@@ -233,6 +233,28 @@ export const c2api = {
|
||||
getCorrelationDebug: (limit: number, orphanHours: number) =>
|
||||
request<unknown>(`/admin/debug/correlation?limit=${limit}&orphan_hours=${orphanHours}`),
|
||||
|
||||
// Replay (admin) — re-run the pipeline over past calls into a sandbox.
|
||||
// See drb-c2-core/app/internal/replay.py.
|
||||
estimateReplay: (p: { date_from: string; date_to: string; mode: import("@/lib/types").ReplayMode; system_ids?: string[] }) => {
|
||||
const qs = new URLSearchParams({ date_from: p.date_from, date_to: p.date_to, mode: p.mode });
|
||||
if (p.system_ids?.length) qs.set("system_ids", p.system_ids.join(","));
|
||||
return request<import("@/lib/types").ReplayEstimate>(`/admin/replay/estimate?${qs}`);
|
||||
},
|
||||
startReplay: (body: {
|
||||
date_from: string; date_to: string; mode: import("@/lib/types").ReplayMode;
|
||||
system_ids?: string[]; source_run_id?: string | null; label?: string;
|
||||
}) =>
|
||||
request<import("@/lib/types").ReplayRun>("/admin/replay", { method: "POST", body: JSON.stringify(body) }),
|
||||
listReplays: () =>
|
||||
request<{ runs: import("@/lib/types").ReplayRun[]; active_run_id: string | null }>("/admin/replay"),
|
||||
getReplay: (runId: string) => request<import("@/lib/types").ReplayRun>(`/admin/replay/${runId}`),
|
||||
cancelReplay: (runId: string) =>
|
||||
request<{ ok: boolean }>(`/admin/replay/${runId}/cancel`, { method: "POST" }),
|
||||
deleteReplay: (runId: string) =>
|
||||
request<{ ok: boolean }>(`/admin/replay/${runId}`, { method: "DELETE" }),
|
||||
getReplayIncidents: (runId: string) =>
|
||||
request<import("@/lib/types").ReplayIncidents>(`/admin/replay/${runId}/incidents`),
|
||||
|
||||
// Preferred bot token per system
|
||||
setPreferredToken: (tokenId: string, systemId: string) =>
|
||||
request<{ ok: boolean; preferred_for_system_id: string | null }>(`/tokens/${tokenId}/prefer/${systemId}`, { method: "PUT" }),
|
||||
|
||||
@@ -306,3 +306,93 @@ export interface TalkgroupPending {
|
||||
talkgroup_name?: string;
|
||||
pending: PendingLocalTerm[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Replay (admin) — drb-c2-core/app/internal/replay.py
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type ReplayMode = "audio" | "transcripts" | "reuse";
|
||||
|
||||
export interface ReplayEstimate {
|
||||
calls: number;
|
||||
calls_with_transcript: number;
|
||||
calls_with_audio: number;
|
||||
audio_minutes: number;
|
||||
est_cost_usd: number;
|
||||
truncated: boolean;
|
||||
max_calls: number;
|
||||
}
|
||||
|
||||
export interface ReplayMetrics {
|
||||
calls: number;
|
||||
calls_linked: number;
|
||||
calls_orphaned: number;
|
||||
incidents: number;
|
||||
single_call_incidents: number;
|
||||
single_call_pct: number | null;
|
||||
median_calls_per_incident: number | null;
|
||||
max_calls_in_incident: number | null;
|
||||
incidents_with_units_cleared: number;
|
||||
incidents_with_coords: number;
|
||||
resolved_via: Record<string, number>;
|
||||
corr_path: Record<string, number>;
|
||||
corr_consensus: Record<string, number>;
|
||||
llm_decisions: number;
|
||||
est_cost_usd: number;
|
||||
}
|
||||
|
||||
export interface ReplayRun {
|
||||
run_id: string;
|
||||
label: string;
|
||||
mode: ReplayMode;
|
||||
source_run_id: string | null;
|
||||
date_from: string;
|
||||
date_to: string;
|
||||
system_ids: string[];
|
||||
git_sha: string;
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
finished_at?: string;
|
||||
status: "running" | "done" | "cancelled" | "failed" | "interrupted";
|
||||
estimate: Omit<ReplayEstimate, "truncated" | "max_calls">;
|
||||
progress: { total: number; done: number; errors: number; skipped?: number };
|
||||
metrics: ReplayMetrics | null;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export interface ReplayCallRow {
|
||||
call_id: string;
|
||||
started_at: string;
|
||||
talkgroup_name: string | null;
|
||||
transcript: string | null;
|
||||
units: string[] | null;
|
||||
cleared_units: string[] | null;
|
||||
location: string | null;
|
||||
skip_reason: string | null;
|
||||
corr_path: string[];
|
||||
incident_ids: string[];
|
||||
}
|
||||
|
||||
export interface ReplayIncident {
|
||||
incident_id: string;
|
||||
title: string | null;
|
||||
type: string | null;
|
||||
severity: string | null;
|
||||
status: string;
|
||||
resolved_via: string | null;
|
||||
started_at: string;
|
||||
updated_at: string | null;
|
||||
resolved_at: string | null;
|
||||
location: string | null;
|
||||
location_coords: { lat: number; lng: number } | null;
|
||||
units: string[] | null;
|
||||
units_active: string[] | null;
|
||||
units_cleared: string[] | null;
|
||||
talkgroup_ids: number[] | null;
|
||||
calls: ReplayCallRow[];
|
||||
}
|
||||
|
||||
export interface ReplayIncidents {
|
||||
incidents: ReplayIncident[];
|
||||
orphans: ReplayCallRow[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user