The tools-plus recipe in the Agora Conversational AI recipes family. This
recipe demonstrates a smart-home voice assistant backed by a custom
OpenAI-compatible POST /chat/completions endpoint. The endpoint implements
three novel capabilities on top of the base tool-calling pattern:
- Room modes (update_tools) — the user switches rooms ("go to the bedroom") and the active device set changes. Only the devices available in the current room can be controlled. Room + device state is stored in SQLite and survives restarts.
- Keyword scenes — saying "movie night", "good night", or "i'm home" runs a preset batch of device commands regardless of the current room.
- Mocked home API — a zero-key SQLite-backed home assistant simulates
real device state.
get_statusrecalls the current state across reconnects.
Agora cloud never sees a tool_call — the home engine runs entirely inside the
/llm endpoint (mounted into the same backend process) and streams back only the
spoken reply. STT (Deepgram nova-3) and TTS (MiniMax) stay Agora-managed.
- Python 3.10+
- Bun
- Agora CLI — makes generating an App ID + App Certificate easy
- ngrok — the backend must be publicly reachable so Agora cloud can call
/llm
# 1. Install + create the Python venv
bun run setup
# 2. Add Agora credentials (CLI), or edit server/.env.local by hand
agora login
agora project use <your-project> # select which project to use
agora project env write server/.env.local # writes App ID/Certificate
# 3. Expose the backend publicly (Agora cloud calls /llm/chat/completions directly)
ngrok http 8000
# 4. Add the tunnel URL to server/.env.local
# CUSTOM_LLM_URL=https://<your-tunnel>.ngrok-free.dev/llm/chat/completions
# 5. Run backend + frontend
bun run devOpen http://localhost:3000 → Start Conversation → speak.
Try: "turn on the TV", "go to the bedroom", "movie night", "what's on".
If you cloned this repo directly, the steps above are complete as written.
bun run setup creates the Python venv and installs web dependencies, then
bun run dev brings up the backend and frontend. You still need Agora credentials
in server/.env.local and a public CUSTOM_LLM_URL tunnel before a conversation
can connect.
Services:
- Frontend — http://localhost:3000
- Backend — http://localhost:8000 (also serves
/llm) - API docs — http://localhost:8000/docs
Deploy web (Next.js) and server (a single publicly reachable FastAPI process
that also serves /llm, so Agora cloud can reach /llm/chat/completions). Set
AGENT_BACKEND_URL in the web deployment so the Next rewrites reach the backend.
Docker image — ghcr.io/AgoraIO-Conversational-AI/recipe-agent-tools-plus
is published on v* tags. The image runs a single process on :8000; it serves
both the token endpoints and the /llm sub-app. Point CUSTOM_LLM_URL at
<public-url>/llm/chat/completions.
Co-public caveat: because
/llm/chat/completionsand/get_configshare one port, the same tunnel or load-balancer rule that exposes/llmto Agora cloud also exposes the token endpoints. For production, add authentication or split behind a reverse proxy.
Backend env file: server/.env.example.
| Variable | Required | Default | Notes |
|---|---|---|---|
AGORA_APP_ID |
✅ | — | Agora Console → Project → App ID |
AGORA_APP_CERTIFICATE |
✅ | — | Agora Console → Project → App Certificate (server only) |
CUSTOM_LLM_URL |
✅ | — | Public URL of the /llm/chat/completions sub-app — same host as the backend, e.g. https://<tunnel>/llm/chat/completions. Agora cloud calls it; cannot be localhost. |
CUSTOM_LLM_API_KEY |
✅ | any-key-here |
Forwarded by Agora cloud as Authorization: Bearer. Required by the CustomLLM vendor. |
CUSTOM_LLM_MODEL |
smarthome-mock |
Model name passed to the endpoint | |
AGENT_GREETING |
built-in | Optional opening line override | |
HOME_DB_PATH |
/tmp/home.db |
SQLite file for device and mode state | |
PORT |
8000 |
Backend port | |
AGENT_BACKEND_URL (web deploy) |
✅ | — | Required in a deployed web app when proxying to the backend |
bun run setup # install web deps + create server/ venv
bun run dev # run backend (:8000, also /llm) + web (:3000)
bun run doctor # prerequisite check (no creds needed)
bun run doctor:local # + .env.local + credentials + CUSTOM_LLM_URL checks
bun run verify # web-only gate (no Agora creds needed)
bun run verify:local # full local gate: backend compile + smoke tests + web build
bun run clean # remove venv and build artifactsTests run standalone (no Agora cloud needed): pytest in server/ (11 tests), plus
bun run verify in web/.
Browser (localhost:3000)
│ fetch /api/*
▼
Next.js ──rewrite──▶ Agent backend (server/, localhost:8000)
│ starts agent session (CustomLLM vendor)
│ also serves /llm/chat/completions (in-process)
▼
Agora ConvoAI Cloud
│ POST <CUSTOM_LLM_URL> (Authorization: Bearer)
▼
/llm sub-app (same process, same port)
▲ public via ngrok tunnel
│ room modes + scenes + SQLite state, streams reply
The browser only ever calls Next /api/*, which rewrites to the agent backend.
The agent backend owns Agora tokens, agent lifecycle, and the /llm sub-app.
See ARCHITECTURE.md.
- A Next.js web client (:3000) that drives the RTC/RTM lifecycle and only
ever calls
/api/*. - A FastAPI agent backend (:8000) that owns Agora token generation, the
agent session lifecycle, and the
/llmsub-app. - The
/api/get_config·/api/startAgent·/api/stopAgentcontract between the web client and the backend (Next rewrites, no Route Handlers). - A smart-home engine inside
server/src/llm.pywith:- Room modes that gate available device tools (update_tools pattern).
- Keyword scenes that run device batches ("movie night", "good night", "i'm home").
- A mocked home API backed by SQLite for device and mode state.
get_statusrecall so the user can ask what devices are on.
- A zero-key mock so the full pipeline runs with no external API key.
- The browser calls
/api/get_config, which Next rewrites to the backend; the backend mints an Agora token fromAGORA_APP_ID+AGORA_APP_CERTIFICATE. - The browser joins the RTC channel, then calls
/api/startAgent; the backend starts an agent session using theCustomLLMvendor pointed atCUSTOM_LLM_URL. - The user speaks. Agora runs STT (Deepgram nova-3), then sends the transcript
to
/llm/chat/completionsas an OpenAIPOST /chat/completionsrequest, forwardingCUSTOM_LLM_API_KEYasAuthorization: Bearer. - Inside the sub-app,
run_agent_turn()routes the turn:- Status recall keywords →
get_status()reads device states from SQLite. - Keyword scenes →
activate_scene()runs a batch of device commands. - Room keywords + switch/go-to →
set_mode()changes the active room mode and updates available device tools (update_tools pattern). - Device keywords →
set_device()controls the device if it's in the current room, or returns a friendly "not available here" message. - The sub-app streams only the spoken reply in OpenAI SSE chunk format.
- Status recall keywords →
- Agora runs TTS (MiniMax) on the streamed reply and plays it back in the channel. Agora cloud never observes the internal tool execution.
/api/stopAgentends the session.
Edit run_agent_turn() and the home-API functions in
server/src/llm.py to call a real home automation system
(e.g. Home Assistant REST API). The sub-app must keep speaking the OpenAI
streaming /chat/completions contract. A production sub-app should also validate
the Authorization: Bearer header.
web/— Next.js frontend (:3000); RTC/RTM lifecycle and UI.server/— FastAPI backend (:8000); Agora tokens + agent lifecycle +/llmsub-app.server/src/llm.py— OpenAI-compatible smart-home mock; room modes, keyword scenes, SQLite device + mode state.ARCHITECTURE.md— system shape and component boundaries.AGENTS.md— guide for coding agents working in this repo.
| Problem | Fix |
|---|---|
| Agent starts but never speaks | CUSTOM_LLM_URL is not public or omits /llm/chat/completions. Use your ngrok URL. |
doctor:local warns about localhost |
Replace the local URL with your public tunnel URL. |
| Local calls fail / hang under a global proxy | Configure your proxy to send 127.0.0.1, localhost, and RFC-1918 ranges DIRECT. |
Released under the MIT License.