Add deterministic geography: precomputed nearest-office distances injected per turn
The 2026-07-05 Weston call (CA3e88ce) showed the model guessing geography: it
recommended Boca Raton for a Weston caller (real answer: Pembroke Pines, ~9 mi),
invented distances ('Weston to Hialeah is twenty miles'), and confirmed a
nonexistent Miami Lakes office when the caller insisted.
- practice.py: office lat/lon + ~70-place South Florida gazetteer + haversine;
geography_block() renders a full distance table (closest first) from the
caller's area to every office. 'Westin' aliased to Weston (STT spelling).
- callstate.py: extractor gains caller_area; CallStateGroomer appends the
GEOGRAPHY block to the system message — zero-lag via a synchronous gazetteer
scan of user turns until the extractor catches up.
- bot.py: DISTANCES prompt rule (answer only from GEOGRAPHY, never estimate),
only-these-8-offices guard, caller-area cities added to Whisper hotwords
('Weston' was heard as 'Western Florida').
Verified: staged replay of the failed exchanges on activeblue-avc:latest —
correct office, correct distances, Miami Lakes denied (5/5 + 3/3 runs).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
31
callstate.py
31
callstate.py
@@ -28,6 +28,8 @@ from loguru import logger
|
||||
from pipecat.frames.frames import BotStoppedSpeakingFrame, Frame, LLMContextFrame
|
||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||
|
||||
import practice
|
||||
|
||||
# Short, in-call variant of the post-call extractor (extract.py): only what's needed to
|
||||
# build the checklist, temperature 0, capped output. Runs on the local Ollama model.
|
||||
_STATE_INSTRUCTIONS = (
|
||||
@@ -54,6 +56,9 @@ _STATE_INSTRUCTIONS = (
|
||||
'"billing question"). Only \'an appointment\' with no visit reason is NOT a reason — '
|
||||
"use null then.\n"
|
||||
' "location": string or null — the office/city the caller wants\n'
|
||||
' "caller_area": string or null — the city/area where the CALLER themselves is located '
|
||||
"or wants an office near (\"I'm in Weston\", \"what's closest to Davie\") — NOT an office "
|
||||
"they merely asked about. Corrections win here too.\n"
|
||||
' "patient_name": string or null — ONLY a name the caller explicitly gave as their own '
|
||||
"(\"my name is…\", \"this is…\", or answering a name question); NEVER a name guessed "
|
||||
"from other words, a garbled transcription, or a name only the assistant used\n"
|
||||
@@ -252,12 +257,34 @@ class CallStateGroomer(FrameProcessor):
|
||||
)
|
||||
self._task.add_done_callback(self._extract_done)
|
||||
|
||||
def _geo_block(self, messages) -> str:
|
||||
"""Precomputed office-distance table for the caller's area (see
|
||||
practice.geography_block). Anchor = the LLM-extracted caller_area when we have
|
||||
one (it understands context and corrections); before that (extraction lags one
|
||||
turn) a synchronous gazetteer scan of the user's own words — first place that
|
||||
isn't one of our office cities, since office names in caller speech are usually
|
||||
about our offices, not where they live."""
|
||||
state = self._state or {}
|
||||
for key in ("caller_area", "location"):
|
||||
v = state.get(key)
|
||||
if isinstance(v, str) and v.strip():
|
||||
anchor = practice.match_place(v)
|
||||
if anchor:
|
||||
return practice.geography_block(anchor)
|
||||
hits = []
|
||||
for m in messages:
|
||||
if m.get("role") == "user" and isinstance(m.get("content"), str):
|
||||
hits += practice.places_in_text(m["content"])
|
||||
non_office = [h for h in hits if h not in practice.OFFICE_PLACE_KEYS]
|
||||
anchor = (non_office or hits or [None])[0]
|
||||
return practice.geography_block(anchor) if anchor else ""
|
||||
|
||||
def _groom_context(self):
|
||||
messages = merge_consecutive_user_messages(list(self._context.messages))
|
||||
block = build_state_block(self._state)
|
||||
blocks = [b for b in (build_state_block(self._state), self._geo_block(messages)) if b]
|
||||
for i, m in enumerate(messages):
|
||||
if m.get("role") == "system":
|
||||
content = self._base_system + ("\n\n" + block if block else "")
|
||||
content = "\n\n".join([self._base_system, *blocks])
|
||||
if m.get("content") != content:
|
||||
messages[i] = {**m, "content": content}
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user