Extraction: model in config, chatter gate, geocode cache (server-26#298, server-26#104, server-26#88)

EXTRACTION_MODEL / EMBEDDING_MODEL replace the gpt-4o-mini and
text-embedding-3-small literals. gate_chatter (classify_chatter with roll_call
narrowed to whole-transcript roll calls) skips correction, extraction,
embedding and geocoding for housekeeping, writes skip_reason=non_event_chatter
and still runs the free clearance/plate backstops; upload.py's no-scene
fallback admits it like transcript_too_short. Ships OFF (CHATTER_GATE_ENABLED
=false): a gated call has no units, so it takes the no-fit-check thin attach,
and #127 requires a clean live shadow window first.
In-process TTL cache (24h, negatives 1h, 5000 entries, LRU) in front of the
Geocoding API, keyed by address+bounds; holds coordinates only, inside the
Maps 30-day lat/lng caching limit; errors and key/quota statuses not cached.
#104: the no-scene fallback passes the call doc's severity.
#88: resolved/reassignment parsed by _llm_bool, not bool().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-27 20:28:46 -04:00
co-authored by Claude Opus 5.5
parent d485eae41a
commit 240bbee5a4
7 changed files with 597 additions and 82 deletions
+162
View File
@@ -0,0 +1,162 @@
"""
server-26#298 — geocode result cache in front of the Google Geocoding API,
and server-26#160 — place verification gets an anchor from the talkgroup's
own name when nobody configured one.
"""
from unittest.mock import AsyncMock, patch
import pytest
import app.internal.intelligence as intel
from app.config import settings
from app.internal import place_verifier as pv
ANCHOR = {"lat": 41.16, "lng": -73.86, "radius_km": 10.0}
class _Counter:
def __init__(self, payload=None, exc=None):
self.calls = 0
self.payload = payload
self.exc = exc
def patch(self):
outer = self
class _Resp:
def raise_for_status(self):
if outer.exc:
raise outer.exc
def json(self):
return outer.payload
class _Client:
async def __aenter__(self): return self
async def __aexit__(self, *a): return False
async def get(self, *a, **k):
outer.calls += 1
return _Resp()
return patch("httpx.AsyncClient", lambda *a, **k: _Client())
def _ok(lat=41.161, lng=-73.861):
return {"status": "OK", "results": [{"geometry": {
"location": {"lat": lat, "lng": lng}, "location_type": "ROOFTOP"}}]}
@pytest.fixture(autouse=True)
def _key():
with patch.object(settings, "google_maps_api_key", "test-key"):
yield
@pytest.mark.asyncio
async def test_repeat_address_is_one_request():
c = _Counter(_ok())
with c.patch():
a = await intel._geocode_location("12 Main Street, Ossining", anchor=ANCHOR)
b = await intel._geocode_location("12 main street, ossining ", anchor=ANCHOR)
assert a == b == {"lat": 41.161, "lng": -73.861, "location_type": "ROOFTOP"}
assert c.calls == 1
assert intel.geocode_cache_stats == {"hits": 1, "misses": 1}
@pytest.mark.asyncio
async def test_cached_answer_is_still_judged_against_the_caller_anchor():
c = _Counter(_ok())
with c.patch():
assert await intel._geocode_location("12 Main Street", anchor=ANCHOR)
far = {"lat": 0.0, "lng": 0.0, "radius_km": 10.0}
# Different bounds -> different key, so a separate request — never a
# cached answer leaking past a different area's distance check.
assert await intel._geocode_location("12 Main Street", anchor=far) is None
assert c.calls == 2
@pytest.mark.asyncio
async def test_zero_results_is_cached_but_errors_are_not():
c = _Counter({"status": "ZERO_RESULTS", "results": []})
with c.patch():
await intel._geocode_location("Cool Parts, Illinois", anchor=ANCHOR)
await intel._geocode_location("Cool Parts, Illinois", anchor=ANCHOR)
assert c.calls == 1
denied = _Counter({"status": "REQUEST_DENIED"})
with denied.patch():
await intel._geocode_location("Snowden Avenue", anchor=ANCHOR)
await intel._geocode_location("Snowden Avenue", anchor=ANCHOR)
assert denied.calls == 2, "a key/quota failure is not an answer about the address"
boom = _Counter(_ok(), exc=RuntimeError("503"))
with boom.patch():
await intel._geocode_location("Croton Point", anchor=ANCHOR)
await intel._geocode_location("Croton Point", anchor=ANCHOR)
assert boom.calls == 2
@pytest.mark.asyncio
async def test_ttl_expiry_and_size_bound():
c = _Counter(_ok())
with c.patch(), patch.object(settings, "geocode_cache_ttl_seconds", 0):
await intel._geocode_location("12 Main Street", anchor=ANCHOR)
await intel._geocode_location("12 Main Street", anchor=ANCHOR)
assert c.calls == 2, "TTL 0 disables the cache"
c = _Counter(_ok())
with c.patch(), patch.object(settings, "geocode_cache_max_entries", 2):
for street in ("A Street", "B Street", "C Street"):
await intel._geocode_location(street, anchor=ANCHOR)
assert len(intel._geocode_cache) == 2
# ── server-26#160 ───────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_unconfigured_channel_gets_an_anchor_from_its_talkgroup_name():
resolved = {"center": {"lat": 40.71, "lng": -74.0}, "radius_km": 25.0}
seen = {}
async def _geo(query, anchor):
seen["query"], seen["anchor"] = query, anchor
return {"lat": 40.7, "lng": -74.0}
with patch.object(pv.settings, "place_verification_enabled", True), \
patch.object(intel.fstore, "doc_get_cached", AsyncMock(return_value={"lat": 41.2, "lon": -73.8})), \
patch.object(intel, "_get_node_state", AsyncMock(return_value="New York")), \
patch.object(pv.area_context, "resolve_anchor", AsyncMock(return_value=resolved)) as res, \
patch.object(pv, "_geocode_in_anchor", _geo):
await pv.verify("c1", "Respond to Lexington Avenue", None, ["Lexington Avenue"],
{}, {}, talkgroup_name="New York City PD", node_id="node-1")
res.assert_awaited_once_with({"municipality": "New York City", "state": "New York"})
assert seen["anchor"] == {"lat": 40.71, "lng": -74.0, "radius_km": 25.0}
assert seen["query"] == "Lexington Avenue, New York City, New York"
@pytest.mark.asyncio
async def test_no_state_means_no_derived_anchor():
"""'Springfield PD' alone could anchor anywhere — skip, never guess."""
with patch.object(pv.settings, "place_verification_enabled", True), \
patch.object(intel.fstore, "doc_get_cached", AsyncMock(return_value={"lat": 41.2, "lon": -73.8})), \
patch.object(intel, "_get_node_state", AsyncMock(return_value="")), \
patch.object(pv, "_geocode_in_anchor") as geo:
out = await pv.verify("c1", "Respond to Main Street", None, ["Main Street"],
{}, {}, talkgroup_name="Springfield PD", node_id="node-1")
assert out == (None, None)
geo.assert_not_called()
@pytest.mark.asyncio
async def test_configured_place_without_anchor_still_skips():
"""A place that is set but too wide (no anchor stored) keeps the
load-bearing skip; the talkgroup-name fallback is only for unset areas."""
with patch.object(pv.settings, "place_verification_enabled", True), \
patch.object(pv, "_talkgroup_anchor", AsyncMock()) as derived, \
patch.object(pv, "_geocode_in_anchor") as geo:
out = await pv.verify("c1", "Main Street", None, ["Main Street"],
{"state": "Colorado"}, {}, talkgroup_name="Denver PD", node_id="n")
assert out == (None, None)
derived.assert_not_awaited()
geo.assert_not_called()