Skip to Content

How do I trace a customer support agent built with Pydantic AI?

Rius traces a Pydantic AI support agent with one call, Agent.instrument_all(True), after rius.init(). Pydantic AI emits the agent run, the model calls with tokens and cost, and the tool calls, and you add spans for retrieval and for each stage of handling a ticket. Scope each run with rius.session() and rius.user() so you can find a conversation and the customer it served.

How it works

Pydantic AI is OpenTelemetry-native. rius.init() installs the global tracer provider, and Agent.instrument_all(True) makes the framework emit its spans through it. No extra and no wrappers are needed for the framework’s own work. The Pydantic AI integration page covers it in full, and Integrations lists the other frameworks. This integration is Python only.

What the framework gives you for each run:

  • The agent run becomes a span named invoke_agent followed by the agent’s name. It is the root unless you wrap it.
  • Each model request becomes a generation named chat followed by the model, with tokens and cost. Rius computes cost on its side.
  • Each tool call becomes a span named execute_tool followed by the tool name. A tool such as an account lookup needs no wrapper.

What the framework does not know about is the work around the agent. A support agent usually searches a knowledge base before it answers, and it handles a ticket in stages, such as classify, find articles and reply. You add those with the tracing helpers:

  • A retrieval span of kind RETRIEVER, with the knowledge base as its data source and the number of documents you asked for. It records document ids and scores, not text.
  • An observe wrapper per stage, which makes a span named after the function.
  • An observe wrapper around the whole ticket, so the agent run and your own stages sit under one span.

Sessions and users give you the grouping. A session is one conversation, and the user is the customer.

Set it up

  1. Install the SDK and Pydantic AI. No extra is needed.

    pip install glassflow-rius pydantic-ai
  2. Initialize Rius, turn on Pydantic AI instrumentation, and wrap your own stages. The lookup, search, classify and ticket_text names below are your own code.

    import rius from rius import SpanKind from pydantic_ai import Agent rius.init(api_key="ri_...", service_name="support-agent") Agent.instrument_all(True) support_agent = Agent( "openai:gpt-5-mini", instructions="Answer support questions; check accounts with the tool.", ) @support_agent.tool_plain def account_status(email: str) -> str: """Fetch a customer's account standing by email.""" return lookup(email) def find_articles(query: str) -> str: with rius.start_as_current_span( kind=SpanKind.RETRIEVER, data_source_id="product-kb", top_k=5 ) as obs: obs.set_input(query) docs = search(query) obs.set_retrieved_documents([{"id": d.id, "score": d.score} for d in docs]) return "\n".join(d.text for d in docs) @rius.observe(name="handle_ticket") def handle_ticket(ticket: str) -> str: classify(ticket) context = find_articles(ticket) result = support_agent.run_sync(f"{ticket}\n\nArticles:\n{context}") return result.output with rius.session(conversation_id): with rius.user(customer_id): handle_ticket(ticket_text)
  3. Give each stage you want to see its own function with @rius.observe, as handle_ticket does. Without a name, an observe span takes the function’s name.

  4. Pass your own conversation id to rius.session() and an opaque customer id to rius.user(). Prefer an opaque id to an email or a name, because the value is stored on every span and shown in the console. Open the scopes per request, not once per process, since a server that handles many customers would otherwise merge them. Pydantic AI’s spans inherit both scopes like any other span.

  5. Run a ticket that calls the tool and open the trace in the console. You should see your handle_ticket span, the retrieval span, the invoke_agent span, the chat generations and the execute_tool span.

What you get

  • One trace per ticket with your stages, the knowledge-base search, the agent run, each model call and each tool call in one tree.
  • Tokens and cost on every model call, shown as list-price estimates in US dollars.
  • The retrieval span lists which articles were returned and their scores, so you can tell a bad answer caused by a bad search from one caused by the model.
  • Traces grouped by conversation through session.id, so a multi-turn exchange reads as one session.
  • The customer on each span through user.id, so you can see their traces, failures and cost together. A span outside any user scope is anonymous.
  • Because you set the span name yourself on your observe wrappers, you can filter by those names in saved searches.

Two behaviours of the integration are worth knowing. Successful framework spans arrive with an UNSET status, and the trace status still reads Ok. What the spans carry (prompts, completions, tool arguments) is controlled by Pydantic AI’s InstrumentationSettings, for example include_content, on top of the SDK’s own privacy controls.

When something else fits

  • If you build the same agent with a different framework, use that framework’s page under Integrations, because the spans you get for free differ.
  • If your code is not Python, Pydantic AI has no TypeScript integration, so trace model calls there with manual generations from Tracing your code.
  • If you do not use the Rius SDK, any OpenTelemetry setup pointed at the ingest endpoint also works for Pydantic AI, without the SDK’s sessions, heartbeats and privacy controls.

Still stuck? Get help

Last updated on