Voice AI Platform

A multi-tenant platform for AI-powered phone agents

RoleSole Engineer
TypeVoice AI Platform
StackPython / LiveKit / Next.js
StatusProprietary · Internal

This is a company project. The write-up is intentionally generalized: no client names, call transcripts, or source code appear here. The performance figures below are my own measurements from the work. What follows is my own account of the problem, the architecture, and the decisions I made as the sole engineer.

VOICE AGENT SPEECH → STT → LLM → TTS → SPEECH
01Overview

This is a multi-tenant platform for building and running AI-powered phone agents. Operators configure an agent once (its persona, script, knowledge, and behaviour), and the platform then places and answers real phone calls on its behalf, holding a natural spoken conversation with the person on the other end and recording everything for later review.

Under each call is a real-time voice pipeline: speech is transcribed to text, the text is handled by an LLM that reasons over the agent's configuration and knowledge base, and the reply is spoken back with a text-to-speech voice, all within the tight latency budget a human conversation demands. Both inbound and outbound calls run over SIP telephony, and every call is stored with its transcript, outcome, and latency metrics for post-call analysis.

I built the whole system as the sole engineer over six months: the API, the voice-agent worker, the retrieval layer, the admin dashboard, and the infrastructure that ties them together. That meant owning every decision from telephony and real-time media all the way up to the operator-facing UI.

02Key Features
Configurable Agents
Operators define an agent's persona, conversation flow, and variables through the dashboard, with no code, and the platform runs it live on real calls.
Inbound & Outbound Calling
Handles both directions over SIP: answering incoming calls and dialling out for campaigns, with call routing to the right agent per tenant.
Real-Time Voice Pipeline
A streaming STT → LLM → TTS loop tuned for low turn-taking latency, so the agent feels like a conversation rather than a form.
Knowledge Base (RAG)
Documents are ingested, embedded, and retrieved at conversation time so agents answer from grounded, tenant-specific knowledge instead of guessing.
Post-Call Review
Every call stores its full transcript, disposition, and per-stage latency metrics, giving operators a clear record to audit and improve agents.
Multi-Tenant Isolation
Each client's agents, data, and knowledge live in their own isolated workspace on shared infrastructure, keeping tenants cleanly separated.
Conversation Flow Builder
A visual conversation-flow builder (“journeys”) built on React Flow that lets operators map out conversational paths.
Test & Evaluation Harness
A dedicated evaluation and load-test harness for dry-run campaigns, automated conversation grading, and comparing STT accuracy.
Call Quality & Barge-In
Audio handling includes background-noise suppression and speech-overlap handling, so callers can interrupt the agent naturally.
03Technical Architecture

The platform is split into focused services rather than one monolith. A FastAPI core API owns the domain (tenants, agents, campaigns, calls, and auth) and is the single source of truth the dashboard talks to. A separate voice-agent worker, built on LiveKit's agents framework, is where each live call actually runs: it bridges the SIP audio, drives the STT → LLM → TTS loop, and streams speech in both directions.

Knowledge lives behind its own retrieval service. Documents are chunked, embedded, and stored in a Qdrant vector database, and the agent queries it mid-conversation to ground its answers. Keeping retrieval as a distinct service meant the ingestion side could evolve independently of the latency-sensitive call path.

Background and cross-service work (ingestion jobs, campaign dialling, and other async tasks) runs through a Redis-backed task queue, so the real-time call path never blocks on slow work. PostgreSQL is the durable store for tenants, agents, and call records; the dashboard is a Next.js app for the internal operators.

The operator experience is built into a Next.js dashboard (the console is 31 routes and 107 components) featuring a visual conversation-flow builder (“journeys”) powered by React Flow, letting operators map out complex call journeys. It also handles SIP trunk administration, inbound number routing, campaign management, call analytics charts built with Recharts, and full agent version history, so operators can safely roll back changes.

A dedicated Python test harness sits alongside the core services to validate changes before they hit production, and the backend carries 2,585 tests. It runs automated load tests, SIP trunk ramp tests, dry-run campaigns that exercise routing without placing real calls, and an automated conversation evaluation and grading harness that compares speech-to-text accuracy.

The recurring design tension across all of it was latency. A phone conversation is unforgiving: too much delay between someone finishing a sentence and the agent replying and it stops feeling human. Much of the engineering went into streaming each stage of the pipeline and measuring per-stage latency so regressions were visible instead of anecdotal. The measured results are in the latency section below.

Architecture: a SIP call reaches LiveKit, a voice-agent worker runs the STT, LLM and TTS loop and queries a retrieval service, and a FastAPI core API with PostgreSQL and Redis serves the Next.js dashboard. Caller / SIP trunk LiveKit + SIP media broker Voice-agent worker one job per live call STT LLM TTS VAD + endpointing, intent guards, flow engine, tools, barge-in Knowledge service FastAPI + Qdrant (RAG) Core API FastAPI + arq worker Postgres Redis Operator console Next.js audio audio retrieve config in, transcript + metrics out
Service layout, simplified. The live call path (top row) never blocks on the task queue.
04Anatomy of a Call
  1. ConnectAn inbound SIP INVITE reaches LiveKit, which opens a room and dispatches a job to the voice-agent worker. Outbound calls leave a queue, pass campaign and dial-lock checks, and dial out over the SIP trunk.
  2. LoadThe agent joins the room, loads the tenant’s configuration, prompts and conversation flow, and waits until the SIP leg is actually answered.
  3. GreetThe greeting plays first, before any caller speech, so the line is never silent on pickup.
  4. Turn loopVoice-activity detection and streaming STT decide when the caller has finished. Intent guards run, the knowledge base is queried, the LLM replies and calls tools, and TTS streams the answer back. The caller can interrupt at any point.
  5. EndThe call ends on an end-call tool, a terminal node in the flow, a hang-up, a maximum duration, or a transfer.
  6. ReviewThe worker saves the transcript and per-stage latency metrics before it leaves the room. The API then grades the call with an LLM to set its disposition and trigger follow-up actions.
05Latency, Measured

One conversational turn, split by stage. These are medians over 1,057 production turns, taken from the per-stage metrics every call stores. The stages add up to 1,318 ms against a measured 1,303 ms round trip, so no time is unaccounted for.

Median turn latency: speech-to-text 580 ms, of which 561 ms is waiting for the caller to finish; language model first token 559 ms; text-to-speech first audio 179 ms; 1,303 ms measured round trip. ONE TURN, p50 · n = 1,057 PRODUCTION TURNS STT 580 ms 561 ms = waiting for the caller LLM first token 559 ms TTS 179 ms 1,303 ms round trip
The largest single term is our own endpointing wait, the configured silence before the agent decides the caller is done. It is a tuning decision, not vendor latency.

Two other fixes were measured the same way. The first was geography: speech-to-text was resolving to a US endpoint 235–255 ms from the servers in Mumbai. The provider’s EU endpoint serves the same models at 118 ms, so moving there took about 120 ms off every turn.

The second fix was audio quality. On a bad call the telephony mixer that feeds the caller was starving for audio, and it dropped 4.37% of the agent’s own speech. I fixed the mixer and added a short pause at pickup, then measured four live calls:

Broken call“Acceptable” callMixer fix+ Pickup pause
Buffer starvations / min71.052.923.520.8
Stalls over 100 ms23300
Agent speech discarded4.37%1.22%0.08%0.00%
06Tech Stack
Python 3.12Backend services
FastAPICore & retrieval APIs
LiveKit AgentsReal-time voice worker
SIPInbound/outbound telephony
QdrantVector database
arq on RedisTask queue
DeepgramStreaming speech-to-text
Cartesia & ElevenLabsText-to-speech
PostgreSQLDurable data store
Next.js + TypeScriptOperator dashboard
React FlowVisual flow builder
DockerInfra & deployment