Overview
Sipclerk answers an inbound phone call for a law firm. A call arrives as a real SIP INVITE over UDP at a small SIP and RTP stack I wrote, the dialled number picks the firm's tenant file, and a Pipecat pipeline carries speech to text, a small local model and speech out. The rules that must not depend on a prompt sit in code: a state machine decides the next question and a guard decides what the agent may never say.
I built it on two sample firms with test callers and voices. A SIPp container places the calls. The README lists what ran.
Key Features
- A real SIP call. The line negotiates the codec, answers, takes G.711 audio and RFC 4733 keypad digits, follows a proxy's route set, and reads the dialled number from the Request-URI or the To header in every written form.
- Rules in code. A caller who asks for legal advice is told once that the agent cannot give any, an existing client or a caller in an emergency is handed to a person, and an out-of-scope matter is declined, all before and after the model, so no prompt decides it.
- Warm transfer. The handover is a second SIP call to the desk, bridged to the caller, and it is cancelled if the caller hangs up first.
- Stored per firm. Records, transcripts, summaries and recordings are encrypted under a key derived for each firm, with roles and a hash-chained audit line for every read, and retention runs on its own schedule.
- Delivered once. A finished intake goes out as a signed, retried and idempotent delivery to a case management receiver and a call console, for about 23 hours before it is given up, and a dead delivery can be requeued.
- Tested by replaying callers. Spoken scenarios replay recorded callers through real SIP and RTP, Whisper, a local model and Kokoro to cover barge in, silence, a noisy line, keypad digits and a corrected name.
Technical Achievements
The suite is about 4,000 tests and a mutation check of 255 deliberate breaks, each of which has to make a named test fail. Three independent review passes, the last on another model family, found problems such as retention that was written and never run, caller data left readable in the outbox, a transfer leg that outlived its call, office hours read from a fixed offset after the clocks changed, and a date the model had read being stored as its year alone. Each was fixed once at its class, and a test generated from a table of the whole class now pins it. Later fresh readers working from a clean clone found more, such as a spoken email kept with the words after it and contractions missing from the emergency rule, and each was fixed at its class the same way.
What I Learned
A feature that is tested and never run is the quietest failure: the first review found a retention method with no caller. I also learned that a spoken test is only as steady as the machine it runs on. Two of the twenty five failed once when other jobs were loading the laptop and passed when run alone, so the README says that instead of leaving the earlier clean run to speak for them.
Tech Stack
Telephony: Python, asyncio, SIP and SDP over UDP, RTP, G.711, RFC 4733 digits, SIPp Voice: Pipecat, Silero VAD, faster-whisper, Kokoro, Ollama Storage and delivery: AES-256-GCM with per-firm keys, hash-chained audit, signed webhooks with an outbox Testing: pytest with generated tables and seeded random calls, a mutation harness Infrastructure: Docker, GitHub Actions