Skip to Content

What should I trace in a voice assistant?

Rius traces a voice assistant best as one root span per conversation turn, with a span for each model call, tool and step, and a session and user on every span. Each turn becomes one trace, so the root span’s duration is the latency the caller heard. The session groups the turns of one call, and the user ties together that person’s calls.

How it works

A trace is one complete run, and it has exactly one root span. For a voice assistant, make a run one turn: the caller speaks, the assistant works, the assistant answers. The tree under the root span looks like this:

  • Root span per turn. Wrap the function that handles a turn with observe and the AGENT kind. Its duration and status are what run-level views show, so a failed root is a failed turn.
  • Generations for model calls. Each call to a language model is a span of kind LLM that records the model, provider, token usage, finish reasons and optionally the messages. Rius computes cost from the model and token counts, so you send usage and never cost.
  • Spans for tools. Each tool the assistant runs, such as a lookup or a booking, is a TOOL span named like execute_tool get_weather.
  • Spans for steps. Anything else in the turn, such as transcribing audio, rewriting the query, retrieving documents or preparing the spoken reply, is its own span. A bare observe makes a CHAIN span named after the function, and RETRIEVER fits a search step.
  • Session and user on every span. The session is the call or conversation, and the user is the person speaking.

If your model calls are streamed, the integration records the first chunk for you; on a manual generation, call gen.record_first_token() when the first chunk arrives. Rius derives time to first token from it, which is the wait a caller feels before the assistant starts to answer.

Set it up

  1. Install and initialize the SDK once. Tracing your code and the quickstart cover the full setup.

  2. Wrap each turn in a root AGENT span, and wrap each tool with the TOOL kind. A tool function you trace yourself needs kind=SpanKind.TOOL. A bare observe makes a CHAIN span, which the Tool kind filter and the Tool Loop alert never see.

  3. Open a session scope per call and a user scope per caller. Both ride OpenTelemetry context, so every span inside them, including spans from auto-instrumentation, carries session.id and user.id.

import rius from rius import SpanKind rius.init(api_key="ri_...", service_name="voice-assistant") @rius.observe(name="transcribe", capture_input=False) def transcribe(audio): ... @rius.observe(kind=SpanKind.TOOL) def get_weather(city: str): ... @rius.observe(kind=SpanKind.AGENT) def handle_turn(audio): text = transcribe(audio) # the model call and get_weather(...) happen here ... with rius.session(call_id): # your own call or conversation id with rius.user(caller_id): # an opaque id, not a phone number or email handle_turn(audio)
  1. Record the model calls. With an integration installed, such as the OpenAI or Anthropic one, each call becomes a generation automatically. If you trace a call by hand with a generation, turn that integration off by passing an explicit instruments list without it to rius.init, or the call is counted twice. Then set the usage yourself, or the generation carries no tokens or cost.

  2. Set the session id per call, not per process. A process-wide RIUS_SESSION_ID would merge every caller into one session on a server that handles many calls. Each turn stays its own trace.

  3. Pass capture_input=False or capture_output=False on steps whose arguments are raw audio or other large payloads. Values are truncated at 32 KB anyway. Setting RIUS_CAPTURE_CONTENT to false strips prompt and response content at export while keeping timing, tokens and cost.

What you get

  • One trace per turn. The waterfall shows where the time went, and the root span’s duration is the turn’s latency.
  • Cost per model call. Rius estimates it in US dollars at list price from the model and token counts.
  • Time to first token. You get this number for streamed generations that record their first chunk.
  • Calls as sessions. The turns of one call group together, even across deploys or replicas. If your session count equals your trace count, nothing is setting session.id.
  • Callers as users. Each caller shows with their traces, failures, cost and time to first token. A span outside any user scope is anonymous.
  • Errors in context. An exception sets ERROR status and error.type on the failing span, and the waterfall highlights the path from the root to it.

For how these pieces fit together, read Concepts. To watch the assistant once it runs, see Monitor AI agents in production.

When something else fits

  • If the assistant is a single model call with no tools or steps, a bare generation under one root span is enough, and you can skip the extra step spans.
  • If one process serves a single call from start to finish, such as a CLI test run, a process-wide session id with RIUS_SESSION_ID is simpler than scoping each call.

Still stuck? Get help

Last updated on