Transcripts and tool calls
What the worker records from inside a conversation, and why it is best-effort.
The worker is the only thing that sees the inside of a conversation: what each
side said, how long the caller waited for each reply, and which tools fired. It
posts that to POST /public/livegrid/ingest when the call is over.
Three tables#
| Table | Holds |
|---|---|
lg_calls | one row per call — lifecycle, duration, outcome, model, cost |
lg_call_events | one row per turn — seq, at, role, text, latency_ms |
lg_call_tools | one row per tool call — tool_name, arguments_json, result_summary, success |
All three come back together from GET /public/calls/<id>.
Calls API
Best-effort, on purpose#
Nothing here is on the conversational path. Turns are appended to an in-memory buffer and flushed in the background; every failure is caught and logged rather than raised.
A call is the product; a row about a call is not. If the database is slow, the API is redeploying, or the network hiccups, the correct outcome is a missing record and a conversation the person on the other end never noticed. The worst case is a lost transcript, which is exactly where things stood before any of this existed.
The consequence, which you should design around rather than against: a transcript can be missing from a call that certainly happened. The call row will be there — the webhook wrote it — with no events under it. Render that as "no transcript", never as "no call".
Why it posts rather than writing to the database#
The worker runs on the media VM, which has a public IP and UDP 50000–60000 open because that is what an SFU needs. Putting a database connection string on the most exposed machine in the system, to save one HTTP hop, widens the blast radius of exactly the wrong box.
It also keeps the worker's dependency set at livekit-agents and
python-dotenv. aiohttp already ships with the agents SDK, so posting costs
nothing new; a database driver would.
Authentication#
A shared secret you issue, not a signature over a body — this is your own worker, not a third party.
Authorization: Bearer $LIVEGRID_INGEST_SECRET
Unset, the endpoint answers 503 {"error": "ingest is not configured"} rather
than accepting anything.
Ordering does not matter#
Ingest and the webhook upsert into the same row on (room_name, region). The
worker usually finishes first and the room_finished event usually arrives
second, but nothing depends on that — either order converges on the same call.