stops review + provisional LLM closure
Replay of433b35d(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 of433b35d: 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
433b35d2ba
commit
9f19750ea6
@@ -267,6 +267,14 @@ async def extract_scenes(
|
|||||||
if cleared_unit:
|
if cleared_unit:
|
||||||
logger.info(f"Intelligence: call {call_id} — short clearance from {cleared_unit!r}")
|
logger.info(f"Intelligence: call {call_id} — short clearance from {cleared_unit!r}")
|
||||||
return [_clearance_scene(transcript, cleared_unit)]
|
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 []
|
return []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -419,7 +427,7 @@ async def extract_scenes(
|
|||||||
)
|
)
|
||||||
|
|
||||||
tags, incident_type, severity = _self_initiated_backstop(
|
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({
|
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
|
# 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
|
# is the deterministic backstop, because a tag is what the creation gate
|
||||||
# counts as substance (incident_correlator.has_event_substance).
|
# 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 = (
|
_SELF_INITIATED = (
|
||||||
(re.compile(r"\b(on (a|the) (traffic |car |vehicle )?stop|traffic stop|car stop|vehicle stop|"
|
(re.compile(r"\bon (a|the) (traffic |car |vehicle |motor vehicle )?stop\b", re.IGNORECASE),
|
||||||
r"pull(ed|ing)? (a car |a vehicle |him |her |them )?over)\b", re.IGNORECASE),
|
|
||||||
"traffic-stop"),
|
"traffic-stop"),
|
||||||
(re.compile(r"\b((put|show) me out with|out with (a|one) (pedestrian|vehicle|disabled|male|female|"
|
(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),
|
r"subject|party|juvenile))\b", re.IGNORECASE),
|
||||||
"self-initiated"),
|
"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(
|
def _self_initiated_backstop(
|
||||||
text: str, tags: list, incident_type: Optional[str], severity: str,
|
text: str, tags: list, incident_type: Optional[str], severity: str,
|
||||||
|
talkgroup_name: Optional[str] = None,
|
||||||
) -> tuple[list, Optional[str], str]:
|
) -> 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:
|
for pattern, tag in _SELF_INITIATED:
|
||||||
m = pattern.search(text or "")
|
m = pattern.search(text or "")
|
||||||
if not m or _NEGATED.search(text[: m.start()]):
|
if not m or _NEGATED.search(text[: m.start()]):
|
||||||
|
|||||||
@@ -376,6 +376,7 @@ async def _run_extraction_pipeline(
|
|||||||
"status": "resolved",
|
"status": "resolved",
|
||||||
"resolved_at": clock.now().isoformat(),
|
"resolved_at": clock.now().isoformat(),
|
||||||
"resolved_via": "llm_closure",
|
"resolved_via": "llm_closure",
|
||||||
|
"reopenable": True, # provisional, see _extract_and_correlate
|
||||||
})
|
})
|
||||||
await incident_correlator.maybe_resolve_parent(incident_id)
|
await incident_correlator.maybe_resolve_parent(incident_id)
|
||||||
logger.info(f"Auto-resolved incident {incident_id} (LLM closure detection)")
|
logger.info(f"Auto-resolved incident {incident_id} (LLM closure detection)")
|
||||||
@@ -466,6 +467,11 @@ async def _extract_and_correlate(
|
|||||||
"status": "resolved",
|
"status": "resolved",
|
||||||
"resolved_at": clock.now().isoformat(),
|
"resolved_at": clock.now().isoformat(),
|
||||||
"resolved_via": "llm_closure",
|
"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)
|
await incident_correlator.maybe_resolve_parent(incident_id)
|
||||||
logger.info(f"Auto-resolved incident {incident_id} (LLM closure detection)")
|
logger.info(f"Auto-resolved incident {incident_id} (LLM closure detection)")
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ def test_traffic_stops_become_events():
|
|||||||
from app.internal.intelligence import _self_initiated_backstop as b
|
from app.internal.intelligence import _self_initiated_backstop as b
|
||||||
for t in ("45 Adam on a stop, Eastbound Central Express.",
|
for t in ("45 Adam on a stop, Eastbound Central Express.",
|
||||||
"11-0. CM2 on the stop, southbound, KFLA on the right.",
|
"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")
|
tags, typ, sev = b(t, [], None, "routine")
|
||||||
assert "traffic-stop" in tags and typ == "police" and sev == "minor", t
|
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")
|
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")
|
assert b("45-8, go ahead.", [], None, "routine") == ([], None, "routine")
|
||||||
# an existing type/severity is never downgraded
|
# an existing type/severity is never downgraded
|
||||||
assert b("on a stop", ["dwi"], "police", "moderate") == (["dwi", "traffic-stop"], "police", "moderate")
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user