PydanticAI Template¶
Production-ready starter for type-safe agents using PydanticAI.
Create Project¶
Project Structure¶
my-agent/
├── app.py # FastAgentic entry point
├── agents/
│ └── main.py # PydanticAI agent
├── tools/
│ ├── __init__.py
│ └── search.py # Example tools
├── models/
│ ├── inputs.py # Input models
│ └── outputs.py # Output models
├── deps/
│ └── __init__.py # Dependency injection
├── config/
│ └── settings.yaml
├── tests/
│ ├── test_agent.py
│ └── test_contracts.py
├── .env.example
├── Dockerfile
├── docker-compose.yml
├── k8s/
│ └── *.yaml
└── pyproject.toml
Core Files¶
app.py¶
"""FastAgentic application entry point."""
import os
from fastagentic import App
from fastagentic.protocols import enable_mcp, enable_a2a
from fastagentic.adapters.pydanticai import PydanticAIAdapter
from fastagentic.auth import configure_oidc
from fastagentic.telemetry import configure_otel
from agents.main import support_agent
from models.inputs import TicketInput
from models.outputs import TriageResult
from deps import create_deps
# Initialize app
app = App(
title="Support Triage Agent",
version="1.0.0",
description="AI-powered support ticket triage",
durable_store=os.getenv("DURABLE_STORE", "redis://localhost:6379"),
)
# Enable protocols
enable_mcp(app, tasks_enabled=True)
enable_a2a(app)
# Optional: Authentication
if os.getenv("OIDC_ISSUER"):
configure_oidc(
app,
issuer=os.getenv("OIDC_ISSUER"),
audience=os.getenv("OIDC_AUDIENCE", "support-triage"),
)
# Optional: Observability
if os.getenv("OTEL_ENDPOINT"):
configure_otel(
app,
service_name="support-triage",
endpoint=os.getenv("OTEL_ENDPOINT"),
)
# Register agent endpoint
@app.agent_endpoint(
path="/triage",
runnable=PydanticAIAdapter(
support_agent,
deps_factory=create_deps,
stream_tokens=True,
),
input_model=TicketInput,
output_model=TriageResult,
stream=True,
durable=True,
# Protocol exposure
mcp_tool="triage_ticket",
a2a_skill="support-triage",
)
async def triage_ticket(ticket: TicketInput) -> TriageResult:
"""Triage a support ticket."""
...
# Health check
@app.get("/health")
async def health():
return {"status": "healthy"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
agents/main.py¶
"""PydanticAI agent definition."""
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from models.inputs import TicketInput
from models.outputs import TriageResult
from deps import AgentDeps
# Initialize model
model = OpenAIModel("gpt-4o")
# Create agent
support_agent = Agent(
model=model,
result_type=TriageResult,
deps_type=AgentDeps,
system_prompt="""You are a support triage assistant.
Your job is to analyze support tickets and:
1. Determine the priority (low, medium, high, urgent)
2. Assign a category (billing, technical, account, feature_request, other)
3. Suggest the best team to handle the ticket
4. Provide a brief summary
Be concise and accurate. Use the available tools to look up customer information.""",
)
@support_agent.tool
async def get_customer_tier(ctx, customer_id: str) -> str:
"""Look up customer's subscription tier."""
# Access deps for database/API calls
customer = await ctx.deps.customer_service.get(customer_id)
return customer.tier if customer else "unknown"
@support_agent.tool
async def search_knowledge_base(ctx, query: str) -> list[str]:
"""Search the knowledge base for relevant articles."""
results = await ctx.deps.knowledge_base.search(query, limit=3)
return [r.title for r in results]
@support_agent.tool
async def get_recent_tickets(ctx, customer_id: str, limit: int = 5) -> list[dict]:
"""Get customer's recent support tickets."""
tickets = await ctx.deps.ticket_service.get_recent(customer_id, limit)
return [{"id": t.id, "subject": t.subject, "status": t.status} for t in tickets]
models/inputs.py¶
"""Input models for the agent."""
from pydantic import BaseModel, Field
class TicketInput(BaseModel):
"""Support ticket to triage."""
ticket_id: str = Field(description="Unique ticket identifier")
customer_id: str = Field(description="Customer identifier")
subject: str = Field(description="Ticket subject line")
description: str = Field(description="Full ticket description")
channel: str = Field(
default="web",
description="Source channel",
json_schema_extra={"enum": ["web", "email", "chat", "phone"]},
)
models/outputs.py¶
"""Output models for the agent."""
from pydantic import BaseModel, Field
from typing import Literal
class TriageResult(BaseModel):
"""Triage decision for a support ticket."""
priority: Literal["low", "medium", "high", "urgent"] = Field(
description="Ticket priority level"
)
category: Literal["billing", "technical", "account", "feature_request", "other"] = Field(
description="Ticket category"
)
team: str = Field(description="Suggested team to handle the ticket")
summary: str = Field(description="Brief summary of the issue")
suggested_response: str | None = Field(
default=None,
description="Optional suggested initial response",
)
escalation_needed: bool = Field(
default=False,
description="Whether immediate escalation is recommended",
)
deps/init.py¶
"""Dependency injection for the agent."""
from dataclasses import dataclass
from fastapi import Request
from services.customer import CustomerService
from services.knowledge import KnowledgeBaseService
from services.tickets import TicketService
@dataclass
class AgentDeps:
"""Dependencies available to the agent."""
customer_service: CustomerService
knowledge_base: KnowledgeBaseService
ticket_service: TicketService
user_id: str | None = None
def create_deps(request: Request) -> AgentDeps:
"""Create dependencies from request context."""
return AgentDeps(
customer_service=request.app.state.customer_service,
knowledge_base=request.app.state.knowledge_base,
ticket_service=request.app.state.ticket_service,
user_id=getattr(request.state, "user_id", None),
)
tests/test_agent.py¶
"""Agent tests."""
import pytest
from pydantic_ai.models.test import TestModel
from agents.main import support_agent
from models.inputs import TicketInput
from deps import AgentDeps
@pytest.fixture
def test_model():
"""Use test model for deterministic responses."""
return TestModel()
@pytest.fixture
def mock_deps():
"""Mock dependencies."""
return AgentDeps(
customer_service=MockCustomerService(),
knowledge_base=MockKnowledgeBase(),
ticket_service=MockTicketService(),
)
@pytest.mark.asyncio
async def test_triage_basic(test_model, mock_deps):
"""Test basic triage flow."""
with support_agent.override(model=test_model):
result = await support_agent.run(
"Triage this ticket",
deps=mock_deps,
)
assert result.data.priority in ["low", "medium", "high", "urgent"]
assert result.data.category is not None
@pytest.mark.asyncio
async def test_triage_urgent_detection(test_model, mock_deps):
"""Test urgent ticket detection."""
test_model.seed_response(
'{"priority": "urgent", "category": "technical", ...}'
)
with support_agent.override(model=test_model):
result = await support_agent.run(
"CRITICAL: Production is down!",
deps=mock_deps,
)
assert result.data.priority == "urgent"
assert result.data.escalation_needed is True
tests/test_contracts.py¶
"""Protocol contract tests."""
from fastagentic.testing import (
validate_mcp_schema,
validate_a2a_card,
assert_schema_parity,
)
from app import app
def test_mcp_schema():
"""MCP schema is valid."""
errors = validate_mcp_schema(app)
assert not errors, f"MCP schema errors: {errors}"
def test_a2a_card():
"""A2A Agent Card is valid."""
errors = validate_a2a_card(app)
assert not errors, f"A2A card errors: {errors}"
def test_schema_parity():
"""OpenAPI, MCP, and A2A schemas align."""
assert_schema_parity(app)
Configuration¶
.env.example¶
# LLM Provider
OPENAI_API_KEY=sk-...
# Durable Store
DURABLE_STORE=redis://localhost:6379
# Authentication (optional)
OIDC_ISSUER=https://auth.example.com
OIDC_AUDIENCE=support-triage
# Observability (optional)
OTEL_ENDPOINT=http://localhost:4318
# Application
LOG_LEVEL=INFO
pyproject.toml¶
[project]
name = "my-agent"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"fastagentic[pydanticai]>=0.2.0",
"pydantic-ai>=0.1.0",
"redis>=5.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
"httpx>=0.27.0",
]
[tool.pytest.ini_options]
asyncio_mode = "auto"
Running¶
Local Development¶
# Install
pip install -e ".[dev]"
# Configure
cp .env.example .env
# Edit .env with your API keys
# Run with reload
fastagentic run --reload
Docker¶
# Build and run
docker-compose up --build
# Or production image
docker build -t my-agent .
docker run -p 8000:8000 --env-file .env my-agent
Kubernetes¶
# Create secrets
kubectl create secret generic agent-secrets \
--from-literal=openai-api-key=$OPENAI_API_KEY
# Deploy
kubectl apply -f k8s/
Testing¶
# Run tests
pytest
# With coverage
pytest --cov=agents --cov=models
# Contract tests only
pytest tests/test_contracts.py
API Usage¶
REST¶
# Triage a ticket
curl -X POST http://localhost:8000/triage \
-H "Content-Type: application/json" \
-d '{
"ticket_id": "T-123",
"customer_id": "C-456",
"subject": "Cannot login",
"description": "Getting 401 error when trying to login..."
}'
Streaming¶
# Stream triage response
curl -N http://localhost:8000/triage/stream \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"ticket_id": "T-123", ...}'
MCP¶
A2A¶
Extending¶
Add a Tool¶
# In agents/main.py
@support_agent.tool
async def check_service_status(ctx, service: str) -> dict:
"""Check if a service is experiencing issues."""
status = await ctx.deps.status_page.get_service(service)
return {
"service": service,
"status": status.state,
"incidents": [i.title for i in status.active_incidents],
}
Add Validation¶
# In models/inputs.py
from pydantic import field_validator
class TicketInput(BaseModel):
...
@field_validator("description")
@classmethod
def description_not_empty(cls, v: str) -> str:
if len(v.strip()) < 10:
raise ValueError("Description must be at least 10 characters")
return v
Add Streaming Events¶
# In app.py
@app.agent_endpoint(
path="/triage",
runnable=PydanticAIAdapter(
support_agent,
stream_tokens=True,
include_tool_calls=True, # Stream tool invocations
include_cost=True, # Stream cost updates
),
...
)
Next Steps¶
- PydanticAI Adapter - Full adapter documentation
- Protocol Support - MCP and A2A details
- Operations Guide - Production deployment