Lab — Build Your Agent
CAP-6640: Computational Understanding of Natural Language
Spencer Lyon
Prerequisites
Outcomes
Build a complete PydanticAI agent with 3+ tools, dependency injection, and
ModelRetryerror handlingAdd multi-turn conversation memory and verify context retention across turns
Evaluate agent behavior using
pydantic-evals— testing tool selection, output quality, and edge cases
References
Lab Overview¶
Today we put together everything from L12.01 and L12.02. You’ll build a complete data analysis agent from scratch — designing tools, wiring up dependency injection, adding memory, and evaluating the result with the pydantic-evals framework from Week 11.
The exercises are open-ended: we provide the dataset and infrastructure, you design the agent. There are many valid approaches — the goal is to practice the patterns, not to arrive at one “correct” implementation.
Setup¶
The shared infrastructure below gives you everything you need to get started. Run these cells first.
Model and Proxy¶
import os
import statistics
from dataclasses import dataclass, field
from datetime import date
from dotenv import load_dotenv
from pydantic_ai import Agent, ModelRetry, RunContext
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
load_dotenv()
PROXY_URL = "https://litellm.6640.ucf.spencerlyon.com"
def get_model(model_name: str) -> OpenAIChatModel:
"""Create a model connection through our LiteLLM proxy."""
return OpenAIChatModel(
model_name,
provider=OpenAIProvider(
base_url=PROXY_URL,
api_key=os.environ["CAP6640_API_KEY"],
),
)Dataset¶
We’ll use a richer dataset than L12.01 — revenue broken out by product and region, giving your agent more to work with:
# Revenue data: quarter -> product -> region -> monthly revenues (in $M)
COMPANY_DATA = {
"Q1": {
"Widget Pro": {"North": [1.2, 1.3, 1.1], "South": [0.8, 0.9, 0.7]},
"Widget Lite": {"North": [0.5, 0.6, 0.5], "South": [0.3, 0.3, 0.4]},
"Enterprise Suite": {"North": [2.0, 2.1, 2.2], "South": [1.5, 1.4, 1.6]},
},
"Q2": {
"Widget Pro": {"North": [1.4, 1.5, 1.3], "South": [0.9, 1.0, 0.8]},
"Widget Lite": {"North": [0.6, 0.7, 0.6], "South": [0.4, 0.4, 0.5]},
"Enterprise Suite": {"North": [2.3, 2.4, 2.5], "South": [1.7, 1.6, 1.8]},
},
"Q3": {
"Widget Pro": {"North": [1.5, 1.6, 1.7], "South": [1.0, 1.1, 0.9]},
"Widget Lite": {"North": [0.7, 0.8, 0.7], "South": [0.5, 0.5, 0.6]},
"Enterprise Suite": {"North": [2.5, 2.6, 2.8], "South": [1.8, 1.9, 2.0]},
},
"Q4": {
"Widget Pro": {"North": [1.8, 1.9, 2.0], "South": [1.2, 1.3, 1.1]},
"Widget Lite": {"North": [0.8, 0.9, 0.8], "South": [0.6, 0.6, 0.7]},
"Enterprise Suite": {"North": [3.0, 3.1, 3.3], "South": [2.1, 2.2, 2.4]},
},
}
PRODUCTS = list(COMPANY_DATA["Q1"].keys())
REGIONS = ["North", "South"]
QUARTERS = list(COMPANY_DATA.keys())
print(f"Products: {PRODUCTS}")
print(f"Regions: {REGIONS}")
print(f"Quarters: {QUARTERS}")Products: ['Widget Pro', 'Widget Lite', 'Enterprise Suite']
Regions: ['North', 'South']
Quarters: ['Q1', 'Q2', 'Q3', 'Q4']
Dependencies¶
A deps dataclass is provided. Feel free to extend it if your agent design needs additional fields.
@dataclass
class SalesDeps:
"""External state for the sales analysis agent."""
db: dict # the COMPANY_DATA dict
user_name: str = "Analyst"
available_quarters: list[str] = field(default_factory=lambda: list(QUARTERS))
available_products: list[str] = field(default_factory=lambda: list(PRODUCTS))
available_regions: list[str] = field(default_factory=lambda: list(REGIONS))
deps = SalesDeps(db=COMPANY_DATA)
print(f"Deps created for {deps.user_name}")
print(f" Quarters: {deps.available_quarters}")
print(f" Products: {deps.available_products}")
print(f" Regions: {deps.available_regions}")Deps created for Analyst
Quarters: ['Q1', 'Q2', 'Q3', 'Q4']
Products: ['Widget Pro', 'Widget Lite', 'Enterprise Suite']
Regions: ['North', 'South']
Helper Functions¶
A few utility functions to make working with the nested data easier. Your tools can use these internally:
def get_revenue(db: dict, quarter: str, product: str = None, region: str = None) -> list[float]:
"""Extract monthly revenue from the nested data structure.
Returns a flat list of monthly revenue values, filtered by product/region if specified.
"""
if quarter not in db:
return []
revenues = []
for prod, regions in db[quarter].items():
if product and prod != product:
continue
for reg, monthly in regions.items():
if region and reg != region:
continue
revenues.extend(monthly)
return revenues
def summarize_revenue(values: list[float]) -> str:
"""Format a revenue summary string."""
if not values:
return "No data found"
total = sum(values)
avg = statistics.mean(values)
return f"total=${total:.1f}M, avg=${avg:.2f}M, min=${min(values):.1f}M, max=${max(values):.1f}M"
# Quick test
q1_all = get_revenue(COMPANY_DATA, "Q1")
print(f"Q1 all revenue: {summarize_revenue(q1_all)}")
q1_widget_north = get_revenue(COMPANY_DATA, "Q1", product="Widget Pro", region="North")
print(f"Q1 Widget Pro (North): {summarize_revenue(q1_widget_north)}")Q1 all revenue: total=$19.4M, avg=$1.08M, min=$0.3M, max=$2.2M
Q1 Widget Pro (North): total=$3.6M, avg=$1.20M, min=$1.1M, max=$1.3M
Exercise 12.6: Build Your Multi-Tool Agent¶
Exercise 12.7: Add Memory and Multi-Turn Conversation¶
Exercise 12.8: Evaluate Your Agent¶
Wrap-Up¶
Key Takeaways¶
What’s Next¶
In Week 13, we’ll scale up from single agents to multi-agent systems:
Agent-as-tool delegation — one agent calling another agent inside a tool, sharing dependencies and usage tracking
Multi-agent communication patterns — hub-and-spoke, pipeline, debate/consensus
The agentic framework landscape — how PydanticAI compares to LangGraph, AutoGen, CrewAI (conceptual survey)
Orchestration patterns — building specialist agents that hand off to each other