From 3a944f35c1da00dfee74aa677593e9243d7d25e6 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Mon, 7 Sep 2026 16:53:03 -0400 Subject: [PATCH] correlator: give the LLM tier what it needs to link, stop it defaulting to "new" (server-26#115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2026-09-07 measurement window (CORRELATION_REVIEW_0907.md) showed the consensus tiebreaker was the dominant over-split driver: it ran on 45% of calls and resolved link/orphan disagreements as "new" ~24/25 of the time, shattering one Mohegan Park car-alarm job into 9 incidents and opening ~7 incidents from radio checks / roll calls. Two causes, two fixes: 1. `_inc_summary` gave the model `id|type|loc|units|tags|idle` — no title, no talkgroup. It literally could not see that two "car alarms, Mohegan Park Ave/Avenue" incidents on TG 9560 were the same. Now includes the incident title (the strongest same-event signal) and talkgroup. 2. `_RULES` told the model "orphan when in doubt — conservative is always correct". For a system that over-splits, that is backwards: a wrong link is cheap, a duplicate incident is the failure. Rewritten to: prefer link for a plausible same-talkgroup continuation (low bar), reserve "new" for a genuinely different event, and explicitly "orphan" non-incidents (radio checks, roll call, 10-8/10-98, mileage logs). Plus `_extract_road_ids` now canonicalises street-type synonyms (Avenue→ave, Street→st, Road→rd, ...), so "Mohegan Park Avenue" and "Mohegan Park Ave" share a road id — that one difference was splitting the car-alarm incident. +tests/test_correlator_115.py. Full c2-core suite green (sandboxed venv). Bigger levers deferred to follow-ups: the consensus escalation itself (should a cheap-LLM "orphan" ever reach a tiebreak?), a first-class road-overlap fit signal in _call_fits_incident, geocode coverage. Co-Authored-By: Claude Sonnet 5 --- .../app/internal/incident_correlator.py | 25 +++++++-- drb-c2-core/app/internal/llm_correlator.py | 34 ++++++++++-- drb-c2-core/tests/test_correlator_115.py | 54 +++++++++++++++++++ 3 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 drb-c2-core/tests/test_correlator_115.py diff --git a/drb-c2-core/app/internal/incident_correlator.py b/drb-c2-core/app/internal/incident_correlator.py index f94c2bf..53a7922 100644 --- a/drb-c2-core/app/internal/incident_correlator.py +++ b/drb-c2-core/app/internal/incident_correlator.py @@ -108,16 +108,31 @@ _ROAD_RE = re.compile( ) +# Street-type synonyms collapsed to one token so "Mohegan Park Avenue" and +# "Mohegan Park Ave" produce the same road id (server-26#115 — that one +# difference was splitting a car-alarm incident into two). +_ROAD_SUFFIX_CANON = { + "avenue": "ave", "street": "st", "road": "rd", "drive": "dr", + "boulevard": "blvd", "lane": "ln", "court": "ct", "place": "pl", + "highway": "hwy", "parkway": "pkwy", +} + + def _extract_road_ids(text: str) -> set[str]: """ Extract normalised road/route identifiers from a location string. e.g. "suspect east on Route 202" → {"route 202"} - "at Main Street and Oak Ave" → {"main street", "oak ave"} + "at Main Street and Oak Ave" → {"main st", "oak ave"} """ - return { - re.sub(r"[\s.\-]+", " ", m.group().lower()).strip() - for m in _ROAD_RE.finditer(text) - } + ids: set[str] = set() + for m in _ROAD_RE.finditer(text): + key = re.sub(r"[\s.\-]+", " ", m.group().lower()).strip() + parts = key.split() + if parts and parts[-1] in _ROAD_SUFFIX_CANON: + parts[-1] = _ROAD_SUFFIX_CANON[parts[-1]] + key = " ".join(parts) + ids.add(key) + return ids def _location_mentions_road_overlap(new_location: str, inc_mentions: list[str]) -> bool: diff --git a/drb-c2-core/app/internal/llm_correlator.py b/drb-c2-core/app/internal/llm_correlator.py index a5ef817..05178fe 100644 --- a/drb-c2-core/app/internal/llm_correlator.py +++ b/drb-c2-core/app/internal/llm_correlator.py @@ -45,7 +45,18 @@ def _fmt_idle(inc: dict, now: datetime) -> str: def _inc_summary(inc: dict, now: datetime) -> str: + # server-26#115: the model was given no title and no talkgroup, so it + # could not tell that "car alarms, Mohegan Park Ave" and "car alarms, + # Mohegan Park Avenue" on the same channel were one incident — it defaulted + # to "new". Title is the single strongest human-readable signal for "is + # this the same event"; talkgroup is what makes same-channel continuation + # obvious. parts = [f"id:{inc['incident_id']}", f"type:{inc.get('type') or '?'}"] + tgs = inc.get("talkgroup_ids") or [] + if tgs: + parts.append(f"tg:[{', '.join(str(t) for t in tgs[:3])}]") + if inc.get("title"): + parts.append(f"title:{inc['title']!r}") if inc.get("location"): parts.append(f"loc:{inc['location']}") units = inc.get("units") or [] @@ -88,11 +99,24 @@ def _call_block(ctx: dict) -> str: _SCHEMA = '{"action": "link" | "new" | "orphan", "incident_id": "", "reasoning": ""}' _RULES = """ -Rules: -- "link" only with clear positive evidence: same units, same geocoded location, or semantically identical scene on the same talkgroup within the last few minutes. -- A call on a DIFFERENT talkgroup than an incident requires unit overlap or geocoded location match — topic similarity alone is not enough. -- "new" only if the call has a clear incident_type AND describes a distinct, identifiable scene. -- "orphan" when in doubt — conservative is always correct. +Rules (this system OVER-SPLITS — a real incident routinely gets shattered into +5-10 duplicates. A wrong link is cheap; a duplicate incident is the failure +mode. Bias accordingly.): +- Prefer "link" when the call plausibly continues a recent incident ON THE SAME + TALKGROUP: same or overlapping units, the same or an adjacent location (treat + "Ave"/"Avenue", "St"/"Street", "Rd"/"Road" as identical; a house number plus + the same street is the same place), the same subject/vehicle/case number, or a + follow-up beat ("units clearing", "negative contact", "tow en route", "event + number 214-201", a status update) to an incident that is only a few minutes + idle. The bar for "link" on the same talkgroup is LOW. +- Reserve "new" for a call that clearly describes a DIFFERENT event from every + recent incident — a different place, different units, and a different subject, + not merely a different transmission about the same job. +- "orphan" a call that is not an incident at all: radio checks, roll call, + a unit marking on/off duty or 10-8/10-98, mileage/log entries, a bare + acknowledgement. Do not open a "new" incident for these. +- A call on a DIFFERENT talkgroup than an incident still requires unit overlap + or a geocoded/location match — topic similarity alone is not enough there. - Do NOT link just because both calls involve police or both mention a road. """ diff --git a/drb-c2-core/tests/test_correlator_115.py b/drb-c2-core/tests/test_correlator_115.py new file mode 100644 index 0000000..2d8e671 --- /dev/null +++ b/drb-c2-core/tests/test_correlator_115.py @@ -0,0 +1,54 @@ +""" +server-26#115 — the tiebreaker manufactured incidents because it was blind to +what would tell it two incidents are one. + +Two low-risk supports for the reframed prompt: + 1. `_extract_road_ids` collapses street-type synonyms, so "Mohegan Park Ave" + and "Mohegan Park Avenue" share a road id (they were splitting one + car-alarm incident into two). + 2. `_inc_summary` now carries the incident title and talkgroup, the two + signals the model needs to recognise a same-channel continuation. +""" +from datetime import datetime, timezone + +from app.internal.incident_correlator import ( + _extract_road_ids, _location_mentions_road_overlap, +) +from app.internal.llm_correlator import _inc_summary + +NOW = datetime(2026, 9, 7, 8, 0, 0, tzinfo=timezone.utc) + + +def test_avenue_and_ave_are_the_same_road_id(): + assert _extract_road_ids("Mohegan Park Avenue") == _extract_road_ids("Mohegan Park Ave") + assert _extract_road_ids("191 Broadway Street") == _extract_road_ids("191 Broadway St") + assert _extract_road_ids("North State Road") == _extract_road_ids("North State Rd") + + +def test_road_overlap_matches_across_the_synonym(): + assert _location_mentions_road_overlap("multiple car alarms Mohegan Park Avenue", + ["patrol to Mohegan Park Ave"]) is True + # still discriminates genuinely different streets + assert _location_mentions_road_overlap("Oak Avenue", ["Elm Avenue"]) is False + + +def test_inc_summary_carries_title_and_talkgroup(): + s = _inc_summary({ + "incident_id": "abc123", + "type": "police", + "talkgroup_ids": [9560], + "title": "Nuisance Alarm at Mohegan Park Ave", + "location": "Mohegan Park Ave", + "units": ["Headquarters"], + "tags": ["car-alarm"], + "updated_at": NOW.isoformat(), + }, NOW) + assert "title:'Nuisance Alarm at Mohegan Park Ave'" in s + assert "tg:[9560]" in s + assert "id:abc123" in s + + +def test_inc_summary_omits_missing_optional_fields(): + s = _inc_summary({"incident_id": "x", "updated_at": NOW.isoformat()}, NOW) + assert "title:" not in s and "tg:" not in s and "loc:" not in s + assert s.startswith("id:x")