From 1aff7ea2fce41251d521910cd9fd7ab2cc4fecf8 Mon Sep 17 00:00:00 2001 From: tocmo0nlord Date: Sun, 5 Jul 2026 19:15:21 +0000 Subject: [PATCH] Add deterministic geography: precomputed nearest-office distances injected per turn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- bot.py | 16 +++- callstate.py | 31 +++++++- practice.py | 207 +++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 242 insertions(+), 12 deletions(-) diff --git a/bot.py b/bot.py index c436b3c..d701a38 100644 --- a/bot.py +++ b/bot.py @@ -103,7 +103,11 @@ WHISPER_COMPUTE = os.environ.get("WHISPER_COMPUTE", "float16") WHISPER_HOTWORDS = os.environ.get( "WHISPER_HOTWORDS", "Advanced Vision Care, eye exam, annual exam, appointment, optometry, contact lens, " - "Hialeah, Kendall, Tamarac, Pembroke Pines, Lauderdale Lakes, Miami Gardens, Boca Raton", + "Hialeah, Kendall, Tamarac, Pembroke Pines, Lauderdale Lakes, Miami Gardens, Boca Raton, " + # Common caller-area cities (not offices) — the 2026-07-05 call heard "Weston" as + # "Western Florida" repeatedly, which broke the nearest-office flow. + "Weston, Davie, Miramar, Plantation, Sunrise, Hollywood, Coral Springs, Doral, " + "Miami Lakes, Aventura, Coral Gables, Homestead, Cutler Bay", ) # Twilio sends 8 kHz mu-law on the wire, but faster-whisper assumes any numpy array is @@ -245,7 +249,15 @@ SYSTEM_PROMPT = ( "that matches one of our offices (for example they say 'Kendall' and we have a Kendall " "office), simply confirm THAT office and move on — do NOT offer, compare, or mention any " "other office or city, and do NOT ask them to choose between offices. Only if their area " - "matches no office should you name the single nearest one. List offices only if asked.\n" + "matches no office should you name the single nearest one. List offices only if asked. " + "The offices in PRACTICE FACTS are our ONLY offices — never agree we have an office " + "somewhere else, even if the caller insists you mentioned one.\n" + "- DISTANCES / CLOSEST OFFICE: when a GEOGRAPHY section appears below, answer distance and " + "closest-office questions from it word for word — it already lists approximate driving " + "miles from the caller's area to every office, closest first. If there is no GEOGRAPHY " + "section yet, ask what city or area they're in. NEVER estimate, compute, or guess a " + "distance or drive time yourself, and never claim an office is 'close to' or 'near' a " + "place unless the GEOGRAPHY section says so.\n" "- INSURANCE — log only, never promise, never guess: ask open-endedly what insurance they " "have (for example, 'What insurance do you have?'). Do NOT read out or suggest plan names " "from the list — let the caller tell you. Capture ONLY what the caller actually says; NEVER " diff --git a/callstate.py b/callstate.py index 47379d0..fb1ebff 100644 --- a/callstate.py +++ b/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 diff --git a/practice.py b/practice.py index 97b7876..7a3993e 100644 --- a/practice.py +++ b/practice.py @@ -16,18 +16,20 @@ from loguru import logger # ───────────────────────────────────────────────────────────────────────────── # Real facts from advancedvisioncareflorida.com # ───────────────────────────────────────────────────────────────────────────── +# "latlon" is block-level approximate (good enough for nearest-office ranking and +# "about X miles" answers; never used for navigation). LOCATIONS = [ # Broward County - {"city": "Hollywood / Fort Lauderdale", "address": "2873 Stirling Rd, Fort Lauderdale, FL 33312", "phone": "(954) 983-4969"}, - {"city": "Tamarac", "address": "5865 N University Dr, Tamarac, FL 33321", "phone": "(954) 720-2720"}, - {"city": "Pembroke Pines", "address": "246 S Flamingo Rd, Pembroke Pines, FL 33027", "phone": "(954) 443-1230"}, - {"city": "Lauderdale Lakes", "address": "3682 W Oakland Park Blvd, Lauderdale Lakes, FL 33311", "phone": "(954) 730-8087"}, + {"city": "Hollywood / Fort Lauderdale", "address": "2873 Stirling Rd, Fort Lauderdale, FL 33312", "phone": "(954) 983-4969", "latlon": (26.047, -80.183)}, + {"city": "Tamarac", "address": "5865 N University Dr, Tamarac, FL 33321", "phone": "(954) 720-2720", "latlon": (26.198, -80.253)}, + {"city": "Pembroke Pines", "address": "246 S Flamingo Rd, Pembroke Pines, FL 33027", "phone": "(954) 443-1230", "latlon": (26.005, -80.312)}, + {"city": "Lauderdale Lakes", "address": "3682 W Oakland Park Blvd, Lauderdale Lakes, FL 33311", "phone": "(954) 730-8087", "latlon": (26.166, -80.198)}, # Miami-Dade County - {"city": "Hialeah", "address": "1770 W 32nd Pl, Hialeah, FL 33012", "phone": "(305) 885-4477"}, - {"city": "Kendall", "address": "11605 N Kendall Dr, Miami, FL 33176", "phone": "(305) 982-8927"}, - {"city": "Miami Gardens", "address": "4771 NW 183rd St, Miami Gardens, FL 33055", "phone": "(305) 390-2467"}, + {"city": "Hialeah", "address": "1770 W 32nd Pl, Hialeah, FL 33012", "phone": "(305) 885-4477", "latlon": (25.876, -80.300)}, + {"city": "Kendall", "address": "11605 N Kendall Dr, Miami, FL 33176", "phone": "(305) 982-8927", "latlon": (25.686, -80.381)}, + {"city": "Miami Gardens", "address": "4771 NW 183rd St, Miami Gardens, FL 33055", "phone": "(305) 390-2467", "latlon": (25.942, -80.272)}, # Palm Beach County - {"city": "Boca Raton", "address": "21673 State Road 7, Boca Raton, FL 33428", "phone": "(561) 470-2310"}, + {"city": "Boca Raton", "address": "21673 State Road 7, Boca Raton, FL 33428", "phone": "(561) 470-2310", "latlon": (26.358, -80.202)}, ] PRACTICE_FACTS = { @@ -94,6 +96,195 @@ def _find_location(name: str): return None +# ───────────────────────────────────────────────────────────────────────────── +# Geography — deterministic nearest-office answers (no LLM, no external API). +# +# Added after the 2026-07-05 Weston call: the model guessed which office was close +# ("Boca Raton is relatively close", "Weston to Hialeah is twenty miles" — invented) +# and hallucinated a Miami Lakes office. Instead we precompute distances from the +# caller's stated area to every office and inject them via CallStateGroomer, so the +# model only ever repeats numbers it was handed. +# +# Coordinates are approximate residential centers — fine for ranking offices and +# "about X miles", which is all a phone answer needs. +# ───────────────────────────────────────────────────────────────────────────── +GAZETTEER = { + # Broward + "weston": (26.080, -80.390), + "davie": (26.076, -80.277), + "southwest ranches": (26.059, -80.337), + "cooper city": (26.057, -80.272), + "plantation": (26.127, -80.250), + "sunrise": (26.157, -80.300), + "lauderhill": (26.164, -80.208), + "tamarac": (26.203, -80.250), + "north lauderdale": (26.217, -80.226), + "margate": (26.245, -80.206), + "coconut creek": (26.284, -80.184), + "coral springs": (26.271, -80.271), + "parkland": (26.310, -80.237), + "pompano beach": (26.238, -80.125), + "deerfield beach": (26.318, -80.100), + "lighthouse point": (26.276, -80.087), + "oakland park": (26.172, -80.132), + "wilton manors": (26.160, -80.139), + "fort lauderdale": (26.122, -80.137), + "lauderdale lakes": (26.166, -80.208), + "hollywood": (26.011, -80.150), + "dania beach": (26.052, -80.144), + "hallandale beach": (25.981, -80.148), + "pembroke pines": (26.008, -80.297), + "pembroke park": (25.988, -80.178), + "west park": (25.984, -80.199), + "miramar": (25.977, -80.303), + # Miami-Dade + "miami": (25.775, -80.194), + "miami beach": (25.790, -80.130), + "brickell": (25.758, -80.193), + "little havana": (25.773, -80.215), + "north miami": (25.890, -80.187), + "north miami beach": (25.933, -80.163), + "aventura": (25.956, -80.139), + "sunny isles beach": (25.943, -80.122), + "miami gardens": (25.942, -80.246), + "opa-locka": (25.902, -80.250), + "opa locka": (25.902, -80.250), + "miami lakes": (25.909, -80.309), + "hialeah": (25.858, -80.278), + "hialeah gardens": (25.865, -80.324), + "medley": (25.838, -80.332), + "doral": (25.819, -80.355), + "sweetwater": (25.763, -80.373), + "fontainebleau": (25.772, -80.346), + "westchester": (25.745, -80.327), + "west miami": (25.757, -80.296), + "coral gables": (25.721, -80.268), + "south miami": (25.707, -80.293), + "pinecrest": (25.667, -80.308), + "kendall": (25.679, -80.355), + "west kendall": (25.706, -80.439), + "kendale lakes": (25.708, -80.407), + "the hammocks": (25.670, -80.445), + "tamiami": (25.758, -80.402), + "country walk": (25.633, -80.432), + "richmond west": (25.611, -80.430), + "palmetto bay": (25.622, -80.320), + "cutler bay": (25.575, -80.336), + "homestead": (25.469, -80.478), + "florida city": (25.448, -80.479), + "key biscayne": (25.691, -80.163), + # Palm Beach (southern) + "boca raton": (26.359, -80.130), + "west boca": (26.351, -80.210), + "delray beach": (26.461, -80.073), + "boynton beach": (26.525, -80.087), + "lake worth": (26.617, -80.072), + "wellington": (26.659, -80.241), + "west palm beach": (26.715, -80.054), +} + +_PLACE_ALIASES = { + "westin": "weston", # frequent STT spelling of Weston (hotel-chain bias) + "ft lauderdale": "fort lauderdale", + "ft. lauderdale": "fort lauderdale", + "kendall west": "west kendall", + "hallandale": "hallandale beach", + "sunny isles": "sunny isles beach", + "the pines": "pembroke pines", +} + +# Gazetteer keys that ARE one of our office cities (used to prefer the caller's own +# area over office names they merely asked about). +OFFICE_PLACE_KEYS = frozenset({ + "hollywood", "fort lauderdale", "tamarac", "pembroke pines", "lauderdale lakes", + "hialeah", "kendall", "miami gardens", "boca raton", +}) + +# Which office a gazetteer key belongs to, for the "right in " special case. +_OFFICE_FOR_KEY = { + "hollywood": "Hollywood / Fort Lauderdale", + "fort lauderdale": "Hollywood / Fort Lauderdale", + "tamarac": "Tamarac", + "pembroke pines": "Pembroke Pines", + "lauderdale lakes": "Lauderdale Lakes", + "hialeah": "Hialeah", + "kendall": "Kendall", + "miami gardens": "Miami Gardens", + "boca raton": "Boca Raton", +} + +# Longest names first so "north miami beach" wins over "north miami" over "miami". +_PLACE_RE = re.compile( + r"\b(" + "|".join( + re.escape(k) for k in sorted(list(GAZETTEER) + list(_PLACE_ALIASES), key=len, reverse=True) + ) + r")\b" +) + +# Straight-line → driving miles fudge factor for the South Florida grid. +_ROAD_FACTOR = 1.3 + + +def _haversine_miles(a, b) -> float: + import math + lat1, lon1, lat2, lon2 = map(math.radians, (*a, *b)) + h = (math.sin((lat2 - lat1) / 2) ** 2 + + math.cos(lat1) * math.cos(lat2) * math.sin((lon2 - lon1) / 2) ** 2) + return 3958.8 * 2 * math.asin(math.sqrt(h)) + + +def _road_miles(a, b) -> int: + return max(1, round(_haversine_miles(a, b) * _ROAD_FACTOR)) + + +def match_place(text: str): + """Canonical gazetteer key for a free-text place mention, or None.""" + if not text: + return None + m = _PLACE_RE.search(text.lower()) + if not m: + return None + k = m.group(1) + return _PLACE_ALIASES.get(k, k) + + +def places_in_text(text: str): + """All gazetteer places mentioned in the text, canonical, in order of appearance.""" + if not text: + return [] + return [_PLACE_ALIASES.get(k, k) for k in _PLACE_RE.findall(text.lower())] + + +def geography_block(place_key: str) -> str: + """Precomputed distance table from the caller's area to every office, closest + first — injected into the system prompt so distance / nearest-office answers are + read, never guessed. Returns "" for an unknown place.""" + coords = GAZETTEER.get(place_key) + if not coords: + return "" + pretty = place_key.title() + ranked = sorted( + ((l["city"], _road_miles(coords, l["latlon"])) for l in LOCATIONS), + key=lambda x: x[1], + ) + table = "; ".join( + f"{city} about {mi} miles" + (" (CLOSEST)" if i == 0 else "") + for i, (city, mi) in enumerate(ranked) + ) + home = _OFFICE_FOR_KEY.get(place_key) + if home: + situated = f"We have an office right in {pretty} (the {home} office)." + else: + situated = (f"We do NOT have an office in {pretty}; the closest is " + f"{ranked[0][0]}, about {ranked[0][1]} miles away.") + return ( + f"GEOGRAPHY (precomputed, approximate driving distances from {pretty} — the ONLY " + f"source you may use for any distance or closest-office answer; NEVER estimate a " + f"distance yourself and NEVER confirm an office at any place not in PRACTICE FACTS):\n" + f"- {situated}\n" + f"- From {pretty}: {table}." + ) + + # ─── Tools (used when ENABLE_TOOLS=true and the model supports tool-calling) ── def persist_appointment(record: dict) -> str: