AI Native Call Center
An open-source call center where the AI is the default answer, not an add-on.
Calls arrive at FreeSWITCH and are answered by a voice model over a speech-to-speech connection the application terminates itself. The model talks; a flow steers the conversation and decides when a person is needed. When one is, the caller is transferred into a real queue where real agents are waiting, their SIP leg held by a companion Chrome extension. Everything the two halves do lands in one call record.
It is one Go binary — REST API, event stream and the whole web interface inside it — plus PostgreSQL and FreeSWITCH.
!Agent desktop: an inbound call is answered, the live transcript fills in, then after-call work
Try it
Linux:
curl -fsSL https://github.com/rasonyang/ai-native-callcenter/releases/latest/download/install.sh | sudo sh
macOS:
curl -fsSL https://github.com/rasonyang/ai-native-callcenter/releases/latest/download/install.sh | sh
Details: deploy/one-line-installer.md.
What it does
Answers with a model, not a menu. The AI leg is a SIP endpoint inside the application: FreeSWITCH bridges the caller to it and audio goes straight to the provider — G.711 passed through byte for byte where the provider accepts it, so nothing decodes or resamples on the way.
Steers without scripting the conversation. The model owns the dialogue; the
flow owns the phase. A phase carries instructions and a list of tools the model
may use; transitions fire on tool results. A phase may also carry a line of its
own — a greeting, a hand-over script, a goodbye — which the bot is meant to say
as written rather than paraphrase: doubao is handed the words to speak, the
other four providers are instructed to repeat them word for word. The built-in
tools may refuse —
"the queue is closed" is something to talk about, not an error — and the
persona, the rules and the bot's voice are published and versioned together.
Hands over to people properly. Transfers go into mod_callcenter queues,
and the agent's screen has popped by the time the phone rings: a
PARTY_RINGING event over SSE says who is calling and carries the call's
userData, and the bot-phase transcript is already there to read, because the
call id is minted before any leg exists and survives the transfer. Agents work
in the browser: presence, call control, callbacks — but not the audio, which
belongs to the web-sip-phone
Chrome extension, a separate repository. It holds the agent's SIP registration
with credentials this application issues at sign-in, and it has no dialpad of
its own: answering, holding and hanging up are REST calls here, carried to the
phone over ESL uuid_phone_event. Supervisors get a live wallboard, the queue
view and the roster.
Keeps one record per conversation. A call that a bot answered, handed to a queue and an agent finished is one CDR with one transcript and one recording — not three fragments. Recordings go to a filesystem or to any S3-compatible store.
Speaks two languages, and admits which provider it runs. English and
Chinese throughout, interface and bot. One provider answers every call in a
deployment, chosen at startup: qwen or doubao inside mainland China,
openai or gemini elsewhere, or gateway for a Realtime gateway of your
own. A call's language never selects it. Three of those are profiles of one
protocol; doubao and gemini each speak a different one and have a client of
their own (how a provider is added).
How it fits together
┌──────────── one Go binary ────────────┐
caller ──▶ FreeSWITCH ──▶ SIP UAS ──▶ provider (Realtime, speech-to-speech)
│ │
│ └─ flow engine: phases, tools, transfers
│
├─ mod_callcenter queues ──▶ agents (browser + web-sip-phone)
│
└─ ESL ──▶ call registry ──▶ REST + SSE ──▶ web interface
│
PostgreSQL
FreeSWITCH reads its directory and its mod_callcenter queues *from the
database*, through Lua. The dialplan is static XML, but its rules decide
nothing by themselves: each one hands the call to a Lua script that looks the
answer up. Adding an extension, a queue or a number is a database change; the
switch's own files change only when the routing itself does.
Two design notes worth knowing before reading the code:
- The domain model is Genesys-lineage. A call aggregates parties; leg
PARTY_, call-scoped ones are CALL_.
- Every live call is an actor — one goroutine as its sole mutator, snapshots by
internal/telephony locks call state, because there is
no shared call state; the AI leg is the exception, holding mutexes over the
playback and recording state its own goroutines share.
The full design is in docs/design/, starting with
the overview.
Building
make dev-up # PostgreSQL in Docker
cd web && npm install && npm run build && cd ..
make build # bin/aicc, with the interface embedded
./bin/aicc useradd -username admin -password '…' -role ADMIN
./bin/aicc
For frontend work, make web-dev runs Vite on 5173 against the API on 8080.
go test -race ./... # always -race; it has caught real bugs here
make lint # go vet, gofmt, oxlint
make api-check # the API contract gate
The HTTP API is spec-first: docs/openapi.json is the
single source of truth, and the Go server and the TypeScript client are
generated from it. Edit the contract, run make api-generate, then implement.
Never the other way round.
Extending it
- A new voice provider is a profile, not a client:
docs/provider-extension.md.
- A new flow is a JSON document validated at load — see
internal/seed/flows/ for a working bilingual one.
- A new screen follows the design system in
web/CLAUDE.md, which is binding rather than advisory.
- An integration with another product is a service behind the flow's
CONTRIBUTING.md.
Status
The human path, the AI path, the product surface and the packaging are built and verified against live FreeSWITCH and live providers.
Performance is not yet a claim this project makes. The design has a capacity budget and a latency target (design 06), and the harness to test them against is in the repository (docs/load-tests.md) — but the benchmark campaign itself is still to come, so treat the budget as an intention rather than a measurement.
What is deliberately not here, and will not be: any cascaded ASR + LLM + TTS pipeline inside this process. That composition belongs in a separate service speaking the same protocol.
License
Apache-2.0. See LICENSE.