Handle non-booking requests: take a message, log a callback note
A caller asking if their already-purchased frames were ready got railroaded through the booking script and hung up. AVA had no path for requests it can't do on the phone. - Prompt: classify intent first — question (answer it), can't-do request (take name + a one-line note, confirm callback number, promise a staff callback; never force booking questions), or booking (the ordered steps). - extract.py: request_type = appointment | callback | none. Callback gate needs a name or a request note. Records kind. - practice.py / odoo_client.py: callbacks write a "📞 Callback request" lead (name, callback number, what they need) instead of an appointment card. Verified the classifier: frames-status -> callback, booking -> appointment, pure question -> none. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
46
extract.py
46
extract.py
@@ -17,18 +17,23 @@ from practice import persist_appointment
|
||||
|
||||
_EXTRACT_INSTRUCTIONS = (
|
||||
"You are reviewing a phone-call transcript between a caller and the receptionist "
|
||||
"for an optometry practice. Extract any APPOINTMENT REQUEST the caller made.\n"
|
||||
"for an optometry practice. Classify what the caller wanted and extract the details.\n"
|
||||
"Respond with ONLY a JSON object with these keys:\n"
|
||||
' "wants_appointment": boolean — true only if the caller asked to book/schedule a visit\n'
|
||||
' "request_type": one of "appointment", "callback", "none":\n'
|
||||
' - "appointment": the caller asked to book/schedule a visit\n'
|
||||
' - "callback": the caller wants something staff must handle off the phone — checking '
|
||||
"an existing order or purchase, whether frames/lenses are ready, a prescription or lab "
|
||||
"status, billing, an account lookup, or to reach a specific person\n"
|
||||
' - "none": just a question that was answered, or nothing actionable\n'
|
||||
' "patient_name": string or null — the caller\'s FULL name (first and last) if given\n'
|
||||
' "phone_ok": boolean or null — true if the caller confirmed their caller-ID number is the '
|
||||
"best one, false if they said to use a different number, null if it never came up\n"
|
||||
' "alternate_number": string or null — a different callback number the caller gave, digits only\n'
|
||||
' "location": string or null (which office/city)\n'
|
||||
' "reason": string or null — WHY they want to be seen: the visit type OR the eye '
|
||||
"problem/symptom they describe. Capture symptoms too (e.g. \"annual eye exam\", \"blurry "
|
||||
'vision", "vision loss / blindness", "eye pain", "broken glasses", "red eye"). If they '
|
||||
"describe any eye or vision problem, that IS the reason.\n"
|
||||
' "location": string or null (which office/city — appointments only)\n'
|
||||
' "reason": string or null — for an APPOINTMENT, why they want to be seen: the visit type '
|
||||
"OR the eye problem/symptom they describe (e.g. \"annual eye exam\", \"blurry vision\", "
|
||||
'"vision loss", "eye pain", "broken glasses"). For a CALLBACK, a one-line note of what they '
|
||||
'need (e.g. "checking if frames ordered last week are ready", "question about a bill").\n'
|
||||
' "insurance": string or null — the insurance plan the caller named, exactly as they said it\n'
|
||||
' "preferred_time": string or null — the day/time in the caller\'s own words\n'
|
||||
' "resolved_date": string or null — the actual calendar date the caller means as YYYY-MM-DD, '
|
||||
@@ -76,16 +81,22 @@ async def extract_and_record(messages, ollama_url, model, call_sid=None, caller_
|
||||
logger.exception("Appointment extraction call failed")
|
||||
return None
|
||||
|
||||
if not data.get("wants_appointment"):
|
||||
logger.info("Post-call extraction: no appointment requested")
|
||||
kind = (data.get("request_type") or "none").strip().lower()
|
||||
if kind not in ("appointment", "callback"):
|
||||
logger.info("Post-call extraction: no actionable request")
|
||||
return None
|
||||
|
||||
# Lead-quality gate: a usable lead needs a NAME or a LOCATION (so staff can act on it) —
|
||||
# a bare reason like "check on my eyes" with no name and no office is not worth a card.
|
||||
name = (data.get("patient_name") or "").strip()
|
||||
location = (data.get("location") or "").strip()
|
||||
if not name and not location:
|
||||
logger.info("Post-call extraction: appointment intent but no name/location captured — skipping card")
|
||||
reason_raw = (data.get("reason") or "").strip()
|
||||
# Lead-quality gate so staff get an actionable card:
|
||||
# - appointment: need a NAME or a LOCATION
|
||||
# - callback: need a NAME or a clear note of what they want
|
||||
if kind == "appointment" and not name and not location:
|
||||
logger.info("Post-call extraction: appointment intent but no name/location — skipping card")
|
||||
return None
|
||||
if kind == "callback" and not name and not reason_raw:
|
||||
logger.info("Post-call extraction: callback intent but no name/request note — skipping card")
|
||||
return None
|
||||
|
||||
# Callback number: default to the verified Twilio caller-ID. If the caller explicitly
|
||||
@@ -114,18 +125,19 @@ async def extract_and_record(messages, ollama_url, model, call_sid=None, caller_
|
||||
|
||||
record = {
|
||||
"call_sid": call_sid,
|
||||
"kind": kind, # "appointment" | "callback"
|
||||
"patient_name": data.get("patient_name"),
|
||||
"callback_number": callback,
|
||||
"phone_confirmed": phone_ok,
|
||||
"location": data.get("location"),
|
||||
"location": data.get("location") if kind == "appointment" else None,
|
||||
"reason": reason,
|
||||
"insurance": (data.get("insurance") or "").strip() or None,
|
||||
"preferred_time": preferred,
|
||||
"insurance": (data.get("insurance") or "").strip() or None if kind == "appointment" else None,
|
||||
"preferred_time": preferred if kind == "appointment" else None,
|
||||
"source": "post_call_extraction",
|
||||
}
|
||||
where = persist_appointment(record)
|
||||
logger.info(
|
||||
f"Post-call appointment saved ({where}): {record['patient_name']} / "
|
||||
f"Post-call {kind} saved ({where}): {record['patient_name']} / "
|
||||
f"{record['location']} / reason={record['reason']} / ins={record['insurance']} / "
|
||||
f"when={record['preferred_time']}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user