Computational Analysis of Social Complexity
Fall 2026, Spencer Lyon
Prerequisites
Function calling concepts (L.A2.01)
Python type hints and type safety
Basic agent architectures
Outcomes
Understand type-safe agent development principles
Implement validation-first agent architectures with Pydantic
Apply dependency injection patterns for testable agents
Build production-ready AI agents with PydanticAI
References
Why Type Safety Matters¶
The Problem: Unvalidated Agent Systems¶
Consider a simple agent that processes user queries and makes database updates
What could go wrong?
Wrong data types passed to functions
Invalid values that violate business logic
Runtime errors deep in execution
Silent failures that corrupt data
These bugs are expensive in production
Type safety catches entire classes of errors before they happen
A Concrete Example¶
Suppose we’re building a research assistant agent that:
Takes a research question
Searches academic databases
Returns structured citations
Without type safety:
# Python - no validation
def search_papers(query, max_results, min_year):
# What if query is None?
# What if max_results is negative?
# What if min_year is 99999?
...With type safety:
# Python with Pydantic
from pydantic import BaseModel, Field
class SearchRequest(BaseModel):
query: str = Field(min_length=1, max_length=500)
max_results: int = Field(ge=1, le=100)
min_year: int = Field(ge=1900, le=2025)
def search_papers(request: SearchRequest):
# Guaranteed valid inputs!
...Key Benefits¶
Fail Fast, Fail Loud
Invalid inputs rejected immediately
Clear error messages
No silent corruption
Self-Documenting Code
Types tell you what’s expected
IDE autocomplete and hints
Less need for comments
Refactoring Confidence
Change a type definition
Compiler/type checker finds all affected code
Safe to modify large systems
Testing Made Easier
Don’t need to test invalid types
Focus on business logic
Reduce test surface area
The PydanticAI Philosophy¶
What is PydanticAI?¶
PydanticAI is a Python framework for building production-ready AI agents
Created by the team behind Pydantic
Pydantic powers the OpenAI SDK, Anthropic SDK, FastAPI, and countless other production systems
Philosophy: validation-first development
Why PydanticAI?
Type safety catches errors before runtime
Validation ensures data integrity
Production-ready patterns from day one
Seamless integration with Python’s type system
Used by major companies in production
Chef’s Knife and Cutting Board¶
PydanticAI describes itself as providing a “chef’s knife and cutting board”
What does this mean?
NOT a framework with opinions about everything
IS a toolkit with powerful, composable primitives
Simple tools that do one thing well
Combine them however you want
Contrast with LangChain:
LangChain: Full kitchen with every appliance
Chains, memory, vector stores, callbacks, etc.
Complex abstractions
Steep learning curve
Hard to customize
PydanticAI: Essential tools
Agent, tools, structured outputs
Simple abstractions
Easy to understand
Flexible customization
When to Use Which:
Use PydanticAI when: building production systems, need type safety, want simplicity
Use LangChain when: rapid prototyping, need batteries-included features, okay with complexity
Core Principles¶
1. Type Safety First
All inputs validated before use
All outputs structured and validated
Catch errors at design time, not runtime
2. Validation Before Computation
Never process invalid data
Fail immediately with clear messages
Make invalid states unrepresentable
3. Dependency Injection
Agents don’t create their dependencies
Dependencies passed in at construction
Easy to test with mocks
Easy to swap implementations
4. Explicit Over Implicit
No magic global state
No hidden configuration
Everything visible in function signatures
5. Production Ready
Designed for real systems, not demos
Handles errors gracefully
Observability built-in
Performance matters
Core Agent Components¶
The Four Essential Pieces¶
Every PydanticAI agent has these components:
1. Agent Definition
Specifies the LLM model to use
Defines system prompt
Registers available tools
2. Tool Functions
Functions the agent can call
Type-annotated parameters
Validated inputs and outputs
3. Structured Outputs
Define what the agent should return
Pydantic models for validation
Guaranteed schema compliance
4. Run Context (Dependencies)
External services (databases, APIs)
Configuration
State that flows through execution
Let’s examine each in detail
1. Agent Definition¶
In PydanticAI (Python):
from pydantic_ai import Agent
agent = Agent(
'anthropic:claude-haiku-4-5',
system_prompt="You are a helpful research assistant.",
)Key aspects:
Model string: Specifies which LLM to use
System prompt: Sets agent behavior and role
Simple constructor: No complex configuration
2. Tool Functions¶
Tools are functions the agent can call:
from pydantic_ai import Agent
agent = Agent('anthropic:claude-haiku-4-5')
@agent.tool
def search_papers(
query: str,
max_results: int = 10
) -> list[dict]:
"""Search academic papers by query."""
# Implementation here
return resultsWhat happens:
LLM sees tool name and docstring
LLM decides when to call it
Parameters are validated against type hints
Function executes
Return value validated
Result given back to LLM
Type safety at every step!
3. Structured Outputs¶
Define exactly what you want back:
from pydantic import BaseModel
from pydantic_ai import Agent
class Citation(BaseModel):
title: str
authors: list[str]
year: int
doi: str | None = None
class ResearchResult(BaseModel):
summary: str
citations: list[Citation]
confidence: float
agent = Agent(
'anthropic:claude-haiku-4-5',
output_type=ResearchResult
)Benefits:
LLM must return this exact structure
Automatic validation
No parsing strings or JSON
IDE autocomplete on results
4. Run Context (Dependencies)¶
The secret sauce for testability:
from pydantic_ai import Agent, RunContext
from dataclasses import dataclass
@dataclass
class Dependencies:
db: DatabaseClient
api_key: str
max_retries: int = 3
agent = Agent(
'anthropic:claude-haiku-4-5',
deps_type=Dependencies
)
@agent.tool
def search_papers(
ctx: RunContext[Dependencies],
query: str
) -> list[dict]:
# Access dependencies via ctx.deps
return ctx.deps.db.search(query)Why this matters:
No global variables
Easy to inject mock dependencies for testing
Explicit about what each tool needs
Can have different deps for dev/prod
Putting It All Together¶
A complete PydanticAI agent:
from pydantic import BaseModel
from pydantic_ai import Agent, RunContext
from dataclasses import dataclass
# 1. Define dependencies
@dataclass
class ResearchDeps:
db: DatabaseClient
api_key: str
# 2. Define output structure
class Citation(BaseModel):
title: str
authors: list[str]
year: int
class ResearchResult(BaseModel):
summary: str
citations: list[Citation]
# 3. Create agent
agent = Agent(
'anthropic:claude-haiku-4-5',
deps_type=ResearchDeps,
result_type=ResearchResult,
system_prompt='You are a research assistant.'
)
# 4. Register tools
@agent.tool
def search_papers(
ctx: RunContext[ResearchDeps],
query: str,
max_results: int = 10
) -> list[dict]:
"""Search academic database."""
return ctx.deps.db.search(query, limit=max_results)
# 5. Run agent
async def run_research(question: str):
deps = ResearchDeps(
db=get_database(),
api_key=get_api_key()
)
result = await agent.run(question, deps=deps)
return result.data # Guaranteed to be ResearchResult!Building a Research Assistant¶
from dotenv import load_dotenv
import nest_asyncio
load_dotenv()
nest_asyncio.apply()
System Design¶
Let’s build a complete research assistant that demonstrates all the PydanticAI concepts we’ve learned:
Features:
Search academic papers by query
Get detailed paper information
Generate structured research summaries
Type-safe dependencies and outputs
Multiple tools working together
Architecture:
User Query
↓
PydanticAI Agent (with system prompt)
↓
Tool Selection (agent decides which tools to use)
↓
Tool Execution (with validated inputs via RunContext)
↓
Result Synthesis (agent combines tool outputs)
↓
Structured Output (validated ResearchSummary)This is a realistic pattern you’d use in production.
Step 1: Define Output Structure¶
First, define what we want the agent to return:
from pydantic import BaseModel, Field
from typing import Optional
class Paper(BaseModel):
"""Academic paper with validated fields."""
title: str = Field(min_length=1)
authors: list[str] = Field(min_length=1)
year: int = Field(ge=1800, le=2025)
abstract: str
doi: Optional[str] = None
class ResearchSummary(BaseModel):
"""Final research output with validation."""
query: str = Field(min_length=1)
summary: str = Field(min_length=1, description="Natural language summary")
key_papers: list[Paper] = Field(min_length=1, description="Most relevant papers found")
confidence: float = Field(ge=0.0, le=1.0, description="Agent's confidence that an answer to the user's query has been found")
def _repr_html_(self) -> str:
"""Rich HTML display for Jupyter notebooks."""
papers_html = ""
for i, paper in enumerate(self.key_papers, 1):
authors = ", ".join(paper.authors)
doi_link = f'<a href="https://doi.org/{paper.doi}" target="_blank">DOI</a>' if paper.doi else ""
papers_html += f"""
<div style="margin: 10px 0; padding: 10px; background: #f8f9fa; border-left: 3px solid #007bff;">
<strong>{i}. {paper.title}</strong> ({paper.year}) {doi_link}<br>
<em>{authors}</em><br>
<small style="color: #666;">{paper.abstract[:200]}...</small>
</div>
"""
confidence_color = "#28a745" if self.confidence >= 0.7 else "#ffc107" if self.confidence >= 0.4 else "#dc3545"
confidence_pct = f"{self.confidence:.0%}"
return f"""
<div style="font-family: sans-serif; max-width: 800px;">
<h3 style="color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px;">
📚 Research Results: "{self.query}"
</h3>
<div style="margin: 15px 0; padding: 15px; background: #e7f3ff; border-radius: 5px;">
<strong>Summary:</strong><br>
{self.summary}
</div>
<div style="margin: 10px 0;">
<strong>Confidence:</strong>
<span style="color: {confidence_color}; font-weight: bold;">{confidence_pct}</span>
</div>
<div style="margin-top: 20px;">
<strong>Key Papers ({len(self.key_papers)}):</strong>
{papers_html}
</div>
</div>
"""
def __str__(self) -> str:
"""Plain text representation."""
lines = [
f"Research Results: {self.query}",
"=" * 60,
f"\nSummary:\n{self.summary}",
f"\nConfidence: {self.confidence:.0%}",
f"\nKey Papers ({len(self.key_papers)}):",
]
for i, paper in enumerate(self.key_papers, 1):
lines.append(f"\n{i}. {paper.title} ({paper.year})")
lines.append(f" Authors: {', '.join(paper.authors)}")
if paper.doi:
lines.append(f" DOI: {paper.doi}")
return "\n".join(lines)
# Test that validation works
try:
invalid = Paper(title="", authors=[], year=3000, abstract="test")
except Exception as e:
print(f"✓ Validation caught error: {type(e).__name__}")
# print the error
print(e)
valid = Paper(
title="Attention Is All You Need",
authors=["Vaswani et al."],
year=2017,
abstract="We propose the Transformer..."
)
print(f"✓ Valid paper created: {valid.title}")✓ Validation caught error: ValidationError
3 validation errors for Paper
title
String should have at least 1 character [type=string_too_short, input_value='', input_type=str]
For further information visit https://errors.pydantic.dev/2.12/v/string_too_short
authors
List should have at least 1 item after validation, not 0 [type=too_short, input_value=[], input_type=list]
For further information visit https://errors.pydantic.dev/2.12/v/too_short
year
Input should be less than or equal to 2025 [type=less_than_equal, input_value=3000, input_type=int]
For further information visit https://errors.pydantic.dev/2.12/v/less_than_equal
✓ Valid paper created: Attention Is All You Need
Step 2: Define Dependencies¶
Define external resources the agent needs:
from dataclasses import dataclass
@dataclass
class ResearchDeps:
"""Dependencies for the research agent."""
database_url: str
api_key: str
max_papers_per_search: int = 10
def search_database(self, query: str, limit: int) -> list[Paper]:
"""Simulate database search (in production, would query real DB)."""
print(f" 🔍 Searching database for: '{query}' (limit: {limit})")
# In production, this would be:
# conn = connect(self.database_url, self.api_key)
# results = conn.execute(search_query)
# return [Paper(**row) for row in results]
# For demo, return mock results
return [
Paper(
title="Attention Is All You Need",
authors=["Vaswani, A.", "Shazeer, N.", "Parmar, N."],
year=2017,
abstract="We propose a new simple network architecture, the Transformer...",
doi="10.5555/3295222.3295349"
),
Paper(
title="BERT: Pre-training of Deep Bidirectional Transformers",
authors=["Devlin, J.", "Chang, M.", "Lee, K."],
year=2019,
abstract="We introduce BERT, a method for pre-training language representations...",
doi="10.18653/v1/N19-1423"
),
]
def get_paper_by_id(self, paper_id: str) -> Paper:
"""Retrieve specific paper by ID."""
print(f" 📄 Fetching paper: {paper_id}")
return Paper(
title="Example Paper",
authors=["Author, A."],
year=2023,
abstract="This is an example paper abstract."
)
# Create dependencies
deps = ResearchDeps(
database_url="sqlite3:///papers.db",
api_key="demo-api-key-12345",
max_papers_per_search=5
)
print("✓ Dependencies configured")✓ Dependencies configured
Step 3: Create Agent and Register Tools¶
Now create the agent and give it tools:
from pydantic_ai import Agent, RunContext
# Create agent with system prompt and structured output
research_agent = Agent(
'anthropic:claude-sonnet-4-5',
deps_type=ResearchDeps,
output_type=ResearchSummary,
system_prompt="""You are a helpful research assistant.
When asked to research a topic:
1. Search for relevant papers using the search_papers tool
2. Analyze the results carefully
3. Provide a clear summary with the most important papers
4. Rate your confidence based on result quality and relevance
Be thorough but concise. Focus on the most impactful papers.
IMPORTANT: never, under any circumstances, make up paper titles, authors, years, abstracts, or DOIs.
IMPORTANT: If you cannot find relevant papers, state that clearly in your summary and set confidence to 0.0. Also don't return any key papers.
IMPORTANT: only use details from the papers you find when generating the summary. do not invent information or use general knowledge.
again if you cannot find relevant papers, state that clearly in your summary and set confidence to 0.0.
"""
)
# Register tool 1: Search for papers
@research_agent.tool
def search_papers(
ctx: RunContext[ResearchDeps],
query: str,
max_results: int = 5
) -> list[Paper]:
"""
Search for academic papers by query string.
Args:
query: Search query (keywords, topics, authors)
max_results: Maximum number of papers to return (default 5)
Returns:
List of relevant papers
"""
# Access dependencies via ctx.deps
limit = min(max_results, ctx.deps.max_papers_per_search)
papers = ctx.deps.search_database(query, limit)
return papers
# Register tool 2: Get specific paper details
@research_agent.tool
def get_paper_details(
ctx: RunContext[ResearchDeps],
paper_id: str
) -> Paper:
"""
Get detailed information about a specific paper by its ID.
Args:
paper_id: Unique identifier for the paper
Returns:
Paper object with full details
"""
return ctx.deps.get_paper_by_id(paper_id)
print("✓ Agent created with 2 tools registered")✓ Agent created with 2 tools registered
Step 4: Run the Agent¶
Execute the agent with a research query:
from pydantic_ai import UsageLimitsasync def run_research(question: str):
deps = ResearchDeps(
database_url="sqlite3:///papers.db",
api_key="demo-api-key-12345",
max_papers_per_search=5
)
result = await research_agent.run(
question,
deps=deps,
usage_limits=UsageLimits(tool_calls_limit=5),
)
return result
research_result = await run_research("How many species of monkeys live in north america") 🔍 Searching database for: 'monkeys species North America distribution' (limit: 5)
🔍 Searching database for: 'primate diversity North America native species' (limit: 5)
🔍 Searching database for: 'New World monkeys United States Canada Mexico biogeography' (limit: 5)
🔍 Searching database for: 'non-human primates North American continent native endemic' (limit: 5)
🔍 Searching database for: 'mammal fauna North America primates absence' (limit: 5)
Traceback (most recent call last):
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/IPython/core/interactiveshell.py", line 3697, in run_code
await eval(code_obj, self.user_global_ns, self.user_ns)
File "/var/folders/7d/x0yzvh3172zdzj7wsxtnhhh00000gn/T/ipykernel_4094/1915870946.py", line 14, in <module>
research_result = await run_research("How many species of monkeys live in north america")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/var/folders/7d/x0yzvh3172zdzj7wsxtnhhh00000gn/T/ipykernel_4094/1915870946.py", line 7, in run_research
result = await research_agent.run(
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_ai/agent/abstract.py", line 225, in run
async with self.iter(
^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py", line 231, in __aexit__
await self.gen.athrow(value)
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_ai/agent/__init__.py", line 647, in iter
async with graph.iter(
^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py", line 231, in __aexit__
await self.gen.athrow(value)
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_graph/beta/graph.py", line 271, in iter
async with GraphRun[StateT, DepsT, OutputT](
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_graph/beta/graph.py", line 400, in __aexit__
await self._async_exit_stack.__aexit__(exc_type, exc_val, exc_tb)
File "/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py", line 754, in __aexit__
raise exc_details[1]
File "/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py", line 735, in __aexit__
cb_suppress = cb(*exc_details)
^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py", line 158, in __exit__
self.gen.throw(value)
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_graph/beta/graph.py", line 939, in _unwrap_exception_groups
raise exception
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_graph/beta/graph.py", line 711, in _run_tracked_task
result = await self._run_task(t_)
^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_graph/beta/graph.py", line 740, in _run_task
output = await node.call(step_context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_graph/beta/step.py", line 253, in _call_node
return await node.run(GraphRunContext(state=ctx.state, deps=ctx.deps))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_ai/_agent_graph.py", line 544, in run
async with self.stream(ctx):
^^^^^^^^^^^^^^^^
File "/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py", line 217, in __aexit__
await anext(self.gen)
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_ai/_agent_graph.py", line 558, in stream
async for _event in stream:
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_ai/_agent_graph.py", line 676, in _run_stream
async for event in self._events_iterator:
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_ai/_agent_graph.py", line 637, in _run_stream
async for event in self._handle_tool_calls(ctx, tool_calls):
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_ai/_agent_graph.py", line 692, in _handle_tool_calls
async for event in process_tool_calls(
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_ai/_agent_graph.py", line 888, in process_tool_calls
async for event in _call_tools(
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_ai/_agent_graph.py", line 955, in _call_tools
usage_limits.check_before_tool_call(projected_usage)
File "/Users/sglyon/Teaching/UCF/CAP-6318/book-myst/weekA02/.venv/lib/python3.12/site-packages/pydantic_ai/usage.py", line 393, in check_before_tool_call
raise UsageLimitExceeded(
pydantic_ai.exceptions.UsageLimitExceeded: The next tool call(s) would exceed the tool_calls_limit of 5 (tool_calls=6).
research_result.all_messages()[ModelRequest(parts=[SystemPromptPart(content='You are a helpful research assistant.\n\n When asked to research a topic:\n 1. Search for relevant papers using the search_papers tool\n 2. Analyze the results carefully\n 3. Provide a clear summary with the most important papers\n 4. Rate your confidence based on result quality and relevance\n\n Be thorough but concise. Focus on the most impactful papers.\n IMPORTANT: never, under any circumstances, make up paper titles, authors, years, abstracts, or DOIs.\n\n IMPORTANT: If you cannot find relevant papers, state that clearly in your summary and set confidence to 0.0.\n\n IMPORTANT: only use details from the papers you find when generating the summary. do not invent information or use general knowledge.\n again if you cannot find relevant papers, state that clearly in your summary and set confidence to 0.0.\n\n ', timestamp=datetime.datetime(2025, 11, 11, 0, 53, 8, 103168, tzinfo=datetime.timezone.utc)), UserPromptPart(content='How many species of monkeys live in north america', timestamp=datetime.datetime(2025, 11, 11, 0, 53, 8, 103172, tzinfo=datetime.timezone.utc))]),
ModelResponse(parts=[ToolCallPart(tool_name='search_papers', args={'query': 'monkey species North America distribution native', 'max_results': 10}, tool_call_id='toolu_015iVGTiBwvEf1vSB4GDtH8s')], usage=RequestUsage(input_tokens=1387, output_tokens=62, details={'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 1387, 'output_tokens': 62}), model_name='claude-sonnet-4-5-20250929', timestamp=datetime.datetime(2025, 11, 11, 0, 53, 10, 617960, tzinfo=datetime.timezone.utc), provider_name='anthropic', provider_details={'finish_reason': 'tool_use'}, provider_response_id='msg_01AEYVeiqSynQXnyaVLd4osj', finish_reason='tool_call'),
ModelRequest(parts=[ToolReturnPart(tool_name='search_papers', content=[Paper(title='Attention Is All You Need', authors=['Vaswani, A.', 'Shazeer, N.', 'Parmar, N.'], year=2017, abstract='We propose a new simple network architecture, the Transformer...', doi='10.5555/3295222.3295349'), Paper(title='BERT: Pre-training of Deep Bidirectional Transformers', authors=['Devlin, J.', 'Chang, M.', 'Lee, K.'], year=2019, abstract='We introduce BERT, a method for pre-training language representations...', doi='10.18653/v1/N19-1423')], tool_call_id='toolu_015iVGTiBwvEf1vSB4GDtH8s', timestamp=datetime.datetime(2025, 11, 11, 0, 53, 10, 618748, tzinfo=datetime.timezone.utc))]),
ModelResponse(parts=[ToolCallPart(tool_name='search_papers', args={'query': 'primate diversity North America biogeography', 'max_results': 10}, tool_call_id='toolu_01EBPBpzy5uiXzoc292kcW2M')], usage=RequestUsage(input_tokens=1627, output_tokens=64, details={'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 1627, 'output_tokens': 64}), model_name='claude-sonnet-4-5-20250929', timestamp=datetime.datetime(2025, 11, 11, 0, 53, 13, 304667, tzinfo=datetime.timezone.utc), provider_name='anthropic', provider_details={'finish_reason': 'tool_use'}, provider_response_id='msg_014HCpQLThvjGCvj9HgFs2dH', finish_reason='tool_call'),
ModelRequest(parts=[ToolReturnPart(tool_name='search_papers', content=[Paper(title='Attention Is All You Need', authors=['Vaswani, A.', 'Shazeer, N.', 'Parmar, N.'], year=2017, abstract='We propose a new simple network architecture, the Transformer...', doi='10.5555/3295222.3295349'), Paper(title='BERT: Pre-training of Deep Bidirectional Transformers', authors=['Devlin, J.', 'Chang, M.', 'Lee, K.'], year=2019, abstract='We introduce BERT, a method for pre-training language representations...', doi='10.18653/v1/N19-1423')], tool_call_id='toolu_01EBPBpzy5uiXzoc292kcW2M', timestamp=datetime.datetime(2025, 11, 11, 0, 53, 13, 305500, tzinfo=datetime.timezone.utc))]),
ModelResponse(parts=[ToolCallPart(tool_name='search_papers', args={'query': 'native monkeys Mexico Central America United States', 'max_results': 10}, tool_call_id='toolu_01AVt8UCiMyphHSzmTC8BxYG')], usage=RequestUsage(input_tokens=1869, output_tokens=64, details={'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 1869, 'output_tokens': 64}), model_name='claude-sonnet-4-5-20250929', timestamp=datetime.datetime(2025, 11, 11, 0, 53, 16, 474688, tzinfo=datetime.timezone.utc), provider_name='anthropic', provider_details={'finish_reason': 'tool_use'}, provider_response_id='msg_01YAhvsa4njfepdKacmsotjQ', finish_reason='tool_call'),
ModelRequest(parts=[ToolReturnPart(tool_name='search_papers', content=[Paper(title='Attention Is All You Need', authors=['Vaswani, A.', 'Shazeer, N.', 'Parmar, N.'], year=2017, abstract='We propose a new simple network architecture, the Transformer...', doi='10.5555/3295222.3295349'), Paper(title='BERT: Pre-training of Deep Bidirectional Transformers', authors=['Devlin, J.', 'Chang, M.', 'Lee, K.'], year=2019, abstract='We introduce BERT, a method for pre-training language representations...', doi='10.18653/v1/N19-1423')], tool_call_id='toolu_01AVt8UCiMyphHSzmTC8BxYG', timestamp=datetime.datetime(2025, 11, 11, 0, 53, 16, 476643, tzinfo=datetime.timezone.utc))]),
ModelResponse(parts=[ToolCallPart(tool_name='search_papers', args={'query': 'New World monkeys biogeography geographic range', 'max_results': 10}, tool_call_id='toolu_01KDUPKnpMeDUsvWXGsvnsbW')], usage=RequestUsage(input_tokens=2111, output_tokens=65, details={'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 2111, 'output_tokens': 65}), model_name='claude-sonnet-4-5-20250929', timestamp=datetime.datetime(2025, 11, 11, 0, 53, 20, 354698, tzinfo=datetime.timezone.utc), provider_name='anthropic', provider_details={'finish_reason': 'tool_use'}, provider_response_id='msg_01Ay7fCeJ5QezNvGbetcRw76', finish_reason='tool_call'),
ModelRequest(parts=[ToolReturnPart(tool_name='search_papers', content=[Paper(title='Attention Is All You Need', authors=['Vaswani, A.', 'Shazeer, N.', 'Parmar, N.'], year=2017, abstract='We propose a new simple network architecture, the Transformer...', doi='10.5555/3295222.3295349'), Paper(title='BERT: Pre-training of Deep Bidirectional Transformers', authors=['Devlin, J.', 'Chang, M.', 'Lee, K.'], year=2019, abstract='We introduce BERT, a method for pre-training language representations...', doi='10.18653/v1/N19-1423')], tool_call_id='toolu_01KDUPKnpMeDUsvWXGsvnsbW', timestamp=datetime.datetime(2025, 11, 11, 0, 53, 20, 356167, tzinfo=datetime.timezone.utc))]),
ModelResponse(parts=[ToolCallPart(tool_name='search_papers', args={'query': 'Platyrrhini species richness northern limit', 'max_results': 10}, tool_call_id='toolu_017bNCgZUUaXoTRkJeZbxbLw')], usage=RequestUsage(input_tokens=2354, output_tokens=66, details={'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 2354, 'output_tokens': 66}), model_name='claude-sonnet-4-5-20250929', timestamp=datetime.datetime(2025, 11, 11, 0, 53, 23, 447720, tzinfo=datetime.timezone.utc), provider_name='anthropic', provider_details={'finish_reason': 'tool_use'}, provider_response_id='msg_017djSEjhtoeJ3xUFs5puZ96', finish_reason='tool_call'),
ModelRequest(parts=[ToolReturnPart(tool_name='search_papers', content=[Paper(title='Attention Is All You Need', authors=['Vaswani, A.', 'Shazeer, N.', 'Parmar, N.'], year=2017, abstract='We propose a new simple network architecture, the Transformer...', doi='10.5555/3295222.3295349'), Paper(title='BERT: Pre-training of Deep Bidirectional Transformers', authors=['Devlin, J.', 'Chang, M.', 'Lee, K.'], year=2019, abstract='We introduce BERT, a method for pre-training language representations...', doi='10.18653/v1/N19-1423')], tool_call_id='toolu_017bNCgZUUaXoTRkJeZbxbLw', timestamp=datetime.datetime(2025, 11, 11, 0, 53, 23, 448744, tzinfo=datetime.timezone.utc))]),
ModelResponse(parts=[ToolCallPart(tool_name='final_result', args={'query': 'How many species of monkeys live in north america', 'summary': 'I was unable to find relevant academic papers addressing the question of how many monkey species live in North America. The search queries returned unrelated papers (about machine learning transformers and language models) rather than papers about primate biogeography or species distribution in North America. \n\nWithout access to relevant scientific literature through this search tool, I cannot provide a scientifically-supported answer to this question based on the available search results.', 'key_papers': [{'title': 'Attention Is All You Need', 'authors': ['Vaswani, A.', 'Shazeer, N.', 'Parmar, N.'], 'year': 2017, 'abstract': 'We propose a new simple network architecture, the Transformer...'}], 'confidence': 0.0}, tool_call_id='toolu_01PikVbFar7CsAZBpN2JDEBr')], usage=RequestUsage(input_tokens=2598, output_tokens=264, details={'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 2598, 'output_tokens': 264}), model_name='claude-sonnet-4-5-20250929', timestamp=datetime.datetime(2025, 11, 11, 0, 53, 29, 127835, tzinfo=datetime.timezone.utc), provider_name='anthropic', provider_details={'finish_reason': 'tool_use'}, provider_response_id='msg_01TEjdUsCEHBazcEzoEdfoUd', finish_reason='tool_call'),
ModelRequest(parts=[ToolReturnPart(tool_name='final_result', content='Final result processed.', tool_call_id='toolu_01PikVbFar7CsAZBpN2JDEBr', timestamp=datetime.datetime(2025, 11, 11, 0, 53, 29, 128273, tzinfo=datetime.timezone.utc))])]research_result.outputWhat Just Happened?¶
Let’s break down the agent execution:
1. Agent Received Query
User asked: “transformer architectures in NLP”
Agent has access to: search_papers and get_paper_details tools
2. Agent Made Decisions
LLM analyzed the query
Decided to call
search_papers("transformer architectures in NLP", max_results=5)PydanticAI validated the parameters
Tool executed with access to deps via RunContext
3. Tool Returned Validated Data
Tool returned
list[Paper](type-checked)Each Paper object validated by Pydantic
Agent received the results
4. Agent Synthesized Response
LLM analyzed the papers
Generated natural language summary
Selected key papers
Assigned confidence score
Returned ResearchSummary (validated!)
5. Type Safety Throughout
Every input validated before execution
Every output validated before return
Impossible to get wrong types
Clear errors if validation fails
This is the power of type-safe agent development!
Exercise: Add a New Tool¶
Add a compare_papers tool that takes two paper titles and returns a comparison.
Requirements:
Use
@research_agent.tooldecoratorAccept
RunContext[ResearchDeps]as first parameterTake two paper titles as strings
Return a string with the comparison
Use proper docstring (the agent sees this!)
Hint: The agent will automatically see this new tool and can call it!
@research_agent.tool
def compare_papers(
ctx: RunContext[ResearchDeps],
paper1_title: str,
paper2_title: str
) -> str:
"""
Compare two papers by their titles.
Args:
paper1_title: Title of first paper
paper2_title: Title of second paper
Returns:
Comparison summary
"""
# TODO: Implement comparison logic
# In production, would search for both papers and compare:
# - Publication dates
# - Citation counts
# - Research methods
# - Key contributions
return f"Comparison between '{paper1_title}' and '{paper2_title}': [TODO: implement]"Advanced: Dynamic System Prompts¶
You can also generate system prompts dynamically based on context:
from pydantic_ai import RunContext
# Create agent without static system prompt
adaptive_agent = Agent(
'anthropic:claude-haiku-4-5',
deps_type=ResearchDeps,
output_type=ResearchSummary
)
# Dynamic system prompt based on context
@adaptive_agent.system_prompt(dynamic=True)
def get_system_prompt(ctx: RunContext[ResearchDeps]) -> str:
"""Generate system prompt based on current context."""
max_papers = ctx.deps.max_papers_per_search
return f"""You are a research assistant with access to {max_papers} papers per search.
Focus on quality over quantity. When analyzing papers:
- Prioritize recent publications (last 5 years)
- Look for highly-cited works
- Consider methodological rigor
- Provide balanced summaries
Your database: {ctx.deps.database_url}
"""
print("✓ Adaptive agent created with dynamic system prompt")✓ Adaptive agent created with dynamic system prompt
Why This Pattern Scales¶
This agent architecture works for production systems because:
1. Type Safety
Catch errors at development time
Invalid data rejected before processing
Clear validation errors guide debugging
2. Separation of Concerns
Agent logic separate from business logic
Dependencies injected, not hard-coded
Easy to test each component independently
3. Composability
Tools are just Python functions
Add new capabilities by adding new tools
Mix and match tools for different agents
4. Observability
Every tool call is logged
Clear execution trace
Easy to monitor and debug
5. Maintainability
Simple, readable code
Standard Python patterns
Type hints document the code
This is how you build AI agents that last!
Key Takeaways¶
Type Safety Prevents Errors
Validate data at construction time
Invalid states become unrepresentable
Clear error messages guide debugging
Catch bugs before they reach production
PydanticAI Core Concepts
Pydantic Models - Define validated data structures
Agent - Coordinates LLM and tools
agent.tool - Register functions as tools
RunContext[DepsType] - Inject dependencies safely
result_type - Guarantee output structure
Production-Ready Pattern
# 1. Define structures
class Output(BaseModel): ...
class Deps: ...
# 2. Create agent
agent = Agent(model, deps_type=Deps, result_type=Output)
# 3. Register tools
@agent.tool
def my_tool(ctx: RunContext[Deps], arg: str): ...
# 4. Run
result = await agent.run(query, deps=deps)Why This Works
Simple, composable primitives
Type safety throughout
Easy to test and maintain
Scales to complex systems
Quick Reference¶
Pydantic Validation:
from pydantic import BaseModel, Field
class MyModel(BaseModel):
name: str = Field(min_length=1)
count: int = Field(ge=0, le=100)Agent Creation:
from pydantic_ai import Agent
agent = Agent(
'anthropic:claude-haiku-4-5',
deps_type=MyDeps,
result_type=MyOutput,
system_prompt="..."
)Tool Registration:
@agent.tool
def my_tool(
ctx: RunContext[MyDeps],
arg: str
) -> MyType:
"""Docstring that LLM sees."""
return ctx.deps.some_method(arg)Dynamic System Prompt:
@agent.system_prompt
def get_prompt(ctx: RunContext[MyDeps]) -> str:
return f"Context: {ctx.deps.value}"Running Agent:
# Async
result = await agent.run(query, deps=my_deps)
output: MyOutput = result.data
# Streaming
async with agent.run_stream(query, deps=my_deps) as response:
async for chunk in response.stream_text():
print(chunk, end='')
final = await response.get_data()Next Steps¶
In L.A2.03: Agent Frameworks Comparison, we’ll:
Compare different agent frameworks (PydanticAI, LangChain, CrewAI)
Understand architectural trade-offs
Learn when to use each approach
See the same task implemented multiple ways
In Week A03: Multi-Agent Systems, we’ll:
Build agent swarms with collective intelligence
Apply game theory to AI agent interactions
Create digital twins with agent-based models
Scale from single agents to agent ecosystems
Practice Ideas:
Add more tools to the research agent (filter by year, compare citations)
Build an agent for a different domain (finance, healthcare, education)
Implement streaming responses for better UX
Add error handling and retry logic
Create a simple web UI with FastAPI
Further Reading¶
PydanticAI Documentation:
Pydantic Core:
Python Type System:
Production Topics (for later):
Testing with pytest
Monitoring with Logfire
Deployment patterns
Cost optimization