Building AI Agents with Pydantic AI
Pydantic AI is a Python agent framework for building production-grade Generative AI applications.
This skill provides patterns, architecture guidance, and tested code examples for building applications with Pydantic AI.
When to Use This Skill
Invoke this skill when:
- User asks to build an AI agent, create an LLM-powered app, or mentions Pydantic AI
- User wants to add tools, capabilities (thinking, web search), or structured output to an agent
- User asks to define agents from YAML/JSON specs or use template strings
- User wants to stream agent events, delegate between agents, or test agent behavior
- Code imports
pydantic_ai or references Pydantic AI classes (Agent, RunContext, Tool)
- User asks about hooks, lifecycle interception, or agent observability with Logfire
- The agent design includes optional instructions, specialist workflows, long-tail tools, or any context the model does not need on most turns
Do not use this skill for:
- The Pydantic validation library alone (
pydantic/BaseModel without agents)
- Other AI frameworks (LangChain, LlamaIndex, CrewAI, AutoGen)
- General Python development unrelated to AI agents
Quick-Start Patterns
Create a Basic Agent
from pydantic_ai import Agent
agent = Agent(
'anthropic:claude-sonnet-4-6',
instructions='Be concise, reply with one sentence.',
)
result = agent.run_sync('Where does "hello world" come from?')
print(result.output)
"""
The first known use of "hello, world" was in a 1974 textbook about the C programming language.
"""
Add Tools to an Agent
import random
from pydantic_ai import Agent, RunContext
agent = Agent(
'google:gemini-3-flash-preview',
deps_type=str,
instructions=(
"You're a dice game, you should roll the die and see if the number "
"you get back matches the user's guess. If so, tell them they're a winner. "
"Use the player's name in the response."
),
)
@agent.tool_plain
def roll_dice() -> str:
"""Roll a six-sided die and return the result."""
return str(random.randint(1, 6))
@agent.tool
def get_player_name(ctx: RunContext[str]) -> str:
"""Get the player's name."""
return ctx.deps
dice_result = agent.run_sync('My guess is 4', deps='Anne')
print(dice_result.output)
#> Congratulations Anne, you guessed correctly! You're a winner!
Structured Output with Pydantic Models
from pydantic import BaseModel
from pydantic_ai import Agent
class CityLocation(BaseModel):
city: str
country: str
agent = Agent('google:gemini-3-flash-preview', output_type=CityLocation)
result = agent.run_sync('Where were the olympics held in 2012?')
print(result.output)
#> city='London' country='United Kingdom'
print(result.usage)
#> RunUsage(input_tokens=57, output_tokens=8, requests=1)
Dependency Injection
from datetime import date
from pydantic_ai import Agent, RunContext
agent = Agent(
'openai:gpt-5.2',
deps_type=str,
instructions="Use the customer's name while replying to them.",
)
@agent.instructions
def add_the_users_name(ctx: RunContext[str]) -> str:
return f"The user's name is {ctx.deps}."
@agent.instructions
def add_the_date() -> str:
return f'The date is {date.today()}.'
result = agent.run_sync('What is the date?', deps='Frank')
print(result.output)
#> Hello Frank, the date today is 2032-01-02.
Testing with TestModel
from pydantic_ai import Agent
from pydantic_ai.models.test import TestModel
my_agent = Agent('openai:gpt-5.2', instructions='...')
async def test_my_agent():
"""Unit test for my_agent, to be run by pytest."""
m = TestModel()
with my_agent.override(model=m):
result = await my_agent.run('Testing my agent...')
assert result.output == 'success (no tool calls)'
assert m.last_model_request_parameters.function_tools == []
Use Capabilities
Capabilities are reusable, composable units of agent behavior — bundling tools, hooks, instructions, and model settings.
from pydantic_ai import Agent
from pydantic_ai.capabilities import Thinking, WebSearch
agent = Agent(
'anthropic:claude-opus-4-6',
instructions='You are a research assistant. Be thorough and cite sources.',
capabilities=[
Thinking(effort='high'),
WebSearch(),
],
)
Add Lifecycle Hooks
Use Hooks to intercept model requests, tool calls, and runs with decorators — no subclassing needed.
from pydantic_ai import Agent, RunContext
from pydantic_ai.capabilities.hooks import Hooks
from pydantic_ai.models import ModelRequestContext
hooks = Hooks()
@hooks.on.before_model_request
async def log_request(ctx: RunContext[None], request_context: ModelRequestContext) -> ModelRequestContext:
print(f'Sending {len(request_context.messages)} messages')
return request_context
agent = Agent('openai:gpt-5.2', capabilities=[hooks])
Define Agent from YAML Spec
Use Agent.from_file to load agents from YAML or JSON — no Python agent construction code needed.
from pydantic_ai import Agent
# agent.yaml:
# model: anthropic:claude-opus-4-6
# instructions: You are a helpful research assistant.
# capabilities:
# - WebSearch
# - Thinking:
# effort: high
agent = Agent.from_file('agent.yaml')
Task Routing Table
Load only the most relevant reference first. Read additional references only if the task spans multiple areas.
| I want to... |
Reference |
| Create/configure agents, choose output types, use deps, define specs, or pick run methods |
Agents Core |
| Bundle reusable behavior or intercept lifecycle events |
Capabilities and Hooks |
Decide what should load eagerly vs on demand, apply progressive disclosure, defer capability loading, or explain load_capability |
Capabilities on Demand |
| Add function tools, toolsets, MCP servers, or explicit search tools |
Tools Core |
| Use provider-native web search, web fetch, or code execution |
Native Tools |
Use advanced tool features such as approval, retries, ToolReturn, validators, timeouts, or tool search |
Tools Advanced |
| Work with multimodal input, message history, or context trimming |
Input and History |
| Test or debug agent behavior |
Testing and Debugging |
| Coordinate multiple agents or build graph workflows |
Orchestration and Integrations |
| Call the model directly, expose A2A, use durable execution, embeddings, evals, or third-party integrations |
Orchestration and Integrations |
| Compare abstractions, output modes, decorators, or model-string patterns |
Architecture and Decision Guide |
Follow an older link into COMMON-TASKS.md |
Task Reference Map |
Architecture and Decisions
Load Architecture and Decision Guide only when the user is choosing between abstractions or wants comparison tables and decision trees:
| Topic |
What it covers |
| Decision Trees |
Tool registration, output modes, multi-agent patterns, capabilities, testing approaches, extensibility |
| Comparison Tables |
Output modes, model provider prefixes, tool decorators, built-in capabilities, agent methods |
| Architecture Overview |
Execution flow, generic types, construction patterns, lifecycle hooks, model string format |
Quick reference — model string format: "provider:model-name" (e.g., "openai:gpt-5.2", "anthropic:claude-sonnet-4-6", "google:gemini-3-pro-preview")
Quick reference — key agent methods: run(), run_sync(), run_stream(), run_stream_sync(), run_stream_events(), iter()
Key Practices
- Python 3.10+ compatibility required
- Progressive disclosure by default: For every capability, explicitly consider whether
defer_loading=True would benefit the agent before choosing eager loading. Do not eagerly load specialist instructions, rarely used tool schemas, or domain context unless the model needs them on most turns. Prefer capabilities on demand for named instruction+tool bundles, and tool search for large flat tool catalogs.
- Observability: Pydantic AI has first-class integration with Logfire for tracing agent runs, tool calls, and model requests. Add it with
logfire.instrument_pydantic_ai(). For deeper HTTP-level visibility, logfire.instrument_httpx(capture_all=True) captures the exact payloads sent to model providers.
- Testing: Use
TestModel for deterministic tests, FunctionModel for custom logic
Common Gotchas
These are mistakes agents commonly make with Pydantic AI. Getting these wrong produces silent failures or confusing errors.
@agent.tool requires RunContext as first param; @agent.tool_plain must not have it. Mixing these up causes runtime errors. Use tool_plain when you don't need deps, usage, or messages.
- Model strings need the provider prefix:
'openai:gpt-5.2' not 'gpt-5.2'. Without the prefix, Pydantic AI can't resolve the provider.
TestModel requires agent.override(): Don't set agent.model directly. Always use the context manager: with agent.override(model=TestModel()):.
str in output_type allows plain text to end the run: If your union includes str (or no output_type is set), the model can return plain text instead of structured output. Omit str from the union to force tool-based output.
- Hook decorator names on
.on don't repeat on_: Use hooks.on.run_error and hooks.on.model_request_error — not hooks.on.on_run_error.
history_processors is deprecated; use capabilities=[ProcessHistory(p), ...], or hook before_model_request directly via capabilities=[Hooks(before_model_request=fn)]. ProcessHistory is a thin wrapper around that hook — the hook itself is the underlying primitive. The kwarg still works in 1.x but emits a PydanticAIDeprecationWarning and will be removed in v2.
Task-Family References
Load exactly one of these unless the task clearly spans multiple families:
| Task family |
Reference |
| Core agent setup, output, deps, specs, models, run methods |
Agents Core |
| Capabilities, hooks, and reusable behavior |
Capabilities and Hooks |
Progressive disclosure, deferred capabilities, capabilities on demand, and load_capability semantics |
Capabilities on Demand |
| Function tools, toolsets, MCP, explicit search tools |
Tools Core |
| Provider-native tools |
Native Tools |
| Approval, retries, validators, timeouts, rich tool returns, tool search, and tool-level deferred loading |
Tools Advanced |
| Multimodal input, message history, history processors |
Input and History |
| Testing, request inspection, and Logfire debugging |
Testing and Debugging |
| Multi-agent patterns, graphs, direct API, A2A, durable execution, embeddings, evals, third-party integrations |
Orchestration and Integrations |
Use Task Reference Map only for compatibility with older links or when you need a pointer from an old section name to the new file.
1---
2name: building-pydantic-ai-agents
3description: Build AI agents with Pydantic AI — tools, capabilities (including on-demand loading), structured output, streaming, testing, and multi-agent patterns. Use when the user mentions Pydantic AI, imports pydantic_ai, or asks to build an AI agent, add tools/capabilities, defer capability loading, stream output, define agents from YAML, or test agent behavior.
4license: MIT
5compatibility: Requires Python 3.10+
6metadata:
7 version: "1.1.0"
8 author: pydantic
9---
10
11# Building AI Agents with Pydantic AI
12
13Pydantic AI is a Python agent framework for building production-grade Generative AI applications.
14This skill provides patterns, architecture guidance, and tested code examples for building applications with Pydantic AI.
15
16## When to Use This Skill
17
18Invoke this skill when:
19- User asks to build an AI agent, create an LLM-powered app, or mentions Pydantic AI
20- User wants to add tools, capabilities (thinking, web search), or structured output to an agent
21- User asks to define agents from YAML/JSON specs or use template strings
22- User wants to stream agent events, delegate between agents, or test agent behavior
23- Code imports `pydantic_ai` or references Pydantic AI classes (`Agent`, `RunContext`, `Tool`)
24- User asks about hooks, lifecycle interception, or agent observability with Logfire
25- The agent design includes optional instructions, specialist workflows, long-tail tools, or any context the model does not need on most turns
26
27Do **not** use this skill for:
28- The Pydantic validation library alone (`pydantic`/`BaseModel` without agents)
29- Other AI frameworks (LangChain, LlamaIndex, CrewAI, AutoGen)
30- General Python development unrelated to AI agents
31
32## Quick-Start Patterns
33
34### Create a Basic Agent
35
36```python
37from pydantic_ai import Agent
38
39agent = Agent(
40 'anthropic:claude-sonnet-4-6',
41 instructions='Be concise, reply with one sentence.',
42)
43
44result = agent.run_sync('Where does "hello world" come from?')
45print(result.output)
46"""
47The first known use of "hello, world" was in a 1974 textbook about the C programming language.
48"""
49```
50
51### Add Tools to an Agent
52
53```python
54import random
55
56from pydantic_ai import Agent, RunContext
57
58agent = Agent(
59 'google:gemini-3-flash-preview',
60 deps_type=str,
61 instructions=(
62 "You're a dice game, you should roll the die and see if the number "
63 "you get back matches the user's guess. If so, tell them they're a winner. "
64 "Use the player's name in the response."
65 ),
66)
67
68
69@agent.tool_plain
70def roll_dice() -> str:
71 """Roll a six-sided die and return the result."""
72 return str(random.randint(1, 6))
73
74
75@agent.tool
76def get_player_name(ctx: RunContext[str]) -> str:
77 """Get the player's name."""
78 return ctx.deps
79
80
81dice_result = agent.run_sync('My guess is 4', deps='Anne')
82print(dice_result.output)
83#> Congratulations Anne, you guessed correctly! You're a winner!
84```
85
86### Structured Output with Pydantic Models
87
88```python
89from pydantic import BaseModel
90
91from pydantic_ai import Agent
92
93
94class CityLocation(BaseModel):
95 city: str
96 country: str
97
98
99agent = Agent('google:gemini-3-flash-preview', output_type=CityLocation)
100result = agent.run_sync('Where were the olympics held in 2012?')
101print(result.output)
102#> city='London' country='United Kingdom'
103print(result.usage)
104#> RunUsage(input_tokens=57, output_tokens=8, requests=1)
105```
106
107### Dependency Injection
108
109```python
110from datetime import date
111
112from pydantic_ai import Agent, RunContext
113
114agent = Agent(
115 'openai:gpt-5.2',
116 deps_type=str,
117 instructions="Use the customer's name while replying to them.",
118)
119
120
121@agent.instructions
122def add_the_users_name(ctx: RunContext[str]) -> str:
123 return f"The user's name is {ctx.deps}."
124
125
126@agent.instructions
127def add_the_date() -> str:
128 return f'The date is {date.today()}.'
129
130
131result = agent.run_sync('What is the date?', deps='Frank')
132print(result.output)
133#> Hello Frank, the date today is 2032-01-02.
134```
135
136### Testing with TestModel
137
138```python
139from pydantic_ai import Agent
140from pydantic_ai.models.test import TestModel
141
142my_agent = Agent('openai:gpt-5.2', instructions='...')
143
144
145async def test_my_agent():
146 """Unit test for my_agent, to be run by pytest."""
147 m = TestModel()
148 with my_agent.override(model=m):
149 result = await my_agent.run('Testing my agent...')
150 assert result.output == 'success (no tool calls)'
151 assert m.last_model_request_parameters.function_tools == []
152```
153
154### Use Capabilities
155
156Capabilities are reusable, composable units of agent behavior — bundling tools, hooks, instructions, and model settings.
157
158```python
159from pydantic_ai import Agent
160from pydantic_ai.capabilities import Thinking, WebSearch
161
162agent = Agent(
163 'anthropic:claude-opus-4-6',
164 instructions='You are a research assistant. Be thorough and cite sources.',
165 capabilities=[
166 Thinking(effort='high'),
167 WebSearch(),
168 ],
169)
170```
171
172### Add Lifecycle Hooks
173
174Use `Hooks` to intercept model requests, tool calls, and runs with decorators — no subclassing needed.
175
176```python
177from pydantic_ai import Agent, RunContext
178from pydantic_ai.capabilities.hooks import Hooks
179from pydantic_ai.models import ModelRequestContext
180
181hooks = Hooks()
182
183
184@hooks.on.before_model_request
185async def log_request(ctx: RunContext[None], request_context: ModelRequestContext) -> ModelRequestContext:
186 print(f'Sending {len(request_context.messages)} messages')
187 return request_context
188
189
190agent = Agent('openai:gpt-5.2', capabilities=[hooks])
191```
192
193### Define Agent from YAML Spec
194
195Use `Agent.from_file` to load agents from YAML or JSON — no Python agent construction code needed.
196
197```python
198from pydantic_ai import Agent
199
200# agent.yaml:
201# model: anthropic:claude-opus-4-6
202# instructions: You are a helpful research assistant.
203# capabilities:
204# - WebSearch
205# - Thinking:
206# effort: high
207
208agent = Agent.from_file('agent.yaml')
209```
210
211## Task Routing Table
212
213Load only the most relevant reference first. Read additional references only if the task spans multiple areas.
214
215| I want to... | Reference |
216|---|---|
217| Create/configure agents, choose output types, use deps, define specs, or pick run methods | [Agents Core](./references/AGENTS-CORE.md) |
218| Bundle reusable behavior or intercept lifecycle events | [Capabilities and Hooks](./references/CAPABILITIES-AND-HOOKS.md) |
219| Decide what should load eagerly vs on demand, apply progressive disclosure, defer capability loading, or explain `load_capability` | [Capabilities on Demand](./references/ON-DEMAND-CAPABILITIES.md) |
220| Add function tools, toolsets, MCP servers, or explicit search tools | [Tools Core](./references/TOOLS-CORE.md) |
221| Use provider-native web search, web fetch, or code execution | [Native Tools](./references/NATIVE-TOOLS.md) |
222| Use advanced tool features such as approval, retries, `ToolReturn`, validators, timeouts, or tool search | [Tools Advanced](./references/TOOLS-ADVANCED.md) |
223| Work with multimodal input, message history, or context trimming | [Input and History](./references/INPUT-AND-HISTORY.md) |
224| Test or debug agent behavior | [Testing and Debugging](./references/TESTING-AND-DEBUGGING.md) |
225| Coordinate multiple agents or build graph workflows | [Orchestration and Integrations](./references/ORCHESTRATION-AND-INTEGRATIONS.md#coordinate-multiple-agents) |
226| Call the model directly, expose A2A, use durable execution, embeddings, evals, or third-party integrations | [Orchestration and Integrations](./references/ORCHESTRATION-AND-INTEGRATIONS.md) |
227| Compare abstractions, output modes, decorators, or model-string patterns | [Architecture and Decision Guide](./references/ARCHITECTURE.md) |
228| Follow an older link into `COMMON-TASKS.md` | [Task Reference Map](./references/COMMON-TASKS.md) |
229
230## Architecture and Decisions
231
232Load [Architecture and Decision Guide](./references/ARCHITECTURE.md) only when the user is choosing between abstractions or wants comparison tables and decision trees:
233
234| Topic | What it covers |
235|---|---|
236| Decision Trees | Tool registration, output modes, multi-agent patterns, capabilities, testing approaches, extensibility |
237| Comparison Tables | Output modes, model provider prefixes, tool decorators, built-in capabilities, agent methods |
238| Architecture Overview | Execution flow, generic types, construction patterns, lifecycle hooks, model string format |
239
240**Quick reference — model string format:** `"provider:model-name"` (e.g., `"openai:gpt-5.2"`, `"anthropic:claude-sonnet-4-6"`, `"google:gemini-3-pro-preview"`)
241
242**Quick reference — key agent methods:** `run()`, `run_sync()`, `run_stream()`, `run_stream_sync()`, `run_stream_events()`, `iter()`
243
244## Key Practices
245
246- **Python 3.10+** compatibility required
247- **Progressive disclosure by default**: For every capability, explicitly consider whether `defer_loading=True` would benefit the agent before choosing eager loading. Do not eagerly load specialist instructions, rarely used tool schemas, or domain context unless the model needs them on most turns. Prefer capabilities on demand for named instruction+tool bundles, and tool search for large flat tool catalogs.
248- **Observability**: Pydantic AI has first-class integration with Logfire for tracing agent runs, tool calls, and model requests. Add it with `logfire.instrument_pydantic_ai()`. For deeper HTTP-level visibility, `logfire.instrument_httpx(capture_all=True)` captures the exact payloads sent to model providers.
249- **Testing**: Use `TestModel` for deterministic tests, `FunctionModel` for custom logic
250
251## Common Gotchas
252
253These are mistakes agents commonly make with Pydantic AI. Getting these wrong produces silent failures or confusing errors.
254
255- **`@agent.tool` requires `RunContext` as first param**; `@agent.tool_plain` must **not** have it. Mixing these up causes runtime errors. Use `tool_plain` when you don't need deps, usage, or messages.
256- **Model strings need the provider prefix**: `'openai:gpt-5.2'` not `'gpt-5.2'`. Without the prefix, Pydantic AI can't resolve the provider.
257- **`TestModel` requires `agent.override()`**: Don't set `agent.model` directly. Always use the context manager: `with agent.override(model=TestModel()):`.
258- **`str` in output_type allows plain text to end the run**: If your union includes `str` (or no `output_type` is set), the model can return plain text instead of structured output. Omit `str` from the union to force tool-based output.
259- **Hook decorator names on `.on` don't repeat `on_`**: Use `hooks.on.run_error` and `hooks.on.model_request_error` — not `hooks.on.on_run_error`.
260- **`history_processors` is deprecated; use `capabilities=[ProcessHistory(p), ...]`**, or hook `before_model_request` directly via `capabilities=[Hooks(before_model_request=fn)]`. `ProcessHistory` is a thin wrapper around that hook — the hook itself is the underlying primitive. The kwarg still works in 1.x but emits a `PydanticAIDeprecationWarning` and will be removed in v2.
261
262## Task-Family References
263
264Load exactly one of these unless the task clearly spans multiple families:
265
266| Task family | Reference |
267|---|---|
268| Core agent setup, output, deps, specs, models, run methods | [Agents Core](./references/AGENTS-CORE.md) |
269| Capabilities, hooks, and reusable behavior | [Capabilities and Hooks](./references/CAPABILITIES-AND-HOOKS.md) |
270| Progressive disclosure, deferred capabilities, capabilities on demand, and `load_capability` semantics | [Capabilities on Demand](./references/ON-DEMAND-CAPABILITIES.md) |
271| Function tools, toolsets, MCP, explicit search tools | [Tools Core](./references/TOOLS-CORE.md) |
272| Provider-native tools | [Native Tools](./references/NATIVE-TOOLS.md) |
273| Approval, retries, validators, timeouts, rich tool returns, tool search, and tool-level deferred loading | [Tools Advanced](./references/TOOLS-ADVANCED.md) |
274| Multimodal input, message history, history processors | [Input and History](./references/INPUT-AND-HISTORY.md) |
275| Testing, request inspection, and Logfire debugging | [Testing and Debugging](./references/TESTING-AND-DEBUGGING.md) |
276| Multi-agent patterns, graphs, direct API, A2A, durable execution, embeddings, evals, third-party integrations | [Orchestration and Integrations](./references/ORCHESTRATION-AND-INTEGRATIONS.md) |
277
278Use [Task Reference Map](./references/COMMON-TASKS.md) only for compatibility with older links or when you need a pointer from an old section name to the new file.