Fix 4 failure modes from the 2026-07-05 live call (the 'Nina' call)

- callstate: corrections win (most recent caller statement replaces old
  slots — 'no, my name is Carlos' was ignored); patient_name only from the
  caller explicitly giving their own name; booking classified only on
  explicit scheduling intent (small talk flipped a call to booking and
  drove a re-ask loop); new will_call_back call type short-circuits to a
  warm close instead of a message-taking flow.
- bot: per-call _today_line() injects the real current date (spoken form,
  answer-only) — AVA guessed 'Tuesday' on a Sunday and argued; hours rule
  now also bans stating open/closed days ('closed on Sundays' was invented).
- EndCallProcessor: phone-confirm gate reads CallState (booking/callback +
  a captured name or reason) instead of the keyword heuristic when tracking
  is on, and the injected confirmation now ends the call — the goodbye had
  already played, and holding the line open confused the caller.

Verified: staged replay of the failed transcript on activeblue-avc:latest
(4 stages x 3 runs, all pass, incl. clean-booking control); ab_replay.py
--state 0 failures, latency med 0.52s. Deployed via systemd restart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
tocmo0nlord
2026-07-05 05:44:12 +00:00
parent db986ecf76
commit e714262155
3 changed files with 137 additions and 27 deletions

View File

@@ -33,17 +33,30 @@ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
_STATE_INSTRUCTIONS = (
"You are tracking the state of a LIVE phone call between a caller and the receptionist "
"of an optometry practice. From the transcript, extract only what the CALLER has clearly "
"provided so far. Respond with ONLY a JSON object with these keys:\n"
' "call_type": "booking" (wants to schedule a visit), "callback" (wants something staff '
"must check off-phone: order/frames/lens/prescription status, billing, account lookup, "
'reach a person), "question" (just asking something), or "unknown"\n'
"provided so far. The assistant's lines are context only and MAY CONTAIN MISTAKES — never "
"treat something as true just because the assistant said it. Corrections win: if the "
"caller corrects a value ('no, my name is Carlos', 'actually Tuesday'), output the "
"caller's MOST RECENT statement and discard the old value entirely. "
"Respond with ONLY a JSON object with these keys:\n"
' "call_type": one of:\n'
' "booking" — ONLY if the caller explicitly asked to schedule/book a visit or come '
"in. Greetings, small talk, or asking what you can help with are NOT booking.\n"
' "callback" — wants something staff must check off-phone: order/frames/lens/'
"prescription status, billing, account lookup, reach a person.\n"
' "will_call_back" — the caller said THEY will call back later, have to go, or want '
"to end the call.\n"
' "question" — just asking something.\n'
' "unknown" — none of the above clearly applies yet. When in doubt, use "unknown", '
'not "booking".\n'
' "reason": string or null — WHAT the caller wants. For booking: the visit type or eye '
'problem (e.g. "annual exam", "eye pain"). For callback: what they want checked or done, '
'even if phrased as a question (e.g. "are my glasses ready", "status of an order", '
'"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'
' "patient_name": string or null — the caller\'s name as given (full or first-only)\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"
' "name_is_full": boolean — true only if it clearly has first AND last name\n'
' "insurance": string or null — the plan the caller named, exactly as said\n'
' "preferred_time": string or null — day/time in the caller\'s own words\n'
@@ -101,6 +114,18 @@ def build_state_block(state) -> str:
if not state:
return ""
ctype = (state.get("call_type") or "unknown").strip().lower()
if ctype == "will_call_back":
# Observed failure (2026-07-05 call): "I'll call you right back" got treated as a
# message-taking flow — the agent pushed for a callback number and invented a
# third-person caller. Cut straight to a warm close instead.
return (
"CALL STATE (auto-tracked from this conversation — trust it over your memory):\n"
"- The caller said THEY will call back. Do NOT ask for anything else — no name, "
"no phone number, no booking questions — and do not offer to take a message. "
"Warmly tell them they're welcome to call back anytime, and end your reply with "
"'Goodbye'."
)
got, needed = [], []
for key, label in _BOOKING_ORDER:
val = (state.get(key) or "").strip() if isinstance(state.get(key), str) else ""
@@ -197,6 +222,12 @@ class CallStateGroomer(FrameProcessor):
self._state = None
self._task = None
@property
def state(self):
"""Latest extracted slot state (dict or None). Read by EndCallProcessor's
phone-confirm gate so it only fires on real booking/callback captures."""
return self._state
def _extract_done(self, task):
self._task = None
if task.cancelled():