A multilingual AI receptionist for small businesses in North Macedonia and the Balkans. It answers phone calls and WhatsApp messages, answers FAQs only from the business's own knowledge base, books/moves/cancels appointments, takes messages when it can't help, and hands off to a human when it's unsure, the caller is upset, or it looks like a medical emergency.
The first target customer type is a private dental clinic. No clinic has signed up yet β this is still a pre-sales build.
Status: Phase 2 β text receptionist running in a local simulator. No deployment, no customers, no WhatsApp or voice yet.
Phase 2 implements the Phase 1 design below for one fictional demo business
(a Skopje dental clinic β all data invented, see
src/receptionist/kb/demo_data.py) and a local CLI/HTTP chat simulator in
place of the real WhatsApp webhook. Full details: docs/PHASE2.md.
Caller (phone) Patient (WhatsApp)
| |
Twilio number WhatsApp Cloud API (Meta)
| (Phase 3) | webhook (Phase 2)
v v
+-------------------------------------------------------------+
| FastAPI backend (Railway) |
| |
| channels/ core/ |
| whatsapp.py ---> conversation.py (one turn loop) |
| voice.py ---> language.py (detect + pin lang) |
| agent.py (LLM + tool loop) |
| tools.py (6 tools, schemas) |
| validators.py (code checks args) |
| policy.py (disclosure, safety |
| keywords, handoff) |
| handoff.py (notify owner) |
| logging.py (turn + run records) |
| |
| llm/ LLMProvider interface -> GroqProvider (others later) |
| voice/ STT/TTS interface -> ElevenLabs (Phase 3) |
+------+---------------------+-------------------+------------+
| | |
Postgres Redis OpenTelemetry
(businesses, KB, (session state, (traces -> OTLP
bookings, logs) locks, dedupe) collector)
^
| REST (JWT)
+------+--------------------+
| Admin UI (Next.js + TS) | edit KB, bookings, transcripts
+---------------------------+
This diagram is the Phase 1 design target; Redis, the admin UI, voice and the real WhatsApp webhook aren't built yet (Phase 2 uses a local simulator instead of the WhatsApp box, and an in-memory store instead of Redis β see docs/PHASE2.md). Full design detail, including the DB schema, API endpoints, agent tool contracts and known risks, is in docs/ARCHITECTURE.md. The actual Phase 2 module layout is in "Repo layout" below.
- A message comes in (the CLI/HTTP simulator in Phase 2; a WhatsApp webhook once that's wired). Session state (business, language, failure counters) loads from an in-memory store, or is created β a new conversation always opens with the AI + recording disclosure, enforced in code even if the model forgets it.
- Language is detected from the caller's message and pinned for the session; it only switches on a confident detection of a different language.
- A code-level safety check runs first: emergency keywords, medical-advice requests, anger/complaint signals, or an explicit request for a human. An emergency skips the model entirely.
- The LLM gets the system prompt, the business knowledge base, the conversation so far and the seven tool schemas.
- Any tool call the model makes is re-validated in code (dates in the future, inside opening hours, no double booking, phone format, matching dentist/service) before it touches the database β the DB's exclusion constraint is the final backstop.
- A tool failure, two consecutive validation failures, or a KB topic missed twice means the agent says so plainly and hands off β it never claims an action worked when it didn't.
- The turn (input, model output, tool calls/args/results, validation errors, latency, tokens) is persisted to Postgres, keyed to the prompt version/model/KB hash, and the reply is sent.
receptionist.cli replay <conversation_id>re-runs a stored conversation and diffs the output.
See docs/PHASE2.md for what's actually implemented vs. still a Phase 1 design placeholder (Redis, voice, the admin UI, WhatsApp itself).
- Phase 1 β Design. Architecture, database schema, folder structure, API/tool contracts, known risks.
- Phase 2 β Text receptionist, local simulator. Conversation loop, seven agent tools, argument validation, replayable logging, CLI + HTTP simulators. WhatsApp itself is still a 501 stub β see docs/PHASE2.md.
- Phase 3 β Voice. Twilio for calls, ElevenLabs for STT/TTS, the voice media stream.
- Phase 4 β Admin UI. Next.js app for editing the knowledge base, managing bookings, and reading transcripts.
- Phase 5 β Evaluation. 20 scripted test conversations across the 4 languages, plus an eval script to score them.
Requires Python 3.12+ and a Postgres 17 server (the commands below assume one
running locally on port 55432 with trust auth β adjust DATABASE_URL /
TEST_DATABASE_URL otherwise; docker compose up postgres also works).
# 1. venv + install
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# 2. apply the schema and seed the fictional demo clinic
export DATABASE_URL="postgresql://postgres@127.0.0.1:55432/receptionist"
python -m receptionist.db.migrate
python -m receptionist.kb.seed_demo_clinic
# 3. run the tests (unit tests need no key/DB; integration tests need TEST_DATABASE_URL)
export TEST_DATABASE_URL="postgresql://postgres@127.0.0.1:55432/receptionist_test"
python -m receptionist.db.migrate "$TEST_DATABASE_URL"
pytest -q
# 4a. chat in the terminal (needs a real LLM key -- mock has no real answers)
export LLM_PROVIDER=gemini GEMINI_API_KEY=...
python -m receptionist.cli chat --phone +38970123456
# 4b. or chat in a browser
uvicorn receptionist.api.main:app --reload
# -> http://localhost:8000/simulatorA Makefile wraps the common commands (make install, make init-db,
make seed, make test, make cli-chat, make api), and
docker compose up --build runs Postgres + the API together (docker-compose.yml).
Macedonian, Albanian, Serbian (Cyrillic and Latin script), English. Language is detected from the caller's first message and held for the rest of the conversation unless the caller clearly switches.
The model can call seven tools; code validates every argument before anything
touches the database (src/receptionist/tools/). The Phase 1 design doc
below describes the original six-tool sketch; Phase 2's actual tool names
and shapes (and why they changed) are in
docs/PHASE2.md and
prompts/receptionist.v2.md.
get_business_info(topic)β answer from the knowledge base, or report "not found"check_availability(service, date, staff?)β look up open slotsbook_appointment(service, staff, starts_at, name, phone)β confirm a bookingreschedule_appointment(reference, phone, new_starts_at, new_staff?)β move an existing bookingcancel_appointment(reference, phone)β cancel an existing bookinglookup_appointment(phone, reference?)β find a booking by phone (+ reference if ambiguous)handoff_to_human(reason, summary)β end AI involvement, notify the owner
- Every conversation opens by disclosing it's an AI assistant and that the conversation may be recorded.
- FAQ answers are grounded only in the business's own knowledge base. The agent never invents a price or policy; a missing topic becomes a message for the owner, not a guess.
- The model decides what to do; code validates every tool argument and is the only thing that writes to the database.
- A database exclusion constraint (not just application logic) makes double-booking the same staff/resource impossible β see docs/SCHEMA.sql.
- Failures are loud: a tool error or a repeated validation failure is told to the caller plainly and triggers a handoff, never a silent retry or a false "done".
- Handoff to a human is triggered by: a medical emergency, an upset/angry caller, an explicit request for a human, repeated misunderstanding, or a knowledge-base miss asked twice.
- No medical advice or diagnosis, ever β emergencies get the clinic's emergency number and an immediate handoff.
This is not legal advice. A clinic deploying this must have its own compliance review. Notes on what the design currently accounts for:
- Transparency. Under the EU AI Act, a system interacting with people must disclose that they're talking to an AI. Every conversation opens with that disclosure, in the caller's language.
- Recording notice. The same opening message also discloses that the conversation may be recorded, before any content is collected.
- Data minimisation. The agent is instructed to collect only name, phone number and reason for a booking or message β not medical history or symptom detail.
- Retention. Transcripts auto-delete after a configurable period, default 90 days (
TRANSCRIPT_RETENTION_DAYS). Bookings and messages outlive the transcript they came from (the conversation link is nulled, not the record). - Erasure. A planned
DELETE /admin/customers/{id}/dataendpoint anonymizes a customer's personal data on request.
- WhatsApp Cloud API: replies outside the 24h window need pre-approved templates; Meta business verification can take days. Start this early.
- Twilio numbers in North Macedonia: local numbers may not be available; the usual workaround is a foreign number with call forwarding from the clinic's line.
- Albanian and Macedonian speech recognition quality varies β this will be measured in Phase 3, not assumed.
- Groq's free tier has rate limits; fine for building, but per-clinic cost needs checking before pricing this.
- Keyword-based emergency detection is a safety net, not a guarantee. The prompt, keyword list and emergency number must be reviewed by each clinic.
README.md
LICENSE .gitignore .env.example
pyproject.toml Makefile Dockerfile docker-compose.yml
docs/
ARCHITECTURE.md # Phase 1 full design: schema, endpoints, tool contracts, risks
PHASE2.md # what Phase 2 actually built, deviations, known limitations
SCHEMA.sql # Postgres 17 schema (tested) -- mirrored in db/migrations/0001
schema_smoke_test.sql # proves the double-booking guard works
examples/ # real-Gemini smoke-test transcripts (mk/sq/en)
prompts/
receptionist.v1.md # Phase 1 design-time system prompt
receptionist.v2.md # Phase 2 runtime system prompt (tool names match the code)
src/receptionist/
config.py # pydantic-settings
cli.py # `python -m receptionist.cli chat|replay`
api/ # FastAPI app: health, simulator routes + static chat page
channels/ # ChannelAdapter, CLI simulator, WhatsApp 501 stub
core/ # conversation loop, language, disclosure, policy, prompts, replay
llm/ # LLMProvider: mock, gemini, groq
tools/ # tool arg schemas, validators, executor
kb/ # knowledge base repository + the fictional demo clinic fixture
db/ # connection, migration runner, migrations/*.sql, turn logging
tests/
unit/ # no DB, no API key
integration/ # needs TEST_DATABASE_URL, skips cleanly without it
Still a Phase 1 design placeholder, not yet built: the admin/ Next.js UI
(Phase 4), the voice channel and railway.toml deployment (Phase 3).
MIT β see LICENSE.