agentsclimarketplace

Pydantic ai agents

Skill Fuenfgeld/pydantic-ai-skills/skills/pydantic-ai-agents

Production-ready Claude Code skills for building AI agents with Pydantic AI. Includes dependency injection, tools, validators, streaming, multi-agent orchestration, and evaluation framework patterns.

Install
npx -y skills add Fuenfgeld/pydantic-ai-skills --skill pydantic-ai-agents

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 10 stars10 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Build and debug Pydantic AI v2 agents using best practices for dependencies, instructions, tools, capabilities, hooks, 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, capabilities, hooks, or dynamic instructions, (4) Integrate OpenRouter for multi-model access, (5) Add Logfire for debugging/observability, (6) Structure agent architecture with dependency injection, (7) Migrate an agent from Pydantic AI v1 to v2.

SKILL.md

16.5 KB, as published. Nobody here has run it

Pydantic AI Reference Skill

Targets Pydantic AI v2 (v2.0.0 released 2026-06-23). See the v1 → v2 migration notes at the end for the breaking changes this skill's examples already incorporate.

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 five 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

Instructions (instructions):

  • Reference: references/02_prompts.py
  • v2 recommended: use instructions= / @agent.instructions (instructions are NOT carried in message history, so they always reflect the current agent)
  • Make them dynamic using the @agent.instructions decorator
  • Inject data from ctx.deps into the prompt string
  • Legacy system_prompt= / @agent.system_prompt still work; their text IS carried in message history - only use when replaying history across agents

Tools (@agent.tool / @agent.tool_plain):

  • Reference: references/03_tools.py
  • v2: @agent.tool REQUIRES ctx: RunContext as first parameter; use @agent.tool_plain for context-free tools (it raises otherwise)
  • 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

Capabilities (capabilities) - NEW in v2:

  • Reference: references/13_capabilities_native_tools.py
  • THE core v2 primitive: one composable unit bundling tools, hooks, instructions, and model settings
  • Provider-adaptive capabilities: Thinking, WebSearch, WebFetch, ImageGeneration, MCP
  • Replaces v1's builtin_tools=, prepare_tools=, history_processors=, instrument=, event_stream_handler= Agent arguments

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. Since Pydantic AI v2 it has first-class support - do NOT use the old OpenAI-compatible base_url workaround anymore.

Key Points:

  • Set OPENROUTER_API_KEY in your .env file
  • Simplest: model string prefix 'openrouter:<vendor>/<model>'
  • Explicit: OpenRouterModel + OpenRouterProvider
  • OpenRouter model ids are always vendor-prefixed (e.g. openai/gpt-5.2, anthropic/claude-sonnet-4.6) - see https://openrouter.ai/models

Example Setup:

from pydantic_ai import Agent
from pydantic_ai.models.openrouter import OpenRouterModel
from pydantic_ai.providers.openrouter import OpenRouterProvider

# Simplest (reads OPENROUTER_API_KEY from environment):
agent = Agent('openrouter:anthropic/claude-sonnet-4.6')

# Explicit:
model = OpenRouterModel(
    'openai/gpt-5.2',
    provider=OpenRouterProvider(api_key=os.getenv('OPENROUTER_API_KEY', '')),
)

OpenRouterModelSettings adds provider routing, reasoning effort (openrouter_reasoning={'effort': 'high'}), and usage accounting.

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)
  4. Instrument: logfire.instrument_pydantic_ai() - traces every agent run, model request, and tool call automatically

v2 note: the default instrumentation format is version 5; agent run spans report token usage under gen_ai.aggregated_usage.* (model request spans keep gen_ai.usage.*). Update dashboards that read usage from run spans.

Usage Pattern:

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 text with async for chunk in response.stream_text()
  • Stream partial structured output with response.stream_output() (v2 rename of v1's .stream(); .stream_structured() is now .stream_response())
  • Get final result with await response.get_output()
  • v2: agent.run_stream_events() is a context manager only (async with agent.run_stream_events(...) as events:)

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 & Usage:

  • 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
  • v2: result.usage is a property (NOT result.usage()), with input_tokens/output_tokens (renamed from request/response tokens)

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

Capabilities & Native Tools (NEW in v2):

  • Reference: references/13_capabilities_native_tools.py
  • Pass capabilities=[...] on the Agent; prefer provider-adaptive capabilities (Thinking(effort='high'), WebSearch(), WebFetch()) so code survives provider changes
  • v2 renamed "builtin tools" to native tools: wrap them as NativeTool(WebSearchTool(...)) when provider-specific config matters
  • WebSearch()/WebFetch() are native-only by default and RAISE on unsupported models; restore local fallback with WebSearch(local='duckduckgo') / WebFetch(local=True)
  • Bundle instructions + tools with Capability(id=..., description=...); add defer_loading=True for on-demand loading (progressive disclosure)
  • Subclass AbstractCapability for reusable bundles with model settings/hooks

Lifecycle Hooks (NEW in v2):

  • Reference: references/14_hooks.py
  • Create hooks = Hooks(), register with @hooks.on.before_model_request, @hooks.on.before_tool_execute(tools=['name']), etc., then pass capabilities=[hooks]
  • Use for logging, metrics, auditing, and light interception
  • Always return the (possibly modified) context/args from a hook

Toolsets & MCP (v2 API):

  • Reference: references/15_toolsets_mcp.py
  • Group reusable tools in a FunctionToolset; share across agents via Agent(toolsets=[...])
  • MCP servers: pydantic_ai.mcp.MCPToolset (replaces v1's MCPServerStdio/ MCPServerSSE/MCPServerStreamableHTTP); manage connections with async with agent: (replaces agent.run_mcp_servers())
  • Load config files with load_mcp_toolsets(...); provider-native MCP via the MCP(url=..., native=True) capability

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:

from pydantic_ai import Agent, ModelMessage

agent = Agent(model=model, instructions="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:

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:

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 messagesModelRequest with UserPromptPart
  • Assistant messagesModelResponse with TextPart
  • Timestamps must be timezone-aware: Use timezone.utc
  • Instructions are NOT in message_history: They're set on the Agent and injected automatically (legacy system_prompt text IS carried in history)

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:

# 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 capabilities/native tools (references/13_capabilities_native_tools.py) for web search, thinking, or reusable behavior bundles
  8. Adding hooks (references/14_hooks.py) for logging, auditing, and interception
  9. Adding toolsets or MCP servers (references/15_toolsets_mcp.py) for reusable/external tools
  10. 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.

10. v1 → v2 Migration Notes

Key breaking changes in Pydantic AI v2 (all examples in this skill are v2):

v1v2
result.usage()result.usage (property)
usage.request_tokens / response_tokensusage.input_tokens / output_tokens; UsageRunUsage
Agent('gpt-4o') (bare model name)Raises UserError - always provider-prefix: Agent('openai:gpt-5.2')
openai: prefix → Chat Completionsopenai: → Responses API; use openai-chat: for Chat Completions
OpenRouter via OpenAIProvider(base_url=...)OpenRouterModel / 'openrouter:' prefix
Agent(builtin_tools=[...])capabilities=[NativeTool(...)]; pydantic_ai.builtin_toolspydantic_ai.native_tools
Agent(prepare_tools=...), history_processors=, event_stream_handler=, instrument=capabilities=[PrepareTools(...)], ProcessHistory(...), ProcessEventStream(...), Instrumentation(...)
MCPServerStdio / MCPServerSSE / MCPServerStreamableHTTP; agent.run_mcp_servers()MCPToolset; async with agent:
stream.stream() / .stream_structured() / .get().stream_output() / .stream_response() / .response
for e in agent.run_stream_events(...)async with agent.run_stream_events(...) as events: (context manager only)
DeferredToolCalls / DeferredToolsetDeferredToolRequests / ExternalToolset
@toolset.tool on ctx-less functionRaises - use @toolset.tool_plain
Default end_strategy='early'Default 'graceful' - function tools alongside a successful output tool now RUN; set end_strategy='early' explicitly to skip them
Un-parameterized Agent infers Agent[None, str]Infers Agent[object, str] - update Agent[None, ...]/RunContext[None] annotations to object
ModelProfile dataclass (.update(), .from_profile())TypedDict - use merge_profile(), profile.get('field'), {**profile, 'field': v}
Instrumentation format ≤4, gen_ai.usage.* on run spansFormat 5 default; run spans use gen_ai.aggregated_usage.*
pip install pydantic-ai bundles all providersSlimmer default (openai/anthropic/google + minimal); add extras: pydantic-ai[bedrock,groq,...]

Full upgrade guide: https://ai.pydantic.dev/changelog/

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.