Capture full appointment details + validate dates in-call
In-call (system prompt + per-call calendar injection): - Gather full name (prompt asks for last name if only first given). - Confirm the caller-ID number; if declined, use the number the caller gives. - Ask for and LOG insurance only — never promise/confirm/deny coverage or treatment based on it; staff verify on callback. - Validate the requested date against an injected 45-day calendar (recomputed per call since the server is long-running). Push back on impossible/mismatched dates, e.g. "Monday lands on the sixth — would you like that date?". - AGENT_NAME=AVA; 4s grace pause before hang-up (HANGUP_DELAY_SECS). Logging (post-call extraction -> Odoo): - Extract full name, phone_confirmed, chosen callback (caller-ID or alternate), insurance, reason, and preferred time annotated with a resolved YYYY-MM-DD date (today's date is fed to the extractor). - odoo_client: insurance row on the lead note (log only — staff verify). .gitignore: ignore rotated avc_run.log* files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
53
extract.py
53
extract.py
@@ -8,6 +8,7 @@ single JSON-mode completion, not mid-conversation tool emission.
|
||||
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@@ -19,11 +20,16 @@ _EXTRACT_INSTRUCTIONS = (
|
||||
"for an optometry practice. Extract any APPOINTMENT REQUEST the caller made.\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'
|
||||
' "patient_name": string or null\n'
|
||||
' "callback_number": string or null (digits the caller gave to be called back)\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 (e.g. eye exam, broken glasses)\n'
|
||||
' "preferred_time": string or null (day/time in the caller\'s words)\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, '
|
||||
"computed from TODAY given below; null if it cannot be determined\n"
|
||||
"Use null for anything not clearly stated. Do not invent values."
|
||||
)
|
||||
|
||||
@@ -45,6 +51,7 @@ async def extract_and_record(messages, ollama_url, model, call_sid=None, caller_
|
||||
if base.endswith("/v1"):
|
||||
base = base[:-3]
|
||||
|
||||
today = datetime.now().strftime("%A, %B %d, %Y").replace(" 0", " ")
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
r = await client.post(
|
||||
@@ -55,7 +62,7 @@ async def extract_and_record(messages, ollama_url, model, call_sid=None, caller_
|
||||
"stream": False,
|
||||
"options": {"temperature": 0},
|
||||
"messages": [
|
||||
{"role": "system", "content": _EXTRACT_INSTRUCTIONS},
|
||||
{"role": "system", "content": f"{_EXTRACT_INSTRUCTIONS}\n\nTODAY is {today}."},
|
||||
{"role": "user", "content": f"Transcript:\n{transcript}"},
|
||||
],
|
||||
},
|
||||
@@ -78,24 +85,44 @@ async def extract_and_record(messages, ollama_url, model, call_sid=None, caller_
|
||||
logger.info("Post-call extraction: appointment intent but no name/reason captured — skipping card")
|
||||
return None
|
||||
|
||||
# Prefer the verified Twilio caller-ID over a number pulled from the transcript —
|
||||
# the model sometimes invents/echoes a phone number. Keep a genuinely different
|
||||
# spoken number as a note for staff.
|
||||
spoken = (data.get("callback_number") or "").strip()
|
||||
callback = caller_number or spoken or None
|
||||
# Callback number: default to the verified Twilio caller-ID. If the caller explicitly
|
||||
# declined it (phone_ok == False) and gave a different number, use the one they gave.
|
||||
alt = (data.get("alternate_number") or "").strip()
|
||||
phone_ok = data.get("phone_ok")
|
||||
if phone_ok is False and alt:
|
||||
callback = alt
|
||||
else:
|
||||
callback = caller_number or alt or None
|
||||
# If they gave a different number but didn't clearly decline the caller-ID, keep it as a note.
|
||||
note_alt = (
|
||||
alt and caller_number and callback != alt
|
||||
and re.sub(r"\D", "", alt) != re.sub(r"\D", "", caller_number)
|
||||
)
|
||||
|
||||
# Preferred time = caller's words, annotated with the resolved calendar date if we got one.
|
||||
preferred = (data.get("preferred_time") or "").strip() or None
|
||||
resolved = (data.get("resolved_date") or "").strip() or None
|
||||
if resolved:
|
||||
preferred = f"{preferred} (resolved: {resolved})" if preferred else resolved
|
||||
|
||||
reason = data.get("reason")
|
||||
if spoken and caller_number and re.sub(r"\D", "", spoken) != re.sub(r"\D", "", caller_number):
|
||||
reason = f"{reason or ''} (caller mentioned alternate number: {spoken})".strip()
|
||||
if note_alt:
|
||||
reason = f"{reason or ''} (caller also mentioned number: {alt})".strip()
|
||||
|
||||
record = {
|
||||
"call_sid": call_sid,
|
||||
"patient_name": data.get("patient_name"),
|
||||
"callback_number": callback,
|
||||
"phone_confirmed": phone_ok,
|
||||
"location": data.get("location"),
|
||||
"reason": reason,
|
||||
"preferred_time": data.get("preferred_time"),
|
||||
"insurance": (data.get("insurance") or "").strip() or None,
|
||||
"preferred_time": preferred,
|
||||
"source": "post_call_extraction",
|
||||
}
|
||||
where = persist_appointment(record)
|
||||
logger.info(f"Post-call appointment saved ({where}): {record['patient_name']} / {record['location']}")
|
||||
logger.info(
|
||||
f"Post-call appointment saved ({where}): {record['patient_name']} / "
|
||||
f"{record['location']} / ins={record['insurance']} / when={record['preferred_time']}"
|
||||
)
|
||||
return record
|
||||
|
||||
Reference in New Issue
Block a user