Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Lab — Build Your Agent

University of Central Florida
Arete Capital Partners

CAP-6640: Computational Understanding of Natural Language

Spencer Lyon

Prerequisites

Outcomes

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: