How do I trace a document processing pipeline built with Pydantic AI?
Rius traces a Pydantic AI document pipeline by reading the spans the framework emits for each agent run, model request and tool call, and you add a few spans of your own for the pipeline around them. Call rius.init() and Agent.instrument_all(True), then wrap the pipeline in one observe function so every stage lands in one trace. Add a retrieval span where you look documents up, and scope each run with a session and a user. The result is one trace per document, with tokens and cost on every model call.
How it works
Pydantic AI is OpenTelemetry-native, so no auto-instrumentation extra is involved. rius.init() installs the global tracer provider, and Agent.instrument_all(True) makes the framework emit its spans through it. The Pydantic AI integration gives you:
- A span named
invoke_agent <agent name>for each agent run, which is the trace root only when nothing wraps it. - A generation named
chat <model>for each model request, with tokens and cost. Rius computes the cost, so you never send it. - A span named
execute_tool <tool name>for each tool call.
A pipeline usually has more than one agent run: for example one agent extracts fields, another classifies, another summarizes. Each run is its own root unless something encloses them, so the framework alone gives you separate pieces. What the pipeline needs on top comes from tracing your code:
- A run root. An
observefunction around the whole pipeline. A bareobservemakes a span named after the function, and the agent runs inside it nest beneath it. - A step per stage. An
observefunction for each non-model stage, such as parsing or chunking, so slow or failing stages show up by name. - A retrieval span.
start_as_current_spanwithSpanKind.RETRIEVERfor lookups. It is namedretrieval <data source id>and recordstop_kand the retrieved document ids and scores. It stores no document text. Put retrieved text inset_output()if you want it captured. - A rerank step. If you reorder retrieved documents, use
kind=SpanKind.RERANKER.
You do not wrap the tools registered on your agents. The framework already records them as execute_tool spans.
Set it up
-
Install the SDK and Pydantic AI. No extra is needed. The integrations overview lists the other frameworks.
pip install glassflow-rius pydantic-ai -
Initialize Rius once at startup and turn on Pydantic AI instrumentation:
import rius from pydantic_ai import Agent rius.init(api_key="ri_...", service_name="doc-pipeline") Agent.instrument_all(True) -
Wrap each stage and the whole run. The names below (
process_document,parse,fetch_templates,extract_agent,search) are your own:from rius import SpanKind extract_agent = Agent( "openai:gpt-5-mini", instructions="Extract the key fields from the document.", ) @rius.observe def parse(raw: bytes) -> str: ... def fetch_templates(text: str): with rius.start_as_current_span( kind=SpanKind.RETRIEVER, data_source_id="templates", top_k=5 ) as obs: obs.set_input(text[:200]) docs = search(text) obs.set_retrieved_documents( [{"id": d.id, "score": d.score} for d in docs] ) return docs @rius.observe def extract(text: str, docs): return extract_agent.run_sync(text).output @rius.observe def process_document(raw: bytes): text = parse(raw) docs = fetch_templates(text) return extract(text, docs) -
Scope each run with a session and a user. Use your own job or document id for the session, and an opaque id for the user:
with rius.user(request.user_id): with rius.session(job_id): process_document(raw)One session groups the traces of related work, such as every document in a batch job. With no session, each trace is its own session. A span outside a user scope is anonymous.
observetakes no user argument because a user belongs to each request. Do not set a process-wide session in a server that handles many users, since their traces would merge. -
Run one document, then open the trace. You should see the
process_documentroot withparse, the retrieval span,extract, and under itinvoke_agent,chatgenerations with model, tokens and cost, and anyexecute_toolspans.
What you get
- One trace per document, with every stage as a named span and the model calls nested under the stage that made them.
- Tokens and cost on each
chatgeneration. Costs are list-price estimates in US dollars. - Time to first token and reasoning tokens for Pydantic AI runs, on
glassflow-rius1.0.1 and 1.0.2 or later. - A retrieval span that shows which index was searched, how many documents were requested and which ids came back with what score.
- Traces grouped by session and filterable by user, so you can follow a batch job or see what one person’s documents cost.
Two behaviors to know. Successful framework spans arrive with status UNSET, and the trace status still reads Ok. What prompts and completions the spans carry is set by Pydantic AI’s InstrumentationSettings (for example include_content), on top of the SDK’s own privacy controls.
When something else fits
- If you want to send Pydantic AI spans without the Rius SDK, point any OpenTelemetry setup at the ingest endpoint instead. You then set up sessions and exporter wiring yourself.
- If a stage calls a model directly rather than through a Pydantic AI agent, trace that call with a manual generation, as the tracing page shows.