🛠️ Hands-On Workshop

Build Your Own Agent

A step-by-step guide to creating an intelligent agent using PydanticAI

What We're Building

In this workshop, you'll build a research assistant agent that can:

Prerequisites

Python 3.10+, a Google Gemini API key (create new onw with free tier), and basic Python knowledge.

1

Environment Setup

~5 minutes

Create Project Directory

$ mkdir agent-workshop && cd agent-workshop
$ python -m venv venv
$ source venv/bin/activate # Windows: venv\Scripts\activate

Install Dependencies

$ pip install pydantic-ai python-dotenv httpx google-generativeai

Set Up API Key

Create a .env file in your project directory:

.env
GEMINI_API_KEY=your-gemini-api-key-here

🔑 Workshop API Key

Enter Google Studio AI Studio API Keys and create a free tier API key!

⚠️ Important

Never commit your .env file to version control. Add it to .gitignore.

Verify Installation

Create a test file to verify everything works:

test_setup.py
from dotenv import load_dotenv
from pydantic_ai import Agent

load_dotenv()

agent = Agent('gemini-2.5-flash')
result = agent.run_sync('Say "Hello, Workshop!" and nothing else.')
print(result.output)
$ python test_setup.py
Hello, Workshop!

✓ Checkpoint

If you see "Hello, Workshop!", your setup is complete!

2

Your First Agent

~10 minutes

Let's create a basic research assistant agent with a system prompt:

agent.py
from dotenv import load_dotenv
from pydantic_ai import Agent

load_dotenv()

# Create the agent with a system prompt
agent = Agent(
    'gemini-2.5-flash',
    system_prompt="""You are a helpful research assistant. 
You provide clear, accurate, and well-structured answers.
When you don't know something, you say so honestly.
Keep responses concise but informative."""
)

# Run a simple query
def main():
    result = agent.run_sync('What is the capital of France?')
    print("Answer:", result.output)
    
    # Try another query
    result = agent.run_sync('Explain photosynthesis in simple terms.')
    print("Answer:", result.output)

if __name__ == "__main__":
    main()

Understanding the Agent

Try Different Models

PydanticAI supports multiple providers:

🔵

Gemini (We're using this!)

gemini-2.5-flash (free tier)

🟢

OpenAI

openai:gpt-4o

🟠

Anthropic

anthropic:claude-3-5-sonnet

Ollama (Local)

ollama:llama3

3

Adding Tools

~15 minutes

Tools let your agent take actions beyond text generation. Let's add a calculator and a web search tool:

agent_with_tools.py
from dotenv import load_dotenv
from pydantic_ai import Agent, RunContext
import httpx
import math

load_dotenv()

agent = Agent(
    'gemini-2.5-flash',
    system_prompt="""You are a research assistant with access to tools.
Use the calculator for math. Use web search for current information.
Always cite your sources when using search results."""
)

@agent.tool
def calculate(ctx: RunContext[None], expression: str) -> str:
    """Evaluate a mathematical expression.
    
    Args:
        expression: A Python math expression like '2 + 2' or 'math.sqrt(16)'
    """
    try:
        # Safe eval with only math operations
        allowed = {"__builtins__": {}, "math": math}
        result = eval(expression, allowed)
        return f"Result: {result}"
    except Exception as e:
        return f"Error: {e}"

@agent.tool
async def search_web(ctx: RunContext[None], query: str) -> str:
    """Search the web for information.
    
    Args:
        query: The search query
    """
    # Using a free API for demo (DuckDuckGo Instant Answer)
    async with httpx.AsyncClient() as client:
        response = await client.get(
            "https://api.duckduckgo.com/",
            params={"q": query, "format": "json", "no_html": "1"}
        )
        data = response.json()
        
        if data.get("Abstract"):
            return f"{data['Abstract']} (Source: {data.get('AbstractSource', 'DuckDuckGo')})"
        elif data.get("RelatedTopics"):
            topics = data["RelatedTopics"][:3]
            results = [t.get("Text", "") for t in topics if "Text" in t]
            return "Related info: " + " | ".join(results)
        else:
            return "No results found."

# Test the agent
async def main():
    # Math question
    result = await agent.run('What is 15% of 847?')
    print("Math:", result.output)
    
    # Search question
    result = await agent.run('What is Python programming language?')
    print("Search:", result.output)

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

Key Concepts

When you call agent.run():

  1. The LLM receives your prompt + descriptions of available tools
  2. If it decides a tool would help, it outputs a structured "tool call"
  3. PydanticAI executes the tool and sends results back to the LLM
  4. The LLM incorporates results into its final response
4

Structured Output

~10 minutes

Instead of free-form text, you can make the agent return validated, typed data using Pydantic models:

structured_agent.py
from dotenv import load_dotenv
from pydantic_ai import Agent
from pydantic import BaseModel, Field
from typing import Literal

load_dotenv()

# Define the output structure
class ResearchResult(BaseModel):
    """Structured research output."""
    topic: str = Field(description="The main topic researched")
    summary: str = Field(description="A 2-3 sentence summary")
    key_points: list[str] = Field(description="3-5 key points")
    confidence: Literal["high", "medium", "low"] = Field(
        description="Confidence in the accuracy of the information"
    )
    sources_needed: bool = Field(
        description="Whether external sources should be consulted"
    )

# Create agent with structured output type
agent = Agent(
    'gemini-2.5-flash',
    output_type=ResearchResult,  # Specify the return type!
    system_prompt="You are a research assistant. Provide structured analysis."
)

def main():
    result = agent.run_sync('Explain the benefits of renewable energy')
    
    # result.output is now a ResearchResult instance!
    research = result.output
    
    print(f"Topic: {research.topic}")
    print(f"Summary: {research.summary}")
    print(f"Confidence: {research.confidence}")
    print("Key Points:")
    for point in research.key_points:
        print(f"  • {point}")

if __name__ == "__main__":
    main()

Why Structured Output?

💡 Pro Tip

Use Field(description=...) to help the LLM understand what each field should contain. Good descriptions lead to better outputs!

5

Conversation Memory

~10 minutes

By default, each run() call is independent. To maintain conversation context, pass message history:

agent_with_memory.py
from dotenv import load_dotenv
from pydantic_ai import Agent
from pydantic_ai.messages import ModelMessage

load_dotenv()

agent = Agent(
    'gemini-2.5-flash',
    system_prompt="You are a helpful assistant with memory of our conversation."
)

def chat():
    """Interactive chat with memory."""
    message_history: list[ModelMessage] = []
    
    print("Chat started! Type 'quit' to exit.\n")
    
    while True:
        user_input = input("You: ").strip()
        
        if user_input.lower() == 'quit':
            print("Goodbye!")
            break
        
        if not user_input:
            continue
        
        # Run with message history
        result = agent.run_sync(
            user_input,
            message_history=message_history  # Pass previous messages
        )
        
        print(f"Agent: {result.output}\n")
        
        # Update history with new messages
        message_history = result.all_messages()

if __name__ == "__main__":
    chat()

Example Conversation

Chat started! Type 'quit' to exit.

You: My name is Alice and I'm learning Python.
Agent: Nice to meet you, Alice! Python is a great choice...

You: What's my name?
Agent: Your name is Alice! You mentioned you're learning Python.

You: What am I learning?
Agent: You're learning Python programming!

How It Works

result.all_messages() returns all messages from the conversation (system, user, assistant, tool calls). Passing this to the next run gives the agent full context.

6

Challenges

Self-guided

Now it's your turn! Try these challenges to deepen your understanding:

🌟 Challenge 1: Weather Agent

Add a weather tool that fetches current weather for a city. Use a free API like wttr.in:

  • httpx.get(f"https://wttr.in/{city}?format=j1")

🌟 Challenge 2: Multi-Step Research

Create an agent that:

  • Searches for information on a topic
  • Summarizes findings in structured format
  • Suggests follow-up questions

🌟 Challenge 3: Code Execution Agent

Add a tool that safely executes Python code snippets in a sandbox (hint: use exec() with restricted globals).

⭐ Advanced: Multi-Agent System

Create two agents that work together:

  • A "researcher" that gathers information
  • A "writer" that formats the findings nicely
  • Coordinate them to produce a mini-report

Next Steps

🎉 Congratulations!

You've built an LLM agent with tools, structured outputs, and memory. The possibilities from here are endless!