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:
|
||||
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()]):
|
||||
|
||||
@@ -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)")
|
||||
|
||||
Reference in New Issue
Block a user