Ori v0.0.2
Ori is a Hindi/Hinglish voice agent that phones Indian transporters and negotiates a freight rate for Ordinal, a logistics company with roughly ₹80 Cr of annual freight spend. This site is the engineering record of the second release: what runs, why it is shaped this way, what was measured, and what is still not stable. Every number on these pages carries a tag — OBSERVED (a command was run and the output kept), SOURCED (from a document) or PROPOSED (an inference, not yet measured).
The one rule
The parser owns the number. The model owns the prose.
A missed rate costs one more question. A wrong rate books a truck at a price
nobody agreed. Every design trade resolves in that direction: refuse, ask
again, escalate to a person, but never emit a figure the system cannot verify.
Mechanically, ceiling.py decides the figure before the call starts, the
model writes the literal token {rate}, SlotFill substitutes the authorised
figure in words, and FigureGate refuses any line carrying a figure the ladder
did not authorise. None of that is a prompt instruction; each is a control.
The system
flowchart LR
subgraph browser["Browser (studio page)"]
mic[microphone] --> webrtc[WebRTC]
webrtc --> spk[speaker]
end
subgraph pipeline["Pipecat pipeline — one process per call"]
direction LR
in[transport.input] --> stt["Deepgram Flux
flux-general-multi
India endpoint"]
stt -->|transcript| ori["OriBrain
normalise · parse · route · phase"]
ori -->|"brief, no figures"| agg[user aggregator]
agg --> llm["Bedrock gemma-3-27b
ap-south-1"]
llm -->|"prose with {rate}"| slot["SlotFill
authorised words in"]
slot --> gate["FigureGate
refuses any other figure"]
gate --> tts["Murf Falcon-2
hi-IN-shweta"]
tts --> out[transport.output]
out --> rec[recorder → stereo WAV]
end
webrtc --> in
out --> webrtc
ori -. "decision, latency, gate verdict" .-> page[studio panels]
ori -. "every turn" .-> jsonl[(session JSONL)]
The decision path is pure domain code (ADR 0008): no I/O, importable without Pipecat, tested in the same interpreter as everything else.
flowchart TD
said["what Flux heard"] --> norm["normalise()
split what the recogniser joined"]
norm --> parse["hear_rate()
the figure, or an honest refusal"]
parse --> route["route()
an ordered rule table; returns the rule"]
route --> phase{"Brain phase"}
phase -->|RATE / PRICE_Q| ladder["the ladder (stance.py + ceiling.py)
anchor → one concession → ceiling"]
phase -->|exit intents| canned["a written line
callback · wrong number · voicemail · abuse × 2"]
phase -->|everything else| brief["a brief for the model
+ load facts + last 3 exchanges"]
ladder --> auth["authorised figures for this turn"]
auth --> brief
What changed in v0.0.2
| area | v0.0.1 | v0.0.2 | evidence |
|---|---|---|---|
| Ending the call | fixed four times; the last fix raised silently and the call never ended | one mechanism, CallEnd: ends on audio that started after the decision, watchdog, idempotent |
test_call_end.py (16 cases); end-to-end: vendor goodbye ended 406 ms after her last audio — OBSERVED |
| The router | 20-branch if-chain; four inflection defects in a day; joining two words moved 29 of 133 routings | an ordered rule table that names the rule that fired; normalise(); verb paradigms; two-strike abuse |
joined-words instability 29 → 1 of 133 — OBSERVED |
| The brain | nine booleans; the wind-down line was the model's default reply | seven phases; out-of-range quotes questioned before the ladder closes; deferral is terminal | replay of 22 live calls: counter_once reached 5 (was 3); a probe left the model a vague brief 1 time (was 7) — OBSERVED |
| Transliteration | a rule speller mangled English ("distance" → दिसतनके) | unknown words stay Latin; the model's 196-word vocabulary is a fixture test | test_every_word_the_model_has_written_has_a_pronunciation |
| Layout | decisions in poc/, reached by sys.path hacks; the bot imported a benchmark script |
decisions under app/domain/calling/; ori_bot.py is 265 lines of wiring |
lint-imports: 2 contracts kept |
| Environments | two venvs, the second never recorded | one uv sync --group dev --group voice |
1,289 tests in one interpreter — OBSERVED |
| Tests | unit + a golden corpus | unit, pinned corpus with perturbation ceilings, mutation, end-to-end on the session record | Testing |
| Comments | half of every decision file was prose, with an emoji legend | one- or two-line whys; the stories in an incident ledger | scripts/comments_only.py proves the code AST unchanged |
The full list is on What changed.
What is not yet stable
| area | state | what it means in practice |
|---|---|---|
| The money path | stable | Has never produced a wrong figure on a live call. Ladder, slot, gate, and the tests behind them. |
| The router | hardened, still lexical | 1 of 133 routings moves under joined words; a leading filler still moves 10. The semantic fallback waits on 200–300 real labels. |
| Conversation quality | measured, being decided | The 12-model bake-off replay is done; judging is in progress. See The model bake-off. |
| Ending the call | stable in the harness | 7 end-to-end scenarios. Unproven on a telephone line. |
| The India STT endpoint | documented, undocumented by the vendor | api.in.deepgram.com: TCP 38 ms vs 270 ms global; no SLA; certificate expires 2026-12-07. One env var flips back. See the Runbook. |
| Real recordings | none | Every benchmark runs on synthesised speech. The largest unknown. |
| Telephony | not started | Pipecat ships Exotel/Twilio/Plivo serializers; a transport swap, unbuilt. |
Where to go next
- One call, end to end — the phases, the hang-up, the turn boundary, as diagrams.
- Architecture — why it is shaped this way.
- Conversation quality — the applied-science pass behind the brain rewrite.
- Every module and the incident ledger — the code, and what it learned.
- Testing and the Runbook — how to prove it and how to run it.
- Experiments, Papers, The model bake-off — the evidence.
- The decisions: ADRs 0003 (money), 0004 (Pipecat), 0005 (evaluation), 0006 (the bake-off), 0007 (the handoff), 0008 (domain code).