From 9f19750ea6fe92d50d106a148d8a6ee00aabe84d Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sat, 26 Sep 2026 20:52:31 -0400 Subject: [PATCH] stops review + provisional LLM closure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replay of 433b35d (server-26#170): traffic stops now open incidents ("45 Adam" stop, "CM2" stop), but the bridge MVA split 33/56 — one transmission ("transport complete") was read as scene-resolved at 14:44 and an LLM closure was final, so the rest of the MVA opened a new one. - LLM closure is now provisional (reopenable), like a timer close: it is inferred from a single transmission. - review of 433b35d: backstop only matches a unit's own "on a stop" self-report (bare "car stop" mentions and "pull over" dropped), never on MTA/rail/bridge/fire/EMS/DPW talkgroups ("Train 4 holding on the stop"), negation looks 5 words back, and <=5-word reports ("Adam 3 on a stop") get a minimal scene instead of being skipped before the backstop. c2-core: 471 pass. Co-Authored-By: Claude Opus 5.5 --- drb-c2-core/app/internal/intelligence.py | 28 +++++++++++++++++--- drb-c2-core/app/routers/upload.py | 6 +++++ drb-c2-core/tests/test_clearance_tracking.py | 27 ++++++++++++++++++- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/drb-c2-core/app/internal/intelligence.py b/drb-c2-core/app/internal/intelligence.py index 5109b50..bc786e8 100644 --- a/drb-c2-core/app/internal/intelligence.py +++ b/drb-c2-core/app/internal/intelligence.py @@ -267,6 +267,14 @@ async def extract_scenes( if cleared_unit: logger.info(f"Intelligence: call {call_id} — short clearance from {cleared_unit!r}") return [_clearance_scene(transcript, cleared_unit)] + # "Adam 3 on a stop" is the whole report of a stop, and it is <=5 + # words: the self-initiated backstop has to run here too or the most + # common phrasing never opens an incident. + tags, typ, sev = _self_initiated_backstop(transcript, [], None, "routine", talkgroup_name) + if tags: + logger.info(f"Intelligence: call {call_id} — short self-initiated report {tags}") + return [{**_clearance_scene(transcript, ""), "units": [], "cleared_units": [], + "tags": tags, "incident_type": typ, "severity": sev}] return [] try: @@ -419,7 +427,7 @@ async def extract_scenes( ) tags, incident_type, severity = _self_initiated_backstop( - scene_transcript or transcript, tags, incident_type, severity + scene_transcript or transcript, tags, incident_type, severity, talkgroup_name, ) processed.append({ @@ -489,20 +497,32 @@ async def extract_scenes( # the stop was visible only in the archive. The prompt now says so too; this # is the deterministic backstop, because a tag is what the creation gate # counts as substance (incident_correlator.has_event_substance). +# Only a unit's own "on a stop" self-report — a bare "traffic stop"/"car stop" +# mention (a plate lookup on a records channel, a dispatcher's question) is +# left to the prompt, and "pull over" is too common in non-stop traffic +# ("Medic 2 pull over to the side") to trust (review of 433b35d). _SELF_INITIATED = ( - (re.compile(r"\b(on (a|the) (traffic |car |vehicle )?stop|traffic stop|car stop|vehicle stop|" - r"pull(ed|ing)? (a car |a vehicle |him |her |them )?over)\b", re.IGNORECASE), + (re.compile(r"\bon (a|the) (traffic |car |vehicle |motor vehicle )?stop\b", re.IGNORECASE), "traffic-stop"), (re.compile(r"\b((put|show) me out with|out with (a|one) (pedestrian|vehicle|disabled|male|female|" r"subject|party|juvenile))\b", re.IGNORECASE), "self-initiated"), ) -_NEGATED = re.compile(r"\b(not|don't|dont|no|never)\s+(\w+\s+){0,2}$", re.IGNORECASE) +_NEGATED = re.compile(r"\b(not|don't|dont|no|never)\s+(\S+\s+){0,5}$", re.IGNORECASE) +# "Train 4 holding on the stop", "out with a disabled on the bridge": on rail, +# bridge/tunnel, fire and EMS channels these phrases are operations, not a +# police stop. Everywhere else — including "Ch 1 (Patched ...)", which is where +# the stops actually are — the backstop applies. +_NO_BACKSTOP_TG = re.compile(r"\b(mta|rail|railroad|train|transit|bridges? and tunnels|fire|ems|" + r"rescue|ambulance|dpw|public works)\b", re.IGNORECASE) def _self_initiated_backstop( text: str, tags: list, incident_type: Optional[str], severity: str, + talkgroup_name: Optional[str] = None, ) -> tuple[list, Optional[str], str]: + if talkgroup_name and _NO_BACKSTOP_TG.search(talkgroup_name): + return tags, incident_type, severity for pattern, tag in _SELF_INITIATED: m = pattern.search(text or "") if not m or _NEGATED.search(text[: m.start()]): diff --git a/drb-c2-core/app/routers/upload.py b/drb-c2-core/app/routers/upload.py index 96e90c0..e142f66 100644 --- a/drb-c2-core/app/routers/upload.py +++ b/drb-c2-core/app/routers/upload.py @@ -376,6 +376,7 @@ async def _run_extraction_pipeline( "status": "resolved", "resolved_at": clock.now().isoformat(), "resolved_via": "llm_closure", + "reopenable": True, # provisional, see _extract_and_correlate }) await incident_correlator.maybe_resolve_parent(incident_id) logger.info(f"Auto-resolved incident {incident_id} (LLM closure detection)") @@ -466,6 +467,11 @@ async def _extract_and_correlate( "status": "resolved", "resolved_at": clock.now().isoformat(), "resolved_via": "llm_closure", + # One transmission read as "it's over" ("transport complete") + # closed the whole 09-22 bridge MVA at 14:44 and its next 56 + # calls opened a second incident (server-26#170). Inferred from + # a single call, so provisional, like a timer close. + "reopenable": True, }) await incident_correlator.maybe_resolve_parent(incident_id) logger.info(f"Auto-resolved incident {incident_id} (LLM closure detection)") diff --git a/drb-c2-core/tests/test_clearance_tracking.py b/drb-c2-core/tests/test_clearance_tracking.py index 2d4fe66..8747256 100644 --- a/drb-c2-core/tests/test_clearance_tracking.py +++ b/drb-c2-core/tests/test_clearance_tracking.py @@ -105,7 +105,7 @@ def test_traffic_stops_become_events(): from app.internal.intelligence import _self_initiated_backstop as b for t in ("45 Adam on a stop, Eastbound Central Express.", "11-0. CM2 on the stop, southbound, KFLA on the right.", - "Car 7, traffic stop, Route 9 at Main"): + "Car 7 on a traffic stop, Route 9 at Main"): tags, typ, sev = b(t, [], None, "routine") assert "traffic-stop" in tags and typ == "police" and sev == "minor", t tags, typ, sev = b("Charlie 1. You put me out with a pedestrian on a parkway", [], None, "routine") @@ -115,3 +115,28 @@ def test_traffic_stops_become_events(): assert b("45-8, go ahead.", [], None, "routine") == ([], None, "routine") # an existing type/severity is never downgraded assert b("on a stop", ["dwi"], "police", "moderate") == (["dwi", "traffic-stop"], "police", "moderate") + + +def test_stop_backstop_stays_off_rail_bridge_and_ems_channels(): + from app.internal.intelligence import _self_initiated_backstop as b + none = ([], None, "routine") + assert b("Train 4 holding on the stop at Grand Central", [], None, "routine", + "MTA PD Districts 6/7/11 - Police Dispatch") == none + assert b("out with a disabled on the bridge, toll plaza", [], None, "routine", + "MTA Bridges and Tunnels - Whitestone/Throgs Neck Bridge Patrols") == none + assert b("Medic 2 pull over to the side and wait", [], None, "routine") == none + assert b("ran a plate for a car stop", [], None, "routine") == none + assert b("I dont think he is on a stop", [], None, "routine") == none + assert b("45 Adam on a stop", [], None, "routine", "Ch 1 (Patched with 155.310)")[0] == ["traffic-stop"] + + +def test_short_stop_report_opens_a_scene(): + import asyncio + from unittest.mock import patch + from app.internal import firestore as fstore, intelligence + + async def run(): + with patch.object(fstore, "doc_set"), patch.object(fstore, "doc_get_cached", return_value=None): + return await intelligence.extract_scenes("c1", "Adam 3 on a stop.", "Ch 1 (Patched with 155.310)") + scenes = asyncio.run(run()) + assert len(scenes) == 1 and scenes[0]["tags"] == ["traffic-stop"] and scenes[0]["incident_type"] == "police" -- 2.54.0