Back to catalog

pydantic-ai-ecosystem

Skill from franklinbaldo/egregora

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • Updated: 2025-11-22

Download the latest version (v1.0.0) from this page.

SKILL.md

# Pydantic AI Ecosystem Skill

## Overview

This skill provides comprehensive guidance for working with the Pydantic AI ecosystem, a suite of Python libraries for building production-grade AI agents, workflows, and evaluations with type safety and observability.

## Ecosystem Components

### 1. **pydantic-ai** - GenAI Agent Framework
The core framework for building AI agents with type safety and observability.

**Official Documentation**: https://ai.pydantic.dev/
**GitHub**: https://github.com/pydantic/pydantic-ai
**PyPI**: https://pypi.org/project/pydantic-ai/

### 2. **pydantic-evals** - Evaluation Framework
Systematic testing and evaluation of AI systems from simple LLM calls to complex multi-agent applications.

**Official Documentation**: https://ai.pydantic.dev/evals/
**PyPI**: https://pypi.org/project/pydantic-evals/

### 3. **pydantic-graph** - Graph/State Machine Library
Define async graphs and state machines using Python type hints for complex workflows.

**Official Documentation**: https://ai.pydantic.dev/graph/
**PyPI**: https://pypi.org/project/pydantic-graph/

### 4. **fasta2a** - Agent-to-Agent (A2A) Protocol
Framework-agnostic implementation of the A2A protocol for agent interoperability.

**Official Documentation**: https://ai.pydantic.dev/api/fasta2a/
**GitHub**: https://github.com/pydantic/fasta2a
**A2A Protocol**: https://ai.pydantic.dev/a2a/

---

## Installation

### Full Installation (All Components)
```bash
# Using pip
pip install pydantic-ai pydantic-evals pydantic-graph

# Using uv (recommended for this workspace)
uv add pydantic-ai pydantic-evals pydantic-graph

# With Logfire observability
uv add 'pydantic-ai[logfire]' 'pydantic-evals[logfire]'
```

### Minimal Installation
```bash
# Just pydantic-ai (includes pydantic-graph as dependency)
uv add pydantic-ai

# Add evals separately if needed
uv add pydantic-evals
```

---

## 1. Pydantic AI - Agent Framework

### Key Features
- **Model-agnostic**: Supports OpenAI, Anthropic, Gemini, Mistral, Ollama, and 20+ providers
- **Type-safe**: Full IDE autocomplete and type checking
- **Observability**: Integrates with Pydantic Logfire for debugging and monitoring
- **Tools**: Register Python functions as agent tools
- **Structured outputs**: Validate responses with Pydantic models
- **Streaming**: Stream responses token-by-token
- **Dependency injection**: Pass context and dependencies to agents

### Quick Start

#### Basic Agent
```python
from pydantic_ai import Agent

# Create agent with model and instructions
agent = Agent(
    'anthropic:claude-sonnet-4-0',  # or 'openai:gpt-4', 'gemini-1.5-pro', etc.
    instructions='Be concise, reply with one sentence.'
)

# Run synchronously
result = agent.run_sync('Where does "hello world" come from?')
print(result.output)

# Run asynchronously
result = await agent.run('Explain quantum computing')
print(result.output)
```

#### Agent with Tools
```python
from pydantic_ai import Agent, RunContext
import httpx

# Define agent with dependency type
agent = Agent(
    'openai:gpt-4',
    instructions='Use the weather tool to answer questions about weather.',
    deps_type=httpx.AsyncClient
)

@agent.tool
async def get_weather(ctx: RunContext[httpx.AsyncClient], city: str) -> dict:
    """Get current weather for a city."""
    response = await ctx.deps.get(f'https://api.weather.com/{city}')
    return response.json()

# Use agent with dependencies
async with httpx.AsyncClient() as client:
    result = await agent.run('What is the weather in Paris?', deps=client)
    print(result.output)
```

#### Structured Output
```python
from pydantic import BaseModel
from pydantic_ai import Agent

class CityInfo(BaseModel):
    name: str
    country: str
    population: int
    famous_for: list[str]

agent = Agent(
    'gemini-1.5-pro',
    result_type=CityInfo,
    instructions='Extract structured city information.'
)

result = agent.run_sync('Tell me about Tokyo')
city: CityInfo = result.output
print(f"{city.name}, {city.country} - Population: {city.population:,}")
```

#### Streaming Responses
```python
from pydantic_ai import Agent

agent = Agent('anthropic:claude-sonnet-4-0')

async with agent.run_stream('Write a haiku about coding') as response:
    async for chunk in response.stream_text():
        print(chunk, end='', flush=True)
```

### Model Configuration
```python
from pydantic_ai import Agent
from pydantic_ai.models import ModelSettings

agent = Agent(
    'openai:gpt-4',
    model_settings=ModelSettings(
        temperature=0.7,
        max_tokens=1000,
        top_p=0.9
    )
)
```

### Environment Variables for API Keys
```bash
# OpenAI
export OPENAI_API_KEY="sk-..."

# Anthropic
export ANTHROPIC_API_KEY="sk-ant-..."

# Google Gemini
export GOOGLE_API_KEY="..."
export GEMINI_API_KEY="..."  # Alternative

# Set in .envrc for this workspace
```

---

## 2. Pydantic Evals - Evaluation Framework

### Key Features
- **Code-first**: Define evaluations entirely in Python
- **Built-in evaluators**: Exact match, type checking, custom logic
- **LLM as a judge**: Use LLMs to evaluate subjective qualities
- **Span-based evaluation**: Analyze internal agent behavior via traces
- **Logfire integration**: Visualize results and track experiments

### Core Concepts
- **Case**: Single test scenario with inputs and expected outputs
- **Dataset**: Collection of test cases
- **Evaluator**: Function that scores task results
- **Experiment**: Running cases against a task

### Quick Start

#### Basic Evaluation
```python
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import IsInstance, ExactMatch

# Define test cases
cases = [
    Case(
        name='capital_france',
        inputs='What is the capital of France?',
        expected_output='Paris'
    ),
    Case(
        name='capital_japan',
        inputs='What is the capital of Japan?',
        expected_output='Tokyo'
    ),
]

# Create dataset with evaluators
dataset = Dataset(
    cases=cases,
    evaluators=[
        IsInstance(type_name='str'),
        ExactMatch()
    ]
)

# Define task to evaluate
async def guess_capital(question: str) -> str:
    # Your LLM call or agent here
    if 'France' in question:
        return 'Paris'
    elif 'Japan' in question:
        return 'Tokyo'
    return 'Unknown'

# Run evaluation
report = await dataset.evaluate(guess_capital)
report.print()
```

#### Evaluating Pydantic AI Agents
```python
from pydantic_ai import Agent
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import LLMJudge

# Create agent
agent = Agent(
    'anthropic:claude-sonnet-4-0',
    instructions='Answer geography questions concisely.'
)

# Define cases
cases = [
    Case(
        name='geography_1',
        inputs='What is the capital of Germany?',
        expected_output='Berlin'
    ),
    Case(
        name='geography_2',
        inputs='What is the largest ocean?',
        expected_output='Pacific Ocean'
    ),
]

# Create dataset with LLM judge
dataset = Dataset(
    cases=cases,
    evaluators=[
        LLMJudge(
            model='openai:gpt-4',
            prompt='Does the answer correctly match the expected output? Consider semantic equivalence.'
        )
    ]
)

# Task wrapper for agent
async def run_agent(question: str) -> str:
    result = await agent.run(question)
    return result.output

# Evaluate
report = await dataset.evaluate(run_agent)
report.print()
```

#### Custom Evaluators
```python
from pydantic_evals import Evaluator, EvalResult

class ContainsKeyword(Evaluator):
    def __init__(self, keyword: str):
        self.keyword = keyword

    async def evaluate(self, case, result, **kwargs) -> EvalResult:
        score = 1.0 if self.keyword.lower() in result.output.lower() else 0.0
        return EvalResult(
            score=score,
            reason=f"Keyword '{self.keyword}' {'found' if score == 1.0 else 'not found'}"
        )

# Use in dataset
dataset = Dataset(
    cases=[...],
    evaluators=[ContainsKeyword('climate')]
)
```

#### Logfire Integration
```bash
# Install with Logfire support
uv add 'pydantic-evals[logfire]'
```

```python
import logfire
from pydantic_evals import Dataset

# Configure Logfire
logfire.configure()

# Run evaluations - automatically logs to Logfire
report = await dataset.evaluate(task, logfire=True)

# View results at https://logfire.pydantic.dev
```

---

## 3. Pydantic Graph - State Machine Library

### Key Features
- **Type-safe graphs**: Define nodes and edges with type hints
- **Async execution**: Built for async/await workflows
- **State persistence**: Snapshot and resume graph execution
- **Dependency injection**: Pass external dependencies to nodes
- **Visual diagrams**: Generate Mermaid diagrams automatically

### Core Concepts
- **Node**: Dataclass with a `run` method
- **Graph**: Collection of connected nodes
- **GraphRunContext**: Holds state and dependencies
- **End**: Signal to terminate graph execution

### Quick Start

#### Simple Graph
```python
from dataclasses import dataclass
from pydantic_graph import BaseNode, End, Graph

@dataclass
class StartNode(BaseNode):
    async def run(self, ctx) -> 'ProcessNode':
        print("Starting workflow")
        return ProcessNode()

@dataclass
class ProcessNode(BaseNode):
    async def run(self, ctx) -> 'EndNode':
        print("Processing data")
        return EndNode()

@dataclass
class EndNode(BaseNode):
    async def run(self, ctx) -> End:
        print("Workflow complete")
        return End()

# Create and run graph
graph = Graph(nodes=[StartNode, ProcessNode, EndNode])
result = await graph.run(StartNode())
```

#### Conditional Routing
```python
from dataclasses import dataclass
from pydantic_graph import BaseNode, End, Graph

@dataclass
class CheckCondition(BaseNode):
    value: int

    async def run(self, ctx) -> 'PathA | PathB':
        if self.value > 10:
            return PathA()
        return PathB()

@dataclass
class PathA(BaseNode):
    async def run(self, ctx) -> End:
        print("Taking path A")
        return End()

@dataclass
class PathB(BaseNode):
    async def run(self, ctx) -> End:
        print("Taking path B")
        return End()

# Run with different inputs
graph = Graph(nodes=[CheckCondition, PathA, PathB])
await graph.run(CheckCondition(value=15))  # Takes PathA
await graph.run(CheckCondition(value=5))   # Takes PathB
```

#### Graph with State
```python
from dataclasses import dataclass, field
from pydantic_graph import BaseNode, End, Graph, GraphRunContext

@dataclass
class WorkflowState:
    count: int = 0
    messages: list[str] = field(default_factory=list)

@dataclass
class IncrementNode(BaseNode):
    async def run(self, ctx: GraphRunContext[WorkflowState]) -> 'CheckNode':
        ctx.state.count += 1
        ctx.state.messages.append(f"Count is now {ctx.state.count}")
        return CheckNode()

@dataclass
class CheckNode(BaseNode):
    async def run(self, ctx: GraphRunContext[WorkflowState]) -> 'IncrementNode | End':
        if ctx.state.count < 3:
            return IncrementNode()
        return End()

# Run with state
graph = Graph(nodes=[IncrementNode, CheckNode])
state = WorkflowState()
result = await graph.run(IncrementNode(), state=state)
print(state.messages)  # ['Count is now 1', 'Count is now 2', 'Count is now 3']
```

#### Generating Graph Diagrams
```python
from pydantic_graph import Graph

graph = Graph(nodes=[StartNode, ProcessNode, EndNode])

# Generate Mermaid diagram
mermaid = graph.mermaid()
print(mermaid)

# Output can be visualized at https://mermaid.live
```

---

## 4. fasta2a - Agent-to-Agent (A2A) Protocol

### Overview
The Agent-to-Agent (A2A) Protocol is an open standard by Google that enables communication and interoperability between AI agents, regardless of framework or vendor. **fasta2a** is Pydantic's framework-agnostic implementation of this protocol in Python.

### Key Features
- **Agent Interoperability**: Standardized communication between agents from different frameworks
- **Context Management**: Maintains conversation continuity across agent interactions
- **Task Management**: Track task states (submitted, working, completed, failed)
- **Protocol Compliance**: Implements A2A protocol version 0.2.5+
- **Easy Integration**: One-line conversion of Pydantic AI agents to A2A servers

### Quick Start

#### Convert Agent to A2A Server
```python
from pydantic_ai import Agent

# Create regular Pydantic AI agent
agent = Agent(
    'openai:gpt-4',
    instructions='Be helpful and concise.'
)

# Convert to A2A server (FastAPI app)
app = agent.to_a2a()

# Run with: uvicorn my_agent:app --host 0.0.0.0 --port 8000
```

#### A2A Protocol Communication
```python
import httpx

# Client communicating with A2A server
async with httpx.AsyncClient() as client:
    # Start new conversation (no context_id)
    response = await client.post(
        'http://localhost:8000/agent',
        json={
            'message': 'Hello, agent!',
            'context_id': None  # New conversation
        }
    )

    context_id = response.json()['context_id']

    # Continue conversation (with context_id)
    response = await client.post(
        'http://localhost:8000/agent',
        json={
            'message': 'Follow up question',
            'context_id': context_id  # Continue conversation
        }
    )
```

#### Agent Card (Describe Capabilities)
```python
from pydantic_ai import Agent
from fasta2a import AgentCard, Skill

agent = Agent('google-gla:gemini-flash-latest')

# Define agent capabilities
card = AgentCard(
    name="My Research Agent",
    description="An agent that helps with research tasks",
    skills=[
        Skill(
            name="web_search",
            description="Search the web for information"
        ),
        Skill(
            name="summarize",
            description="Summarize long documents"
        )
    ]
)

app = agent.to_a2a(agent_card=card)
```

### Task Management

#### Submit and Track Tasks
```python
from fasta2a import Task, TaskStatus

# Task is submitted to agent
task = Task(
    id="task-123",
    status=TaskStatus.SUBMITTED,
    input="Write a blog post about AI",
    artifacts=[]
)

# Agent updates task status
task.status = TaskStatus.WORKING

# Agent completes task with artifacts
task.status = TaskStatus.COMPLETED
task.artifacts = [
    {
        "type": "text/markdown",
        "content": "# AI Blog Post\n\n..."
    }
]
```

### Storage Architecture

#### Context Storage
Stores conversation context in format optimized for agent implementation:
```python
from fasta2a import ContextStorage

# Context includes conversation history
context = {
    "context_id": "ctx-123",
    "messages": [
        {"role": "user", "content": "Hello"},
        {"role": "assistant", "content": "Hi there!"}
    ]
}
```

#### Task Storage
Stores tasks in A2A protocol format:
```python
from fasta2a import TaskStorage

# Task with full history
task_data = {
    "task_id": "task-123",
    "status": "completed",
    "artifacts": [...],
    "history": [...]
}
```

### When to Use A2A Protocol

Use **fasta2a** when:
- Building multi-agent systems with agents from different frameworks
- Creating agent marketplaces or registries
- Need standardized agent-to-agent communication
- Want to expose agents as interoperable services
- Building agent orchestration platforms

**Note**: A2A protocol is for agent-to-agent communication, not for building general web APIs. For REST APIs with Pydantic AI agents, use FastAPI directly (see examples in Common Patterns section).

---

## Common Patterns

### 1. Agent with Evals
```python
from pydantic_ai import Agent
from pydantic_evals import Case, Dataset
from pydantic_evals.evaluators import LLMJudge

# Create agent
agent = Agent('anthropic:claude-sonnet-4-0')

# Create evaluation dataset
dataset = Dataset(
    cases=[
        Case(name='test1', inputs='Question 1', expected_output='Answer 1'),
        Case(name='test2', inputs='Question 2', expected_output='Answer 2'),
    ],
    evaluators=[LLMJudge(model='openai:gpt-4')]
)

# Evaluate agent
async def run_agent(question: str) -> str:
    result = await agent.run(question)
    return result.output

report = await dataset.evaluate(run_agent)
report.print()
```

### 2. Graph-based Agent Workflow
```python
from dataclasses import dataclass
from pydantic_ai import Agent
from pydantic_graph import BaseNode, End, Graph, GraphRunContext

@dataclass
class AgentState:
    user_query: str
    analysis: str = ""
    response: str = ""

agent = Agent('openai:gpt-4')

@dataclass
class AnalyzeNode(BaseNode):
    async def run(self, ctx: GraphRunContext[AgentState]) -> 'GenerateNode':
        result = await agent.run(f"Analyze this query: {ctx.state.user_query}")
        ctx.state.analysis = result.output
        return GenerateNode()

@dataclass
class GenerateNode(BaseNode):
    async def run(self, ctx: GraphRunContext[AgentState]) -> End:
        result = await agent.run(
            f"Based on this analysis: {ctx.state.analysis}, generate a response"
        )
        ctx.state.response = result.output
        return End()

# Run graph
graph = Graph(nodes=[AnalyzeNode, GenerateNode])
state = AgentState(user_query="How do I build an AI agent?")
await graph.run(AnalyzeNode(), state=state)
print(state.response)
```

### 3. FastAPI + Agent + Evals
```python
from fastapi import FastAPI
from pydantic import BaseModel
from pydantic_ai import Agent
from pydantic_evals import Case, Dataset

app = FastAPI()
agent = Agent('gemini-1.5-pro')

# Store evaluation results
evals_dataset = Dataset(cases=[], evaluators=[])

class Query(BaseModel):
    question: str

@app.post("/ask")
async def ask(query: Query):
    result = await agent.run(query.question)

    # Log for evaluation
    evals_dataset.cases.append(
        Case(name=f"query_{len(evals_dataset.cases)}", inputs=query.question)
    )

    return {"answer": result.output}

@app.post("/evaluate")
async def evaluate():
    async def run_agent(q: str) -> str:
        r = await agent.run(q)
        return r.output

    report = await evals_dataset.evaluate(run_agent)
    return report.to_dict()
```

---

## Best Practices

### 1. Type Safety
- Always specify `deps_type` for agents with dependencies
- Use Pydantic models for structured outputs
- Leverage type hints for graph nodes

### 2. Error Handling
```python
from pydantic_ai import Agent, ModelRetry

agent = Agent('openai:gpt-4')

@agent.tool
async def fetch_data(ctx, url: str) -> str:
    try:
        # Fetch data
        return data
    except Exception as e:
        # Retry with context
        raise ModelRetry(f"Failed to fetch: {e}")

result = await agent.run('Get data from https://example.com')
```

### 3. Observability with Logfire
```python
import logfire
from pydantic_ai import Agent

logfire.configure()

agent = Agent('anthropic:claude-sonnet-4-0')

# Automatically traced
with logfire.span('user_query'):
    result = await agent.run('Question')
```

### 4. Testing
```python
import pytest
from pydantic_ai import Agent
from pydantic_evals import Case, Dataset

@pytest.fixture
def agent():
    return Agent('openai:gpt-4')

@pytest.fixture
def eval_dataset():
    return Dataset(cases=[
        Case(name='test1', inputs='Q1', expected_output='A1'),
    ])

@pytest.mark.asyncio
async def test_agent(agent, eval_dataset):
    async def run(q: str) -> str:
        r = await agent.run(q)
        return r.output

    report = await eval_dataset.evaluate(run)
    assert report.average_score() > 0.8
```

### 5. Cost Management
```python
from pydantic_ai import Agent, RunContext

agent = Agent('openai:gpt-4')

result = await agent.run('Question')

# Access usage info
print(f"Tokens: {result.usage().total_tokens}")
print(f"Cost estimate: ${result.cost()}")
```

---

## Environment Setup for This Workspace

### 1. Install Dependencies
```bash
cd /home/frank/workspace
uv add pydantic-ai pydantic-evals pydantic-graph fastapi uvicorn
```

### 2. Configure API Keys in `.envrc`
```ba