pydantic-ai-agents
skillBuild and debug Pydantic AI agents using best practices for dependencies, dynamic system prompts, tools, and structured output validation. Use when the user wants to: (1) Create a new Pydantic AI agent, (2) Debug or fix an existing agent, (3) Add features like tools, validators, or dynamic prompts, (4) Integrate OpenRouter for multi-model access, (5) Add Logfire for debugging/observability, (6) Structure agent architecture with dependency injection.
apm::install
apm install @fuenfgeld/pydantic-ai-agentsapm::skill.md
---
name: pydantic-ai-agents
description: "Build and debug Pydantic AI agents using best practices for dependencies, dynamic system prompts, tools, and structured output validation. Use when the user wants to: (1) Create a new Pydantic AI agent, (2) Debug or fix an existing agent, (3) Add features like tools, validators, or dynamic prompts, (4) Integrate OpenRouter for multi-model access, (5) Add Logfire for debugging/observability, (6) Structure agent architecture with dependency injection."
---
# Pydantic AI Reference Skill
## Pydantic AI Developer Guide
### 0. Environment Setup
Store API keys in a `.env` file and add it to `.gitignore`:
```
OPENAI_API_KEY=your_key
OPENROUTER_API_KEY=your_key
LOGFIRE_API_KEY=your_key
```
Load with `python-dotenv`: `load_dotenv()`. Never hardcode keys in source code.
### 1. Core Architecture
Pydantic AI agents have four key components:
#### Dependencies (deps):
- **Reference**: `references/01_dependencies.py`
- Use dataclasses to hold API keys, database connections, and user context
- Never use global variables for state
#### System Prompts (system_prompt):
- **Reference**: `references/02_prompts.py`
- Make prompts dynamic using `@agent.system_prompt` decorator
- Inject data from `ctx.deps` into the prompt string
#### Tools (@agent.tool):
- **Reference**: `references/03_tools.py`
- **IMPORTANT**: When `deps_type` is set on the agent, ALL tools must have `ctx: RunContext` as first parameter - even if they don't use it
- Use `ctx.deps` to access injected dependencies
#### Validators (output_type):
- **Reference**: `references/04_validators.py`
- Use Pydantic models to enforce structured output
- Use `@field_validator` for logic checks
### 2. Promoting Instructions (System Prompt Engineering)
Follow these rules when writing system prompts:
1. **Role Definition**: Start with "You are a specialized agent for..."
2. **Context Awareness**: Explicitly mention the data available in the dependencies.
- **Bad**: "I help users."
- **Good**: "I help user {ctx.deps.user_name} (ID: {ctx.deps.user_id}) manage their account."
3. **Tool Coercion**: If tools are defined, instruct the model when to use them.
- **Example**: "Use the lookup_order tool immediately if the user provides an order ID."
4. **Failure Modes**: Define what to do if a tool fails or data is missing.
- **Example**: "If the database returns no results, politely ask the user for clarification."
### 3. OpenRouter Integration
**Reference**: `references/06_openrouter.py`
OpenRouter is an API gateway that provides access to multiple LLM models through a unified API.
#### Key Points:
- OpenRouter uses OpenAI-compatible API format
- Set `OPENROUTER_API_KEY` in your `.env` file
- Use `OpenAIProvider` with `base_url='https://openrouter.ai/api/v1'`
- Access to models like GPT-4o-mini, Claude, Llama, and more
- See https://openrouter.ai/models for available models
#### Example Setup:
```python
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
provider = OpenAIProvider(
api_key=os.getenv('OPENROUTER_API_KEY'),
base_url='https://openrouter.ai/api/v1'
)
model = OpenAIChatModel(
model_name='gpt-4o-mini',
provider=provider
)
```
### 4. Debugging with Logfire
**Reference**: `references/07_logfire.py`
Logfire is a platform tightly integrated with Pydantic AI for debugging and observability.
#### Key Features:
- **Spans**: Track execution time and context of operations
- **Logging Levels**: notice, info, debug, warn, error, fatal
- **Exception Tracking**: Capture stack traces and error context
- **Tracing**: Visualize agent execution flow
#### Setup:
1. Get API key from https://logfire.pydantic.dev/
2. Set `LOGFIRE_API_KEY` in your `.env` file
3. Configure: `logfire.configure(token=LOGFIRE_API_KEY)`
#### Usage Pattern:
```python
with logfire.span('Calling Agent') as span:
result = agent.run_sync("user query")
span.set_attribute('result', result.output)
logfire.info('{result=}', result=result.output)
```
### 5. Advanced Patterns
#### Streaming Responses:
- **Reference**: `references/08_streaming.py`
- Use `agent.run_stream()` for real-time output
- Stream with `async for chunk in response.stream_text()`
- Get final result with `await response.get_output()`
#### Result Validators & Retry:
- **Reference**: `references/09_result_validators.py`
- Use `@agent.output_validator` for custom validation
- Raise `ModelRetry("feedback")` to trigger retry with guidance
- Set `retries=3` on Agent for auto-retry on validation failure
#### Model Settings:
- **Reference**: `references/10_model_settings.py`
- Pass `model_settings={'temperature': 0.7, 'max_tokens': 500}` to `agent.run()`
- Use low temperature (0.0-0.3) for factual tasks
- Use high temperature (0.7-1.0) for creative tasks
#### Multi-Agent Systems:
- **Reference**: `references/11_multi_agent.py`
- Orchestrate multiple specialized agents for complex tasks
- Use `asyncio.gather()` for parallel agent execution
- Implement routing for intent-based agent selection
### 6. Conversation History (Persistent Memory)
**Reference**: `references/12_conversation_history.py`
By default, each `agent.run()` call is stateless - the agent has no memory of previous interactions. To maintain conversation context across multiple turns, you must pass `message_history`.
#### Key Concepts:
1. **Get messages from result**: After each `run()`, call `result.all_messages()` to get the full conversation
2. **Pass history to next call**: Use `message_history=` parameter on subsequent `run()` calls
3. **Messages are immutable**: Each call returns a NEW list; the original is not modified
#### Basic Pattern:
```python
from pydantic_ai import Agent, ModelMessage
agent = Agent(model=model, system_prompt="You are helpful.")
# First turn - no history
result1 = agent.run_sync("My name is Alice")
messages: list[ModelMessage] = result1.all_messages()
# Second turn - pass history so agent remembers
result2 = agent.run_sync(
"What is my name?",
message_history=messages # Agent now knows "Alice"
)
messages = result2.all_messages() # Updated history
# Third turn - continue the conversation
result3 = agent.run_sync(
"Tell me a joke about my name",
message_history=messages
)
```
#### Function Signature Pattern:
When building conversation loops, return both the output and messages:
```python
from pydantic_ai import Agent, ModelMessage
def run_agent_with_history(
user_input: str,
message_history: list[ModelMessage] | None = None,
) -> tuple[str, list[ModelMessage]]:
"""Run agent and return output + updated history."""
result = agent.run_sync(
user_input,
message_history=message_history or [],
)
return result.output, result.all_messages()
# Usage in a conversation loop
history = []
while True:
user_input = input("You: ")
response, history = run_agent_with_history(user_input, history)
print(f"Agent: {response}")
```
#### Converting Custom Message Types:
If you store conversation history in your own format (e.g., database), convert to Pydantic AI format:
```python
from datetime import timezone
from pydantic_ai import ModelMessage
from pydantic_ai.messages import (
ModelRequest, ModelResponse,
UserPromptPart, TextPart
)
def convert_to_model_messages(my_messages: list[MyMessage]) -> list[ModelMessage]:
"""Convert custom message format to Pydantic AI format."""
result: list[ModelMessage] = []
for msg in my_messages:
# Ensure timezone-aware timestamp
ts = msg.timestamp.replace(tzinfo=timezone.utc) if msg.timestamp.tzinfo is None else msg.timestamp
if msg.role == "user":
result.append(ModelRequest(
parts=[UserPromptPart(content=msg.content, timestamp=ts)],
kind="request",
))
elif msg.role == "assistant":
result.append(ModelResponse(
parts=[TextPart(content=msg.content)],
kind="response",
timestamp=ts,
))
return result
```
#### Important Notes:
- **ModelMessage is a union type**: It's `ModelRequest | ModelResponse`, not a class you instantiate directly
- **User messages** → `ModelRequest` with `UserPromptPart`
- **Assistant messages** → `ModelResponse` with `TextPart`
- **Timestamps must be timezone-aware**: Use `timezone.utc`
- **System prompt is NOT in message_history**: It's set on the Agent and injected automatically
### 7. Testing Best Practices
#### Async/Sync Test Separation
**Warning**: When testing Pydantic AI agents, do NOT mix sync and async tests in the same file when using module-level agents.
**Problem**: Module-level agents create httpx clients at import time. When sync tests call `run_sync()`, they create/destroy temporary event loops which can corrupt the httpx connection pool. Subsequent async tests then fail with `Connection error`.
**Solution**: Separate async and sync tests into different files:
```python
# tests/test_agent_sync.py
from src.agent import run_agent_sync
def test_sync_behavior():
result = run_agent_sync("input")
assert result.field == expected
# tests/test_agent_async.py (SEPARATE FILE)
# Does NOT import run_agent_sync!
import pytest
from pydantic_ai import Agent
@pytest.mark.asyncio
async def test_async_behavior():
# Create fresh agent inside test
agent = Agent(model=model, output_type=Response)
result = await agent.run("input")
assert result.output.field == expected
```
**Alternative**: Convert all tests to async to use the same event loop consistently.
### 8. Usage
Build a new agent by:
1. Reading the requirement
2. Selecting the relevant components from the `references/` directory
3. Combining them into a single file following the pattern in `references/05_main.py`
4. Using OpenRouter (`references/06_openrouter.py`) for multi-model access
5. Adding Logfire (`references/07_logfire.py`) for debugging and monitoring
6. Adding conversation history (`references/12_conversation_history.py`) for multi-turn conversations
7. Adding advanced patterns (streaming, validators, multi-agent) as needed
### 9. Complete Example
See `references/05_main.py` for a complete working agent that demonstrates all patterns.