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>
157 lines
6.8 KiB
Python
157 lines
6.8 KiB
Python
"""Advanced Vision Care practice facts + the phone agent's tools.
|
|
|
|
Facts sourced from advancedvisioncareflorida.com (8 locations across Broward,
|
|
Miami-Dade, Palm Beach). NOTE: the website does NOT publish office hours, so we do
|
|
NOT assert hours — the agent must offer to have staff confirm them instead of
|
|
inventing them. Fill HOURS in if/when you have them.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
from datetime import datetime, timezone
|
|
|
|
from loguru import logger
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Real facts from advancedvisioncareflorida.com
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
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"},
|
|
# 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"},
|
|
# Palm Beach County
|
|
{"city": "Boca Raton", "address": "21673 State Road 7, Boca Raton, FL 33428", "phone": "(561) 470-2310"},
|
|
]
|
|
|
|
PRACTICE_FACTS = {
|
|
"name": "Advanced Vision Care",
|
|
"locations": LOCATIONS,
|
|
"insurance": [
|
|
"CarePlus", "Doctors Health", "Florida Blue Medicare", "Optum", "Spectera",
|
|
"Sunshine Health", "VSP", "WellCare",
|
|
],
|
|
"services": (
|
|
"routine and medical eye exams, contact lens exams, pediatric eye exams, "
|
|
"and LASIK consultations"
|
|
),
|
|
# Website does not publish hours — leave None so the agent won't invent them.
|
|
"hours": None,
|
|
}
|
|
|
|
REQUESTS_LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "appointment_requests.jsonl")
|
|
|
|
|
|
# Expand street abbreviations so the TTS speaks "North Kendall Drive", not "N … D-R".
|
|
_ABBREV = {
|
|
"NW": "Northwest", "NE": "Northeast", "SW": "Southwest", "SE": "Southeast",
|
|
"N": "North", "S": "South", "E": "East", "W": "West",
|
|
"Dr": "Drive", "Rd": "Road", "Blvd": "Boulevard", "St": "Street",
|
|
"Ave": "Avenue", "Pl": "Place", "Ln": "Lane", "Ct": "Court", "Hwy": "Highway",
|
|
"FL": "Florida",
|
|
}
|
|
|
|
|
|
def _spoken_address(addr: str) -> str:
|
|
"""Expand directional + street-type abbreviations for natural speech."""
|
|
return re.sub(
|
|
r"\b(" + "|".join(re.escape(k) for k in _ABBREV) + r")\b",
|
|
lambda m: _ABBREV[m.group(1)],
|
|
addr,
|
|
)
|
|
|
|
|
|
def practice_summary() -> str:
|
|
"""Compact facts block for the system prompt."""
|
|
f = PRACTICE_FACTS
|
|
loc_lines = "\n".join(f" - {l['city']}: {_spoken_address(l['address'])} — {l['phone']}" for l in f["locations"])
|
|
hours = f["hours"] or (
|
|
"NOT published — do not state specific hours; offer to have the office confirm."
|
|
)
|
|
return (
|
|
f"Practice name: {f['name']}\n"
|
|
f"Locations ({len(f['locations'])} offices across South Florida):\n{loc_lines}\n"
|
|
f"Insurance accepted (these EXACT plans only): {', '.join(f['insurance'])}.\n"
|
|
f"Services: {f['services']}\n"
|
|
f"Hours: {hours}\n"
|
|
)
|
|
|
|
|
|
def _find_location(name: str):
|
|
"""Loose match a caller's city/location text to a known office."""
|
|
if not name:
|
|
return None
|
|
n = name.lower()
|
|
for l in LOCATIONS:
|
|
if n in l["city"].lower() or l["city"].lower() in n:
|
|
return l
|
|
return None
|
|
|
|
|
|
# ─── Tools (used when ENABLE_TOOLS=true and the model supports tool-calling) ──
|
|
|
|
def persist_appointment(record: dict) -> str:
|
|
"""Write an appointment request to Odoo (a crm.lead) if configured, else to the
|
|
JSONL fallback so a request is never lost. Returns where it landed. Used by both
|
|
the post-call extraction and the (optional) in-call tool."""
|
|
record.setdefault("ts", datetime.now(timezone.utc).isoformat())
|
|
if os.environ.get("ODOO_USER") and os.environ.get("ODOO_API_KEY"):
|
|
try:
|
|
from odoo_client import create_appointment_request
|
|
|
|
model, rec_id = create_appointment_request(
|
|
patient_name=record.get("patient_name"),
|
|
callback_number=record.get("callback_number"),
|
|
reason=f"[{record.get('location') or 'location TBD'}] {record.get('reason') or ''}".strip(),
|
|
preferred_time=record.get("preferred_time"),
|
|
insurance=record.get("insurance"),
|
|
call_sid=record.get("call_sid"),
|
|
)
|
|
logger.info(f"Appointment -> Odoo {model} id={rec_id}: {record.get('patient_name')}")
|
|
return f"odoo:{model}:{rec_id}"
|
|
except Exception as e:
|
|
logger.warning(f"Odoo write failed ({e!r}); falling back to local log")
|
|
record["odoo_error"] = repr(e)
|
|
|
|
with open(REQUESTS_LOG, "a") as fh:
|
|
fh.write(json.dumps(record) + "\n")
|
|
logger.info(f"Appointment -> JSONL: {record.get('patient_name')}")
|
|
return "jsonl"
|
|
|
|
|
|
async def record_appointment_request(params):
|
|
"""In-call tool path (only used when ENABLE_TOOLS=true). Wraps persist_appointment."""
|
|
args = params.arguments or {}
|
|
persist_appointment({
|
|
"call_sid": getattr(params, "call_sid", None),
|
|
"patient_name": args.get("patient_name"),
|
|
"callback_number": args.get("callback_number"),
|
|
"location": args.get("location"),
|
|
"reason": args.get("reason"),
|
|
"preferred_time": args.get("preferred_time"),
|
|
"source": "in_call_tool",
|
|
})
|
|
await params.result_callback(
|
|
{"status": "captured", "message": "Got it — our staff will call you back to confirm the time."}
|
|
)
|
|
|
|
|
|
async def get_practice_info(params):
|
|
"""Return practice facts (optionally narrowed to one location) for accurate answers."""
|
|
args = params.arguments or {}
|
|
loc = _find_location(args.get("location", ""))
|
|
result = {
|
|
"name": PRACTICE_FACTS["name"],
|
|
"insurance": PRACTICE_FACTS["insurance"],
|
|
"services": PRACTICE_FACTS["services"],
|
|
"hours": "not published — offer to have the office confirm",
|
|
}
|
|
result["location"] = loc if loc else PRACTICE_FACTS["locations"]
|
|
await params.result_callback(result)
|