Informon
Pydantic AI Typed Agent
Scout Loop · 40 minutes

Pydantic AI Typed Agent

A Scout Loop around Pydantic AI, Python, Agents, Structured Output: build evidence, then choose the next action.

Did this fit you?

What It Is

Pydantic AI is a Python framework for building agentic LLM applications with the same typed, validation-first feel that Pydantic brings to data models. The core object you will touch is an Agent: it can carry dependency types into prompts and tools, and it can return a validated Pydantic output model instead of loose text.

In this Scout Loop, you will build a tiny support-ticket triage agent. The point is not to make a polished chatbot; it is to test whether Pydantic AI gives you a useful engineering shape for agent workflows: typed context in, structured decision out, and a way to smoke-test behavior without a real model call.

Why It Matters

For AI/data consulting and automation work, the interesting question is whether an agent framework makes messy LLM behavior easier to package, test, and demo. Pydantic AI is worth a bounded look because it treats dependencies and outputs as explicit types, which fits workflows where a client record, policy object, retriever, or database handle should be passed in deliberately rather than hidden in globals.

The practical signal to look for is simple: after one small build, can you imagine using this pattern for a lead scorer, ticket router, data-quality explainer, or internal analyst assistant without creating an untestable prompt pile? If the typed result and test override feel natural, the framework may be worth a deeper loop.

Guided First Play

You are going to create a local file that models a small production-shaped agent: a dependency dataclass carries account context, a tool reads that context, and a Pydantic model defines the triage decision you want back.

Create a tiny workspace

bash
✓ checkpoint
mkdir -p pydantic-ai-scout
cd pydantic-ai-scout
python -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
python -m pip install pydantic-ai pytest
✓ Checkpoint

If installation fails, treat that as the first signal: check the official install guidance before spending more time. The rest of this loop assumes the package imports successfully.

Start with the domain shape, not the model provider. This keeps the experiment focused on the framework's type flow: what data the agent may use, and what kind of answer your application expects.

Typed triage agent · Path: triage_agent.py

python
✓ checkpoint
from dataclasses import dataclass
from typing import Literal

from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext, models
from pydantic_ai.models.test import TestModel

models.ALLOW_MODEL_REQUESTS = False


@dataclass
class AccountContext:
    customer_id: str
    plan: Literal["free", "pro", "enterprise"]
    open_tickets: int


class TriageDecision(BaseModel):
    priority: Literal["low", "normal", "high"]
    route_to: Literal["support", "success", "engineering"]
    summary: str = Field(description="One sentence explaining the decision.")


agent = Agent(
    "openai:gpt-4o",
    deps_type=AccountContext,
    output_type=TriageDecision,
    system_prompt=(
        "You triage B2B SaaS support tickets. Use account context, "
        "prefer concise explanations, and escalate enterprise-impacting bugs."
    ),
)


@agent.tool
def account_snapshot(ctx: RunContext[AccountContext]) -> str:
    return (
        f"customer={ctx.deps.customer_id}; "
        f"plan={ctx.deps.plan}; "
        f"open_tickets={ctx.deps.open_tickets}"
    )


def run_demo() -> TriageDecision:
    with agent.override(model=TestModel()):
        result = agent.run_sync(
            "Checkout fails for all users in our EU workspace.",
            deps=AccountContext(customer_id="acme", plan="enterprise", open_tickets=3),
        )
    return result.output


if __name__ == "__main__":
    decision = run_demo()
    print(decision.model_dump_json(indent=2))
    print("route:", decision.route_to)
✓ Checkpoint

Your editor should be able to treat run_demo() as returning TriageDecision, so decision.route_to and decision.priority are typed fields rather than parsed strings.

Run it once. This pass uses Pydantic AI's test-model pattern, so it is about plumbing and typing rather than answer quality. You are checking whether the agent can be constructed, receive typed dependencies, call a typed tool, and produce an object shaped like TriageDecision.

Run the smoke test

bash
✓ checkpoint
python triage_agent.py
✓ Checkpoint

You should see JSON with priority, route_to, and summary, followed by a printed route field. If you get an import or model error, fix that before adding any real provider credentials.

Now make the type payoff visible. Add a deliberately invalid route in a separate quick check. The goal is to remind yourself that the boundary is a Pydantic model: bad structured data should be rejected close to the agent boundary, not discovered later in application code.

Validation check · Path: check_validation.py

python
✓ checkpoint
from pydantic import ValidationError

from triage_agent import TriageDecision

try:
    TriageDecision(
        priority="high",
        route_to="finance",
        summary="This should fail because finance is not an allowed route.",
    )
except ValidationError as exc:
    print("validation worked")
    print(exc.errors()[0]["loc"], exc.errors()[0]["msg"])
✓ Checkpoint

Run python check_validation.py. Success is not a pretty answer; success is seeing validation worked and an error pointing at route_to.

Try one focused variation: change the demo dependency from enterprise to free and the prompt text from a checkout outage to a billing-question ticket. Keep the output schema fixed. This shows whether the framework makes the useful part of an agent easy to vary: inputs and context change, while the application contract stays stable.

Variation to try inside run_demo · Path: triage_agent.py

python
✓ checkpoint
result = agent.run_sync(
    "Can you explain why my invoice changed this month?",
    deps=AccountContext(customer_id="solo", plan="free", open_tickets=0),
)
✓ Checkpoint

Run python triage_agent.py again. The exact test-model wording is less important than this invariant: result.output is still a TriageDecision with the same fields.

If you already have model credentials configured and want one live call, replace the model string with a real provider-backed model from the official docs and rerun the original enterprise-outage case. Keep the same output_type. Your comparison question is whether the live model gives a better decision while preserving the same typed contract.

Stop before building a full ticket system. The evidence you need from this Scout Loop is whether typed dependencies plus validated output make the agent feel easier to reason about, demo, and test.

Final verification: you should have three concrete observations. First, the Agent is parameterized by dependency and output types. Second, ctx.deps is available inside a tool, so runtime context does not need to be global. Third, the application receives a Pydantic object, which gives you a stable contract for downstream code and tests.

Your Next Move

Choose based on the engineering signal, not the novelty. Did the typed agent shape make the workflow feel more testable and productizable, or did it feel like framework overhead around a prompt?

Go Deeper would turn this into a two-hour loop with a real provider, two or three deterministic eval cases, and a cleaner test harness using model overrides. Project would mean picking a consulting-shaped use case, such as ticket triage or lead qualification, and turning the typed output into a small demo service or notebook workflow.

Did this fit you?

Sources

This loop is grounded in the official Pydantic AI documentation for agents, dependencies, structured output, testing, and evals, plus the primary public repository. The testing emphasis comes from the documented use of test models and model overrides to avoid real LLM usage, latency, and variability during unit tests.