VaaniEval
Quickstart

Python

Record your first voice-agent call from Python and open it in the console — about ten minutes, no agent framework required.

By the end of this page you will have a recorded call sitting in a local dashboard, with audio you can play and a trace you can expand.

Before you start

  • Python 3.10 or newer for the SDK
  • Python 3.11 or newer for the dashboard
  • Access to the private repositories. Neither SDK is on PyPI yet, so both are installed from Git.

VaaniEval is in closed beta

The repositories below are private while the product is in closed beta, so the links will 404 unless your GitHub account has been granted access. Neither SDK is published to a public package registry yet — both install from Git.

To get access, email shubham@vaanieval.com or book a call — a short conversation about your use case tells us whether VaaniEval fits, and gets you onboarded with the setup that matches your stack.

Nothing here requires LiveKit, OpenAI, or any particular provider. If you are on LiveKit Agents, do this page first anyway — then swap in the LiveKit integration, which wires all of it up for you.

1. Run the dashboard

Clone and install

git clone https://github.com/shubhamofbce/vaanieval-observer-backend.git dashboard
cd dashboard
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Start it

uvicorn app.main:app --reload --port 8000

The console is served at http://localhost:8000/. It will be empty — you have not recorded anything yet.

The local dashboard has no authentication and no tenant isolation, and it accepts any non-empty API key without validating it. That is deliberate for a developer loop. Do not expose it beyond localhost or a private network. See Self-hosting.

2. Install the SDK

pip install "git+https://github.com/shubhamofbce/vaanieval-observer-python-sdk.git"

The SDK has no required runtime dependencies. Install the extra that matches the HTTP client your agent already uses, so provider calls are instrumented automatically:

pip install "vaanieval-observer[httpx] @ git+https://github.com/shubhamofbce/vaanieval-observer-python-sdk.git"

3. Record a call

This script records one synthetic call: two turns, a transcription span, a model span, and audio on both channels. Run it against the dashboard you just started.

record_a_call.py
import asyncio
import math
import struct

from vaani_observer import VaaniObserver

SAMPLE_RATE = 16000
FORMAT = {"encoding": "pcm_s16le", "sample_rate_hz": SAMPLE_RATE, "channels": 1}


def tone(hz: int, ms: int) -> bytes:
    """A second of audible PCM, so the recording has something to play."""
    frames = int(SAMPLE_RATE * ms / 1000)
    return b"".join(
        struct.pack("<h", int(9000 * math.sin(2 * math.pi * hz * n / SAMPLE_RATE)))
        for n in range(frames)
    )


async def main() -> None:
    vaani = VaaniObserver(
        endpoint="http://localhost:8000",
        api_key="local-dev",              # the local dashboard does not validate this
        spool_directory="./.vaani-spool",
        endpoints=[
            {"id": "llm", "type": "llm", "url": "https://api.openai.com/v1"},
        ],
    )

    session = vaani.start_session(agent_id="quickstart-agent")

    for index in range(2):
        turn = session.start_turn()

        # The caller speaks.
        stt = session.start_operation(type="stt", turn_id=turn.id, provider="demo")
        stt.event("speech_started")
        session.record_inbound_audio(tone(220, 900), FORMAT)
        await asyncio.sleep(0.9)
        stt.event("final_transcript")
        stt.end(status="ok", response={"transcript": f"caller line {index + 1}"})

        # The model thinks.
        llm = session.start_operation(
            type="llm", turn_id=turn.id, provider="demo", model="demo-model"
        )
        await asyncio.sleep(0.4)
        llm.event("first_token")
        llm.end(status="ok", response={"tokens": 128})

        # The agent speaks.
        tts = session.start_operation(type="tts", turn_id=turn.id, provider="demo")
        tts.event("first_byte")
        session.record_outbound_audio(tone(440, 1200), FORMAT)
        tts.end(status="ok")

        turn.end()

    finalized = await session.end(outcome="completed")
    print("package written to", finalized.directory)

    await vaani.upload_package(finalized)
    print("uploaded session", finalized.session_id)


asyncio.run(main())
python record_a_call.py

You should see something like:

package written to ./.vaani-spool/6f1c...-...
uploaded session 6f1c...-...

4. Open the call

Refresh http://localhost:8000/. Your call is in the rail on the left. Click it and you get the waveform, the turn markers, and a trace you can expand down to individual milestones.

The trace view expanded to milestone level, showing a TTS span with 'text handed to the voice', 'first audio byte' and 'audio streaming' milestones, and an LLM span whose framework operation covers two HTTP attempts.

Look at the spool directory too — .vaani-spool/&lt;session-id&gt;/ contains manifest.json, events.jsonl and call.audio. Everything the dashboard shows is derived from those three files. See Session package.

What just happened

VaaniObserver was configured, not connected

Constructing the observer installs HTTP and websocket instrumentation in-process and validates your endpoint rules. It does not open a connection to the dashboard. endpoint and api_key are only used by upload_package().

The session spooled to disk as the call ran

Every event and audio chunk was appended to ./.vaani-spool/<session-id>/ by a dedicated writer thread, so no filesystem syscall ran on the event loop. Audio arrives every 20 ms in a real agent, and a blocking write there is audible.

end() finalized the package

It closed open spans, flushed the writer, composed the two mono tracks into one timeline-aligned stereo call.audio (agent left, caller right), and wrote manifest.json last — via a temp file and rename, so a reader never sees a half-written manifest.

upload_package() shipped it, after the call

Three steps: POST /v1/sessions with the manifest, PUT each object to the URL the dashboard returned, then POST .../complete with byte sizes and SHA-256 digests. Upload is always explicit and always post-call — the library never uploads on the live media path.

Wire it into a real agent

The synthetic script called start_operation() by hand. In a real agent you will usually want two things instead:

  • Automatic HTTP capture. Any httpx or aiohttp request made inside with session.context(): whose URL matches one of your configured endpoints is timed and recorded for you, with no code change at the call site.
  • Turn grouping. Wrap the work for one caller utterance in with session.with_turn(turn.id): so auto-instrumented calls land on the right turn.
session = vaani.start_session(agent_id="support")
turn = session.start_turn()

with session.with_turn(turn.id):
    # Instrumented automatically because api.openai.com matches an endpoint rule.
    response = await client.chat.completions.create(...)

Read Recording a call next, or go straight to LiveKit Agents if that is your framework.

Troubleshooting

On this page