clearance: act on drb-correlation-review of f0a88d4
Review of the first clearance fix found it pushes toward early resolve:
- parser cleared units on questions ("45-9, are you clear?"), negations
("not clear yet"), orders ("clear the scene", "clear to transport"),
places/times ("Room 2 clear", "1400 hours, clear") and split "45 9" into
unit 45. Now rejects "?", not/is/are/you, anything after the status word
but sign-offs, place/time words; joins "45 9" -> "45-9".
- a clear from a unit never active on an incident was recorded in
units_cleared and could pass the all-clear gate. Only units actually
active there can clear there now.
- clearance-only calls skip the LLM tier (same as thin calls): only the
rules engine's unit match can say which incident a 10-8 belongs to.
- replay incident view carries srcaddr/srcaddrs for the radio-ID clearance
investigation.
Replay 09-22 10:00-12:00 ET with f0a88d4: real clears 0 -> 2 (both LLM
closure), unit clears still 0 — the parsed clears are right but those
units were never recorded as assigned (Whisper mangles unit IDs at
dispatch), which this commit does not fix.
c2-core: 465 pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f0a88d401c
commit
3c642e2946
@@ -1934,10 +1934,14 @@ def _apply_unit_clearance(inc: dict, cleared: list[str]) -> tuple[list[str], lis
|
||||
units_cleared = list(inc.get("units_cleared") or [])
|
||||
# Compared by normalised key: the unit that cleared as "11-Adam" is the
|
||||
# one that went active as "11 Adam", and exact equality left it active.
|
||||
# Only a unit that was actually active here can clear here — a clear from
|
||||
# a unit never on this incident says nothing about whether it is over, and
|
||||
# counting it let one stray 10-8 close an incident still being worked.
|
||||
cleared_keys = _unit_keys(cleared)
|
||||
releasing = [u for u in units_active if _normalize_unit(u) in cleared_keys]
|
||||
units_active = [u for u in units_active if _normalize_unit(u) not in cleared_keys]
|
||||
known_cleared = _unit_keys(units_cleared)
|
||||
for u in cleared:
|
||||
for u in releasing:
|
||||
if _normalize_unit(u) not in known_cleared:
|
||||
units_cleared.append(u)
|
||||
known_cleared.add(_normalize_unit(u))
|
||||
|
||||
@@ -490,24 +490,44 @@ _CLEAR_WORD_RE = re.compile(
|
||||
)
|
||||
_TEN_CODE_TOKEN_RE = re.compile(r"^10-?\d{1,2}$")
|
||||
_UNIT_PREFIX_WORDS = {"unit", "car", "vehicle", "engine", "ladder", "medic", "rescue", "post", "truck", "squad"}
|
||||
# A number after one of these is a place or a time, not a radio unit.
|
||||
_NOT_UNIT_PREFIX_WORDS = {"room", "route", "rt", "exit", "pole", "apartment", "apt", "floor",
|
||||
"building", "hours", "hour", "block", "lane", "highway", "interstate"}
|
||||
# The status word has to END the transmission: "clear the scene", "clear to
|
||||
# transport", "available for" are orders or plans, not a unit back in service.
|
||||
_TRAILING_OK = {"10-4", "thanks", "thank", "you", "k", "over", "now", "again", "from", "headquarters", "hq",
|
||||
"central", "dispatch"}
|
||||
|
||||
|
||||
def _short_clearance_unit(transcript: str) -> Optional[str]:
|
||||
m = _CLEAR_WORD_RE.search(transcript or "")
|
||||
text = (transcript or "").strip()
|
||||
if not text or "?" in text:
|
||||
return None # "45-9, are you clear?" asks; it doesn't report
|
||||
m = _CLEAR_WORD_RE.search(text)
|
||||
if not m:
|
||||
return None
|
||||
before = [t.strip(".,;:!?") for t in transcript[: m.start()].split()]
|
||||
before = [t.strip(".,;:!") for t in text[: m.start()].split()]
|
||||
before = [t for t in before if t]
|
||||
after = [t.strip(".,;:!").lower() for t in text[m.end():].split()]
|
||||
if any(t and t not in _TRAILING_OK for t in after):
|
||||
return None
|
||||
if any(t.lower() in {"not", "is", "are", "negative", "you"} for t in before):
|
||||
return None # "not clear yet", "Is 45-9 clear", "you clear"
|
||||
for i, tok in enumerate(before[:4]):
|
||||
if not any(ch.isdigit() for ch in tok) or _TEN_CODE_TOKEN_RE.match(tok):
|
||||
continue
|
||||
prev = before[i - 1] if i else ""
|
||||
if prev.lower() in _UNIT_PREFIX_WORDS:
|
||||
return f"{prev} {tok}"
|
||||
prev = before[i - 1].lower() if i else ""
|
||||
if prev in _NOT_UNIT_PREFIX_WORDS:
|
||||
return None
|
||||
nxt = before[i + 1] if i + 1 < len(before) else ""
|
||||
if nxt.isalpha() and nxt.lower() not in {"i'm", "im", "is", "are", "to", "we're", "copy"} \
|
||||
and nxt[0].isupper():
|
||||
return f"{tok} {nxt}" # "11 Adam, clear"
|
||||
if nxt.lower() in _NOT_UNIT_PREFIX_WORDS:
|
||||
return None # "1400 hours, clear"
|
||||
if nxt.isdigit():
|
||||
tok = f"{tok}-{nxt}" # "45 9 clear" is unit 45-9, not unit 45
|
||||
elif nxt.isalpha() and nxt[0].isupper() and nxt.lower() not in {"i'm", "im", "we're", "copy"}:
|
||||
tok = f"{tok} {nxt}" # "11 Adam, clear"
|
||||
if prev in _UNIT_PREFIX_WORDS:
|
||||
return f"{before[i - 1]} {tok}"
|
||||
return tok
|
||||
return None
|
||||
|
||||
|
||||
@@ -275,6 +275,12 @@ async def decide(call_id: str, ctx: dict) -> Optional[dict]:
|
||||
if ctx["is_thin_call"]:
|
||||
return None # thin calls have no transcript/units/coords to reason about
|
||||
|
||||
if _is_clearance_only(ctx):
|
||||
# "45-9, I'm clear." carries one fact: which unit is done. Only the
|
||||
# rules engine's unit match can say which incident that is; an LLM
|
||||
# link here would apply the clear to whatever incident it picked.
|
||||
return None
|
||||
|
||||
if not ctx["recent"]:
|
||||
return None # no incidents to correlate against — rules handles new-only
|
||||
|
||||
@@ -295,6 +301,14 @@ async def decide(call_id: str, ctx: dict) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
def _is_clearance_only(ctx: dict) -> bool:
|
||||
units = ctx.get("call_units") or []
|
||||
cleared = ctx.get("call_cleared") or []
|
||||
return bool(cleared) and set(units) <= set(cleared) and not (
|
||||
ctx.get("tags") or ctx.get("location") or ctx.get("call_vehicles") or ctx.get("incident_type")
|
||||
)
|
||||
|
||||
|
||||
_dead_models: set[str] = set()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user