Compare commits
2
Commits
53965e1a19
...
0bd92269d2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bd92269d2 | ||
|
|
96625fabd0 |
@@ -731,8 +731,16 @@ def _run_decision(ctx: dict) -> dict:
|
|||||||
# Anything the extractor judged a real event, or that carries any concrete
|
# Anything the extractor judged a real event, or that carries any concrete
|
||||||
# content, now opens an incident under the neutral "other" type. Only
|
# content, now opens an incident under the neutral "other" type. Only
|
||||||
# content-free routine traffic is still left for the thin path to attach.
|
# content-free routine traffic is still left for the thin path to attach.
|
||||||
|
#
|
||||||
|
# `call_units` and `location` are NOT substance. Radio protocol puts a unit
|
||||||
|
# ID in essentially every transmission and a place name in most of them, so
|
||||||
|
# including them made has_substance true almost always and the severity check
|
||||||
|
# dead code — the first version of this gate turned "11-Victor, 72 at Holland
|
||||||
|
# Station" into its own incident and left 37 of 50 incidents open, one call
|
||||||
|
# each. A vehicle, a geocode, or a tag means the extractor found something
|
||||||
|
# beyond who was speaking and where they stood.
|
||||||
if not resolved_type:
|
if not resolved_type:
|
||||||
has_substance = bool(call_units or call_vehicles or coords or location or tags)
|
has_substance = bool(call_vehicles or coords or tags)
|
||||||
if call_severity in ("minor", "moderate", "major") or has_substance:
|
if call_severity in ("minor", "moderate", "major") or has_substance:
|
||||||
resolved_type = "other"
|
resolved_type = "other"
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -68,6 +68,13 @@ System: {system_id}
|
|||||||
Talkgroup: {talkgroup_name}
|
Talkgroup: {talkgroup_name}
|
||||||
{ten_codes_block}{vocabulary_block}{transcript_block}"""
|
{ten_codes_block}{vocabulary_block}{transcript_block}"""
|
||||||
|
|
||||||
|
# The incident_type enum offered to the model in EXTRACTION_PROMPT. Kept here
|
||||||
|
# rather than only in the prompt so a model that invents a value cannot write it
|
||||||
|
# into incident.type. "unknown" is deliberately absent — it is a real answer
|
||||||
|
# from the model but not a usable type, and is normalised to None alongside
|
||||||
|
# anything unrecognised.
|
||||||
|
_VALID_INCIDENT_TYPES = frozenset({"fire", "ems", "police", "accident", "other"})
|
||||||
|
|
||||||
# Geographic bias radius for geocoding — half-width in degrees (~55 km)
|
# Geographic bias radius for geocoding — half-width in degrees (~55 km)
|
||||||
_GEO_DELTA = 0.5
|
_GEO_DELTA = 0.5
|
||||||
|
|
||||||
@@ -240,7 +247,19 @@ async def extract_scenes(
|
|||||||
# and is kept. Collapsing it to None used to make the call untypeable,
|
# and is kept. Collapsing it to None used to make the call untypeable,
|
||||||
# and an untypeable call could never open an incident — see the creation
|
# and an untypeable call could never open an incident — see the creation
|
||||||
# gate in incident_correlator._run_decision().
|
# gate in incident_correlator._run_decision().
|
||||||
if incident_type in ("unknown", ""):
|
#
|
||||||
|
# Anything outside the enum is a model error, not a new category. The
|
||||||
|
# value is written straight through to incident.type and rendered as the
|
||||||
|
# incident title, so on 2026-08-16 a model that answered the severity
|
||||||
|
# question in the type field produced an incident literally titled
|
||||||
|
# "Routine — TGID 9563". Unrecognised values become None and fall to the
|
||||||
|
# tag/severity path, which is the same treatment "unknown" already got.
|
||||||
|
if incident_type not in _VALID_INCIDENT_TYPES:
|
||||||
|
if incident_type and incident_type != "unknown":
|
||||||
|
logger.warning(
|
||||||
|
f"Intelligence: discarding invalid incident_type {incident_type!r} "
|
||||||
|
f"(not in {sorted(_VALID_INCIDENT_TYPES)})"
|
||||||
|
)
|
||||||
incident_type = None
|
incident_type = None
|
||||||
|
|
||||||
# Geocode this scene's location.
|
# Geocode this scene's location.
|
||||||
@@ -420,11 +439,17 @@ async def _geocode_location(
|
|||||||
return None
|
return None
|
||||||
result = data["results"][0]
|
result = data["results"][0]
|
||||||
location_type = result.get("geometry", {}).get("location_type", "")
|
location_type = result.get("geometry", {}).get("location_type", "")
|
||||||
# Only accept address-level precision. GEOMETRIC_CENTER (city/neighborhood
|
# Reject only APPROXIMATE — a region/city boundary centroid, which is
|
||||||
# centroid) and APPROXIMATE (region boundary) produce coordinates that look
|
# what an ungeocodable string degrades to and is genuinely useless.
|
||||||
# valid but are too vague for 0.5km proximity matching — they often resolve
|
#
|
||||||
# to the same point as the node's position and create false proximity matches.
|
# ROOFTOP-only was too strict and emptied the map: dispatch names
|
||||||
if location_type not in ("ROOFTOP", "RANGE_INTERPOLATED"):
|
# places the way people speak, and Google returns GEOMETRIC_CENTER for
|
||||||
|
# exactly those forms — intersections ("Lake Street and Veterans
|
||||||
|
# Memorial Drive") and named POIs ("Brewster Station"). Both are
|
||||||
|
# precise enough to plot and to proximity-match; requiring a street
|
||||||
|
# address threw away nearly every real dispatch location, leaving only
|
||||||
|
# numbered addresses geocoded.
|
||||||
|
if location_type not in ("ROOFTOP", "RANGE_INTERPOLATED", "GEOMETRIC_CENTER"):
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Geocoding rejected '{location_str}' — imprecise result "
|
f"Geocoding rejected '{location_str}' — imprecise result "
|
||||||
f"(location_type={location_type!r}), returning None"
|
f"(location_type={location_type!r}), returning None"
|
||||||
|
|||||||
@@ -7,4 +7,12 @@ logging.basicConfig(
|
|||||||
handlers=[logging.StreamHandler(sys.stdout)],
|
handlers=[logging.StreamHandler(sys.stdout)],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# httpx logs every request at INFO as a full URL *including the query string*,
|
||||||
|
# which puts API keys in plaintext in container logs — the Google Maps key was
|
||||||
|
# leaking on every geocode call (`?address=...&key=AIza...`). Nothing here needs
|
||||||
|
# per-request client logging, so drop httpx to WARNING; failures still surface
|
||||||
|
# because the callers log their own errors.
|
||||||
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||||
|
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||||
|
|
||||||
logger = logging.getLogger("drb-c2-core")
|
logger = logging.getLogger("drb-c2-core")
|
||||||
|
|||||||
@@ -67,10 +67,8 @@ def test_any_real_severity_opens_an_untyped_incident(severity):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("field,value", [
|
@pytest.mark.parametrize("field,value", [
|
||||||
("call_units", ["6 Adam"]),
|
|
||||||
("call_vehicles", ["RMP 22146"]),
|
("call_vehicles", ["RMP 22146"]),
|
||||||
("coords", {"lat": 41.0, "lng": -73.8}),
|
("coords", {"lat": 41.0, "lng": -73.8}),
|
||||||
("location", "District 6"),
|
|
||||||
("tags", ["prisoner-transport"]),
|
("tags", ["prisoner-transport"]),
|
||||||
])
|
])
|
||||||
def test_concrete_content_opens_an_untyped_incident(field, value):
|
def test_concrete_content_opens_an_untyped_incident(field, value):
|
||||||
@@ -80,6 +78,32 @@ def test_concrete_content_opens_an_untyped_incident(field, value):
|
|||||||
assert decision["incident_type"] == "other"
|
assert decision["incident_type"] == "other"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("field,value", [
|
||||||
|
("call_units", ["11-Victor"]),
|
||||||
|
("location", "Holland Station"),
|
||||||
|
])
|
||||||
|
def test_ambient_radio_fields_are_not_substance(field, value):
|
||||||
|
"""
|
||||||
|
A unit ID and a place name appear in nearly every transmission, so treating
|
||||||
|
them as substance made the severity check dead code: "11-Victor, 72 at
|
||||||
|
Holland Station" opened its own incident, and 37 of 50 incidents were single
|
||||||
|
routine calls left permanently active.
|
||||||
|
"""
|
||||||
|
assert _run_decision(_ctx(**{field: value}))["action"] == "orphan"
|
||||||
|
|
||||||
|
|
||||||
|
def test_units_and_location_together_still_orphan():
|
||||||
|
decision = _run_decision(_ctx(call_units=["11-Victor"], location="Holland Station"))
|
||||||
|
assert decision["action"] == "orphan"
|
||||||
|
|
||||||
|
|
||||||
|
def test_units_with_real_severity_still_open_an_incident():
|
||||||
|
"""Severity is the gate — ambient fields don't block it, they just can't open it alone."""
|
||||||
|
decision = _run_decision(_ctx(call_units=["11-Victor"], call_severity="moderate"))
|
||||||
|
assert decision["action"] == "new"
|
||||||
|
assert decision["incident_type"] == "other"
|
||||||
|
|
||||||
|
|
||||||
def test_explicit_type_is_never_downgraded_to_other():
|
def test_explicit_type_is_never_downgraded_to_other():
|
||||||
decision = _run_decision(_ctx(incident_type="police", call_severity="moderate"))
|
decision = _run_decision(_ctx(incident_type="police", call_severity="moderate"))
|
||||||
assert decision["action"] == "new"
|
assert decision["action"] == "new"
|
||||||
|
|||||||
Reference in New Issue
Block a user