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:
41
callstate.py
41
callstate.py
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user