From 97f428681084c1bff04108d8d43032ba1bb124b1 Mon Sep 17 00:00:00 2001 From: Logan Date: Mon, 4 May 2026 01:46:56 -0400 Subject: [PATCH] Add debugging --- .../app/internal/incident_correlator.py | 42 ++++++++++++++++++- drb-frontend/components/CallRow.tsx | 23 ++++++++++ drb-frontend/lib/types.ts | 7 ++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/drb-c2-core/app/internal/incident_correlator.py b/drb-c2-core/app/internal/incident_correlator.py index a91660c..7cc61d9 100644 --- a/drb-c2-core/app/internal/incident_correlator.py +++ b/drb-c2-core/app/internal/incident_correlator.py @@ -128,6 +128,7 @@ async def correlate_call( coords: Optional[dict] = location_coords or call_doc.get("location_coords") matched_incident: Optional[dict] = None + corr_debug: dict = {} # A "thin" call carries no scene-identifying information — it is a pure # status transmission (10-4, en route, acknowledgement). Detected by the @@ -170,6 +171,10 @@ async def correlate_call( # Status/ack call — no scene data to reason about. # Attach to whichever recent incident was most recently active on this TGID. matched_incident = max(tg_recent, key=lambda inc: inc.get("updated_at", "")) + corr_debug = { + "corr_path": "fast/thin", + "corr_incident_idle_min": round(_incident_idle_minutes(matched_incident, now), 1), + } logger.info( f"Correlator fast-path (thin→last TGID incident): " f"call {call_id} → {matched_incident['incident_id']}" @@ -181,6 +186,10 @@ async def correlate_call( settings.location_proximity_km, is_dispatch=is_dispatch, ): matched_incident = candidate + corr_debug = { + "corr_path": "fast/single", + "corr_incident_idle_min": round(_incident_idle_minutes(candidate, now), 1), + } logger.info( f"Correlator fast-path: call {call_id} → {candidate['incident_id']}" ) @@ -193,6 +202,11 @@ async def correlate_call( matched_incident = _disambiguate( tg_recent, call_units, call_vehicles, coords, call_embedding ) + corr_debug = { + "corr_path": "fast/disambig", + "corr_incident_idle_min": round(_incident_idle_minutes(matched_incident, now), 1), + "corr_candidates": len(tg_recent), + } logger.info( f"Correlator fast-path (disambig {len(tg_recent)} candidates): " f"call {call_id} → {matched_incident['incident_id']}" @@ -210,6 +224,7 @@ async def correlate_call( ) if dist_km <= settings.location_proximity_km: matched_incident = inc + corr_debug = {"corr_path": "location", "corr_distance_km": round(dist_km, 3)} logger.info( f"Correlator location-path: call {call_id} → {inc['incident_id']} " f"(dist={dist_km:.2f}km)" @@ -246,10 +261,15 @@ async def correlate_call( best_cross_inc = inc if best_cross_inc and best_cross_score >= settings.embedding_cross_tg_threshold: matched_incident = best_cross_inc + shared = len(call_unit_set & set(best_cross_inc.get("units") or [])) + corr_debug = { + "corr_path": "cross-tg", + "corr_score": round(best_cross_score, 4), + "corr_shared_units": shared, + } logger.info( f"Correlator cross-TG path: call {call_id} → {best_cross_inc['incident_id']} " - f"(sim={best_cross_score:.3f}, " - f"shared_units={len(call_unit_set & set(best_cross_inc.get('units') or []))})" + f"(sim={best_cross_score:.3f}, shared_units={shared})" ) # ── 3. Slow path: embedding similarity (time-limited, same type) ────────── @@ -282,6 +302,11 @@ async def correlate_call( ) if dist_km <= settings.location_proximity_km * 4: matched_incident = best_inc + corr_debug = { + "corr_path": "slow", + "corr_score": round(best_score, 4), + "corr_distance_km": round(dist_km, 3), + } logger.info( f"Correlator slow-path: call {call_id} → {best_inc['incident_id']} " f"(sim={best_score:.3f}, dist={dist_km:.2f}km)" @@ -290,6 +315,10 @@ async def correlate_call( # High-confidence semantic match; geocode unavailable on one or # both sides — content similarity alone is sufficient evidence. matched_incident = best_inc + corr_debug = { + "corr_path": "slow/no-location", + "corr_score": round(best_score, 4), + } logger.info( f"Correlator slow-path (high-confidence, no location): " f"call {call_id} → {best_inc['incident_id']} (sim={best_score:.3f})" @@ -309,10 +338,19 @@ async def correlate_call( tags, location, location_coords, call_units, call_vehicles, call_embedding, call_severity, now, ) + corr_debug["corr_path"] = "new" else: # No match and either no type or creation suppressed — nothing to do return None + # Persist the correlation decision to the call document so it can be + # inspected in Firestore or the admin UI without log-scraping. + if corr_debug: + try: + await fstore.doc_set("calls", call_id, corr_debug) + except Exception as e: + logger.warning(f"Could not write corr_debug for call {call_id}: {e}") + return incident_id diff --git a/drb-frontend/components/CallRow.tsx b/drb-frontend/components/CallRow.tsx index de1f232..2e4435b 100644 --- a/drb-frontend/components/CallRow.tsx +++ b/drb-frontend/components/CallRow.tsx @@ -138,6 +138,29 @@ export function CallRow({ call, systemName, isAdmin }: Props) { )} + {/* Correlation debug — admin only */} + {isAdmin && call.corr_path && ( +
+ corr: + {call.corr_path} + {call.corr_incident_idle_min != null && ( + idle {call.corr_incident_idle_min}min + )} + {call.corr_score != null && ( + sim={call.corr_score.toFixed(3)} + )} + {call.corr_distance_km != null && ( + dist={call.corr_distance_km}km + )} + {call.corr_shared_units != null && ( + {call.corr_shared_units} shared units + )} + {call.corr_candidates != null && ( + {call.corr_candidates} candidates + )} +
+ )} + {/* Transcript */} {editing ? (
e.stopPropagation()}> diff --git a/drb-frontend/lib/types.ts b/drb-frontend/lib/types.ts index 694e0a2..7b38a8b 100644 --- a/drb-frontend/lib/types.ts +++ b/drb-frontend/lib/types.ts @@ -56,6 +56,13 @@ export interface CallRecord { location: string | null; tags: string[]; status: "active" | "ended"; + // Correlation debug — written by the correlator, present after a call is linked + corr_path?: string | null; + corr_score?: number | null; + corr_distance_km?: number | null; + corr_incident_idle_min?: number | null; + corr_shared_units?: number | null; + corr_candidates?: number | null; } export interface IncidentRecord {