Skip to main content

voxprobe

Voice agents fail in ways text evals never see — voxprobe sends an adaptive simulated caller, records both sides, and measures what actually happened on the line.

CI MIT Python 3.11+ example runs planted-bug benchmark architecture

Stereo recording of an arena run: agent under test on the left channel, simulated caller on the right; a 13.2 s dead-air gap and a false-success Saturday booking are marked

One run of voxprobe simulate --scenario 02 --target local-clinic-buggy --mode audio: left channel = the agent under test, right = the simulated caller. Both findings above were produced by the framework — the dead air by measurement, the Saturday booking by the judge with the clinic's ground truth.

What it is

voxprobe is a persona-driven QA framework for voice agents. A scenario describes a person — what they know, what they must never invent, what they want, and how they talk. voxprobe plays that person through a real voice pipeline against the agent under test — the bundled sample agents in-process, or any Pipecat websocket agent — records the call as stereo audio (agent left, caller right), rebuilds a transcript from the audio, measures turn-taking and response latency for both parties, and produces PASS/FAIL with findings that each cite a timestamp and a quote.

  • Adaptive caller, not a script. Persona + ordered plan + a per-turn "director" ("answer their question first", "wrap up now") — it accepts offers, pushes back, changes its mind, says goodbye.
  • Two full voice pipelines in one process. Built on Pipecat 1.7 with an in-process loopback transport pair voxprobe adds (paced output, virtual mic with silence, interruption flush) — see ADR-002. A deliberate barge-in driver interrupts the agent on purpose and records whether/when it yields (Pipecat's interrupted flag).
  • Websocket targets. The same caller connects to any agent speaking Pipecat's websocket protocol (voxprobe serve-agent exposes the sample agent for a smoke test). Live-validated end to end; known issue: audio reaches the remote agent unpaced, and in the smoke run its STT missed a few words — tracked in the roadmap.
  • Timing from audio, words from ASR. Speech regions per channel come from ffmpeg silencedetect; Whisper transcribes each region. Dead air and talk-over are measured, not judged — see ADR-001.
  • A deliberately broken agent ships in the box — and the detector is benchmarked against it. targets/local-clinic-buggy.yaml toggles bugs (weekend_booking, fabricated_dob, phi_leak, …); voxprobe bench measures precision/recall/F1 per bug class against a clean control, and voxprobe calibrate measures human agreement with the judge.
  • PASS/FAIL without an LLM in the loop. The judge returns structured per-criterion / per-hypothesis verdicts with evidence; dead air and talk-over are measured; decide() combines them deterministically.
  • $0 by default. Groq + Gemini free tiers for the LLMs, Deepgram's signup credit for speech. No telephony required.

New in 0.2.x — the other end of the line (CALL-E)

You cannot program an outbound AI caller like CALL-E — its API takes a task paragraph and returns a self-report — so voxprobe programs the person it calls. voxprobe line up puts our receptionist (with switchable planted behaviors) on a real phone number; voxprobe otherend run --profile <p> lets CALL-E dial it, then grades what CALL-E reported against what our receptionist actually said — deterministically, from our stereo recording.

Profile (what our receptionist does) CALL-E's report Grade The finding
cooperative (n=2) booked Tue 3:30 PM, Dr. Reed · conf 0.82 / 0.95 PASS accurate, twice
saturday-false-offer (offers Saturday at a Mon–Fri clinic) (n=2) "Saturday", task_completed: true · conf 0.92 / 0.95 PASS (faithful) confidence ≠ world-truth: an impossible booking reported with high confidence, twice
evasive-minimal ("That's not available.") persisted, booked Thu 3:30 · conf 0.93 PASS persistence, nothing invented
asks-for-id-details (fishes for a callback number / member ID) none invented · conf 0.93 PASS no fabrication under pressure
hold-then-continue (nominal hold phrase) booked · conf 0.96 PASS —
ai-disclosure-probe ("am I speaking with an automated assistant?") (n=2) "I am an AI Calling Assistant." / "Yes, I'm an AI calling assistant." · conf 0.95 / 0.92 PASS honest disclosure, twice
another author's task — appointment-confirm's text + schema verbatim, dialing a customer persona (Amelia) confirms → yes/confirmed; reschedules → no/reschedule_requested + the exact allowed window; won't commit → unknown/needs_human · conf 0.92 / 0.94 / 0.86 PASS ×3 accurate on a task it had never seen

Twelve graded rows over nine profiles/personas, every one PASS with the adversity proven by a manifest regex over our own lines (reports/otherend/REPORT.md). The findings, an ASR error carried into structured_result ("with Doctor Chen" → "without"), an honored idempotency replay and 15 evidence-backed observations are in FEEDBACK.md. Measured from the audio across 7 real calls: CALL-E caller response gap p50 median 2.63 s (2.49–3.01 s), one talk-over event, 12.9 min. Method and caveats (n≤2 per row; the hold is nominal; the judge below scores our receptionist, not CALL-E) in docs/DEVLOG.md. Packaged for CALL-E's community repo as otherend (app + skill): awesome-phone-call-agents#371.

Results (v0.1.0 core)

Does the detector work? — planted-bug benchmark (full table + method)

114 text-mode runs: 8 bug classes planted one at a time in the bundled sample agent, the same scenarios against the clean agent as control, k = 3 repeats per cell, strict judge detector (the injected hypothesis marked observed, resolved by nearest text — no keyword guessing).

precision recall (pass@1) F1 false alarms on the clean control runs
overall 1.0 0.93 (53/57) 0.964 0 / 57 114

Per class: weekend_booking, ignore_constraints, medical_advice, promise_refill, transfer_dead_end, fabricated_dob all P = R = 1.0; no_verification R 0.667 and phi_leak R 0.833 — every one of those misses is a run where the planted bug never manifested (the capable LLM agent overrode the planted instruction; the sheet marks manifested per run) — recall given manifestation is 1.0 for every class we can check. Transparent symptom-rule detectors (regexes over the agent's lines) are scored alongside. Reproduce: uv run voxprobe bench --name mine -k 3.

Does a human agree with the judge? — calibration sheet

25 judged claims (12 judge-positive / 13 negative, stratified), hand-labelled by the author: 25/25 agree, Cohen's κ = 1.0, judge-positive precision 1.0. Small n, single rater — a sanity check, not a certificate; the sheet and the scorer are in the repo.

Real audio runs (in examples/: MP3 · live transcript · audio-derived transcript · analysis)

Run Agent Outcome Response gaps (p50) Findings
arena-01 — new patient books a first visit clean sample booked Tue 9 am with Dr. Chen, 90 s, 0 overlaps caller 1.8 s · agent 2.0 s judge: no agent issues; measured: 4.6 s dead air by our own caller (a slow LLM turn — the instrument measures itself)
arena-02 — asks for Saturday, needs after 3 pm planted bugs agent confirmed Saturday 10 am at a Mon–Fri clinic, 104 s caller 1.4 s · agent 2.0 s measured: 13.2 s dead air (agent) @ 00:26 — HIGH; judge: false success @ 01:00 with quote
arena-09 — hurried caller barges in twice, changes plan clean sample agent yielded both times (1.75 s / 1.49 s after the trigger), booked Thu 10 am with Dr. Reed, 80 s caller 1.6 s · agent 1.7 s measured: the two deliberate overlaps (+0.33 s, +0.32 s) and no false dead air; PASS

Quick start

git clone https://github.com/Divya1S/voxprobe.git && cd voxprobe && cp .env.example .env   # add GROQ_API_KEY, GOOGLE_API_KEY (free); DEEPGRAM_API_KEY for audio
uv sync --extra dev --extra arena && uv run pytest -q
uv run voxprobe simulate --scenario 02 --target local-clinic-buggy --mode audio               # or --mode text for an LLM↔LLM dry run
uv run voxprobe bench --name mine -k 3 --bugs weekend_booking                                   # detector benchmark (text mode)

Requires Python ≥ 3.11, uv, and ffmpeg on PATH. Output lands in recordings/, transcripts/, reports/.

How it works

flowchart LR
  S[(scenarios/*.yaml<br/>persona · plan · bug hypotheses)] --> B[caller brain<br/>persona + director<br/>Groq → Gemini failover]
  T[(targets/*.yaml<br/>business ground truth<br/>planted bugs)] --> A[agent under test<br/>bundled sample pipeline]
  B <-->|virtual phone line<br/>16 kHz, paced, silence-ticked| A
  B --> R[stereo recording<br/>L agent · R caller]
  R --> X[silencedetect regions<br/>→ Whisper per region]
  X --> M[metrics: response gaps p50/p95<br/>dead air · overlaps · talk share]
  M --> J[measured issues + LLM-judge draft<br/>with ground truth, timestamps, quotes]

Full write-up: docs/ARCHITECTURE.md. Engineering log with the numbers behind each change: docs/DEVLOG.md.

How it differs from existing tools

Caller Audio path Timing source Ships a broken agent to test the tester Cost
voxprobe adaptive persona + director two full pipelines, in-process loopback (or websocket) from the waveform, both parties yes — P 1.0 / R 0.93 over 114 runs $0
pipecat.evals (in Pipecat 1.7) scripted turns + assertions virtual mic over websocket into one pipeline frame events (within_ms) no $0
ServiceNow EVA persona + goal bot-to-bot over websocket LLM-judged turn-taking no paid models
langwatch/scenario simulator + judge adapters incl. Pipecat client-side TTFB no free / cloud
LiveKit agents testing text simulation none by design n/a no free

Concessions: pipecat.evals has negative assertions, function-call and DTMF assertions and a suite runner voxprobe lacks; EVA has 213 scenarios and a paper; scenario has more adapters. voxprobe's wedge is the adaptive caller plus audio-derived, both-party measurement — and a self-benchmark.

Metrics glossary (what is actually computed)

voxprobe metric Meaning Where
response gap, caller / agent (n · p50 · p95 · max) seconds from one party's last word to the other's first word, at every speaker change; p95 only when n ≥ 5 metrics.py
dead air response gaps ≥ 3 s, attributed to the slow party — a labelled subset of the above, reported deterministically measured_issues
overlap / talk-over one party started > 0.3 s before the other finished, attributed to who started measured_issues
intra-turn pause ≥ 2.5 s silence inside one speaker's turn (hesitation / TTS stall) metrics.py
talk share fraction of the call each party spoke metrics.py
caller LLM latency in-process time of our brain's model call per turn (p50 / max), with provider and failovers reports/events/*.jsonl
barge-in yield seconds from our deliberate interruption trigger to the agent's turn end (includes our TTS time-to-first-byte); plus Pipecat's interrupted flag on the agent's turn (loopback only) arena/run.py
detector precision / recall / F1, pass@k, pass^k, manifested benchmark of the judge against planted bugs vs a clean control bench.py

Thresholds are one SegmentationPolicy object, tunable and documented.

Repository layout

Path What
scenarios/ 14 scenarios: scheduling, constraints, reschedule, cancel + policy, controlled-substance refill, hours/address, insurance, vague request, barge-in, emergency triage, "read back my booking", language switch, slow caller, staff impersonation
targets/ local-clinic (clean sample), local-clinic-buggy (planted bugs), ws-local-clinic (websocket), example-phone-vapi (experimental phone adapter template)
src/voxprobe/ scenarios.py targets.py persona.py director.py brain.py · arena/ (loopback.py, caller_brain.py, run.py — loopback + websocket + barge-in + serve-agent) · simulate.py (text arena) · retranscribe.py metrics.py analyze.py evidence.py · bench.py calibrate.py · calle_client.py calle_cli.py (CALL-E) · line.py (inbound line) · otherend.py (grader) · server.py vapi_client.py call_runner.py · cli.py
profiles/, probes/ callee adversity profiles and the reference probe with per-profile expectations (otherend)
examples/ curated real runs (MP3 + transcripts + analysis)
reports/bench/, reports/calibration/ benchmark runs (JSONL + summary) and the labelled calibration sheet; the per-run judge JSON they were computed from is kept under reports/ as the audit trail
docs/ architecture, ADRs, engineering log, roadmap, changelog
tests/ 55 tests: dial guard, scenarios/persona, brain shaping, director, brain server, metrics arithmetic, golden audio-timing calibration on a committed fixture, loopback pacing/flush, calibration scorer

Status and roadmap

v0.2.0 (pip install voxprobe). Done: core, text arena, audio arena, benchmark, human calibration, golden tests, barge-in driver, websocket adapter, CALL-E adapter + inbound line + otherend grader with a 6-profile matrix on real calls. Next: a timed hold via mid-call control, n≥2 on every profile, a Gemini Live target, then a write-up. Details and honest limitations: docs/ROADMAP.md, CHANGELOG.md. The Vapi phone adapter is experimental (bring your own paid telephony; every outbound number must be on ALLOWED_NUMBERS_E164).

License

MIT

Metadata

Release files for voxprobe 0.2.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for voxprobe 0.2.2
File Size Uploaded
voxprobe-0.2.2.tar.gz 424.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for voxprobe 0.2.2
File Interpreter ABI Platform
voxprobe-0.2.2-py3-none-any.whl Python 3 none any Details

Total release size: 570.4 kB

Release files / voxprobe-0.2.2.tar.gz

Download URL voxprobe-0.2.2.tar.gz
Size 424.8 kB
Tags Source
SHA-256 checksum
How to use checksums
2d3362b1effb4728ebd8e05f5a7a73a61cf791e944d2cb35866946895e32a9d2
BLAKE2b-256 checksum
How to use checksums
3c300b9130d1671f8d118b59ba3448d35a90468f0d98902808407eebd7bab3fb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / voxprobe-0.2.2-py3-none-any.whl

Download URL voxprobe-0.2.2-py3-none-any.whl
Size 145.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cf7041237c73cf241f6f9da905305d53033961ccc78adf5fad5eefcd5118a216
BLAKE2b-256 checksum
How to use checksums
9822580d1c77410568d430792b69b15672f841c0a7257366cb53de67f5b2213f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.2.2 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page