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