diff --git a/drb-c2-core/app/internal/incident_correlator.py b/drb-c2-core/app/internal/incident_correlator.py index 086ab05..18db086 100644 --- a/drb-c2-core/app/internal/incident_correlator.py +++ b/drb-c2-core/app/internal/incident_correlator.py @@ -283,6 +283,13 @@ async def correlate_call( ) elif len(tg_recent) == 1: candidate = tg_recent[0] + logger.info( + f"Correlator fast/single: call {call_id} vs incident {candidate['incident_id']} " + f"tg_name={talkgroup_name!r} is_dispatch={is_dispatch} " + f"idle={round(_incident_idle_minutes(candidate, now), 1)}min " + f"call_units={call_units} inc_units={candidate.get('units')} " + f"call_coords={'yes' if coords else 'no'} inc_coords={'yes' if candidate.get('location_coords') else 'no'}" + ) fit, fit_signal = _call_fits_incident( candidate, call_units, call_vehicles, coords, settings.location_proximity_km, is_dispatch=is_dispatch, @@ -294,13 +301,14 @@ async def correlate_call( "corr_path": "fast/single", "corr_incident_idle_min": round(_incident_idle_minutes(candidate, now), 1), "corr_fit_signal": fit_signal, + "corr_is_dispatch": is_dispatch, } if fit_signal == "unit_overlap" and call_units: inc_unit_set = set(candidate.get("units") or []) corr_debug["corr_matched_units"] = [u for u in call_units if u in inc_unit_set] logger.info( f"Correlator fast-path: call {call_id} → {candidate['incident_id']} " - f"(signal={fit_signal})" + f"(signal={fit_signal}, is_dispatch={is_dispatch})" ) else: logger.info( @@ -314,6 +322,13 @@ async def correlate_call( # Disambiguate picks the best candidate, but still verify the call # actually fits before committing — a new unrelated call on a busy # dispatch channel should create its own incident, not be force-merged. + logger.info( + f"Correlator fast/disambig: call {call_id} vs incident {candidate['incident_id']} " + f"tg_name={talkgroup_name!r} is_dispatch={is_dispatch} " + f"idle={round(_incident_idle_minutes(candidate, now), 1)}min " + f"call_units={call_units} inc_units={candidate.get('units')} " + f"call_coords={'yes' if coords else 'no'} inc_coords={'yes' if candidate.get('location_coords') else 'no'}" + ) fit, fit_signal = _call_fits_incident( candidate, call_units, call_vehicles, coords, settings.location_proximity_km, is_dispatch=is_dispatch, @@ -326,6 +341,7 @@ async def correlate_call( "corr_incident_idle_min": round(_incident_idle_minutes(candidate, now), 1), "corr_candidates": len(tg_recent), "corr_fit_signal": fit_signal, + "corr_is_dispatch": is_dispatch, } if fit_signal == "unit_overlap" and call_units: inc_unit_set = set(candidate.get("units") or []) @@ -770,10 +786,12 @@ def _call_fits_incident( they are intercepted before it in correlate_call. """ idle_min = _incident_idle_minutes(inc, now) if now is not None else 9999.0 + inc_id = inc.get("incident_id", "?") # ── 1. Unit overlap ─────────────────────────────────────────────────────── inc_units = set(inc.get("units") or []) - if inc_units and call_units and any(u in inc_units for u in call_units): + matched_units = [u for u in call_units if u in inc_units] if (inc_units and call_units) else [] + if matched_units: if is_dispatch: if call_coords: # Hard location conflict: geocoded on both sides and clearly different. @@ -784,6 +802,7 @@ def _call_fits_incident( inc_coords_u["lat"], inc_coords_u["lng"], ) if dist_km > proximity_km: + logger.info(f" fits[{inc_id}]: unit_overlap({matched_units}) but location_conflict dist={dist_km:.2f}km → unit_loc_conflict") return False, "unit_loc_conflict" elif call_embedding and idle_min >= 15: # No geocode available AND old incident: use content divergence as a @@ -796,12 +815,15 @@ def _call_fits_incident( if inc_emb_u: sim = _cosine_similarity(call_embedding, inc_emb_u) if sim < 0.82: + logger.info(f" fits[{inc_id}]: unit_overlap({matched_units}) but content_divergence sim={sim:.3f} → content_divergence") return False, "content_divergence" + logger.info(f" fits[{inc_id}]: unit_overlap matched={matched_units} is_dispatch={is_dispatch} → unit_overlap") return True, "unit_overlap" # ── 2. Vehicle overlap ──────────────────────────────────────────────────── inc_vehicles = set(inc.get("vehicles") or []) if inc_vehicles and call_vehicles and any(v in inc_vehicles for v in call_vehicles): + logger.info(f" fits[{inc_id}]: vehicle_overlap → vehicle_overlap") return True, "vehicle_overlap" # ── 3. Location proximity ───────────────────────────────────────────────── @@ -812,11 +834,19 @@ def _call_fits_incident( inc_coords["lat"], inc_coords["lng"], ) if dist_km <= proximity_km: + logger.info(f" fits[{inc_id}]: location_proximity dist={dist_km:.2f}km → location_proximity") return True, "location_proximity" # Conflicting location, no other positive signal → different scene. + logger.info(f" fits[{inc_id}]: location_conflict dist={dist_km:.2f}km → location_conflict") return False, "location_conflict" # ── 4. No positive signals ──────────────────────────────────────────────── + logger.info( + f" fits[{inc_id}]: no positive signal — is_dispatch={is_dispatch} idle={idle_min:.1f}min " + f"inc_units={list(inc_units)} call_units={call_units} " + f"inc_vehicles={list(inc_vehicles)} call_vehicles={call_vehicles} " + f"call_coords={call_coords is not None} inc_coords={inc_coords is not None}" + ) if is_dispatch: # Conversational continuity: the call arrived during the same conversation # thread (< 2 min since last incident activity) with no contradicting evidence.