Attach call recording + transcript to the Odoo lead (Phase 3 item)

odoo_client.attach_call_artifacts posts the transcript to the lead's chatter
(readable in place) and attaches transcript_<sid>.txt + the stereo call WAV.
bot.py wires it post-call: extract_and_record now returns the Odoo ref
(persisted_to), the in-call tool path stamps it too, the audiobuffer handler
records the saved WAV path, and the attach runs in a thread after the lead
lands (brief wait for the async WAV write; failures only log — the lead is
already safe). Verified e2e against db1: create -> attach -> verify -> delete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tocmo0nlord
2026-07-04 17:03:00 +00:00
parent 1e7b7844a4
commit bbfff482a3
4 changed files with 96 additions and 3 deletions

39
bot.py
View File

@@ -650,12 +650,17 @@ async def run_agent(transport, caller_number=None, call_sid=None, do_capture=Tru
# can stamp the verified caller-ID + call_sid onto the lead (the model never supplies a
# phone number — we don't ask for one). With tools on, this writes the Odoo lead IN-CALL,
# so the post-call extraction is skipped below to avoid a duplicate.
# Where the call's artifacts land, filled in as the call runs: the saved recording
# path (audiobuffer handler) and the Odoo ref of the created lead ("odoo:<model>:<id>"),
# so the WAV + transcript can be attached to the lead after the call.
artifacts = {"recording": None, "odoo_ref": None}
if ENABLE_TOOLS:
async def _record_appointment(params):
args = params.arguments or {}
if do_capture:
from practice import persist_appointment
persist_appointment({
artifacts["odoo_ref"] = persist_appointment({
"call_sid": call_sid,
"patient_name": args.get("patient_name"),
"callback_number": caller_number, # verified caller-ID, not model-supplied
@@ -807,6 +812,7 @@ async def run_agent(transport, caller_number=None, call_sid=None, do_capture=Tru
wf.setsampwidth(2)
wf.setframerate(sample_rate)
wf.writeframes(audio)
artifacts["recording"] = path
logger.info(f"Saved call recording: {path} "
f"({len(audio)} bytes, {num_channels}ch @ {sample_rate}Hz)")
@@ -840,13 +846,42 @@ async def run_agent(transport, caller_number=None, call_sid=None, do_capture=Tru
try:
from extract import extract_and_record
await extract_and_record(
rec = await extract_and_record(
context.messages, OLLAMA_URL, OLLAMA_MODEL,
call_sid=call_sid, caller_number=caller_number,
)
if rec:
artifacts["odoo_ref"] = rec.get("persisted_to")
except Exception:
logger.exception("Post-call appointment extraction failed")
# Attach the call recording + transcript to the Odoo lead (Phase 3: staff review a
# lead with the audio and transcript right on it). Only when a lead actually landed
# in Odoo; JSONL-fallback leads get artifacts when re-imported... they stay on disk.
if do_capture and (artifacts["odoo_ref"] or "").startswith("odoo:"):
try:
from extract import format_transcript
from odoo_client import attach_call_artifacts
_, odoo_model, odoo_id = artifacts["odoo_ref"].split(":")
# The recording is written by an async handler after stop_recording —
# give it a moment if it hasn't landed yet.
for _ in range(10):
if not RECORD_CALLS or artifacts["recording"]:
break
await asyncio.sleep(0.5)
await asyncio.to_thread(
attach_call_artifacts,
odoo_model, int(odoo_id),
wav_path=artifacts["recording"],
transcript=format_transcript(context.messages, agent_name=AGENT_NAME),
call_sid=call_sid,
)
logger.info(f"Attached recording+transcript to {artifacts['odoo_ref']} "
f"(wav={artifacts['recording']})")
except Exception:
logger.exception("Attaching call artifacts to Odoo failed (lead itself is safe)")
async def run_call(websocket, serializer: TwilioFrameSerializer, caller_number=None, call_sid=None):
"""Phone entrypoint: wrap the Twilio Media Stream in a transport, run the shared agent."""