Files
server-26/drb-c2-core/tests/test_chatter_classifier.py
T
Logan CusanoandClaude Sonnet 5 05ddec8284 intelligence: shadow-mode upstream dispatch-vs-chatter classifier (server-26#127)
Three live measurement windows and two consensus-layer fixes (#125, #126)
converged on one decision (CORRELATION_REVIEW_0907b.md, _0912.md): stop
iterating the correlator's consensus layer, the actual lever is upstream —
a classifier in scene extraction that recognizes radio housekeeping (roll
call, bare 10-4/10-8/98 acknowledgements, unit check-ins) before it ever
becomes a scene for the correlator to judge.

Adds app/internal/chatter_classifier.py: a pure classify_chatter(transcript)
function recognizing two shapes drawn from hand-labeled examples in the
review docs, cross-referenced against the real dumps — not invented regexes.
Deliberately conservative: anything that doesn't cleanly reduce to a known
shape returns (False, None) and the existing pipeline runs unchanged.

SHADOW MODE ONLY. intelligence.extract_scenes computes the verdict next to
the existing _is_garbage_transcript / transcript_too_short gates and writes
chatter_classifier_verdict / chatter_classifier_reason onto the call doc,
but does not skip extraction. admin.py's correlation-debug _call_summary
surfaces both fields, same pattern as corr_gate_veto (#115/#126), so the
next live window can measure the real-world false-positive rate before
anything is wired to actually skip extraction. TODO(server-26#127) marks
the call site.

Backtest against all three existing dumps (1002 calls): 154 flagged, 0
false positives (no flagged call carries tags, coords, non-routine severity,
or matches any review-doc-named dangerous-to-drop transcript — the major
extinguishing-fire call, geocoded calls, pursuit updates, the Pelham Station
subject check, the property-retrieval call, all individually verified).

tests/test_chatter_classifier.py: real transcripts from the dumps/review
docs in both directions. Sandboxed pytest 332 -> 364, green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tbknwttzou4s46PAykmtix
2026-09-12 23:59:53 -04:00

145 lines
7.4 KiB
Python

"""
server-26#127 — upstream dispatch-vs-chatter classifier, shadow mode.
Fixtures are real transcripts, not invented ones: pulled from
`corr_dump_9-7_0437am.json`, `corr_dump_9-7_pm.json`, `corr_dump_9-12.json`
and the hand-labeled examples in `CORRELATION_REVIEW_0907b.md` /
`CORRELATION_REVIEW_0912.md`. The "must classify False" set specifically
includes every transcript those review docs flagged as dangerous to drop —
a false positive here is a real event silently losing its scene once this
classifier ever goes live, which is a much worse failure than a missed
chatter call staying in the existing (already-working) pipeline.
"""
import pytest
from app.internal.chatter_classifier import classify_chatter
# ─────────────────────────────────────────────────────────────────────────────
# Must classify as chatter
# ─────────────────────────────────────────────────────────────────────────────
CHATTER_EXAMPLES = [
# Bare acknowledgements / unit check-ins (CORRELATION_REVIEW_0907b.md)
("114 Paul.\n114 Paul, Metro Central.\n10-4.", "bare_acknowledgement"),
("Affirmative, in charge of 10-8. 10-8, 10-4.", "bare_acknowledgement"),
("6-8, you can show me 98. 10-4.", "bare_acknowledgement"),
("10-4, 10-4 Central, 98. 10-4, 98.", "bare_acknowledgement"),
("7 for Post 1 and 2, 98. Affirm.", "bare_acknowledgement"),
("11-Victor to Central. 11-Victor. 72-Holland, 1-5. Central.", "bare_acknowledgement"),
# Roll call (CORRELATION_REVIEW_0907b.md / _0912.md)
("Post 4, Ossining. And to volunteer patrol, stand by for roll call.", "roll_call"),
("Headquarters to all cars, stand by for roll call.", "roll_call"),
("All Troop NYC Patrols, stand by for roll call.", "roll_call"),
(
"Car 100, roll call.\nHenry 1.\nHenry 1.\nSam 1.\nSam 1.\n45 Baker.\n"
"45 Baker.\n11 Adam.\nAdam.\n11 Baker.\nBaker.\nStaff 1.\n1.\nStaff 2.",
"roll_call",
),
(
"Headquarters, all cars on a roll call. Baker 1? Baker 1. Henry 1? "
"Henry 1. Sam 2? Sam 2. 11 Adam? 11. 11 Baker? 11 Baker.",
"roll_call",
),
("Because all cars came out for roll call.", "roll_call"),
("10-1. KL Cars, that concludes roll call, time is 3-31.", "roll_call"),
# Minimal single-word / bare-code transmissions (orphan pool, all 3 dumps)
("10-4.", "bare_acknowledgement"),
("Roger.", "bare_acknowledgement"),
("Clear.", "bare_acknowledgement"),
("Affirmative.", "bare_acknowledgement"),
("Received.", "bare_acknowledgement"),
("10-8, clear. 10-4.", "bare_acknowledgement"),
("Post 4, 10-8. 10-4.", "bare_acknowledgement"),
("Central to 6 Henry.", "bare_acknowledgement"),
]
@pytest.mark.parametrize("transcript,expected_reason", CHATTER_EXAMPLES)
def test_classifies_chatter(transcript, expected_reason):
is_chatter, reason = classify_chatter(transcript)
assert is_chatter is True
assert reason == expected_reason
# ─────────────────────────────────────────────────────────────────────────────
# Must NOT classify as chatter — real events, including every transcript the
# review docs specifically named as dangerous to drop.
# ─────────────────────────────────────────────────────────────────────────────
REAL_EVENT_EXAMPLES = [
# The major "extinguishing fire" call (severity=major, tags=[extinguishing-fire])
("Dispatch, this is 7-4, extinguishing fire.", "extinguishing_fire"),
# Geocoded 911-hangup call (has location_coords)
(
"7, Charlie. Charlie, check and advise, we've got a call for service "
"coming over, it's going to be a 9-1-1 hangout, no voice contact. "
"Looks like it was an automated message saying it's the Doral Hat Company.",
"geocoded_911_hangup",
),
# Pursuit updates (severity=major, tags include pursuit / low-speed-pursuit)
("I'm aware of that one. It's a low-speed pursuit. It's refusing to pull over.", "low_speed_pursuit"),
(
"1. Headquarters to 5-charlie. I'm going to say the last thing to anyone.\n"
"2. Info, Sgt. Repeat.\n"
"3. The SP is on a pursuit southbound on I-684. It's approaching the airport.\n"
"4. Okay, thank you.\n5. 23-59.",
"pursuit_i684",
),
# "6 Alpha ... Pelham Station" subject check (CORRELATION_REVIEW_0907b.md's
# own "genuinely distinct events" list) — looks like a bare check-in but
# dispatches a unit to a specific location.
(
"6 Alpha, this is Central. 7 Alpha here.\n"
"6 Alpha, can you show me on scene at Pelham Station? Stand by.",
"pelham_station_subject_check",
),
# Property-retrieval call (tags=[property-retrieval])
(
"Property was retrieved with a 911. Can I get a phone number? 10-4. "
"Phone number is 214792. 214792.",
"property_retrieval",
),
# Subject check south of Maronex Station (tags=[subject-check])
(
"Proceed. Show me on a subject south of Maronex Station. Can I get a "
"15 check by New York client ID?",
"maronex_subject_check",
),
# Trespassing at milepost 13.7 (tags=[trespassing])
(
"Can you just 10-5 that job? You came over real muffled.\n"
"10-4, there's going to be a trespass on the tracks.\n"
"Train 8755 reports two juveniles, one male, one female, both wearing "
"white shirts, track three side, at milepost 13.7.",
"trespass_milepost_13_7",
),
# MVA (severity=moderate, tags=[traffic-accident])
("1. Train patrol 9.\n2. MVA 4, how close is it?\n3. 10-4.", "mva"),
]
@pytest.mark.parametrize("transcript,label", REAL_EVENT_EXAMPLES, ids=[l for _, l in REAL_EVENT_EXAMPLES])
def test_does_not_classify_real_events_as_chatter(transcript, label):
is_chatter, reason = classify_chatter(transcript)
assert is_chatter is False, f"{label}: false positive, reason={reason!r}"
assert reason is None
# ─────────────────────────────────────────────────────────────────────────────
# Edge cases
# ─────────────────────────────────────────────────────────────────────────────
def test_empty_transcript_not_chatter():
assert classify_chatter("") == (False, None)
assert classify_chatter(None) == (False, None)
assert classify_chatter(" ") == (False, None)
def test_unrecognized_content_defaults_to_not_chatter():
# Anything containing real descriptive words the classifier doesn't
# recognize must fall through to (False, None), not guess.
is_chatter, reason = classify_chatter("Shots fired, officer down, requesting backup immediately.")
assert is_chatter is False
assert reason is None