mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-13 04:26:28 +03:00
Edits accepted and merged: - Framework comparison table: PydanticAI vs LangGraph vs using both together - Boundary conditions: when to choose each framework - Hybrid pattern reference: PydanticAI agent as LangGraph StateGraph node - Updated 'When to Load Which Reference' table with boundaries entry - Updated Directory Structure listing New reference file: references/hybrid-pydanticai-langgraph.md (7KB) Version bumped from 1.0.3 to 1.0.4. All 3 validation tasks passed with no regressions: - Val-1: Multi-agent delegation (pass) - Val-2: Hybrid PydanticAI+LangGraph pattern (pass, all 6 criteria) - Val-3: Streaming agent (pass) Signed-off-by: Magnus Hedemark <magnus919@pm.me>
11 KiB
11 KiB
Complete Worked Examples
Example 1: Bank Support Agent
Complete agent with dependencies, structured output, tools, and streaming.
from dataclasses import dataclass
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext
# --- Dependencies ---
@dataclass
class SupportDeps:
customer_id: int
db: dict # Mock database
# --- Output Type ---
class SupportResult(BaseModel):
support_advice: str = Field(description='Advice for the customer')
block_card: bool = Field(description='Whether to block card')
risk: int = Field(description='Risk level 0-10', ge=0, le=10)
# --- Agent ---
support_agent = Agent(
'openai:gpt-5.2',
deps_type=SupportDeps,
output_type=SupportResult,
system_prompt='You are a bank support agent. Be helpful and assess risk.',
)
@support_agent.tool
async def get_balance(ctx: RunContext[SupportDeps]) -> float:
"""Get the customer's current account balance."""
return ctx.deps.db.get('balance', 0.0)
@support_agent.tool
async def recent_transactions(ctx: RunContext[SupportDeps], limit: int = 5) -> list[str]:
"""Get recent transactions."""
return ctx.deps.db.get('transactions', [])[:limit]
# --- Run ---
async def main():
deps = SupportDeps(customer_id=123, db={
'balance': 1500.00,
'transactions': ['Amazon -$42.00', 'Paycheck +$2000.00'],
})
result = await support_agent.run('I lost my card!', deps=deps)
print(result.output.support_advice)
print(f"Block card: {result.output.block_card}, Risk: {result.output.risk}")
Example 2: Weather Agent with Streaming Events
Full streaming visibility into tool calls and responses.
from datetime import date
from pydantic_ai import (
Agent, RunContext,
PartStartEvent, PartDeltaEvent,
FunctionToolCallEvent, FunctionToolResultEvent,
FinalResultEvent, TextPartDelta, ToolCallPartDelta,
)
weather_agent = Agent(
'openai:gpt-5.2',
system_prompt='Providing weather forecasts.',
)
@weather_agent.tool
async def get_forecast(ctx: RunContext, location: str, forecast_date: date) -> str:
"""Get weather forecast for a location on a date."""
return f'The forecast in {location} on {forecast_date} is 24°C and sunny.'
async def main():
async with weather_agent.run_stream_events(
'What is the weather in Paris on Tuesday?'
) as events:
async for event in events:
if isinstance(event, FunctionToolCallEvent):
print(f"[Tool Call] {event.part.tool_name}({event.part.args})")
elif isinstance(event, FunctionToolResultEvent):
print(f"[Tool Result] {event.part.content}")
elif isinstance(event, PartStartEvent):
print(f"[Part Start] {type(event.part).__name__}")
elif isinstance(event, PartDeltaEvent):
if isinstance(event.delta, TextPartDelta):
print(f"[Text] {event.delta.content_delta}", end='')
elif isinstance(event, FinalResultEvent):
print(f"\n[Final Result Starting]")
Example 3: Vending Machine (PydanticGraph Stateful)
Complete stateful graph with user interaction.
from __future__ import annotations
from dataclasses import dataclass
from pydantic_graph import BaseNode, End, GraphBuilder, GraphRunContext, StepContext
PRODUCTS = {'water': 1.25, 'soda': 1.50, 'crisps': 1.75, 'chocolate': 2.00}
@dataclass
class VendingState:
balance: float = 0.0
product: str | None = None
@dataclass
class InsertCoin(BaseNode[VendingState]):
async def run(self, ctx: GraphRunContext[VendingState]) -> CoinInserted:
return CoinInserted(1.00) # Simulated coin insert
@dataclass
class CoinInserted(BaseNode[VendingState]):
amount: float
async def run(self, ctx: GraphRunContext[VendingState]) -> SelectProduct | Purchase:
ctx.state.balance += self.amount
if ctx.state.product:
return Purchase(ctx.state.product)
return SelectProduct()
@dataclass
class SelectProduct(BaseNode[VendingState]):
async def run(self, ctx: GraphRunContext[VendingState]) -> Purchase:
return Purchase('soda') # Simulated selection
@dataclass
class Purchase(BaseNode[VendingState, None, None]):
product: str
async def run(self, ctx: GraphRunContext[VendingState]) -> End | InsertCoin | SelectProduct:
price = PRODUCTS.get(self.product)
if not price:
print(f"No such product: {self.product}")
return SelectProduct()
ctx.state.product = self.product
if ctx.state.balance >= price:
ctx.state.balance -= price
print(f"Dispensing {self.product}. Change: ${ctx.state.balance:.2f}")
return End(None)
else:
short = price - ctx.state.balance
print(f"Need ${short:.2f} more for {self.product}")
return InsertCoin()
g = GraphBuilder(state_type=VendingState)
@g.step
async def start(ctx: StepContext[VendingState, None, None]) -> InsertCoin:
return InsertCoin()
g.add(g.node(InsertCoin), g.node(CoinInserted),
g.node(SelectProduct), g.node(Purchase),
g.edge_from(g.start_node).to(start))
async def main():
result = await g.build().run(state=VendingState())
Example 4: Parallel Processing Graph
Using GraphBuilder's map operation for parallel execution.
from dataclasses import dataclass
from pydantic_graph import GraphBuilder, StepContext, reduce_list_append
@dataclass
class State:
processed: int = 0
g = GraphBuilder(state_type=State, input_type=list[int], output_type=list[int])
@g.step
async def double(ctx: StepContext[State, None, int]) -> int:
ctx.state.processed += 1
return ctx.inputs * 2
collect = g.join(reduce_list_append, initial_factory=list[int])
g.add(
g.edge_from(g.start_node).map().to(double), # Parallel fan-out
g.edge_from(double).to(collect), # Gather results
g.edge_from(collect).to(g.end_node),
)
async def main():
state = State()
result = await g.build().run(state=state, inputs=[1, 2, 3, 4, 5])
print(result) # [2, 4, 6, 8, 10]
print(state.processed) # 5
Example 5: Multi-Agent Flight Booking
Programmatic hand-off between two agents with shared usage tracking.
from pydantic import BaseModel
from pydantic_ai import Agent, RunUsage
class Flight(BaseModel):
airline: str
flight_number: str
price: float
class Booking(BaseModel):
flight: Flight
seat: str
confirmed: bool
search_agent = Agent('openai:gpt-5.2', output_type=Flight,
instructions='Search for the best flight matching criteria.')
book_agent = Agent('openai:gpt-5.2', output_type=Booking,
instructions='Book the specified flight with the given seat preference.')
async def book_flight(origin: str, dest: str, date: str, seat: str) -> Booking:
usage = RunUsage()
flight_result = await search_agent.run(
f'Find a flight from {origin} to {dest} on {date}',
usage=usage,
)
booking_result = await book_agent.run(
f'Book flight {flight_result.output.flight_number}, seat {seat}',
usage=usage,
message_history=flight_result.new_messages(),
)
return booking_result.output
Example 7: Testing an Agent with TestModel
Complete pytest test suite showing module-level agent declaration with defer_model_check, TestModel injection via Agent.override, capture_run_messages, and ALLOW_MODEL_REQUESTS safety guard.
"""pytest test file for PydanticAI agent with tools and dependencies."""
from __future__ import annotations
import pytest
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext, capture_run_messages
from pydantic_ai import models
from pydantic_ai.models.test import TestModel
# Safety guard — no real LLM calls during testing
models.ALLOW_MODEL_REQUESTS = False
@dataclass
class WeatherService:
"""Dependency injected into the agent's tools."""
api_key: str = "test-key-abc"
base_url: str = "https://api.weather.example"
# Module-level agent — defer_model_check=True prevents import-time
# model resolution failure when no API credentials are configured.
weather_agent: Agent[WeatherService, str] = Agent(
"openai:gpt-5.2",
deps_type=WeatherService,
output_type=str,
instructions="You are a helpful weather assistant.",
defer_model_check=True,
)
@weather_agent.tool
async def get_forecast(
ctx: RunContext[WeatherService],
city: str,
units: str = "celsius",
) -> str:
"""Get the current weather forecast for a city.
Args:
city: The city name to get a forecast for.
units: Temperature units — 'celsius' or 'fahrenheit'.
"""
return f"24°{'C' if units == 'celsius' else 'F'} and sunny in {city}"
@pytest.fixture
def override_agent() -> None:
"""Replace the real model with TestModel for all tests."""
with weather_agent.override(model=TestModel()):
yield
class TestWeatherAgent:
"""Tests using TestModel — no LLM calls made."""
def test_sync_run_with_tool(self, override_agent: None) -> None:
result = weather_agent.run_sync(
"What is the weather in London?",
deps=WeatherService(),
)
assert isinstance(result.output, str)
assert len(result.output) > 0
@pytest.mark.asyncio
async def test_async_run_inspects_messages(self) -> None:
with weather_agent.override(model=TestModel()):
with capture_run_messages() as messages:
result = await weather_agent.run(
"What is the weather in Paris?",
deps=WeatherService(),
)
from pydantic_ai.messages import ModelRequest, ModelResponse
assert len(messages) > 0
assert isinstance(messages[0], ModelRequest)
assert len([m for m in messages if isinstance(m, ModelResponse)]) >= 1
assert isinstance(result.output, str)
@pytest.mark.asyncio
async def test_no_real_llm_requests_allowed(self) -> None:
assert models.ALLOW_MODEL_REQUESTS is False
with weather_agent.override(model=TestModel()):
result = await weather_agent.run(
"Test query — should never hit real LLM",
deps=WeatherService(),
)
assert isinstance(result.output, str)
Key patterns demonstrated:
defer_model_check=Trueon theAgentconstructor — required for module-level agents tested withTestModelAgent.override(model=TestModel())— injects a fake model that returns schema-conforming data without API callscapture_run_messages()context manager — captures allModelRequest/ModelResponsepairs for assertionmodels.ALLOW_MODEL_REQUESTS = False— global safety net preventing accidental real LLM calls- Fixture-based override pattern — reusable across tests via
@pytest.fixture - Requires
pytest-asynciofor@pytest.mark.asyncioasync test support