Build voice AI agents you can talk to — a tutor, a coding helper, a support agent, a coach, an interviewer, or a phone receptionist — and share each one as a live page or embed it on a website. An agent's instructions are its behaviour, so the same engine powers any of them. Answering real phone calls is an optional add-on, not a requirement.
The hard part — a real-time speech-to-text → reasoning → text-to-speech pipeline — is built to be provider-pluggable: every AI layer runs on either a free open-source model, a free API, or a paid API, chosen per-layer from config. And it ships with built-in mock providers so the whole product runs end-to-end with zero API keys and zero model downloads.
┌────────────────┐ public link / <iframe> ┌──────────────────────────────┐
│ Anyone, in the │ ──────────────────────────▶ │ FastAPI backend │
│ browser │ (no phone needed) │ │
└────────────────┘ │ ┌────────────────────────┐ │
│ │ Voice loop │ │
┌────────────────┐ optional: webhook / WS │ │ STT → Agent → TTS │ │
│ Twilio · Exotel│ ──────────────────────────▶ │ └───────────┬────────────┘ │
│ (phone calls) │ one thin adapter each │ │ │
└────────────────┘ │ ┌──────────▼───────────┐ │
│ │ instructions-driven │ │
┌────────────────┐ REST + WebSocket │ │ agent (+ RAG) │ │
│ React dashboard│ ◀───────────────────────────│ │ — or — │ │
│ (owner only) │ │ │ receptionist graph: │ │
└────────────────┘ │ │ faq/book/msg/escalate│ │
│ └──────────┬───────────┘ │
└──────────────┬──────────────┘
▼
SQLite / Postgres
| Instruction-driven (default) | Receptionist | |
|---|---|---|
| Behaviour | your instructions (persona, rules, tone) | fixed graph: FAQ → book → message → escalate |
| Good for | tutor, coding helper, support, coach, interviewer | appointment businesses |
| Extras | knowledge base (RAG) | + services, hours, bookings, escalation |
| Created via | /new → pick a template |
/new → Receptionist → full setup wizard |
Each capability is an interface with four families of implementation. Pick one
per layer in backend/.env:
| Layer | mock (no setup) |
Open-source (self-host) | Free API (key, no cost) | Paid API |
|---|---|---|---|---|
| STT | browser speech recognition | faster_whisper |
groq (Whisper) |
openai · together (Whisper) |
| TTS | browser system voices | piper |
— | together (Kokoro) · fish (cheap, WAV) · elevenlabs |
| LLM | rule-based keywords | ollama |
gemini |
together (open models, hosted) · anthropic · openai |
| Embeddings | hash trick |
ollama · sentence_transformers |
gemini |
openai |
One Together AI key covers the whole voice loop. Together
hosts an OpenAI-compatible LLM endpoint, a Whisper-compatible transcription
endpoint, and a TTS endpoint (Kokoro) behind a single account — set
STT_PROVIDER=together, TTS_PROVIDER=together, LLM_PROVIDER=together and
one TOGETHER_API_KEY drives listening, reasoning and speaking, with no local
models and no juggling separate provider keys.
Free real AI (no cost, no local models). Get a free key from Google AI Studio and Groq, then:
LLM_PROVIDER=gemini GEMINI_API_KEY=...
EMBEDDING_PROVIDER=gemini
STT_PROVIDER=groq GROQ_API_KEY=...That's genuine conversational AI + real Whisper transcription for $0 — ideal for a cloud deploy where there's no Ollama. (Voice output stays on the browser's free voices, or Piper — see below.)
About the mock tier — read this. It exists so the app runs with zero setup, but be clear about what it is:
- TTS
mockstill gives you a real voice in the dashboard. The frontend detects there's no TTS engine and speaks through your browser's built-in system voices, so you hear actual speech. It cannot do this over a real phone call — setTTS_PROVIDER=piper(free, local) orfish(paid, cheap) before going live.elevenlabsreturns MP3, so it only covers the browser path. - LLM
mockis not AI. It's keyword matching that returns canned replies. For real conversation setLLM_PROVIDER=ollama(free, local) or point it at Claude/GPT.
Voice input in the browser uses your browser's own recognition by default; when
a server STT is configured (groq/openai/faster_whisper) the mic records and
sends audio to Whisper via /api/transcribe instead.
Mix freely — e.g. Groq Whisper for STT, Gemini for reasoning, Piper for voice. The agent, dashboard, and API are identical regardless of what's behind each layer. The dashboard's Pipeline strip and Settings → AI pipeline show exactly which engine and mode (open-source / free API / paid / mock) is active.
Runs on the mock providers: no API keys, no model downloads, nothing to configure.
| OS | Do this |
|---|---|
| Windows | Double-click start.bat |
| macOS / Linux | chmod +x start.sh && ./start.sh |
It creates the Python environment, installs both sets of dependencies, starts the API and web app, waits until they're actually answering, and opens your browser at http://localhost:5273. First run takes a minute or two; after that it's a few seconds.
To stop: stop.bat (Windows) / ./stop.sh (macOS/Linux), or just close the
two server windows.
You land on the home page, where you can press Answer the call and actually hear the agent handle a caller — the voice is synthesized live by whichever TTS engine you've configured, so you hear the real thing before setting anything up. You can also tap through the voices and see which open-source or paid engine is driving each layer right now.
From there, Set up my agent opens the template gallery (/new): pick
Assistant, Tutor, Coding helper, Support, Coach, Interview practice,
Receptionist, or start from scratch, then write its instructions, greeting,
language and voice. Instructions are the agent's behaviour — a tutor tutors,
a support agent supports, because you told it to, not because of hardcoded
logic.
Picking Receptionist is different: it has no instructions to write, because it runs the structured booking graph instead of the general brain, so it opens the full five-step wizard — business info and hours, what people can book, the questions callers always ask, booking/escalation rules, and a voice.
Sonari ships empty, and neither path assumes a vertical: a salon, a tutoring business, a law firm and a support team all configure the same way, each agent answering from its own instructions and knowledge base.
The agent isn't English-only. Each business picks the language its callers hear (English, Spanish, French, German, Hindi, Portuguese) in the wizard or in Settings. It's set per-business and applies to the whole call:
- Reasoning — the model replies in that language (it understands and speaks
many; the prompts pin the output language). Works best with a real LLM
(
LLM_PROVIDER=ollama/anthropic/openai) — the zero-setupmockclassifier is English-only. - Fixed replies — booking confirmations, message/escalation lines, closings and dates are hand-translated per language with locale-aware date phrasing (e.g. "le he agendado corte de pelo para el martes 28 de julio a las 10:00").
- Voice & speech — the in-browser demo speaks and listens in the business language using your device's voices; real phone calls pass the language to Twilio. (Voice quality in a given language depends on the TTS voices installed on the device or your TTS provider.)
The dashboard interface itself stays in English.
In a hurry? "Explore with sample data" fills in a complete sample business (a dental clinic) with FAQs, calls, and bookings, so you get a populated dashboard instantly for a demo. While you're in it, a banner across the top offers "Exit sample data" on every page — one click erases it and drops you back on the home page. (Settings → Start over does the same thing.)
Then go to Voice agent and talk to it right in the browser — the same pipeline a real caller or a shared link hits.
| Route | |
|---|---|
/ |
No agents yet → the landing page. One or more agents → your agents home (open, share, create, delete) |
/new |
Create an agent from a template |
/dashboard |
The active agent's workspace — stats, conversations, knowledge, settings |
/agent/:id |
Public, no login — the shareable/embeddable voice agent |
/setup |
The full receptionist wizard (services, hours, FAQs) |
/welcome |
The landing page, always reachable (handy for demo videos) |
Opening an agent from the agents home makes it the active one (carried in an
X-Business-Id header) and takes you to /dashboard. The nav pill's agent
chip always links back to / to switch. Bookings and the booking-only
call filters only appear in the nav for the receptionist — an
instruction-driven agent's dashboard shows conversations instead.
Settings → Share your voice agent gives you a public link and an <iframe>
snippet. Anyone with the link can talk to that agent; nobody can reach your
dashboard through it.
The dashboard is open by default so a fresh clone just runs. Set a password before exposing it to the internet — otherwise anyone who finds the URL can read, edit and delete every agent:
ADMIN_PASSWORD=something-long-and-randomPublic voice agents, telephony webhooks and /api/health stay reachable without
it. Two independent guards keep a shared link from burning your quota: a
per-IP rate limit (caps how fast one visitor can call the AI endpoints)
and a global daily spend budget (caps how much everyone combined can use
per UTC day — PUBLIC_DAILY_TURN_LIMIT / PUBLIC_DAILY_MEDIA_LIMIT in
.env, 0 = unlimited). The budget is what actually tracks a bill: a per-IP
limit alone is defeated by enough visitors, or a spoofed IP. Once exhausted it
fails closed with 429 Too Many Requests and a Retry-After header, and a
signed-in owner (when ADMIN_PASSWORD is set) is always exempt. Current usage
against both limits is visible on GET /api/health.
Prerequisites: Python 3.11+ and Node 18+ on your PATH. The launcher checks for both and tells you what's missing.
One self-contained image — FastAPI serves the built React app and the API together, on one port:
docker build -t sonari .
docker run -p 8000:8000 sonari # open http://localhost:8000The docker-compose.yml (backend + Redis + Celery worker + nginx frontend) is
still there if you want the split, multi-service layout for local development.
# backend
cd backend
python -m venv .venv
# Windows: .venv\Scripts\activate macOS/Linux: source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8100 # → http://localhost:8100 (docs at /docs)# frontend
cd frontend
npm install
npm run dev # → http://localhost:5273The launcher deliberately runs uvicorn without
--reload: the reloader respawns itself through a different interpreter, which breaks inside the venv on Windows. Use the manual command above when you want hot-reload.
The whole thing is one container — FastAPI serves the built React app and the API from a single process, so there's no separate frontend host and no CORS to configure. It deploys on the mock providers with no keys, and you flip on real AI later with one env var.
- Render — the repo has a
render.yaml; New → Blueprint → select the repo → Apply. - Railway / Fly / any Docker host — build and run the
Dockerfile.
Full instructions, env vars, real-AI setup, and Postgres persistence are in DEPLOY.md.
Edit backend/.env (copy from .env.example). Examples:
Real AI for free, in about a minute (recommended)
If you have Ollama, this gets you a genuinely conversational agent and real semantic search at no cost:
ollama pull llama3.1:8b
ollama pull nomic-embed-textLLM_PROVIDER=ollama
OLLAMA_MODEL=llama3.1:8b # must be a model you've pulled
EMBEDDING_PROVIDER=ollama
OLLAMA_EMBED_MODEL=nomic-embed-textExpect roughly 2–3s per turn for an 8B model on CPU (the first call is slower
while the model loads). Use a smaller model like llama3.2:3b if you want to
get closer to the 1.5s budget.
Fully open-source, including voice over the phone
STT_PROVIDER=faster_whisper
TTS_PROVIDER=piper
LLM_PROVIDER=ollama
EMBEDDING_PROVIDER=ollamapip install faster-whisper piper-tts
# download a Piper voice into backend/piper_voices/Paid APIs (best quality / lowest latency)
STT_PROVIDER=openai
TTS_PROVIDER=elevenlabs
LLM_PROVIDER=anthropic
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
ELEVENLABS_API_KEY=...pip install anthropic openai elevenlabsNo code changes — the factories in app/providers/ resolve your selection.
Phone calls are an add-on — the browser agent works without any of this.
- Expose the backend publicly (e.g.
ngrok http 8100) and setPUBLIC_BASE_URLto that URL. - In the Twilio console, point your number's A Call Comes In webhook to
POST {PUBLIC_BASE_URL}/call/incomingand the status callback to/call/status. - Set
TWILIO_*in.env, and the agent's number under Settings → Phone calls.
Exotel (India) streams audio instead of sending text, so it uses the
WebSocket adapter: add a Voicebot applet pointing at
wss://YOUR_HOST/exotel/media, and configure real STT/TTS (Exotel supplies
neither) — e.g. STT_PROVIDER=groq and TTS_PROVIDER=piper.
Adding another provider means one small adapter in app/telephony/; the agent
itself never changes.
Two telephony paths are implemented:
<Gather input="speech">(default) — uses Twilio's built-in speech recognition; the simplest reliable path.- Media Streams WebSocket (
/media/twilio) — the low-latency streaming pipeline through your own STT/TTS providers. FlipUSE_MEDIA_STREAMinapp/api/calls.py.
sonari/
├── Dockerfile single-container image (serves SPA + API)
├── render.yaml Render one-click blueprint
├── DEPLOY.md deployment guide
├── start.bat / start.sh one-click launcher (setup + run + open browser)
├── stop.bat / stop.sh stop both servers
├── backend/ FastAPI · agent · providers · RAG · MCP tools · workers
│ ├── app/
│ │ ├── providers/ pluggable STT / TTS / LLM / embeddings (mock+OSS+free+paid)
│ │ ├── agent/ general.py (instruction-driven brain) + the receptionist
│ │ │ graph.py/nodes/prompts/state for the structured brain
│ │ ├── telephony/ provider-agnostic streaming core + Twilio/Exotel adapters
│ │ ├── voice/ VAD + audio helpers for the streaming path
│ │ ├── rag/ FAQ/knowledge ingest + cosine retrieval
│ │ ├── mcp_tools/ calendar + notification tool servers
│ │ ├── api/ REST routers, auth, rate limiting, Twilio/Exotel webhooks
│ │ ├── services/ call orchestration + post-call finalizer
│ │ └── workers/ Celery app + post-call job
│ └── tests/ agent brain, providers, telephony, onboarding, i18n, rate limits
├── frontend/ React + TS + Tailwind dashboard (fully responsive)
│ └── src/
│ ├── pages/ Landing, Agents (home), CreateAgent (templates),
│ │ Onboarding (receptionist wizard), Login, Dashboard,
│ │ Calls, CallDetail, Bookings, Knowledge, Analytics,
│ │ Settings, Simulator, PublicAgent (shareable/embeddable)
│ └── components/ TranscriptViewer, BookingCalendar, VoicePicker, charts…
├── docker-compose.yml
└── README.md
Owner-only (behind ADMIN_PASSWORD when one is set):
| Method | Path | Purpose |
|---|---|---|
GET |
/api/businesses |
List every agent you've built |
GET/PATCH |
/api/businesses/me |
The active agent (404 = no agents yet) |
POST |
/api/businesses |
Create an agent (template gallery or wizard) |
DELETE |
/api/businesses/{id} |
Delete an agent and cascade its data |
POST |
/api/demo/seed · /api/demo/reset |
Load / erase the sample business |
GET/POST/PATCH/DELETE |
/api/faqs |
Knowledge base (re-embeds on write) |
GET |
/api/bookings |
Receptionist-made appointments |
GET |
/api/dashboard/stats · /calls · /analytics |
Dashboard data |
Public (no password — this is what a shared agent link uses; per-IP rate limited and capped by the daily spend budget):
| Method | Path | Purpose |
|---|---|---|
GET |
/api/health |
Status + active providers |
GET |
/api/businesses/{id}/agent |
Safe public config for the embeddable agent |
POST |
/api/simulate/turn |
One conversation turn → reply |
GET |
/api/tts?text=… |
Synthesize audio via the active TTS provider |
POST |
/api/transcribe |
Transcribe a recorded clip via the active STT provider |
GET/POST |
/api/auth/status · /login |
Dashboard session (no-ops if no password is set) |
Telephony webhooks (called by the provider, not a browser):
| Method | Path | Purpose |
|---|---|---|
POST |
/call/incoming · /call/gather · /call/status |
Twilio webhooks |
WS |
/media/twilio |
Twilio Media Streams pipeline |
WS |
/exotel/media |
Exotel Voicebot/AgentStream pipeline |
Full interactive docs at /docs.
cd backend
pip install pytest
pytest -q # end-to-end pipeline tests on the mock providers
python smoke_test.py # quick manual walkthrough of a callFastAPI · WebSockets · SQLAlchemy (async) · SQLite/Postgres(pgvector) · Celery · Redis · Twilio Voice · Exotel AgentStream · faster-whisper / Piper / Ollama (open-source) · Groq / Gemini (free API) · Together AI / OpenAI / ElevenLabs / Fish Audio / Anthropic (paid) · React · TypeScript · Tailwind · Vite.
Agent builder — a template gallery (Assistant, Tutor, Coding helper, Support, Coach, Interview practice, Receptionist, or from scratch); each agent's instructions drive a general conversational brain, with a knowledge base (RAG) it can draw on. Manage every agent from one home, switch between them, delete one and its data cascades cleanly.
Receptionist — the one template that opts out of the general brain for a structured graph instead: FAQ / book / message / escalate, with services, hours, booking rules, and an owner number read out on escalation.
Sharing — every agent gets a public, no-login page and an <iframe> embed
snippet, so it's usable with zero telephony setup.
Telephony (optional) — Twilio (webhook or Media Streams) and Exotel (AgentStream WebSocket) via a shared adapter interface; a new provider is one small file, never a change to the agent.
Ops — password-gated dashboard (ADMIN_PASSWORD, off by default), per-IP
rate limiting plus a global daily spend budget on the public AI endpoints
(owner-exempt, fails closed with 429), transcripts/analytics per agent, and
a browser simulator for testing any agent without a phone number.