What We're Building
In this workshop, you'll build a research assistant agent that can:
- Answer questions using its knowledge
- Search the web for real-time information
- Perform calculations
- Return structured, validated outputs
- Remember conversation context
Prerequisites
Python 3.10+, a Google Gemini API key (create new onw with free tier), and basic Python knowledge.
Environment Setup
~5 minutes
Create Project Directory
$ python -m venv venv
$ source venv/bin/activate # Windows: venv\Scripts\activate
Install Dependencies
Set Up API Key
Create a .env file in your project directory:
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:
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)
Hello, Workshop!
✓ Checkpoint
If you see "Hello, Workshop!", your setup is complete!
Your First Agent
~10 minutes
Let's create a basic research assistant agent with a system prompt:
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
Agent()- Creates an agent instance'gemini-2.5-flash'- The model to use (Gemini's fast free-tier model)system_prompt- Instructions that define the agent's behaviorrun_sync()- Runs the agent synchronously (there's alsorun()for async)result.output- The agent's response
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
Adding Tools
~15 minutes
Tools let your agent take actions beyond text generation. Let's add a calculator and a web search tool:
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
@agent.tool- Decorator to register a function as a toolRunContext- Provides context about the current run (useful for dependencies)- Docstrings matter! - The LLM reads the docstring to understand when/how to use the tool
- Tools can be sync or async
When you call agent.run():
- The LLM receives your prompt + descriptions of available tools
- If it decides a tool would help, it outputs a structured "tool call"
- PydanticAI executes the tool and sends results back to the LLM
- The LLM incorporates results into its final response
Structured Output
~10 minutes
Instead of free-form text, you can make the agent return validated, typed data using Pydantic models:
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?
- Type safety: Catch errors early with validated data
- Reliability: Guaranteed schema for downstream processing
- Integration: Easy to feed into APIs, databases, UIs
- Testing: Predictable output format enables proper testing
💡 Pro Tip
Use Field(description=...) to help the LLM understand what each field should contain. Good descriptions lead to better outputs!
Conversation Memory
~10 minutes
By default, each run() call is independent. To maintain conversation context, pass message history:
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
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.
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
- Explore the PydanticAI Documentation
- Try different LLM providers (Anthropic, Gemini, local models)
- Build a real project: personal assistant, code reviewer, data analyst
- Combine with RAG for document-grounded agents
🎉 Congratulations!
You've built an LLM agent with tools, structured outputs, and memory. The possibilities from here are endless!