├── .env.example ├── .gitignore ├── LICENCE ├── README.md ├── data ├── invoice_query.json ├── product_query.json └── shipping_query.json ├── requirements.txt └── src ├── introduction.py └── utils └── markdown.py /.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables go here, can be read by `python-dotenv` package: 2 | # 3 | # ---------------------------------------------------------------- 4 | # from dotenv import load_dotenv 5 | # 6 | # load_dotenv() 7 | # API_KEY = os.getenv("API_KEY") 8 | # ---------------------------------------------------------------- 9 | # 10 | # DO NOT ADD THIS FILE TO VERSION CONTROL! 11 | 12 | 13 | OPENAI_API_KEY=your-api-key -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *.cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # DotEnv configuration 60 | .env 61 | 62 | # Database 63 | *.db 64 | *.rdb 65 | 66 | # Pycharm 67 | .idea 68 | 69 | # VS Code 70 | .vscode/ 71 | *.code-workspace 72 | 73 | # Spyder 74 | .spyproject/ 75 | 76 | # Jupyter NB Checkpoints 77 | .ipynb_checkpoints/ 78 | 79 | # Mac OS-specific storage files 80 | .DS_Store 81 | 82 | # vim 83 | *.swp 84 | *.swo 85 | 86 | # Mypy cache 87 | .mypy_cache/ 88 | 89 | # Exclude virtual environment 90 | .venv/ 91 | 92 | # Exclude trained models 93 | /models/ 94 | 95 | # exclude data from source control by default 96 | # /data/ -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 DrivenData, Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This repository contains examples and explanations of how to use [PydanticAI](https://ai.pydantic.dev/) - a Python Agent Framework designed to make it easier to build production-grade applications with Generative AI. 4 | 5 | ## 👋🏻 About Me 6 | 7 | Hi there! I’m Dave Ebbelaar, founder of Datalumina®, and I’m passionate about helping data professionals and developers like you succeed in the world of data science and AI. If you enjoy the tutorial, make sure to check out the links below for more resources to help you grow. 8 | 9 | At [Datalumina](https://www.datalumina.com/), we help individuals and businesses unlock the full potential of AI and data by turning complexity into capability. Whether you're learning Python, freelancing, or building cutting-edge AI apps, we provide the tools, guidance, and expertise to help you succeed. 10 | 11 | ### 📚 Explore More Resources 12 | 13 | Whether you're a learner, a freelancer, or a business looking for AI expertise, we've got something for you: 14 | 15 | 1. **Learning Python for AI and Data Science?** 16 | Join our **free community, Data Alchemy**, where you’ll find resources, tutorials, and support: 17 | [▶︎ Data Alchemy on Skool](https://www.skool.com/data-alchemy) 18 | 19 | 2. **Ready to start or scale your freelancing career?** 20 | Learn how to land clients and grow your business with the **Data Freelancer program**: 21 | [▶︎ Data Freelancer](https://www.datalumina.com/data-freelancer) 22 | 23 | 3. **Need expert help on your next project?** 24 | Work with me and my team to solve your data and AI challenges: 25 | [▶︎ Consulting Services](https://www.datalumina.com/solutions) 26 | 27 | 4. **Building AI-powered applications?** 28 | Access the **GenAI Launchpad** to accelerate your AI app development: 29 | [▶︎ GenAI Launchpad](https://launchpad.datalumina.com/) 30 | 31 | ## Introduction to PydanticAI 32 | 33 | PydanticAI is a Python Agent Framework created by the team behind Pydantic, designed to streamline the development of production-grade applications with Generative AI. Building on the success and widespread adoption of Pydantic in the Python AI ecosystem, PydanticAI offers a type-safe, model-agnostic approach that seamlessly integrates with popular LLM providers like OpenAI, Gemini, and Groq. The framework emphasizes developer ergonomics by combining structured response validation, streamed responses, and a dependency injection system, all while allowing developers to leverage standard Python development practices for control flow and agent composition. 34 | 35 | ### Pydantic AI Core Concepts 36 | 37 | 1. [Agents](https://ai.pydantic.dev/agents/): The primary interface for interacting with LLMs, allowing you to define system prompts and manage interactions. 38 | 2. [Dependencies](https://ai.pydantic.dev/dependencies/): A type-safe system for injecting runtime context and accessing external services, making testing and integration easier. 39 | 3. [Results](https://ai.pydantic.dev/results/): Agents can return plain text, structured data, or streamed responses, all validated by Pydantic models. 40 | 4. [Messages and Chat History](https://ai.pydantic.dev/message-history/): Provides access to complete message history and tools for analyzing agent behavior and continuing conversations. 41 | 5. [Testing and Evals](https://ai.pydantic.dev/testing-evals/): Supports unit tests and evaluations to assess model performance and ensure application reliability. 42 | 6. [Debugging and Monitoring](https://ai.pydantic.dev/logfire/): Integrates with Pydantic Logfire for real-time debugging, performance monitoring, and querying of agent runs. 43 | 44 | ### Getting Started 45 | 46 | To begin using PydanticAI, follow these steps: 47 | 48 | 1. **Python**: Ensure you have Python installed on your system. PydanticAI requires Python 3.9 or later. 49 | 50 | 2. **Install Requirements**: Navigate to the root directory of the repository and install the necessary dependencies by running: 51 | 52 | ```bash 53 | pip install -r requirements.txt 54 | ``` 55 | 56 | 3. **Set Up Environment Variables**: Copy the provided `.env.example` file to a new file named `.env`. Open the `.env` file and add your OpenAI API key: 57 | 58 | ```bash 59 | OPENAI_API_KEY=your_openai_api_key_here 60 | ``` 61 | 62 | Make sure to replace `your_openai_api_key_here` with your actual OpenAI API key. 63 | 64 | 4. **Run the Introduction Script**: To get a feel for how PydanticAI works, execute the `introduction.py` script: 65 | 66 | 67 | This script will guide you through the basic functionalities of PydanticAI, demonstrating how to interact with language models using the framework. 68 | 69 | By following these steps, you'll be set up to explore and build applications with PydanticAI. For more detailed examples and documentation, refer to the [PydanticAI documentation](https://ai.pydantic.dev/). 70 | 71 | ### Problems I've Encountered 72 | 73 | PydanticAI is in early beta, and the API is still subject to change. There is still a lot more to do. 74 | 75 | - **Model Parameters**: Currently, I couldn't find a way to adjust model parameters like temperature. 76 | 77 | - **Message History with Tools**: There is a problem with message history when using tools. The following error occurs: 78 | 79 | ``` 80 | BadRequestError: Error code: 400 - {'error': {'message': "An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_KMMn5Bo6wPN3aZosdstleZO2", 'type': 'invalid_request_error', 'param': 'messages.[6].role', 'code': None}} 81 | ``` -------------------------------------------------------------------------------- /data/invoice_query.json: -------------------------------------------------------------------------------- 1 | { 2 | "ticket_id": "INV-001", 3 | "customer_name": "John Doe", 4 | "email": "john.doe@example.com", 5 | "query_type": "invoice", 6 | "description": "I need a copy of the invoice for my order #ORD-2024-123. I can't find it in my email.", 7 | "order_id": "ORD-2024-123" 8 | } -------------------------------------------------------------------------------- /data/product_query.json: -------------------------------------------------------------------------------- 1 | { 2 | "ticket_id": "PRD-001", 3 | "customer_name": "Alice Johnson", 4 | "email": "alice.j@example.com", 5 | "query_type": "product", 6 | "description": "I'm interested in the Pro Model X-1000. Does it come with a warranty? What are the technical specifications?", 7 | "order_id": null 8 | } -------------------------------------------------------------------------------- /data/shipping_query.json: -------------------------------------------------------------------------------- 1 | { 2 | "ticket_id": "SHP-001", 3 | "customer_name": "Jane Smith", 4 | "email": "jane.smith@example.com", 5 | "query_type": "shipping", 6 | "description": "What is your shipping policy for international orders? I'm based in Canada and want to know the estimated delivery time.", 7 | "order_id": null 8 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pydantic>=2.0.0 2 | pydantic-ai>=0.1.0 3 | openai>=1.0.0 4 | python-dotenv>=1.0.0 5 | pytest>=7.0.0 -------------------------------------------------------------------------------- /src/introduction.py: -------------------------------------------------------------------------------- 1 | """ 2 | Introduction to PydanticAI. 3 | 4 | This module demonstrates how PydanticAI makes it easier to build 5 | production-grade LLM-powered systems with type safety and structured responses. 6 | """ 7 | 8 | from typing import Dict, List, Optional 9 | import nest_asyncio 10 | from pydantic import BaseModel, Field 11 | from pydantic_ai import Agent, ModelRetry, RunContext, Tool 12 | from pydantic_ai.models.openai import OpenAIModel 13 | 14 | from utils.markdown import to_markdown 15 | 16 | 17 | nest_asyncio.apply() 18 | 19 | 20 | model = OpenAIModel("gpt-4o") 21 | 22 | # -------------------------------------------------------------- 23 | # 1. Simple Agent - Hello World Example 24 | # -------------------------------------------------------------- 25 | """ 26 | This example demonstrates the basic usage of PydanticAI agents. 27 | Key concepts: 28 | - Creating a basic agent with a system prompt 29 | - Running synchronous queries 30 | - Accessing response data, message history, and costs 31 | """ 32 | 33 | agent1 = Agent( 34 | model=model, 35 | system_prompt="You are a helpful customer support agent. Be concise and friendly.", 36 | ) 37 | 38 | # Example usage of basic agent 39 | response = agent1.run_sync("How can I track my order #12345?") 40 | print(response.data) 41 | print(response.all_messages()) 42 | print(response.cost()) 43 | 44 | 45 | response2 = agent1.run_sync( 46 | user_prompt="What was my previous question?", 47 | message_history=response.new_messages(), 48 | ) 49 | print(response2.data) 50 | 51 | # -------------------------------------------------------------- 52 | # 2. Agent with Structured Response 53 | # -------------------------------------------------------------- 54 | """ 55 | This example shows how to get structured, type-safe responses from the agent. 56 | Key concepts: 57 | - Using Pydantic models to define response structure 58 | - Type validation and safety 59 | - Field descriptions for better model understanding 60 | """ 61 | 62 | 63 | class ResponseModel(BaseModel): 64 | """Structured response with metadata.""" 65 | 66 | response: str 67 | needs_escalation: bool 68 | follow_up_required: bool 69 | sentiment: str = Field(description="Customer sentiment analysis") 70 | 71 | 72 | agent2 = Agent( 73 | model=model, 74 | result_type=ResponseModel, 75 | system_prompt=( 76 | "You are an intelligent customer support agent. " 77 | "Analyze queries carefully and provide structured responses." 78 | ), 79 | ) 80 | 81 | response = agent2.run_sync("How can I track my order #12345?") 82 | print(response.data.model_dump_json(indent=2)) 83 | 84 | 85 | # -------------------------------------------------------------- 86 | # 3. Agent with Structured Response & Dependencies 87 | # -------------------------------------------------------------- 88 | """ 89 | This example demonstrates how to use dependencies and context in agents. 90 | Key concepts: 91 | - Defining complex data models with Pydantic 92 | - Injecting runtime dependencies 93 | - Using dynamic system prompts 94 | """ 95 | 96 | 97 | # Define order schema 98 | class Order(BaseModel): 99 | """Structure for order details.""" 100 | 101 | order_id: str 102 | status: str 103 | items: List[str] 104 | 105 | 106 | # Define customer schema 107 | class CustomerDetails(BaseModel): 108 | """Structure for incoming customer queries.""" 109 | 110 | customer_id: str 111 | name: str 112 | email: str 113 | orders: Optional[List[Order]] = None 114 | 115 | 116 | # Agent with structured output and dependencies 117 | agent5 = Agent( 118 | model=model, 119 | result_type=ResponseModel, 120 | deps_type=CustomerDetails, 121 | retries=3, 122 | system_prompt=( 123 | "You are an intelligent customer support agent. " 124 | "Analyze queries carefully and provide structured responses. " 125 | "Always great the customer and provide a helpful response." 126 | ), # These are known when writing the code 127 | ) 128 | 129 | 130 | # Add dynamic system prompt based on dependencies 131 | @agent5.system_prompt 132 | async def add_customer_name(ctx: RunContext[CustomerDetails]) -> str: 133 | return f"Customer details: {to_markdown(ctx.deps)}" # These depend in some way on context that isn't known until runtime 134 | 135 | 136 | customer = CustomerDetails( 137 | customer_id="1", 138 | name="John Doe", 139 | email="john.doe@example.com", 140 | orders=[ 141 | Order(order_id="12345", status="shipped", items=["Blue Jeans", "T-Shirt"]), 142 | ], 143 | ) 144 | 145 | response = agent5.run_sync(user_prompt="What did I order?", deps=customer) 146 | 147 | response.all_messages() 148 | print(response.data.model_dump_json(indent=2)) 149 | 150 | print( 151 | "Customer Details:\n" 152 | f"Name: {customer.name}\n" 153 | f"Email: {customer.email}\n\n" 154 | "Response Details:\n" 155 | f"{response.data.response}\n\n" 156 | "Status:\n" 157 | f"Follow-up Required: {response.data.follow_up_required}\n" 158 | f"Needs Escalation: {response.data.needs_escalation}" 159 | ) 160 | 161 | 162 | # -------------------------------------------------------------- 163 | # 4. Agent with Tools 164 | # -------------------------------------------------------------- 165 | 166 | """ 167 | This example shows how to enhance agents with custom tools. 168 | Key concepts: 169 | - Creating and registering tools 170 | - Accessing context in tools 171 | """ 172 | 173 | shipping_info_db: Dict[str, str] = { 174 | "12345": "Shipped on 2024-12-01", 175 | "67890": "Out for delivery", 176 | } 177 | 178 | 179 | def get_shipping_info(ctx: RunContext[CustomerDetails]) -> str: 180 | """Get the customer's shipping information.""" 181 | return shipping_info_db[ctx.deps.orders[0].order_id] 182 | 183 | 184 | # Agent with structured output and dependencies 185 | agent5 = Agent( 186 | model=model, 187 | result_type=ResponseModel, 188 | deps_type=CustomerDetails, 189 | retries=3, 190 | system_prompt=( 191 | "You are an intelligent customer support agent. " 192 | "Analyze queries carefully and provide structured responses. " 193 | "Use tools to look up relevant information." 194 | "Always great the customer and provide a helpful response." 195 | ), # These are known when writing the code 196 | tools=[Tool(get_shipping_info, takes_ctx=True)], # Add tool via kwarg 197 | ) 198 | 199 | 200 | @agent5.system_prompt 201 | async def add_customer_name(ctx: RunContext[CustomerDetails]) -> str: 202 | return f"Customer details: {to_markdown(ctx.deps)}" 203 | 204 | 205 | response = agent5.run_sync( 206 | user_prompt="What's the status of my last order?", deps=customer 207 | ) 208 | 209 | response.all_messages() 210 | print(response.data.model_dump_json(indent=2)) 211 | 212 | print( 213 | "Customer Details:\n" 214 | f"Name: {customer.name}\n" 215 | f"Email: {customer.email}\n\n" 216 | "Response Details:\n" 217 | f"{response.data.response}\n\n" 218 | "Status:\n" 219 | f"Follow-up Required: {response.data.follow_up_required}\n" 220 | f"Needs Escalation: {response.data.needs_escalation}" 221 | ) 222 | 223 | 224 | # -------------------------------------------------------------- 225 | # 5. Agent with Reflection and Self-Correction 226 | # -------------------------------------------------------------- 227 | 228 | """ 229 | This example demonstrates advanced agent capabilities with self-correction. 230 | Key concepts: 231 | - Implementing self-reflection 232 | - Handling errors gracefully with retries 233 | - Using ModelRetry for automatic retries 234 | - Decorator-based tool registration 235 | """ 236 | 237 | # Simulated database of shipping information 238 | shipping_info_db: Dict[str, str] = { 239 | "#12345": "Shipped on 2024-12-01", 240 | "#67890": "Out for delivery", 241 | } 242 | 243 | customer = CustomerDetails( 244 | customer_id="1", 245 | name="John Doe", 246 | email="john.doe@example.com", 247 | ) 248 | 249 | # Agent with reflection and self-correction 250 | agent5 = Agent( 251 | model=model, 252 | result_type=ResponseModel, 253 | deps_type=CustomerDetails, 254 | retries=3, 255 | system_prompt=( 256 | "You are an intelligent customer support agent. " 257 | "Analyze queries carefully and provide structured responses. " 258 | "Use tools to look up relevant information. " 259 | "Always greet the customer and provide a helpful response." 260 | ), 261 | ) 262 | 263 | 264 | @agent5.tool_plain() # Add plain tool via decorator 265 | def get_shipping_status(order_id: str) -> str: 266 | """Get the shipping status for a given order ID.""" 267 | shipping_status = shipping_info_db.get(order_id) 268 | if shipping_status is None: 269 | raise ModelRetry( 270 | f"No shipping information found for order ID {order_id}. " 271 | "Make sure the order ID starts with a #: e.g, #624743 " 272 | "Self-correct this if needed and try again." 273 | ) 274 | return shipping_info_db[order_id] 275 | 276 | 277 | # Example usage 278 | response = agent5.run_sync( 279 | user_prompt="What's the status of my last order 12345?", deps=customer 280 | ) 281 | 282 | response.all_messages() 283 | print(response.data.model_dump_json(indent=2)) 284 | -------------------------------------------------------------------------------- /src/utils/markdown.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | def to_markdown(data, indent=0): 5 | markdown = "" 6 | if isinstance(data, BaseModel): 7 | data = data.model_dump() 8 | if isinstance(data, dict): 9 | for key, value in data.items(): 10 | markdown += f"{'#' * (indent + 2)} {key.upper()}\n" 11 | if isinstance(value, (dict, list, BaseModel)): 12 | markdown += to_markdown(value, indent + 1) 13 | else: 14 | markdown += f"{value}\n\n" 15 | elif isinstance(data, list): 16 | for item in data: 17 | if isinstance(item, (dict, list, BaseModel)): 18 | markdown += to_markdown(item, indent) 19 | else: 20 | markdown += f"- {item}\n" 21 | markdown += "\n" 22 | else: 23 | markdown += f"{data}\n\n" 24 | return markdown 25 | --------------------------------------------------------------------------------