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
|
||||
# content, now opens an incident under the neutral "other" type. Only
|
||||
# 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:
|
||||
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:
|
||||
resolved_type = "other"
|
||||
logger.info(
|
||||
|
||||
@@ -68,6 +68,13 @@ System: {system_id}
|
||||
Talkgroup: {talkgroup_name}
|
||||
{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)
|
||||
_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 an untypeable call could never open an incident — see the creation
|
||||
# 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
|
||||
|
||||
# Geocode this scene's location.
|
||||
@@ -420,11 +439,17 @@ async def _geocode_location(
|
||||
return None
|
||||
result = data["results"][0]
|
||||
location_type = result.get("geometry", {}).get("location_type", "")
|
||||
# Only accept address-level precision. GEOMETRIC_CENTER (city/neighborhood
|
||||
# centroid) and APPROXIMATE (region boundary) produce coordinates that look
|
||||
# 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.
|
||||
if location_type not in ("ROOFTOP", "RANGE_INTERPOLATED"):
|
||||
# Reject only APPROXIMATE — a region/city boundary centroid, which is
|
||||
# what an ungeocodable string degrades to and is genuinely useless.
|
||||
#
|
||||
# ROOFTOP-only was too strict and emptied the map: dispatch names
|
||||
# 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(
|
||||
f"Geocoding rejected '{location_str}' — imprecise result "
|
||||
f"(location_type={location_type!r}), returning None"
|
||||
|
||||
@@ -7,4 +7,12 @@ logging.basicConfig(
|
||||
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")
|
||||
|
||||
@@ -67,10 +67,8 @@ def test_any_real_severity_opens_an_untyped_incident(severity):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field,value", [
|
||||
("call_units", ["6 Adam"]),
|
||||
("call_vehicles", ["RMP 22146"]),
|
||||
("coords", {"lat": 41.0, "lng": -73.8}),
|
||||
("location", "District 6"),
|
||||
("tags", ["prisoner-transport"]),
|
||||
])
|
||||
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"
|
||||
|
||||
|
||||
@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():
|
||||
decision = _run_decision(_ctx(incident_type="police", call_severity="moderate"))
|
||||
assert decision["action"] == "new"
|
||||
|
||||
Reference in New Issue
Block a user