From f67ce9f1094777d282e26f62f0720202bebb5ba9 Mon Sep 17 00:00:00 2001 From: Aayush Date: Tue, 21 Apr 2026 17:54:23 -0400 Subject: [PATCH 1/3] Updating API notebook to adhere to standards: --- tutorials/tutorial_pydanticAI/__init__.py | 0 .../tutorial_pydanticAI/pydanticai.API.ipynb | 1404 ++++++++++------- .../tutorial_pydanticAI/pydanticai.API.py | 740 ++++----- .../pydanticai_API_utils.py | 328 +++- .../test/test_pydanticai_API_utils.py | 678 ++++++++ 5 files changed, 2165 insertions(+), 985 deletions(-) create mode 100644 tutorials/tutorial_pydanticAI/__init__.py create mode 100644 tutorials/tutorial_pydanticAI/test/test_pydanticai_API_utils.py diff --git a/tutorials/tutorial_pydanticAI/__init__.py b/tutorials/tutorial_pydanticAI/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tutorials/tutorial_pydanticAI/pydanticai.API.ipynb b/tutorials/tutorial_pydanticAI/pydanticai.API.ipynb index bd51d93b9..39ae78362 100644 --- a/tutorials/tutorial_pydanticAI/pydanticai.API.ipynb +++ b/tutorials/tutorial_pydanticAI/pydanticai.API.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "72ad5d56", "metadata": {}, "outputs": [], @@ -10,71 +10,87 @@ "%load_ext autoreload\n", "%autoreload 2\n", "\n", + "# System libraries.\n", "import logging\n", "\n", + "# Third party libraries.\n", "\n", - "import helpers.hnotebook as ut\n", - "\n", - "ut.config_notebook()\n", - "\n", - "# Initialize logger.\n", - "logging.basicConfig(level=logging.INFO)\n", - "_LOG = logging.getLogger(__name__)" + "# Common plotting and dataframe libraries are loaded for notebook exploration." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "a066f6ee", "metadata": {}, "outputs": [], "source": [ - "import pydanticai_API_utils as utils" + "# System libraries.\n", + "import asyncio\n", + "import dataclasses\n", + "import os\n", + "\n", + "# Third party libraries.\n", + "import dotenv\n", + "import nest_asyncio\n", + "import pydantic\n", + "import pydantic_ai\n", + "\n", + "# Local utilities.\n", + "import pydanticai_API_utils as utils\n", + "\n", + "# Notebook-specific imports are ready for tutorial examples." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "ef3d968c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[0mWARNING: Running in Jupyter\n", + "INFO > cmd='/opt/venv/lib/python3.12/site-packages/ipykernel_launcher.py -f /root/.local/share/jupyter/runtime/kernel-71998996-d1a2-4339-8c62-8114df169cc5.json'\n", + "Notebook logger initialized.\n" + ] + } + ], + "source": [ + "# Configure notebook logging.\n", + "_LOG = logging.getLogger(__name__)\n", + "utils.init_logger(_LOG)\n", + "_LOG.info(\"Notebook logger initialized.\")\n", + "# Notebook and utility logs now print in Jupyter." ] }, { "cell_type": "markdown", - "id": "784a674e", + "id": "8e2b9ddc", "metadata": {}, "source": [ - "## PydanticAI API Tutorial Introduction\n", - "\n", - "PydanticAI is a lightweight framework for building LLM-powered applications with **structured outputs using Pydantic models**.\n", - "\n", - "Unlike traditional LLM APIs that return unstructured text, PydanticAI ensures responses conform to a predefined schema.\n", - "\n", - "This notebook covers:\n", + "# Summary\n", "\n", - "- Core concepts\n", - "- Agent API\n", - "- Structured outputs\n", - "- Tool usage\n", - "- Validation and retries\n", - "- Async execution\n", + "This notebook introduces `PydanticAI` APIs for building LLM workflows.\n", "\n", - "By the end, you will understand how to build reliable LLM pipelines using structured outputs." + "Topics include structured outputs, tools, dependencies, validators, streaming,\n", + "provider configuration, run metadata, and usage limits." ] }, { "cell_type": "markdown", - "id": "e6f09140", + "id": "784a674e", "metadata": {}, "source": [ - "# Table of Contents\n", - "\n", - "1. Introduction\n", - "2. Why PydanticAI exists\n", - "3. Installation\n", - "4. Minimal Example\n", - "5. Core Concepts\n", - "6. Structured Outputs\n", - "7. Validation\n", - "8. Tools\n", - "9. Dependencies\n", - "10. Async Execution\n", - "11. Advanced Features\n", - "12. Best Practices\n", - "13. Summary" + "# PydanticAI API Tutorial Introduction\n", + "\n", + "`PydanticAI` is a lightweight framework for building LLM-powered applications\n", + "with structured outputs using `Pydantic` models.\n", + "\n", + "Unlike traditional LLM APIs that return unstructured text, `PydanticAI`\n", + "ensures responses conform to a predefined schema." ] }, { @@ -82,30 +98,32 @@ "id": "1d6d6c57", "metadata": {}, "source": [ - "### Why PydanticAI Exists\n", + "## Why PydanticAI Exists\n", "\n", - "LLMs typically return unstructured text.\n", + "Key problem: LLMs typically return unstructured text.\n", "\n", - "Example:\n", + "Example prompt:\n", "\n", - "User prompt:\n", "\"Extract product information from this description\"\n", "\n", - "LLM output:\n", + "Example LLM output:\n", + "\n", "\"The product is an iPhone 15 priced at $999.\"\n", "\n", "This output is difficult to use programmatically.\n", "\n", - "What we want instead:\n", + "Desired structured output:\n", "\n", + "```json\n", "{\n", " \"product_name\": \"iPhone 15\",\n", " \"price\": 999\n", "}\n", + "```\n", "\n", - "PydanticAI solves this problem by:\n", + "`PydanticAI` solves this problem by:\n", "\n", - "- Defining schemas using **Pydantic models**\n", + "- Defining schemas using Pydantic models\n", "- Enforcing structured outputs\n", "- Automatically retrying when validation fails\n", "- Providing a simple agent abstraction for LLM interaction" @@ -116,9 +134,9 @@ "id": "9f09d9ed", "metadata": {}, "source": [ - "### Mental Model\n", + "## Mental Model\n", "\n", - "```\n", + "```text\n", "User Prompt\n", " v\n", "PydanticAI Agent\n", @@ -133,143 +151,51 @@ "```" ] }, - { - "cell_type": "markdown", - "id": "64b752ff", - "metadata": {}, - "source": [ - "## Installation\n", - "\n", - "We install a minimal set of packages to keep the notebook self-contained and reproducible.\n", - "\n", - "This notebook uses `pydantic-ai`, `pydantic`, and `python-dotenv`.\n" - ] - }, { "cell_type": "code", - "execution_count": 5, - "id": "bba9f441", + "execution_count": 4, + "id": "eff13ce6", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "The autoreload extension is already loaded. To reload it, use:\n", - " %reload_ext autoreload\n" + "dotenv path: /git_root/tutorials/tutorial_pydanticAI/.env\n" ] } ], "source": [ - "!pip install -q pydantic-ai" + "# Load environment variables from a local dotenv file if one exists.\n", + "env_path = dotenv.find_dotenv(usecwd=True)\n", + "dotenv.load_dotenv(env_path, override=True)\n", + "_LOG.info(\"dotenv path: %s\", env_path or \"\")\n", + "env_path or \"\"\n", + "# Environment variables are available to the model configuration cells." ] }, { "cell_type": "code", - "execution_count": 6, - "id": "eff13ce6", + "execution_count": 5, + "id": "9bb08251", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "dotenv path: /curr_dir/.env\n", - "PYDANTIC_AI_MODEL: openai:gpt-5-2025-08-07\n", - "OPENAI_API_KEY: sk-...8A\n" + "dotenv path: /git_root/tutorials/tutorial_pydanticAI/.env\n", + "PYDANTIC_AI_MODEL: openai:gpt-4.1-mini\n", + "OPENAI_API_KEY: sk-...UA\n" ] } ], "source": [ - "import os\n", - "from dotenv import load_dotenv, find_dotenv\n", - "import nest_asyncio\n", - "\n", - "nest_asyncio.apply()\n", - "\n", - "\n", - "env_path = find_dotenv(usecwd=True)\n", - "load_dotenv(env_path, override=True)\n", - "\n", - "MODEL_ID = os.getenv(\"PYDANTIC_AI_MODEL\", \"openai:gpt-4.1-mini\")\n", - "print(\"dotenv path:\", env_path or \"\")\n", - "print(\"PYDANTIC_AI_MODEL:\", MODEL_ID)\n", - "print(\"OPENAI_API_KEY:\", utils._mask(os.getenv(\"OPENAI_API_KEY\")))" - ] - }, - { - "cell_type": "markdown", - "id": "f2e6d162", - "metadata": {}, - "source": [ - "### Running the Notebook\n", - "\n", - "To run the examples you must set your API key.\n", - "\n", - "Example:\n", - "```\n", - "export OPENAI_API_KEY=\"your_key_here\"\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "8569d597", - "metadata": {}, - "source": [ - "## Minimal Example\n", - "\n", - "The quickest way to understand PydanticAI is through a small example.\n", - "\n", - "We define a schema using Pydantic and instruct the agent to produce that structured output." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "b7e487b4", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "City(name='Paris', country='France', population=2140526)" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from pydantic import BaseModel\n", - "from pydantic_ai import Agent\n", - "\n", - "\n", - "class City(BaseModel):\n", - " name: str\n", - " country: str\n", - " population: int\n", - "\n", - "\n", - "agent = Agent(\"openai:gpt-4o-mini\", output_type=City)\n", - "\n", - "result = agent.run_sync(\"Tell me about Paris\")\n", - "\n", - "result.output" - ] - }, - { - "cell_type": "markdown", - "id": "86efa23c", - "metadata": {}, - "source": [ - "### What Happened?\n", - "\n", - "1. A Pydantic schema (`City`) defines the expected output structure.\n", - "2. The `Agent` sends the prompt to the LLM.\n", - "3. The LLM response is validated against the schema.\n", - "4. If validation succeeds, the structured result is returned." + "# Read the model identifier from the environment.\n", + "MODEL_ID = os.getenv(\"PYDANTIC_AI_MODEL\")\n", + "utils.log_environment(env_path, MODEL_ID)\n", + "{\"model_id\": MODEL_ID}\n", + "# The tutorial examples will use the configured model identifier." ] }, { @@ -277,11 +203,11 @@ "id": "15a7a6e3", "metadata": {}, "source": [ - "## Core Concepts\n", + "# Core Concepts\n", "\n", "PydanticAI revolves around a few important abstractions.\n", "\n", - "### Agent\n", + "## Agent\n", "\n", "The `Agent` is the main interface for interacting with the model.\n", "\n", @@ -292,13 +218,13 @@ "- retries\n", "- tool usage\n", "\n", - "### output_type\n", + "## output_type\n", "\n", "Defines the expected structured output.\n", "\n", "This must be a Pydantic model.\n", "\n", - "### Tools\n", + "## Tools\n", "\n", "Functions that the agent can call during reasoning.\n", "\n", @@ -308,282 +234,478 @@ }, { "cell_type": "markdown", - "id": "e0f3aa76", - "metadata": {}, - "source": [ - "## Structured Outputs with Pydantic" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "895da5b2", + "id": "8569d597", "metadata": { "lines_to_next_cell": 2 }, - "outputs": [ - { - "data": { - "text/plain": [ - "Product(name='Apple AirPods Pro', price=249.0, category='Electronics')" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "from pydantic import BaseModel\n", - "\n", - "\n", - "class Product(BaseModel):\n", - " name: str\n", - " price: float\n", - " category: str\n", - "\n", + "# Minimal Example\n", "\n", - "agent = Agent(\"openai:gpt-4o-mini\", output_type=Product)\n", + "The quickest way to understand PydanticAI is through a small example.\n", "\n", - "agent.run_sync(\"Describe the Apple AirPods Pro\").output" - ] - }, - { - "cell_type": "markdown", - "id": "d8d15d06-6d82-42cf-b003-7b85cf45eb2d", - "metadata": {}, - "source": [ - "### What happened in the code\n", + "We define a schema using Pydantic and instruct the agent to produce that structured output.\n", "\n", - "- We defined a `Product` schema (name, price, category).\n", - "- The agent is configured to produce outputs that conform to this schema.\n", - "- When the model answers, PydanticAI validates that:\n", - " - `price` is a number\n", - " - fields exist with the right types\n", - " - the structure matches exactly\n", "\n", - "**Why PydanticAI is useful here:**\n", - "This turns LLM responses into structured data you can store in databases, feed into analytics, or pass downstream in an application without brittle string parsing." + "#############################################################################\n", + "City\n", + "#############################################################################" ] }, { - "cell_type": "markdown", - "id": "5716df9d", + "cell_type": "code", + "execution_count": null, + "id": "5d68a76d", "metadata": { "lines_to_next_cell": 2 }, + "outputs": [], "source": [ - "## Validation and Retries\n", + "# Define the output schema for the minimal example.\n", + "class City(pydantic.BaseModel):\n", + " name: str\n", + " country: str\n", + " population: int\n", "\n", - "If the LLM produces an output that does not match the schema, PydanticAI automatically retries.\n", "\n", - "This greatly improves reliability." + "City\n", + "# The schema defines the exact output shape expected from the model." ] }, { "cell_type": "code", - "execution_count": 8, - "id": "775f32dd", - "metadata": { - "lines_to_next_cell": 2 - }, + "execution_count": 6, + "id": "b7e487b4", + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AgentRunResult(output=Person(name='Albert Einstein', age=76))" + "Agent(model=OpenAIChatModel(), name=None, end_strategy='early', model_settings=None, output_type=, instrument=None)" ] }, - "execution_count": 8, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "class Person(BaseModel):\n", - " name: str\n", - " age: int\n", - "\n", - "\n", - "agent = Agent(\"openai:gpt-4o-mini\", output_type=Person, retries=2)\n", - "\n", - "agent.run_sync(\"Tell me about Albert Einstein\")" - ] - }, - { - "cell_type": "markdown", - "id": "c55fb759-4d81-4a4d-8899-9759b8d82f27", - "metadata": {}, - "source": [ - "### What happened in the code\n", - "\n", - "- We defined a `Person` schema with `name` and `age`.\n", - "- We set `retries=2` on the agent.\n", - "- If the model output fails schema validation (missing fields, wrong types), PydanticAI automatically retries the model call to get a valid output.\n", - "\n", - "**Why PydanticAI is useful here:**\n", - "Real LLM outputs are inconsistent. Automatic schema validation + retry gives you reliability without writing custom parsing and retry logic for every prompt." - ] - }, - { - "cell_type": "markdown", - "id": "3948bb6c", - "metadata": {}, - "source": [ - "## Tools\n", - "\n", - "Agents can call Python functions as tools." + "# Create an agent that must return `City`.\n", + "agent = pydantic_ai.Agent(MODEL_ID, output_type=City)\n", + "agent\n", + "# The agent is configured to validate model output against class `City`." ] }, { "cell_type": "code", - "execution_count": 9, - "id": "099d9d99", + "execution_count": 7, + "id": "5545ded5", "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "AgentRunResult(output='The weather in Tokyo is sunny.')" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" + "ename": "RuntimeError", + "evalue": "This event loop is already running", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mRuntimeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# Run the minimal example agent.\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m result = agent.run_sync(\u001b[33m\"Tell me about Paris\"\u001b[39m)\n\u001b[32m 3\u001b[39m \n\u001b[32m 4\u001b[39m result.output\n\u001b[32m 5\u001b[39m \u001b[38;5;66;03m# The result is a validated `City` object.\u001b[39;00m\n", + "\u001b[36mFile \u001b[39m\u001b[32m/opt/venv/lib/python3.12/site-packages/pydantic_ai/agent/abstract.py:452\u001b[39m, in \u001b[36mAbstractAgent.run_sync\u001b[39m\u001b[34m(self, user_prompt, output_type, message_history, deferred_tool_results, model, instructions, deps, model_settings, usage_limits, usage, metadata, infer_name, toolsets, builtin_tools, event_stream_handler, spec)\u001b[39m\n\u001b[32m 449\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m infer_name \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28mself\u001b[39m.name \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 450\u001b[39m \u001b[38;5;28mself\u001b[39m._infer_name(inspect.currentframe())\n\u001b[32m--> \u001b[39m\u001b[32m452\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_utils\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget_event_loop\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun_until_complete\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 453\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 454\u001b[39m \u001b[43m \u001b[49m\u001b[43muser_prompt\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 455\u001b[39m \u001b[43m \u001b[49m\u001b[43moutput_type\u001b[49m\u001b[43m=\u001b[49m\u001b[43moutput_type\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 456\u001b[39m \u001b[43m \u001b[49m\u001b[43mmessage_history\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmessage_history\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 457\u001b[39m \u001b[43m \u001b[49m\u001b[43mdeferred_tool_results\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdeferred_tool_results\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 458\u001b[39m \u001b[43m \u001b[49m\u001b[43mmodel\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmodel\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 459\u001b[39m \u001b[43m \u001b[49m\u001b[43minstructions\u001b[49m\u001b[43m=\u001b[49m\u001b[43minstructions\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 460\u001b[39m \u001b[43m \u001b[49m\u001b[43mdeps\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdeps\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 461\u001b[39m \u001b[43m \u001b[49m\u001b[43mmodel_settings\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmodel_settings\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 462\u001b[39m \u001b[43m \u001b[49m\u001b[43musage_limits\u001b[49m\u001b[43m=\u001b[49m\u001b[43musage_limits\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 463\u001b[39m \u001b[43m \u001b[49m\u001b[43musage\u001b[49m\u001b[43m=\u001b[49m\u001b[43musage\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 464\u001b[39m \u001b[43m \u001b[49m\u001b[43mmetadata\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmetadata\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 465\u001b[39m \u001b[43m \u001b[49m\u001b[43minfer_name\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 466\u001b[39m \u001b[43m \u001b[49m\u001b[43mtoolsets\u001b[49m\u001b[43m=\u001b[49m\u001b[43mtoolsets\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 467\u001b[39m \u001b[43m \u001b[49m\u001b[43mbuiltin_tools\u001b[49m\u001b[43m=\u001b[49m\u001b[43mbuiltin_tools\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 468\u001b[39m \u001b[43m \u001b[49m\u001b[43mevent_stream_handler\u001b[49m\u001b[43m=\u001b[49m\u001b[43mevent_stream_handler\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 469\u001b[39m \u001b[43m \u001b[49m\u001b[43mspec\u001b[49m\u001b[43m=\u001b[49m\u001b[43mspec\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 470\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 471\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m/usr/local/lib/python3.12/asyncio/base_events.py:667\u001b[39m, in \u001b[36mBaseEventLoop.run_until_complete\u001b[39m\u001b[34m(self, future)\u001b[39m\n\u001b[32m 656\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"Run until the Future is done.\u001b[39;00m\n\u001b[32m 657\u001b[39m \n\u001b[32m 658\u001b[39m \u001b[33;03mIf the argument is a coroutine, it is wrapped in a Task.\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 664\u001b[39m \u001b[33;03mReturn the Future's result, or raise its exception.\u001b[39;00m\n\u001b[32m 665\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 666\u001b[39m \u001b[38;5;28mself\u001b[39m._check_closed()\n\u001b[32m--> \u001b[39m\u001b[32m667\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_check_running\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 669\u001b[39m new_task = \u001b[38;5;129;01mnot\u001b[39;00m futures.isfuture(future)\n\u001b[32m 670\u001b[39m future = tasks.ensure_future(future, loop=\u001b[38;5;28mself\u001b[39m)\n", + "\u001b[36mFile \u001b[39m\u001b[32m/usr/local/lib/python3.12/asyncio/base_events.py:626\u001b[39m, in \u001b[36mBaseEventLoop._check_running\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 624\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_check_running\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[32m 625\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.is_running():\n\u001b[32m--> \u001b[39m\u001b[32m626\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\u001b[33m'\u001b[39m\u001b[33mThis event loop is already running\u001b[39m\u001b[33m'\u001b[39m)\n\u001b[32m 627\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m events._get_running_loop() \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 628\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mRuntimeError\u001b[39;00m(\n\u001b[32m 629\u001b[39m \u001b[33m'\u001b[39m\u001b[33mCannot run the event loop while another loop is running\u001b[39m\u001b[33m'\u001b[39m)\n", + "\u001b[31mRuntimeError\u001b[39m: This event loop is already running" + ] } ], "source": [ - "agent = Agent(\"openai:gpt-4o-mini\", tools=[utils.get_weather])\n", + "# Run the minimal example agent.\n", + "result = agent.run_sync(\"Tell me about Paris\")\n", "\n", - "agent.run_sync(\"What is the weather in Tokyo?\")" + "result.output\n", + "# The result is a validated `City` object." ] }, { "cell_type": "markdown", - "id": "57381ed0-cf9f-467c-8437-d3858c7b29a7", + "id": "ba8f4833-dacb-435c-8bc8-1daeb718262e", "metadata": {}, "source": [ - "### What happened in the code\n", - "\n", - "- We defined a Python function `get_weather(city)` that returns a deterministic string.\n", - "- We passed it into the agent via `tools=[get_weather]`.\n", - "- When the user asks about weather, the agent can choose to call the tool to get the answer instead of hallucinating.\n", - "\n", - "**Why PydanticAI is useful here:**\n", - "Tools let the model interact with real functions and external systems. This is how you build agents that do real work (APIs, databases, calculations) rather than confidently inventing facts." + "# Resolving the RuntimeError in Jupyter" ] }, { "cell_type": "markdown", - "id": "6bbc710d", + "id": "ce72edf2-d1f4-4d60-ac36-29680d884d9a", "metadata": {}, "source": [ - "## Dependencies\n", + "Key thing to remember: Jupyter already runs an active event loop.\n", "\n", - "Dependencies allow agents to access external resources or shared state." + "- `agent.run_sync()` can raise a `RuntimeError` in notebook environments\n", + "- `nest_asyncio` patches the notebook event loop so nested async execution can work\n", + "- After applying `nest_asyncio`, the async `PydanticAI` examples can run inside cells" ] }, { "cell_type": "code", - "execution_count": 10, - "id": "772c04ee", + "execution_count": 8, + "id": "bba9f441", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "The configured company is OpenAI.\n" + "Nested event loop support enabled.\n" ] } ], "source": [ - "from dataclasses import dataclass\n", - "from pydantic_ai import Agent\n", - "\n", - "\n", - "@dataclass\n", - "class Config:\n", - " company: str\n", - "\n", - "\n", - "agent = Agent(\"openai:gpt-4o-mini\", deps_type=Config, tools=[utils.company_name])\n", - "\n", - "result = agent.run_sync(\n", - " \"What company is configured?\", deps=Config(company=\"OpenAI\")\n", - ")\n", - "print(result.output)" + "# Enable nested event loops for notebook execution.\n", + "nest_asyncio.apply()\n", + "_LOG.info(\"Nested event loop support enabled.\")\n", + "# Async PydanticAI examples can now run from notebook cells." ] }, { "cell_type": "markdown", - "id": "9c263739-f6e0-4cb7-ae54-15b9f6e87a9d", + "id": "46db7bd2-16ae-46ec-8b03-361b80a9aa40", "metadata": {}, "source": [ - "### What happened in the code\n", + "Now try running the previous cell that had the error." + ] + }, + { + "cell_type": "markdown", + "id": "e0f3aa76", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ + "# Structured Outputs with Pydantic\n", "\n", - "- `deps_type=Config` declares the *shape* of runtime context the agent can receive.\n", - "- At run time, we pass an instance like `Config(company=\"OpenAI\")`.\n", - "- Tools (or other agent logic) can access this via `RunContext.deps`, so the agent can use configuration/state without hardcoding it into prompts.\n", + "`PydanticAI` turns LLM responses into structured data.\n", "\n", - "**Why PydanticAI is useful here:**\n", - "Dependencies are a clean way to inject runtime configuration (tenant ID, API clients, feature flags, environment context) into agents and tools without relying on global variables or string formatting prompts." + "- Store validated outputs in databases\n", + "- Feed typed objects into analytics\n", + "- Pass structured data downstream without brittle string parsing\n", + "\n", + "\n", + "#############################################################################\n", + "Product\n", + "#############################################################################" ] }, { - "cell_type": "markdown", - "id": "6c1d10c1", - "metadata": {}, + "cell_type": "code", + "execution_count": null, + "id": "636df5ab", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], "source": [ - "## Async Execution\n", + "# Define a product schema for structured extraction.\n", + "class Product(pydantic.BaseModel):\n", + " name: str\n", + " price: float\n", + " category: str\n", + "\n", "\n", - "PydanticAI supports asynchronous execution for scalable applications." + "Product\n", + "# The schema captures the product fields we want to extract." ] }, { "cell_type": "code", - "execution_count": 11, - "id": "b9bf9835", + "execution_count": 9, + "id": "895da5b2", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'Tokyo, the capital city of Japan, is a vibrant metropolis known for its blend of traditional culture and modern innovation. Here are some key highlights about Tokyo:\\n\\n1. **Geography**: Located on the eastern coast of Honshu, Tokyo is situated in the Kanto region. It is part of the Tokyo Metropolis, which includes 23 special wards, and is surrounded by the Saitama, Chiba, and Kanagawa prefectures.\\n\\n2. **Population**: Tokyo is one of the most populous cities in the world, with a metropolitan area that has over 37 million residents, making it a major hub for business and culture.\\n\\n3. **Culture and History**: Tokyo was originally a small fishing village named Edo. It became the political center of Japan in the early 17th century when Tokugawa Ieyasu, the founder of the Tokugawa shogunate, established his government there. The city was renamed Tokyo, meaning \"Eastern Capital,\" in 1868.\\n\\n4. **Architecture and Urban Design**: Tokyo is known for its eclectic architecture, featuring a mix of traditional structures (like temples and shrines) and modern skyscrapers. The Tokyo Tower and the Tokyo Skytree are two iconic landmarks that symbolize the city’s skyline.\\n\\n5. **Transport**: Tokyo has one of the most efficient public transportation systems in the world, including an extensive network of trains, subways, and buses. The Tokyo Metro and JR East train services are particularly notable for their punctuality and coverage.\\n\\n6. **Economy**: As a global financial center, Tokyo hosts numerous multinational corporations and is a leading city in technology, manufacturing, and commerce. The Tokyo Stock Exchange is one of the largest stock exchanges in the world.\\n\\n7. **Cuisine**: Tokyo boasts a rich culinary scene, offering everything from sushi and ramen to high-end dining experiences. It has more Michelin-starred restaurants than any other city in the world.\\n\\n8. **Tourist Attractions**: Popular destinations in Tokyo include the historic Senso-ji Temple in Asakusa, the busy shopping districts of Shibuya and Harajuku, the Imperial Palace, and the vibrant nightlife of Shinjuku.\\n\\n9. **Arts and Entertainment**: Tokyo is a cultural hub, known for its museums, art galleries, theaters, and music venues. Events like the Tokyo Anime and Comic Market celebrate Japan’s pop culture.\\n\\n10. **Parks and Nature**: Despite being a bustling urban environment, Tokyo offers several green spaces, including Ueno Park and the picturesque Shinjuku Gyoen National Garden, where residents and visitors can enjoy nature.\\n\\nTokyo\\'s unique blend of the old and new makes it a fascinating destination both for residents and tourists alike.'" + "Agent(model=OpenAIChatModel(), name=None, end_strategy='early', model_settings=None, output_type=, instrument=None)" ] }, - "execution_count": 11, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "import asyncio\n", - "\n", - "asyncio.run(utils.run_agent(agent))" + "# Create an agent that must return `Product`.\n", + "agent = pydantic_ai.Agent(MODEL_ID, output_type=Product)\n", + "agent\n", + "# The agent is configured to return product data with typed fields." ] }, { - "cell_type": "markdown", - "id": "c41412c5-70b4-44c1-bdb8-9c98da932144", + "cell_type": "code", + "execution_count": 10, + "id": "9b141b60", "metadata": {}, - "source": [ - "### What happened in the code\n", - "\n", - "- We defined an async function that calls `await agent.run(...)`.\n", - "- Async execution is helpful for applications that need concurrency (web servers, batch pipelines, background jobs).\n", - "- `asyncio.run(...)` runs the coroutine in a notebook-safe way.\n", - "\n", - "**Why PydanticAI is useful here:**\n", - "Most real systems are async. PydanticAI supports async natively, so you can run many agent calls concurrently without blocking your app." - ] - }, - { - "cell_type": "markdown", - "id": "9968fba5", + "outputs": [ + { + "data": { + "text/plain": [ + "Product(name='Apple AirPods Pro', price=249.0, category='Wireless Earbuds')" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Ask the model for structured product information.\n", + "agent.run_sync(\"Describe the Apple AirPods Pro\").output\n", + "# The response is validated as a `Product` class object." + ] + }, + { + "cell_type": "markdown", + "id": "5716df9d", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ + "# Validation and Retries\n", + "\n", + "Real LLM outputs are inconsistent.\n", + "\n", + "- Schema validation checks the generated structure\n", + "- Retries let `PydanticAI` ask the model to repair invalid output\n", + "- The notebook avoids custom parsing and retry logic in each prompt\n", + "\n", + "\n", + "\n", + "#############################################################################\n", + "Person\n", + "#############################################################################" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4b256f36", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "# Define a schema that requires an integer age.\n", + "class Person(pydantic.BaseModel):\n", + " name: str\n", + " age: int\n", + "\n", + "\n", + "Person\n", + "# The schema enforces integer typing for age values." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "775f32dd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Agent(model=OpenAIChatModel(), name=None, end_strategy='early', model_settings=None, output_type=, instrument=None)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Configure retries so schema validation failures can be corrected.\n", + "agent = pydantic_ai.Agent(MODEL_ID, output_type=Person, retries=2)\n", + "agent\n", + "# The agent can retry when model output does not match `Person`." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "5d8126e2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AgentRunResult(output=Person(name='Albert Einstein', age=76))" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Run the retry-enabled agent.\n", + "agent.run_sync(\"Tell me about Albert Einstein\")\n", + "# The result is a validated `Person` run result." + ] + }, + { + "cell_type": "markdown", + "id": "3948bb6c", + "metadata": {}, + "source": [ + "# Tools\n", + "\n", + "Agents can call Python functions as tools.\n", + "\n", + "- Tools let the model interact with real functions and external systems\n", + "- Tools are useful for APIs, databases, calculations, and deterministic helpers\n", + "- Tool calls reduce the chance that the model invents facts" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "099d9d99", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Agent(model=OpenAIChatModel(), name=None, end_strategy='early', model_settings=None, output_type=, instrument=None)" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create an agent with a deterministic weather tool.\n", + "agent = pydantic_ai.Agent(MODEL_ID, tools=[utils.get_weather])\n", + "agent\n", + "# The agent can call `utils.get_weather()` while answering." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "3a58783d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AgentRunResult(output='The weather in Tokyo is sunny. Would you like to know the weather in any other cities?')" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Ask a question that should use the weather tool.\n", + "agent.run_sync(\"What is the weather in Tokyo?\")\n", + "# The run result includes the tool-backed weather answer." + ] + }, + { + "cell_type": "markdown", + "id": "6bbc710d", + "metadata": { + "lines_to_next_cell": 2 + }, + "source": [ + "# Dependencies\n", + "\n", + "Dependencies inject runtime context into agents and tools.\n", + "\n", + "- Example values: tenant IDs, API clients, feature flags, and environment context\n", + "- Benefit: tools can access context without global variables or prompt string formatting\n", + "\n", + "\n", + "#############################################################################\n", + "Config\n", + "#############################################################################" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ffc2657", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "# Define the dependency object passed into the agent at run time.\n", + "@dataclasses.dataclass\n", + "class Config:\n", + " company: str\n", + "\n", + "\n", + "Config\n", + "# The dependency schema describes runtime context available to tools." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "772c04ee", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Agent(model=OpenAIChatModel(), name=None, end_strategy='early', model_settings=None, output_type=, instrument=None)" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create an agent that receives `Config` dependencies.\n", + "# `deps_type=Config` declares the shape of runtime context the agent can receive.\n", + "agent = pydantic_ai.Agent(MODEL_ID, deps_type=Config, tools=[utils.company_name])\n", + "agent\n", + "# Tools can access `Config` through the PydanticAI run context." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "1b9e4981", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'The configured company is OpenAI.'" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Run the dependency-aware agent with a concrete configuration.\n", + "result = agent.run_sync(\n", + " \"What company is configured?\", deps=Config(company=\"OpenAI\")\n", + ")\n", + "result.output\n", + "# The answer reflects the runtime dependency value." + ] + }, + { + "cell_type": "markdown", + "id": "9968fba5", "metadata": {}, "source": [ - "## Advanced API Features\n", + "# Advanced API Features\n", "\n", "The following sections demonstrate more advanced capabilities of PydanticAI.\n", "\n", @@ -603,7 +725,7 @@ "id": "1ec1cef2", "metadata": {}, "source": [ - "## Result Validators\n", + "# Result Validators\n", "\n", "Result validators allow you to enforce additional rules on model outputs.\n", "\n", @@ -614,128 +736,238 @@ ] }, { - "cell_type": "code", - "execution_count": 12, - "id": "c66c4d20", + "cell_type": "markdown", + "id": "6f49d16c-71a4-4d5b-9cfd-7149cdcad70f", "metadata": { "lines_to_next_cell": 2 }, + "source": [ + "## Validation Flow\n", + "\n", + "In this section, validation happens in two stages:\n", + "\n", + "1. `Schema validation`: the model output must match `AnswerWithSources`.\n", + "2. `Business-rule validation`: the registered `output_validator` enforces\n", + " citation quality rules that schema alone cannot enforce.\n", + "\n", + "Execution order:\n", + "\n", + "```text\n", + "model output -> Pydantic schema validation -> output_validator -> final result\n", + "```\n", + "\n", + "\n", + "#############################################################################\n", + "SourceRef\n", + "#############################################################################" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "c66c4d20", + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - " Any>" + "{'available_doc_ids': ['api',\n", + " 'billing',\n", + " 'integrations',\n", + " 'limits',\n", + " 'overview',\n", + " 'security',\n", + " 'support',\n", + " 'troubleshooting'],\n", + " 'validator_tools': ['search_documents']}" ] }, - "execution_count": 12, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "from pydantic import BaseModel\n", - "from pydantic_ai import Agent\n", - "\n", - "MODEL_ID = \"openai:gpt-4o-mini\"\n", - "\n", - "\n", - "class SourceRef(BaseModel):\n", + "# Define source citation schemas for validator examples.\n", + "class SourceRef(pydantic.BaseModel):\n", " doc_id: str\n", " quote: str\n", "\n", "\n", - "class AnswerWithSources(BaseModel):\n", + "# #############################################################################\n", + "# AnswerWithSources\n", + "# #############################################################################\n", + "\n", + "\n", + "class AnswerWithSources(pydantic.BaseModel):\n", " answer: str\n", " sources: list[SourceRef]\n", "\n", "\n", - "validator_agent = Agent(\n", + "AnswerWithSources\n", + "# The schemas describe answers that include source citations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96f6f0ac", + "metadata": {}, + "outputs": [], + "source": [ + "# Build validator instructions from local document ids.\n", + "available_doc_ids = utils.get_available_document_ids()\n", + "validator_instructions = (\n", + " \"Use the search_documents tool to retrieve evidence from local documents. \"\n", + " f\"Cite only these doc ids: {available_doc_ids}. \"\n", + " \"For each source, copy the quote text exactly from tool output.\"\n", + ")\n", + "{\n", + " \"available_doc_ids\": available_doc_ids,\n", + " \"validator_instruction_length\": len(validator_instructions),\n", + "}\n", + "# The instructions constrain citations to the local document ids." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a0e840b", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "# Create an agent that returns answers with source references.\n", + "validator_agent = pydantic_ai.Agent(\n", " MODEL_ID,\n", " output_type=AnswerWithSources,\n", - " instructions=(\n", - " \"Answer with short factual statements. \"\n", - " \"If you reference documents, include sources.\"\n", - " ),\n", + " instructions=validator_instructions,\n", + " tools=[utils.search_documents],\n", ")\n", - "validator_agent.output_validator(utils.validate_sources)" + "validator_agent\n", + "# The validator agent can retrieve documents and return cited answers." ] }, { "cell_type": "code", - "execution_count": 13, - "id": "975c50ca-65ae-4838-8d44-599fee1d461f", + "execution_count": null, + "id": "1a6d9743", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [], + "source": [ + "# Register a result validator that checks citations against local documents.\n", + "@validator_agent.output_validator\n", + "def validate_output(\n", + " result: AnswerWithSources,\n", + ") -> AnswerWithSources:\n", + " result = utils.validate_document_sources(result)\n", + " return result\n", + "\n", + "\n", + "{\"validator_registered\": True}\n", + "# The validator agent now enforces schema and source-reference rules." + ] + }, + { + "cell_type": "markdown", + "id": "10fa3cab-b7f9-45e7-acbd-2f819933213c", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Validator failure example: Answer references documents but sources are empty.\n" - ] - } - ], "source": [ - "try:\n", - " utils.validate_sources(\n", - " AnswerWithSources(answer=\"According to the documents...\", sources=[])\n", - " )\n", - "except Exception as e:\n", - " print(\"Validator failure example:\", e)" + "## What `@validator_agent.output_validator` Does\n", + "\n", + "The `@validator_agent.output_validator` decorator registers a post-processing\n", + "validator for this specific agent.\n", + "\n", + "The validator receives the already schema-validated `AnswerWithSources` object.\n", + "Then the validator calls `utils.validate_document_sources(...)` to enforce:\n", + "\n", + "- Source list required when answer claims document-backed statements\n", + "- Maximum number of sources\n", + "- No duplicate `(doc_id, quote)` pairs\n", + "- Each `doc_id` must exist in the local dataset\n", + "- Each `quote` must appear in the cited document" ] }, { "cell_type": "markdown", - "id": "7899e03f-bd34-4e97-b345-2cf206a33de0", + "id": "bf679b98-5677-4300-baea-47292420beed", "metadata": {}, "source": [ - "### What happened in the code\n", - "\n", - "- We defined a schema `AnswerWithSources` where the model must return:\n", - " - `answer` (string)\n", - " - `sources` (list of `{doc_id, quote}`)\n", - "- We attached an `output_validator` that enforces *logical rules* beyond the schema:\n", - " - if the answer mentions docs, sources must not be empty\n", - " - max 3 sources\n", - " - no duplicate sources\n", - "- If rules fail, we raise `ModelRetry`, which tells PydanticAI to retry the model call.\n", - "\n", - "**Why PydanticAI is useful here:**\n", - "Schemas catch structural mistakes. Validators catch logical mistakes. Together, they make LLM outputs production-grade by enforcing business rules automatically." + "## Why `ModelRetry` Is Important\n", + "\n", + "When a rule is violated, the validator raises `ModelRetry`.\n", + "\n", + "`ModelRetry` tells `PydanticAI` to ask the model for another attempt instead\n", + "of accepting bad output.\n", + "\n", + "## Why `available_doc_ids` Is Included in Instructions\n", + "\n", + "`available_doc_ids` constrains citations to known local documents.\n", + "\n", + "- Reduces hallucinated references\n", + "- Gives the model a concrete allowed set of document identifiers" ] }, { "cell_type": "markdown", - "id": "df790772-6554-4e41-b21c-626d73c8ad79", + "id": "8911b1db", "metadata": {}, "source": [ - "### Validator Failure Example\n", + "## Purpose of the Manual Failure Cell\n", "\n", - "The validator can also be tested manually.\n", + "The manual failure example builds the same retry object used by the validator path.\n", "\n", - "If the validation rule fails, the validator raises `ModelRetry`, which instructs the agent to retry the LLM call with improved instructions." + "- Bypasses the model call\n", + "- Shows the retry message used when citation requirements are not met\n", + "- Keeps the notebook executable without intentionally raising an exception" ] }, { "cell_type": "code", - "execution_count": 14, - "id": "1d332ae9-b4de-4501-84c8-3cea4fa772a9", + "execution_count": 18, + "id": "975c50ca-65ae-4838-8d44-599fee1d461f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\n", - "Validated output:\n", - "\n", - "answer='The greenhouse effect is a natural process that warms the Earth’s surface. When the Sun\\'s energy reaches the Earth, some of it is reflected back to space and the rest is absorbed, warming the planet. The absorbed energy is then re-radiated as infrared energy (heat). Greenhouse gases—such as carbon dioxide, methane, and water vapor—trap some of this heat in the atmosphere, preventing it from escaping back into space, which keeps the Earth warm enough to support life.\\n\\nIncreased levels of these gases due to human activities, such as burning fossil fuels and deforestation, enhance the greenhouse effect, leading to global warming and climate change. \\n\\nSources:\\n1. Intergovernmental Panel on Climate Change (IPCC) - \"Climate Change 2021: The Physical Science Basis\".\\n2. National Aeronautics and Space Administration (NASA) - \"The Greenhouse Effect\".' sources=[SourceRef(doc_id='1', quote='Greenhouse gases trap heat in the atmosphere, preventing it from escaping back into space.'), SourceRef(doc_id='2', quote='Increased levels of these gases due to human activities lead to global warming.')]\n" + "Validator failure example: Answer references documents but sources are empty.\n" ] } ], "source": [ - "import asyncio\n", - "\n", - "asyncio.run(utils.run_validator_example(validator_agent))" + "# Build the retry exception used by the missing-sources validator path.\n", + "retry = utils.build_missing_sources_retry()\n", + "_LOG.info(\"Validator failure example: %s\", retry)\n", + "retry\n", + "# The retry object shows the message returned when sources are missing." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "1d332ae9-b4de-4501-84c8-3cea4fa772a9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AnswerWithSources(answer='Atlas billing plans include options for Team and Enterprise levels, which among other features, support two-factor authentication (2FA). Users can manage billing by downloading invoices through the Settings > Billing section of the platform. Unfortunately, more detailed distinctions or pricing specifics between plans were not found in the available documents.', sources=[SourceRef(doc_id='security', quote='Atlas supports two-factor authentication (2FA) for Team and Enterprise plans.'), SourceRef(doc_id='billing', quote='You can download invoices from Settings > Billing.')])" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Run the validator example through the async API helper.\n", + "asyncio.run(utils.run_validator_example(validator_agent))\n", + "# The output has passed both Pydantic schema validation and custom validation." ] }, { @@ -743,9 +975,9 @@ "id": "7828a0ab", "metadata": {}, "source": [ - "## Streaming\n", + "# Streaming\n", "\n", - "Streaming allows tokens to be returned as they are generated.\n", + "Streaming returns tokens as the model generates them.\n", "\n", "Benefits:\n", "\n", @@ -756,62 +988,59 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 20, "id": "7fbec717", - "metadata": { - "lines_to_next_cell": 2 - }, + "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Streaming:\n", - "Unit tests are a type of software testingUnit tests are a type of software testing that focuses on verifying the correctness of individualUnit tests are a type of software testing that focuses on verifying the correctness of individual components or functions of a program in isolationUnit tests are a type of software testing that focuses on verifying the correctness of individual components or functions of a program in isolation. Typically written by developers, these testsUnit tests are a type of software testing that focuses on verifying the correctness of individual components or functions of a program in isolation. Typically written by developers, these tests evaluate the smallest parts of the software—Unit tests are a type of software testing that focuses on verifying the correctness of individual components or functions of a program in isolation. Typically written by developers, these tests evaluate the smallest parts of the software—usually functions or methods—ensuring thatUnit tests are a type of software testing that focuses on verifying the correctness of individual components or functions of a program in isolation. Typically written by developers, these tests evaluate the smallest parts of the software—usually functions or methods—ensuring that they behave as expected under various conditions.Unit tests are a type of software testing that focuses on verifying the correctness of individual components or functions of a program in isolation. Typically written by developers, these tests evaluate the smallest parts of the software—usually functions or methods—ensuring that they behave as expected under various conditions. The goal of unit testing is to identifyUnit tests are a type of software testing that focuses on verifying the correctness of individual components or functions of a program in isolation. Typically written by developers, these tests evaluate the smallest parts of the software—usually functions or methods—ensuring that they behave as expected under various conditions. The goal of unit testing is to identify bugs early in the development processUnit tests are a type of software testing that focuses on verifying the correctness of individual components or functions of a program in isolation. Typically written by developers, these tests evaluate the smallest parts of the software—usually functions or methods—ensuring that they behave as expected under various conditions. The goal of unit testing is to identify bugs early in the development process, promote code reliability, andUnit tests are a type of software testing that focuses on verifying the correctness of individual components or functions of a program in isolation. Typically written by developers, these tests evaluate the smallest parts of the software—usually functions or methods—ensuring that they behave as expected under various conditions. The goal of unit testing is to identify bugs early in the development process, promote code reliability, and facilitate easier debugging and future codeUnit tests are a type of software testing that focuses on verifying the correctness of individual components or functions of a program in isolation. Typically written by developers, these tests evaluate the smallest parts of the software—usually functions or methods—ensuring that they behave as expected under various conditions. The goal of unit testing is to identify bugs early in the development process, promote code reliability, and facilitate easier debugging and future code changes by providing a safety netUnit tests are a type of software testing that focuses on verifying the correctness of individual components or functions of a program in isolation. Typically written by developers, these tests evaluate the smallest parts of the software—usually functions or methods—ensuring that they behave as expected under various conditions. The goal of unit testing is to identify bugs early in the development process, promote code reliability, and facilitate easier debugging and future code changes by providing a safety net that confirms existing functionality remains intact. ByUnit tests are a type of software testing that focuses on verifying the correctness of individual components or functions of a program in isolation. Typically written by developers, these tests evaluate the smallest parts of the software—usually functions or methods—ensuring that they behave as expected under various conditions. The goal of unit testing is to identify bugs early in the development process, promote code reliability, and facilitate easier debugging and future code changes by providing a safety net that confirms existing functionality remains intact. By automating these tests, teams can increaseUnit tests are a type of software testing that focuses on verifying the correctness of individual components or functions of a program in isolation. Typically written by developers, these tests evaluate the smallest parts of the software—usually functions or methods—ensuring that they behave as expected under various conditions. The goal of unit testing is to identify bugs early in the development process, promote code reliability, and facilitate easier debugging and future code changes by providing a safety net that confirms existing functionality remains intact. By automating these tests, teams can increase efficiency and streamline the development workflow.---\n", - "Streaming failed; falling back to run(). 'StreamedRunResult' object has no attribute 'get_final_result'\n", - "\n", - "\n", - "Non-streamed: AgentRunResult(output='Unit tests are a type of software testing that focuses on validating individual components or functions of a program in isolation to ensure they behave as expected. By testing small, discrete sections of code—usually at the level of functions or methods—developers can identify and fix bugs early in the development process, thereby enhancing code reliability and maintainability. Unit tests are typically automated and executed frequently, allowing for rapid feedback and promoting confidence in the stability of the codebase as it evolves.')\n" - ] + "data": { + "text/plain": [ + "Agent(model=OpenAIChatModel(), name=None, end_strategy='early', model_settings=None, output_type=, instrument=None)" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "stream_agent = Agent(\n", + "# Create an agent for the streaming example.\n", + "stream_agent = pydantic_ai.Agent(\n", " MODEL_ID, instructions=\"Write one short paragraph about unit tests.\"\n", ")\n", - "\n", - "if not hasattr(stream_agent, \"run_stream\"):\n", - " print(\"Streaming API not available; falling back to run().\")\n", - " result = await stream_agent.run(\"What are unit tests?\")\n", - " _print_result(\"Non-streamed:\", result)\n", - "else:\n", - " try:\n", - " async with stream_agent.run_stream(\"What are unit tests?\") as stream:\n", - " print(\"Streaming:\")\n", - " async for chunk in stream.stream_text():\n", - " print(chunk, end=\"\", flush=True)\n", - " print(\"---\")\n", - " result = await stream.get_final_result()\n", - " print(\"\\n\\nFinal result:\", result)\n", - " except Exception as e:\n", - " print(\"Streaming failed; falling back to run().\", e)\n", - " result = await stream_agent.run(\"What are unit tests?\")\n", - " print(\"\\n\\nNon-streamed:\", result)" + "stream_agent\n", + "# The streaming agent is ready to produce incremental text." ] }, { - "cell_type": "markdown", - "id": "fced043d-9333-42d2-8b3d-0f80b1ed1c7b", + "cell_type": "code", + "execution_count": 21, + "id": "5a4a5245", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Streaming output:\n", + "Unit tests are automated tests that verify the functionality of the smallest parts of an application, typically individual functions or methods, to ensure they work as intended. By isolating each unit of code, these tests help detect bugs early, simplify debugging, and provide confidence that changes or additions do not break existing functionality. Unit tests are a fundamental practice in software development that contribute to more reliable and maintainable code.\n" + ] + }, + { + "data": { + "text/plain": [ + "'Unit tests are automated tests that verify the functionality of the smallest parts of an application, typically individual functions or methods, to ensure they work as intended. By isolating each unit of code, these tests help detect bugs early, simplify debugging, and provide confidence that changes or additions do not break existing functionality. Unit tests are a fundamental practice in software development that contribute to more reliable and maintainable code.'" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "### What happened in the code\n", - "\n", - "- We created an agent and attempted to call the model using streaming mode.\n", - "- With streaming, tokens are yielded as the model generates them instead of waiting for the full response.\n", - "- This improves perceived responsiveness for chat apps and UIs.\n", - "\n", - "**Why PydanticAI is useful here:**\n", - "Streaming helps build better user experiences. You can display partial output instantly while the model continues generating, which is critical for interactive assistants." + "# Run the streaming helper and return the final result.\n", + "asyncio.run(utils.run_streaming_demo(stream_agent))\n", + "# The helper logs streamed text and returns the final result." ] }, { @@ -819,92 +1048,64 @@ "id": "52c6072a", "metadata": {}, "source": [ - "## Provider Configuration\n", + "# Provider Configuration\n", "\n", - "Model objects let you configure providers directly (e.g., base URLs).\n", + "Model objects let you configure providers directly, such as base URLs.\n", "\n", - "You can supply an explicit model object instead of a string ID. This is where you would set provider-specific options (e.g., `base_url`).\n" + "Use an explicit model object when provider-specific options, such as `base_url`, are needed.\n" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 25, "id": "c6e3973b", - "metadata": { - "lines_to_next_cell": 2 - }, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Explicit model unavailable; using string model ID. OpenAIChatModel.__init__() got an unexpected keyword argument 'model'\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/tmp/ipykernel_24/2437657520.py:4: DeprecationWarning: `OpenAIModel` was renamed to `OpenAIChatModel` to clearly distinguish it from `OpenAIResponsesModel` which uses OpenAI's newer Responses API. Use that unless you're using an OpenAI Chat Completions-compatible API, or require a feature that the Responses API doesn't support yet like audio.\n", - " explicit_model = OpenAIModel(\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Explicit model (or fallback): AgentRunResult(output='Hello! How can I assist you today?')\n" + "Using OpenAI model with model_name='gpt-5-nano'.\n", + "Using explicit model object.\n" ] } ], "source": [ - "explicit_model = None\n", - "try:\n", - " from pydantic_ai.models.openai import OpenAIModel\n", - "\n", - " explicit_model = OpenAIModel(\n", - " model=MODEL_ID.split(\":\", 1)[-1],\n", - " api_key=os.getenv(\"OPENAI_API_KEY\"),\n", - " base_url=os.getenv(\"OPENAI_BASE_URL\"),\n", - " )\n", - " print(\"Using explicit OpenAIModel.\")\n", - "except Exception:\n", - " try:\n", - " from pydantic_ai.models.openai import OpenAIChatModel\n", - "\n", - " explicit_model = OpenAIChatModel(\n", - " model=MODEL_ID.split(\":\", 1)[-1],\n", - " api_key=os.getenv(\"OPENAI_API_KEY\"),\n", - " base_url=os.getenv(\"OPENAI_BASE_URL\"),\n", - " )\n", - " print(\"Using explicit OpenAIChatModel.\")\n", - " except Exception as e2:\n", - " print(\"Explicit model unavailable; using string model ID.\", e2)\n", - "\n", - "agent = Agent(explicit_model or MODEL_ID, instructions=\"Be concise.\")\n", - "try:\n", - " result = await agent.run(\"Say hello in one sentence.\")\n", - " print(\"Explicit model (or fallback):\", result)\n", - "except Exception as e:\n", - " print(\"Error: \", e)" + "# Build an explicit provider model object when the installed API supports it.\n", + "explicit_model = utils.build_explicit_openai_model(MODEL_ID)\n", + "if explicit_model is None:\n", + " _LOG.info(\"Explicit model unavailable; using string model ID.\")\n", + "else:\n", + " _LOG.info(\"Using explicit model object.\")\n", + "{\"explicit_model_available\": explicit_model is not None}\n", + "# Provider configuration is either explicit or falls back to `MODEL_ID`." ] }, { - "cell_type": "markdown", - "id": "867a9074-dc6b-435d-b2ee-ff41eb7ce217", - "metadata": {}, + "cell_type": "code", + "execution_count": 26, + "id": "6b8fc187", + "metadata": { + "lines_to_next_cell": 2 + }, + "outputs": [ + { + "data": { + "text/plain": [ + "AgentRunResult(output='Hello!')" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "### What happened in the code\n", - "\n", - "- Instead of using a string model ID, we attempted to create an explicit provider model object.\n", - "- This allows provider-specific configuration such as:\n", - " - custom base URLs\n", - " - custom API keys\n", - " - proxy settings\n", - "- If explicit model classes aren't available in the installed version, we fall back to using the string model ID.\n", - "\n", - "**Why PydanticAI is useful here:**\n", - "Explicit provider configuration is what you use in real deployments: enterprise gateways, self-hosted endpoints, proxies, and custom routing." + "# Run an agent with the explicit provider model when available.\n", + "agent = pydantic_ai.Agent(explicit_model or MODEL_ID, instructions=\"Be concise.\")\n", + "result = asyncio.run(agent.run(\"Say hello in one sentence.\"))\n", + "result\n", + "# The result confirms that the provider configuration can execute a request." ] }, { @@ -912,7 +1113,7 @@ "id": "5c47562a", "metadata": {}, "source": [ - "## 11) AgentRun\n", + "# AgentRun\n", "\n", "AgentRun objects contain metadata about an agent execution.\n", "\n", @@ -921,52 +1122,49 @@ "- token usage\n", "- message history\n", "- tool calls\n", - "- final output" + "- final output\n", + "\n", + "Run metadata helps debug and control agents.\n", + "\n", + "- Observability: inspect messages and tool calls\n", + "- Cost tracking: inspect token usage\n", + "- Governance: keep execution details available for review" ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 27, "id": "52652ef6", "metadata": { "lines_to_next_cell": 2 }, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Output: A unit test is a type of software testing that involves testing individual components or functions of a program in isolation to ensure they work as intended.\n", - "Messages (new): 2\n", - "Usage: \n" - ] + "data": { + "text/plain": [ + "{'output': 'A unit test is a type of software test that verifies the functionality of a small, specific section of code, usually a single function or method, to ensure it works as intended.',\n", + " 'messages_new': 2,\n", + " 'usage': }" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "meta_agent = Agent(MODEL_ID, instructions=\"Answer in one sentence.\")\n", - "result = await meta_agent.run(\"What is a unit test?\")\n", + "# Run an agent and collect execution metadata.\n", + "meta_agent = pydantic_ai.Agent(MODEL_ID, instructions=\"Answer in one sentence.\")\n", + "result = asyncio.run(meta_agent.run(\"What is a unit test?\"))\n", "usage = getattr(result, \"usage\", None)\n", "message_count = len(result.new_messages())\n", - "print(\"Output:\", result.output)\n", - "print(\"Messages (new):\", message_count)\n", - "print(\"Usage:\", usage)" - ] - }, - { - "cell_type": "markdown", - "id": "86d0d2b4-7b95-40e5-ba82-1a9083c41c2f", - "metadata": {}, - "source": [ - "### What happened in the code\n", - "\n", - "- We ran an agent and inspected the returned result object.\n", - "- The result object can include metadata such as:\n", - " - token usage (cost visibility)\n", - " - message history (debugging)\n", - " - tool calls (auditing agent behavior)\n", - "\n", - "**Why PydanticAI is useful here:**\n", - "When agents behave unexpectedly, metadata is how you debug and control them. This is essential for observability, cost tracking, and governance." + "run_metadata = {\n", + " \"output\": result.output,\n", + " \"messages_new\": message_count,\n", + " \"usage\": usage,\n", + "}\n", + "run_metadata\n", + "# The metadata summarizes output, message count, and usage details." ] }, { @@ -974,88 +1172,100 @@ "id": "ed489922", "metadata": {}, "source": [ - "## 12) Usage limits and model settings\n", + "# Usage Limits and Model Settings\n", "\n", "Usage limits help control:\n", "\n", "- API cost\n", "- runaway loops\n", - "- excessive token usage" + "- excessive token usage\n", + "\n", + "`PydanticAI` supports safety and cost controls for production LLM systems." ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 28, "id": "76413843", - "metadata": {}, + "metadata": { + "lines_to_next_cell": 2 + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Model settings + usage limits:\n", - "Unit tests are automated tests that validate individual components or functions of a software application to ensure they work as intended, typically by checking their outputs against expected results.\n" + "Loaded ModelSettings and UsageLimits classes.\n" ] } ], "source": [ - "from pydantic_ai import Agent\n", - "\n", - "\n", - "# Version-tolerant imports for ModelSettings + UsageLimits\n", - "try:\n", - " # common in newer versions\n", - " from pydantic_ai import ModelSettings, UsageLimits\n", - "except Exception:\n", - " # fallback seen in some versions\n", - " from pydantic_ai.models import ModelSettings # type: ignore\n", - " from pydantic_ai.usage import UsageLimits # type: ignore\n", - "\n", - "\n", - "settings_agent = Agent(\n", - " MODEL_ID,\n", - " instructions=\"Answer in a single sentence.\",\n", - " model_settings=ModelSettings(temperature=0.2),\n", - ")\n", - "\n", - "result = await settings_agent.run(\n", - " \"Explain what unit tests are.\",\n", - " usage_limits=UsageLimits(request_limit=3),\n", - ")\n", - "\n", - "print(\"Model settings + usage limits:\")\n", - "print(result.output)" + "# Load version-tolerant classes for model settings and usage limits.\n", + "ModelSettings, UsageLimits = utils.get_settings_classes()\n", + "_LOG.info(\"Loaded ModelSettings and UsageLimits classes.\")\n", + "{\n", + " \"model_settings_class\": ModelSettings.__name__,\n", + " \"usage_limits_class\": UsageLimits.__name__,\n", + "}\n", + "# The installed PydanticAI version determines where these classes come from." ] }, { - "cell_type": "markdown", - "id": "cc8440b6-4d88-43db-9eac-6d86061d6dc4", + "cell_type": "code", + "execution_count": 29, + "id": "459e5581", "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Agent(model=OpenAIChatModel(), name=None, end_strategy='early', model_settings={'temperature': 0.2}, output_type=, instrument=None)" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "### What happened in the code\n", - "\n", - "- `ModelSettings(temperature=0.2)` controls response randomness:\n", - " - lower temperature = more deterministic outputs\n", - "- `UsageLimits(request_limit=3)` sets guardrails on usage:\n", - " - helps prevent runaway retries or excessive calls\n", - "- We ran the agent with these settings applied.\n", - "\n", - "**Why PydanticAI is useful here:**\n", - "PydanticAI makes it easy to add safety and cost controls to LLM systems. These controls matter in production where reliability and spend both need limits." + "# Create an agent with deterministic model settings.\n", + "settings_agent = pydantic_ai.Agent(\n", + " MODEL_ID,\n", + " instructions=\"Answer in a single sentence.\",\n", + " model_settings=ModelSettings(temperature=0.2),\n", + ")\n", + "settings_agent\n", + "# The agent has a low-temperature model setting." ] }, { - "cell_type": "markdown", - "id": "cddca283", + "cell_type": "code", + "execution_count": 30, + "id": "ad306084", "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Unit tests are automated tests that verify the correctness of individual components or functions of a software application in isolation.'" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "## Best Practices\n", + "# Run the settings example with a request limit.\n", + "result = asyncio.run(\n", + " settings_agent.run(\n", + " \"Explain what unit tests are.\",\n", + " usage_limits=UsageLimits(request_limit=3),\n", + " )\n", + ")\n", "\n", - "1. Always define clear schemas using Pydantic models.\n", - "2. Keep schemas simple and explicit.\n", - "3. Use retries for robustness.\n", - "4. Add tools for external integrations.\n", - "5. Use async execution for production systems." + "result.output\n", + "# The response was generated with model settings and usage limits applied." ] }, { @@ -1063,7 +1273,7 @@ "id": "e1bedef2", "metadata": {}, "source": [ - "## Troubleshooting\n", + "# Troubleshooting\n", "- Missing API key: set `OPENAI_API_KEY` (or your provider-specific key).\n", "- Event loop errors in notebooks: use `await agent.run(...)` instead of `run_sync`.\n", "- Validation errors: revise `output_type` or the validator to match expected output.\n" diff --git a/tutorials/tutorial_pydanticAI/pydanticai.API.py b/tutorials/tutorial_pydanticAI/pydanticai.API.py index d15fe8e17..5b47e1db1 100644 --- a/tutorials/tutorial_pydanticAI/pydanticai.API.py +++ b/tutorials/tutorial_pydanticAI/pydanticai.API.py @@ -6,7 +6,7 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.19.0 +# jupytext_version: 1.17.1 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -17,88 +17,89 @@ # %load_ext autoreload # %autoreload 2 +# System libraries. import logging +# Third party libraries. -import helpers.hnotebook as ut +# Common plotting and dataframe libraries are loaded for notebook exploration. -ut.config_notebook() +# %% +# System libraries. +import asyncio +import dataclasses +import os -# Initialize logger. -logging.basicConfig(level=logging.INFO) -_LOG = logging.getLogger(__name__) +# Third party libraries. +import dotenv +import nest_asyncio +import pydantic +import pydantic_ai -# %% +# Local utilities. import pydanticai_API_utils as utils +# Notebook-specific imports are ready for tutorial examples. + +# %% +# Configure notebook logging. +_LOG = logging.getLogger(__name__) +utils.init_logger(_LOG) +_LOG.info("Notebook logger initialized.") +# Notebook and utility logs now print in Jupyter. + # %% [markdown] -# ## PydanticAI API Tutorial Introduction -# -# PydanticAI is a lightweight framework for building LLM-powered applications with **structured outputs using Pydantic models**. -# -# Unlike traditional LLM APIs that return unstructured text, PydanticAI ensures responses conform to a predefined schema. -# -# This notebook covers: +# # Summary # -# - Core concepts -# - Agent API -# - Structured outputs -# - Tool usage -# - Validation and retries -# - Async execution +# This notebook introduces `PydanticAI` APIs for building LLM workflows. # -# By the end, you will understand how to build reliable LLM pipelines using structured outputs. +# Topics include structured outputs, tools, dependencies, validators, streaming, +# provider configuration, run metadata, and usage limits. # %% [markdown] -# # Table of Contents -# -# 1. Introduction -# 2. Why PydanticAI exists -# 3. Installation -# 4. Minimal Example -# 5. Core Concepts -# 6. Structured Outputs -# 7. Validation -# 8. Tools -# 9. Dependencies -# 10. Async Execution -# 11. Advanced Features -# 12. Best Practices -# 13. Summary +# # PydanticAI API Tutorial Introduction +# +# `PydanticAI` is a lightweight framework for building LLM-powered applications +# with structured outputs using `Pydantic` models. +# +# Unlike traditional LLM APIs that return unstructured text, `PydanticAI` +# ensures responses conform to a predefined schema. # %% [markdown] -# ### Why PydanticAI Exists +# ## Why PydanticAI Exists # -# LLMs typically return unstructured text. +# Key problem: LLMs typically return unstructured text. # -# Example: +# Example prompt: # -# User prompt: # "Extract product information from this description" # -# LLM output: +# Example LLM output: +# # "The product is an iPhone 15 priced at $999." # # This output is difficult to use programmatically. # -# What we want instead: +# Desired structured output: # +# ```json # { # "product_name": "iPhone 15", # "price": 999 # } +# ``` # -# PydanticAI solves this problem by: +# `PydanticAI` solves this problem by: # -# - Defining schemas using **Pydantic models** +# - Defining schemas using Pydantic models # - Enforcing structured outputs # - Automatically retrying when validation fails # - Providing a simple agent abstraction for LLM interaction # %% [markdown] -# ### Mental Model +# ## Mental Model # -# ``` +# ```text # User Prompt # v # PydanticAI Agent @@ -112,239 +113,246 @@ # Structured Output # ``` -# %% [markdown] -# ## Installation -# -# We install a minimal set of packages to keep the notebook self-contained and reproducible. -# -# This notebook uses `pydantic-ai`, `pydantic`, and `python-dotenv`. -# - # %% -# !pip install -q pydantic-ai +# Load environment variables from a local dotenv file if one exists. +env_path = dotenv.find_dotenv(usecwd=True) +dotenv.load_dotenv(env_path, override=True) +_LOG.info("dotenv path: %s", env_path or "") +env_path or "" +# Environment variables are available to the model configuration cells. # %% -import os -from dotenv import load_dotenv, find_dotenv -import nest_asyncio - -nest_asyncio.apply() - - -env_path = find_dotenv(usecwd=True) -load_dotenv(env_path, override=True) - -MODEL_ID = os.getenv("PYDANTIC_AI_MODEL", "openai:gpt-4.1-mini") -print("dotenv path:", env_path or "") -print("PYDANTIC_AI_MODEL:", MODEL_ID) -print("OPENAI_API_KEY:", utils._mask(os.getenv("OPENAI_API_KEY"))) +# Read the model identifier from the environment. +MODEL_ID = os.getenv("PYDANTIC_AI_MODEL") +utils.log_environment(env_path, MODEL_ID) +{"model_id": MODEL_ID} +# The tutorial examples will use the configured model identifier. # %% [markdown] -# ### Running the Notebook +# # Core Concepts +# +# PydanticAI revolves around a few important abstractions. +# +# ## Agent +# +# The `Agent` is the main interface for interacting with the model. +# +# It manages: +# +# - LLM calls +# - structured outputs +# - retries +# - tool usage +# +# ## output_type +# +# Defines the expected structured output. +# +# This must be a Pydantic model. +# +# ## Tools +# +# Functions that the agent can call during reasoning. +# +# Tools allow agents to interact with external systems such as APIs or databases. # -# To run the examples you must set your API key. # -# Example: -# ``` -# export OPENAI_API_KEY="your_key_here" -# ``` # %% [markdown] -# ## Minimal Example +# # Minimal Example # # The quickest way to understand PydanticAI is through a small example. # # We define a schema using Pydantic and instruct the agent to produce that structured output. - -# %% -from pydantic import BaseModel -from pydantic_ai import Agent +# +# +# ############################################################################# +# City +# ############################################################################# -class City(BaseModel): +# %% +# Define the output schema for the minimal example. +class City(pydantic.BaseModel): name: str country: str population: int -agent = Agent("openai:gpt-4o-mini", output_type=City) +City +# The schema defines the exact output shape expected from the model. + +# %% +# Create an agent that must return `City`. +agent = pydantic_ai.Agent(MODEL_ID, output_type=City) +agent +# The agent is configured to validate model output against class `City`. + +# %% +# Run the minimal example agent. result = agent.run_sync("Tell me about Paris") result.output +# The result is a validated `City` object. # %% [markdown] -# ### What Happened? -# -# 1. A Pydantic schema (`City`) defines the expected output structure. -# 2. The `Agent` sends the prompt to the LLM. -# 3. The LLM response is validated against the schema. -# 4. If validation succeeds, the structured result is returned. +# # Resolving the RuntimeError in Jupyter # %% [markdown] -# ## Core Concepts -# -# PydanticAI revolves around a few important abstractions. -# -# ### Agent -# -# The `Agent` is the main interface for interacting with the model. -# -# It manages: -# -# - LLM calls -# - structured outputs -# - retries -# - tool usage +# Key thing to remember: Jupyter already runs an active event loop. # -# ### output_type -# -# Defines the expected structured output. -# -# This must be a Pydantic model. +# - `agent.run_sync()` can raise a `RuntimeError` in notebook environments +# - `nest_asyncio` patches the notebook event loop so nested async execution can work +# - After applying `nest_asyncio`, the async `PydanticAI` examples can run inside cells + +# %% +# Enable nested event loops for notebook execution. +nest_asyncio.apply() +_LOG.info("Nested event loop support enabled.") +# Async PydanticAI examples can now run from notebook cells. + +# %% [markdown] +# Now try running the previous cell that had the error. + +# %% [markdown] +# # Structured Outputs with Pydantic # -# ### Tools +# `PydanticAI` turns LLM responses into structured data. # -# Functions that the agent can call during reasoning. -# -# Tools allow agents to interact with external systems such as APIs or databases. +# - Store validated outputs in databases +# - Feed typed objects into analytics +# - Pass structured data downstream without brittle string parsing # # +# ############################################################################# +# Product +# ############################################################################# -# %% [markdown] -# ## Structured Outputs with Pydantic # %% -from pydantic import BaseModel - - -class Product(BaseModel): +# Define a product schema for structured extraction. +class Product(pydantic.BaseModel): name: str price: float category: str -agent = Agent("openai:gpt-4o-mini", output_type=Product) +Product +# The schema captures the product fields we want to extract. -agent.run_sync("Describe the Apple AirPods Pro").output +# %% +# Create an agent that must return `Product`. +agent = pydantic_ai.Agent(MODEL_ID, output_type=Product) +agent +# The agent is configured to return product data with typed fields. + +# %% +# Ask the model for structured product information. +agent.run_sync("Describe the Apple AirPods Pro").output +# The response is validated as a `Product` class object. # %% [markdown] -# ### What happened in the code +# # Validation and Retries # -# - We defined a `Product` schema (name, price, category). -# - The agent is configured to produce outputs that conform to this schema. -# - When the model answers, PydanticAI validates that: -# - `price` is a number -# - fields exist with the right types -# - the structure matches exactly +# Real LLM outputs are inconsistent. +# +# - Schema validation checks the generated structure +# - Retries let `PydanticAI` ask the model to repair invalid output +# - The notebook avoids custom parsing and retry logic in each prompt # -# **Why PydanticAI is useful here:** -# This turns LLM responses into structured data you can store in databases, feed into analytics, or pass downstream in an application without brittle string parsing. - -# %% [markdown] -# ## Validation and Retries # -# If the LLM produces an output that does not match the schema, PydanticAI automatically retries. # -# This greatly improves reliability. +# ############################################################################# +# Person +# ############################################################################# # %% -class Person(BaseModel): +# Define a schema that requires an integer age. +class Person(pydantic.BaseModel): name: str age: int -agent = Agent("openai:gpt-4o-mini", output_type=Person, retries=2) +Person +# The schema enforces integer typing for age values. -agent.run_sync("Tell me about Albert Einstein") +# %% +# Configure retries so schema validation failures can be corrected. +agent = pydantic_ai.Agent(MODEL_ID, output_type=Person, retries=2) +agent +# The agent can retry when model output does not match `Person`. -# %% [markdown] -# ### What happened in the code -# -# - We defined a `Person` schema with `name` and `age`. -# - We set `retries=2` on the agent. -# - If the model output fails schema validation (missing fields, wrong types), PydanticAI automatically retries the model call to get a valid output. -# -# **Why PydanticAI is useful here:** -# Real LLM outputs are inconsistent. Automatic schema validation + retry gives you reliability without writing custom parsing and retry logic for every prompt. +# %% +# Run the retry-enabled agent. +agent.run_sync("Tell me about Albert Einstein") +# The result is a validated `Person` run result. # %% [markdown] -# ## Tools +# # Tools # # Agents can call Python functions as tools. +# +# - Tools let the model interact with real functions and external systems +# - Tools are useful for APIs, databases, calculations, and deterministic helpers +# - Tool calls reduce the chance that the model invents facts # %% -agent = Agent("openai:gpt-4o-mini", tools=[utils.get_weather]) +# Create an agent with a deterministic weather tool. +agent = pydantic_ai.Agent(MODEL_ID, tools=[utils.get_weather]) +agent +# The agent can call `utils.get_weather()` while answering. +# %% +# Ask a question that should use the weather tool. agent.run_sync("What is the weather in Tokyo?") +# The run result includes the tool-backed weather answer. # %% [markdown] -# ### What happened in the code +# # Dependencies # -# - We defined a Python function `get_weather(city)` that returns a deterministic string. -# - We passed it into the agent via `tools=[get_weather]`. -# - When the user asks about weather, the agent can choose to call the tool to get the answer instead of hallucinating. +# Dependencies inject runtime context into agents and tools. # -# **Why PydanticAI is useful here:** -# Tools let the model interact with real functions and external systems. This is how you build agents that do real work (APIs, databases, calculations) rather than confidently inventing facts. - -# %% [markdown] -# ## Dependencies +# - Example values: tenant IDs, API clients, feature flags, and environment context +# - Benefit: tools can access context without global variables or prompt string formatting # -# Dependencies allow agents to access external resources or shared state. - -# %% -from dataclasses import dataclass -from pydantic_ai import Agent +# +# ############################################################################# +# Config +# ############################################################################# -@dataclass +# %% +# Define the dependency object passed into the agent at run time. +@dataclasses.dataclass class Config: company: str -agent = Agent("openai:gpt-4o-mini", deps_type=Config, tools=[utils.company_name]) - -result = agent.run_sync( - "What company is configured?", deps=Config(company="OpenAI") -) -print(result.output) +Config +# The dependency schema describes runtime context available to tools. -# %% [markdown] -# ### What happened in the code -# -# - `deps_type=Config` declares the *shape* of runtime context the agent can receive. -# - At run time, we pass an instance like `Config(company="OpenAI")`. -# - Tools (or other agent logic) can access this via `RunContext.deps`, so the agent can use configuration/state without hardcoding it into prompts. -# -# **Why PydanticAI is useful here:** -# Dependencies are a clean way to inject runtime configuration (tenant ID, API clients, feature flags, environment context) into agents and tools without relying on global variables or string formatting prompts. - -# %% [markdown] -# ## Async Execution -# -# PydanticAI supports asynchronous execution for scalable applications. # %% -import asyncio - -asyncio.run(utils.run_agent(agent)) +# Create an agent that receives `Config` dependencies. +# `deps_type=Config` declares the shape of runtime context the agent can receive. +agent = pydantic_ai.Agent(MODEL_ID, deps_type=Config, tools=[utils.company_name]) +agent +# Tools can access `Config` through the PydanticAI run context. -# %% [markdown] -# ### What happened in the code -# -# - We defined an async function that calls `await agent.run(...)`. -# - Async execution is helpful for applications that need concurrency (web servers, batch pipelines, background jobs). -# - `asyncio.run(...)` runs the coroutine in a notebook-safe way. -# -# **Why PydanticAI is useful here:** -# Most real systems are async. PydanticAI supports async natively, so you can run many agent calls concurrently without blocking your app. +# %% +# Run the dependency-aware agent with a concrete configuration. +result = agent.run_sync( + "What company is configured?", deps=Config(company="OpenAI") +) +result.output +# The answer reflects the runtime dependency value. # %% [markdown] -# ## Advanced API Features +# # Advanced API Features # # The following sections demonstrate more advanced capabilities of PydanticAI. # @@ -359,7 +367,7 @@ class Config: # Beginners can safely skip this section on a first read. # %% [markdown] -# ## Result Validators +# # Result Validators # # Result validators allow you to enforce additional rules on model outputs. # @@ -368,73 +376,142 @@ class Config: # # Example: if an answer claims to use documents, it must include at least one source. -# %% -from pydantic import BaseModel -from pydantic_ai import Agent - -MODEL_ID = "openai:gpt-4o-mini" +# %% [markdown] +# ## Validation Flow +# +# In this section, validation happens in two stages: +# +# 1. `Schema validation`: the model output must match `AnswerWithSources`. +# 2. `Business-rule validation`: the registered `output_validator` enforces +# citation quality rules that schema alone cannot enforce. +# +# Execution order: +# +# ```text +# model output -> Pydantic schema validation -> output_validator -> final result +# ``` +# +# +# ############################################################################# +# SourceRef +# ############################################################################# -class SourceRef(BaseModel): +# %% +# Define source citation schemas for validator examples. +class SourceRef(pydantic.BaseModel): doc_id: str quote: str -class AnswerWithSources(BaseModel): +# ############################################################################# +# AnswerWithSources +# ############################################################################# + + +class AnswerWithSources(pydantic.BaseModel): answer: str sources: list[SourceRef] -validator_agent = Agent( +AnswerWithSources +# The schemas describe answers that include source citations. + +# %% +# Build validator instructions from local document ids. +available_doc_ids = utils.get_available_document_ids() +validator_instructions = ( + "Use the search_documents tool to retrieve evidence from local documents. " + f"Cite only these doc ids: {available_doc_ids}. " + "For each source, copy the quote text exactly from tool output." +) +{ + "available_doc_ids": available_doc_ids, + "validator_instruction_length": len(validator_instructions), +} +# The instructions constrain citations to the local document ids. + +# %% +# Create an agent that returns answers with source references. +validator_agent = pydantic_ai.Agent( MODEL_ID, output_type=AnswerWithSources, - instructions=( - "Answer with short factual statements. " - "If you reference documents, include sources." - ), + instructions=validator_instructions, + tools=[utils.search_documents], ) -validator_agent.output_validator(utils.validate_sources) +validator_agent +# The validator agent can retrieve documents and return cited answers. # %% -try: - utils.validate_sources( - AnswerWithSources(answer="According to the documents...", sources=[]) - ) -except Exception as e: - print("Validator failure example:", e) +# Register a result validator that checks citations against local documents. +@validator_agent.output_validator +def validate_output( + result: AnswerWithSources, +) -> AnswerWithSources: + result = utils.validate_document_sources(result) + return result + + +{"validator_registered": True} +# The validator agent now enforces schema and source-reference rules. + # %% [markdown] -# ### What happened in the code +# ## What `@validator_agent.output_validator` Does +# +# The `@validator_agent.output_validator` decorator registers a post-processing +# validator for this specific agent. +# +# The validator receives the already schema-validated `AnswerWithSources` object. +# Then the validator calls `utils.validate_document_sources(...)` to enforce: +# +# - Source list required when answer claims document-backed statements +# - Maximum number of sources +# - No duplicate `(doc_id, quote)` pairs +# - Each `doc_id` must exist in the local dataset +# - Each `quote` must appear in the cited document + +# %% [markdown] +# ## Why `ModelRetry` Is Important +# +# When a rule is violated, the validator raises `ModelRetry`. # -# - We defined a schema `AnswerWithSources` where the model must return: -# - `answer` (string) -# - `sources` (list of `{doc_id, quote}`) -# - We attached an `output_validator` that enforces *logical rules* beyond the schema: -# - if the answer mentions docs, sources must not be empty -# - max 3 sources -# - no duplicate sources -# - If rules fail, we raise `ModelRetry`, which tells PydanticAI to retry the model call. +# `ModelRetry` tells `PydanticAI` to ask the model for another attempt instead +# of accepting bad output. # -# **Why PydanticAI is useful here:** -# Schemas catch structural mistakes. Validators catch logical mistakes. Together, they make LLM outputs production-grade by enforcing business rules automatically. +# ## Why `available_doc_ids` Is Included in Instructions +# +# `available_doc_ids` constrains citations to known local documents. +# +# - Reduces hallucinated references +# - Gives the model a concrete allowed set of document identifiers # %% [markdown] -# ### Validator Failure Example +# ## Purpose of the Manual Failure Cell # -# The validator can also be tested manually. +# The manual failure example builds the same retry object used by the validator path. # -# If the validation rule fails, the validator raises `ModelRetry`, which instructs the agent to retry the LLM call with improved instructions. +# - Bypasses the model call +# - Shows the retry message used when citation requirements are not met +# - Keeps the notebook executable without intentionally raising an exception # %% -import asyncio +# Build the retry exception used by the missing-sources validator path. +retry = utils.build_missing_sources_retry() +_LOG.info("Validator failure example: %s", retry) +retry +# The retry object shows the message returned when sources are missing. +# %% +# Run the validator example through the async API helper. asyncio.run(utils.run_validator_example(validator_agent)) +# The output has passed both Pydantic schema validation and custom validation. # %% [markdown] -# ## Streaming +# # Streaming # -# Streaming allows tokens to be returned as they are generated. +# Streaming returns tokens as the model generates them. # # Benefits: # @@ -443,94 +520,46 @@ class AnswerWithSources(BaseModel): # - progressive display of responses # %% -stream_agent = Agent( +# Create an agent for the streaming example. +stream_agent = pydantic_ai.Agent( MODEL_ID, instructions="Write one short paragraph about unit tests." ) +stream_agent +# The streaming agent is ready to produce incremental text. -if not hasattr(stream_agent, "run_stream"): - print("Streaming API not available; falling back to run().") - result = await stream_agent.run("What are unit tests?") - _print_result("Non-streamed:", result) -else: - try: - async with stream_agent.run_stream("What are unit tests?") as stream: - print("Streaming:") - async for chunk in stream.stream_text(): - print(chunk, end="", flush=True) - print("---") - result = await stream.get_final_result() - print("\n\nFinal result:", result) - except Exception as e: - print("Streaming failed; falling back to run().", e) - result = await stream_agent.run("What are unit tests?") - print("\n\nNon-streamed:", result) - - -# %% [markdown] -# ### What happened in the code -# -# - We created an agent and attempted to call the model using streaming mode. -# - With streaming, tokens are yielded as the model generates them instead of waiting for the full response. -# - This improves perceived responsiveness for chat apps and UIs. -# -# **Why PydanticAI is useful here:** -# Streaming helps build better user experiences. You can display partial output instantly while the model continues generating, which is critical for interactive assistants. +# %% +# Run the streaming helper and return the final result. +asyncio.run(utils.run_streaming_demo(stream_agent)) +# The helper logs streamed text and returns the final result. # %% [markdown] -# ## Provider Configuration +# # Provider Configuration # -# Model objects let you configure providers directly (e.g., base URLs). +# Model objects let you configure providers directly, such as base URLs. # -# You can supply an explicit model object instead of a string ID. This is where you would set provider-specific options (e.g., `base_url`). +# Use an explicit model object when provider-specific options, such as `base_url`, are needed. # # %% -explicit_model = None -try: - from pydantic_ai.models.openai import OpenAIModel - - explicit_model = OpenAIModel( - model=MODEL_ID.split(":", 1)[-1], - api_key=os.getenv("OPENAI_API_KEY"), - base_url=os.getenv("OPENAI_BASE_URL"), - ) - print("Using explicit OpenAIModel.") -except Exception: - try: - from pydantic_ai.models.openai import OpenAIChatModel - - explicit_model = OpenAIChatModel( - model=MODEL_ID.split(":", 1)[-1], - api_key=os.getenv("OPENAI_API_KEY"), - base_url=os.getenv("OPENAI_BASE_URL"), - ) - print("Using explicit OpenAIChatModel.") - except Exception as e2: - print("Explicit model unavailable; using string model ID.", e2) - -agent = Agent(explicit_model or MODEL_ID, instructions="Be concise.") -try: - result = await agent.run("Say hello in one sentence.") - print("Explicit model (or fallback):", result) -except Exception as e: - print("Error: ", e) +# Build an explicit provider model object when the installed API supports it. +explicit_model = utils.build_explicit_openai_model(MODEL_ID) +if explicit_model is None: + _LOG.info("Explicit model unavailable; using string model ID.") +else: + _LOG.info("Using explicit model object.") +{"explicit_model_available": explicit_model is not None} +# Provider configuration is either explicit or falls back to `MODEL_ID`. +# %% +# Run an agent with the explicit provider model when available. +agent = pydantic_ai.Agent(explicit_model or MODEL_ID, instructions="Be concise.") +result = asyncio.run(agent.run("Say hello in one sentence.")) +result +# The result confirms that the provider configuration can execute a request. -# %% [markdown] -# ### What happened in the code -# -# - Instead of using a string model ID, we attempted to create an explicit provider model object. -# - This allows provider-specific configuration such as: -# - custom base URLs -# - custom API keys -# - proxy settings -# - If explicit model classes aren't available in the installed version, we fall back to using the string model ID. -# -# **Why PydanticAI is useful here:** -# Explicit provider configuration is what you use in real deployments: enterprise gateways, self-hosted endpoints, proxies, and custom routing. # %% [markdown] -# ## 11) AgentRun +# # AgentRun # # AgentRun objects contain metadata about an agent execution. # @@ -540,89 +569,74 @@ class AnswerWithSources(BaseModel): # - message history # - tool calls # - final output +# +# Run metadata helps debug and control agents. +# +# - Observability: inspect messages and tool calls +# - Cost tracking: inspect token usage +# - Governance: keep execution details available for review # %% -meta_agent = Agent(MODEL_ID, instructions="Answer in one sentence.") -result = await meta_agent.run("What is a unit test?") +# Run an agent and collect execution metadata. +meta_agent = pydantic_ai.Agent(MODEL_ID, instructions="Answer in one sentence.") +result = asyncio.run(meta_agent.run("What is a unit test?")) usage = getattr(result, "usage", None) message_count = len(result.new_messages()) -print("Output:", result.output) -print("Messages (new):", message_count) -print("Usage:", usage) +run_metadata = { + "output": result.output, + "messages_new": message_count, + "usage": usage, +} +run_metadata +# The metadata summarizes output, message count, and usage details. # %% [markdown] -# ### What happened in the code -# -# - We ran an agent and inspected the returned result object. -# - The result object can include metadata such as: -# - token usage (cost visibility) -# - message history (debugging) -# - tool calls (auditing agent behavior) -# -# **Why PydanticAI is useful here:** -# When agents behave unexpectedly, metadata is how you debug and control them. This is essential for observability, cost tracking, and governance. - -# %% [markdown] -# ## 12) Usage limits and model settings +# # Usage Limits and Model Settings # # Usage limits help control: # # - API cost # - runaway loops # - excessive token usage +# +# `PydanticAI` supports safety and cost controls for production LLM systems. # %% -from pydantic_ai import Agent - +# Load version-tolerant classes for model settings and usage limits. +ModelSettings, UsageLimits = utils.get_settings_classes() +_LOG.info("Loaded ModelSettings and UsageLimits classes.") +{ + "model_settings_class": ModelSettings.__name__, + "usage_limits_class": UsageLimits.__name__, +} +# The installed PydanticAI version determines where these classes come from. -# Version-tolerant imports for ModelSettings + UsageLimits -try: - # common in newer versions - from pydantic_ai import ModelSettings, UsageLimits -except Exception: - # fallback seen in some versions - from pydantic_ai.models import ModelSettings # type: ignore - from pydantic_ai.usage import UsageLimits # type: ignore - -settings_agent = Agent( +# %% +# Create an agent with deterministic model settings. +settings_agent = pydantic_ai.Agent( MODEL_ID, instructions="Answer in a single sentence.", model_settings=ModelSettings(temperature=0.2), ) +settings_agent +# The agent has a low-temperature model setting. -result = await settings_agent.run( - "Explain what unit tests are.", - usage_limits=UsageLimits(request_limit=3), +# %% +# Run the settings example with a request limit. +result = asyncio.run( + settings_agent.run( + "Explain what unit tests are.", + usage_limits=UsageLimits(request_limit=3), + ) ) -print("Model settings + usage limits:") -print(result.output) - -# %% [markdown] -# ### What happened in the code -# -# - `ModelSettings(temperature=0.2)` controls response randomness: -# - lower temperature = more deterministic outputs -# - `UsageLimits(request_limit=3)` sets guardrails on usage: -# - helps prevent runaway retries or excessive calls -# - We ran the agent with these settings applied. -# -# **Why PydanticAI is useful here:** -# PydanticAI makes it easy to add safety and cost controls to LLM systems. These controls matter in production where reliability and spend both need limits. - -# %% [markdown] -# ## Best Practices -# -# 1. Always define clear schemas using Pydantic models. -# 2. Keep schemas simple and explicit. -# 3. Use retries for robustness. -# 4. Add tools for external integrations. -# 5. Use async execution for production systems. +result.output +# The response was generated with model settings and usage limits applied. # %% [markdown] -# ## Troubleshooting +# # Troubleshooting # - Missing API key: set `OPENAI_API_KEY` (or your provider-specific key). # - Event loop errors in notebooks: use `await agent.run(...)` instead of `run_sync`. # - Validation errors: revise `output_type` or the validator to match expected output. diff --git a/tutorials/tutorial_pydanticAI/pydanticai_API_utils.py b/tutorials/tutorial_pydanticAI/pydanticai_API_utils.py index f91e9c456..e7a3cd381 100644 --- a/tutorials/tutorial_pydanticAI/pydanticai_API_utils.py +++ b/tutorials/tutorial_pydanticAI/pydanticai_API_utils.py @@ -1,14 +1,50 @@ -"""Utility functions for tutorials/tutorial_pydanticAI/pydanticai.API notebook.""" +""" +Utility functions for tutorials/tutorial_pydanticAI/pydanticai.API notebook. +Import as: + +import tutorials.tutorial_pydanticAI.pydanticai_API_utils as ttppaput +""" + +import importlib +import importlib.util +import inspect +import logging +import os +import pathlib from typing import Any -from pydantic_ai import ModelRetry, RunContext +import pydantic_ai # type: ignore[import-not-found] + +import helpers.hdbg as hdbg +import helpers.hnotebook as hnotebo +_LOG = logging.getLogger(__name__) +_DOCUMENTS_CACHE: dict[str, str] | None = None -# ######################################################################### + +# ############################################################################# # Code for setup and masking. -# ######################################################################### +# ############################################################################# +def init_logger(notebook_log: logging.Logger) -> None: + """ + Initialize notebook and utility logging. + + :param notebook_log: logger from the paired notebook + """ + hnotebo.config_notebook() + hdbg.init_logger(verbosity=logging.INFO, use_exec_path=False) + hnotebo.set_logger_to_print(notebook_log) + hnotebo.set_logger_to_print(_LOG) + + def _mask(value: str | None) -> str: + """ + Mask a secret value for notebook display. + + :param value: value to mask + :return: masked value + """ if not value: return "" if len(value) <= 6: @@ -16,46 +52,288 @@ def _mask(value: str | None) -> str: return f"{value[:3]}...{value[-2:]}" -# ######################################################################### +def log_environment(env_path: str, model_id: str) -> None: + """ + Log notebook environment settings. + + :param env_path: dotenv file path + :param model_id: configured model identifier + """ + _LOG.info("dotenv path: %s", env_path or "") + _LOG.info("PYDANTIC_AI_MODEL: %s", model_id) + _LOG.info("OPENAI_API_KEY: %s", _mask(os.getenv("OPENAI_API_KEY"))) + + +# ############################################################################# # Code for tools and dependencies. -# ######################################################################### +# ############################################################################# def get_weather(city: str) -> str: - return f"The weather in {city} is sunny." + """ + Get deterministic demo weather for a city. + + :param city: city name + :return: weather response + """ + weather = f"The weather in {city} is sunny." + return weather -def company_name(ctx: RunContext[Any]) -> str: - return ctx.deps.company +def company_name(ctx: pydantic_ai.RunContext[Any]) -> str: + """ + Get the configured company from an agent run context. + :param ctx: PydanticAI run context + :return: configured company name + """ + company = str(ctx.deps.company) + return company -# ######################################################################### + +# ############################################################################# # Code for async execution and validation demos. -# ######################################################################### -async def run_agent(agent: Any) -> Any: - result = await agent.run("Tell me about Tokyo") - return result.output +# ############################################################################# +def load_example_documents() -> dict[str, str]: + """ + Load tutorial documents used by validator and retrieval demos. + + :return: mapping from document id to document text + """ + global _DOCUMENTS_CACHE + if _DOCUMENTS_CACHE is not None: + return _DOCUMENTS_CACHE + dataset_dir = pathlib.Path(__file__).resolve().parent / "example_dataset" + documents = {} + for path in sorted(dataset_dir.glob("*.md")): + documents[path.stem] = path.read_text() + _DOCUMENTS_CACHE = documents + return documents + + +def get_available_document_ids() -> list[str]: + """ + Get sorted document ids from the example dataset. + + :return: sorted list of document ids + """ + document_ids = sorted(load_example_documents()) + return document_ids + + +def search_documents(query: str, max_results: int = 3) -> str: + """ + Search local tutorial documents and return snippets for citation. + + :param query: search query + :param max_results: maximum number of snippets to return + :return: formatted snippets with doc ids and quotes + """ + documents = load_example_documents() + query_terms = [term for term in query.lower().split() if len(term) > 2] + candidates = [] + for doc_id, content in documents.items(): + for line in content.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + line_l = line.lower() + score = sum(1 for term in query_terms if term in line_l) + if score == 0 and query_terms: + continue + candidates.append((score, doc_id, line)) + candidates.sort(key=lambda item: (-item[0], item[1], item[2])) + if not candidates: + return "No matching snippets found." + snippets = [] + for _, doc_id, line in candidates[:max_results]: + snippets.append(f"doc_id={doc_id} | quote={line}") + snippets_out = "\n".join(snippets) + return snippets_out + + +async def run_agent(agent: Any, *, prompt: str = "Tell me about Tokyo") -> Any: + """ + Run an agent asynchronously. + + :param agent: PydanticAI agent + :param prompt: prompt to send to the agent + :return: agent output + """ + result = await agent.run(prompt) + output = result.output + return output def validate_sources(result: Any) -> Any: + """ + Validate answer source references. + + :param result: model output to validate + :return: validated model output + """ answer_l = result.answer.lower() mentions_docs = any( token in answer_l for token in ["doc", "document", "according", "source"] ) if mentions_docs and not result.sources: - raise ModelRetry("Answer references documents but sources are empty.") + raise pydantic_ai.ModelRetry( + "Answer references documents but sources are empty." + ) if len(result.sources) > 3: - raise ModelRetry("Too many sources. Maximum allowed is 3.") - seen = set() - for s in result.sources: - key = (s.doc_id, s.quote) + raise pydantic_ai.ModelRetry("Too many sources. Maximum allowed is 3.") + seen: set[tuple[str, str]] = set() + for source in result.sources: + key = (source.doc_id, source.quote) if key in seen: - raise ModelRetry("Duplicate sources found.") + raise pydantic_ai.ModelRetry("Duplicate sources found.") seen.add(key) return result -async def run_validator_example(validator_agent: Any) -> None: - result = await validator_agent.run( - "Explain something using documents and cite sources." +def validate_document_sources(result: Any) -> Any: + """ + Validate sources against local tutorial documents. + + :param result: model output to validate + :return: validated model output + """ + result = validate_sources(result) + documents = load_example_documents() + for source in result.sources: + if source.doc_id not in documents: + raise pydantic_ai.ModelRetry( + f"Unknown doc_id '{source.doc_id}'. Use ids from example_dataset." + ) + doc_text = " ".join(documents[source.doc_id].lower().split()) + quote_text = " ".join(source.quote.lower().split()) + if quote_text not in doc_text: + raise pydantic_ai.ModelRetry( + f"Quote not found in cited document '{source.doc_id}'." + ) + return result + + +def build_missing_sources_retry() -> pydantic_ai.ModelRetry: + """ + Build the retry exception used by the missing-sources demo. + + :return: retry exception + """ + retry = pydantic_ai.ModelRetry( + "Answer references documents but sources are empty." ) - print("\nValidated output:\n") - print(result.output) + return retry + + +async def run_validator_example( + validator_agent: Any, + *, + prompt: str = "Use local documents to explain Atlas billing plans and cite sources.", +) -> Any: + """ + Run the result validator example. + + :param validator_agent: configured validator agent + :return: validated output + """ + result = await validator_agent.run(prompt) + output = result.output + return output + + +# ############################################################################# +# Code for advanced API demos. +# ############################################################################# +async def run_streaming_demo(stream_agent: Any) -> Any: + """ + Run a streaming demo and log streamed text. + + :param stream_agent: configured streaming agent + :return: final streaming result or non-streamed result + """ + if not hasattr(stream_agent, "run_stream"): + _LOG.info("Streaming API not available; falling back to run().") + result = await stream_agent.run("What are unit tests?") + return result + async with stream_agent.run_stream("What are unit tests?") as stream: + stream_text = stream.stream_text + parameters = inspect.signature(stream_text).parameters + if "delta" in parameters: + text_stream = stream_text(delta=True) + else: + text_stream = stream_text() + chunks = [] + async for chunk in text_stream: + chunks.append(chunk) + if hasattr(stream, "get_final_result"): + result = await stream.get_final_result() + else: + result = "".join(chunks) + _LOG.info("Streaming output:\n%s", "".join(chunks)) + return result + + +def _get_openai_model_class() -> Any | None: + """ + Get the available explicit OpenAI model class. + + :return: model class, or None if unavailable + """ + if importlib.util.find_spec("pydantic_ai") is None: + return None + if importlib.util.find_spec("pydantic_ai.models.openai") is None: + return None + module = importlib.import_module("pydantic_ai.models.openai") + for class_name in ("OpenAIModel", "OpenAIChatModel"): + if hasattr(module, class_name): + model_class = getattr(module, class_name) + return model_class + return None + + +def build_explicit_openai_model(model_id: str) -> Any | None: + """ + Build an explicit OpenAI model object when the installed API supports it. + + :param model_id: configured model identifier + :return: explicit model object, or None + """ + model_class = _get_openai_model_class() + if model_class is None: + return None + hdbg.dassert_isinstance(model_id, str) + hdbg.dassert_ne(model_id, "", "Model id cannot be empty") + model_name = model_id.removeprefix("openai:") + _LOG.info("Using OpenAI model with model_name='%s'.", model_name) + signature = inspect.signature(model_class) + parameters = signature.parameters + base_kwargs = { + "api_key": os.getenv("OPENAI_API_KEY"), + "base_url": os.getenv("OPENAI_BASE_URL"), + } + args: list[Any] = [] + kwargs: dict[str, Any] = {} + if "model_name" in parameters: + kwargs["model_name"] = model_name + elif "model" in parameters: + kwargs["model"] = model_name + else: + args.append(model_name) + for key, value in base_kwargs.items(): + if key in parameters: + kwargs[key] = value + model = model_class(*args, **kwargs) + return model + + +def get_settings_classes() -> tuple[Any, Any]: + """ + Get ModelSettings and UsageLimits classes for the installed version. + + :return: ModelSettings and UsageLimits classes + """ + module = importlib.import_module("pydantic_ai") + if hasattr(module, "ModelSettings") and hasattr(module, "UsageLimits"): + return module.ModelSettings, module.UsageLimits + models_module = importlib.import_module("pydantic_ai.models") + usage_module = importlib.import_module("pydantic_ai.usage") + return models_module.ModelSettings, usage_module.UsageLimits diff --git a/tutorials/tutorial_pydanticAI/test/test_pydanticai_API_utils.py b/tutorials/tutorial_pydanticAI/test/test_pydanticai_API_utils.py new file mode 100644 index 000000000..0dbb12405 --- /dev/null +++ b/tutorials/tutorial_pydanticAI/test/test_pydanticai_API_utils.py @@ -0,0 +1,678 @@ +""" +Test utility functions for tutorials/tutorial_pydanticAI/pydanticai.API. +""" + +import asyncio +import importlib.util +import logging +import sys +import types +import typing +import unittest.mock + +import helpers.hunit_test as hunitest + +if importlib.util.find_spec("pydantic_ai") is None: + + class ModelRetry(Exception): + """ + Minimal stub for pydantic_ai.ModelRetry. + """ + + class RunContext: + """ + Minimal stub for pydantic_ai.RunContext. + """ + + def __class_getitem__(cls, _item: object) -> type["RunContext"]: + """ + Support type annotations that use RunContext[Any]. + + :param _item: type argument + :return: RunContext class + """ + return cls + + pydantic_ai_stub: typing.Any = types.ModuleType("pydantic_ai") + pydantic_ai_stub.ModelRetry = ModelRetry + pydantic_ai_stub.RunContext = RunContext + sys.modules["pydantic_ai"] = pydantic_ai_stub + +import pydantic_ai # type: ignore[import-not-found] # pylint: disable=wrong-import-position +import pydanticai_API_utils as put # type: ignore[import-not-found] # pylint: disable=wrong-import-position + +_LOG = logging.getLogger(__name__) + + +# ############################################################################# +# Test_mask +# ############################################################################# + + +class Test_mask(hunitest.TestCase): + """ + Test secret masking for notebook environment output. + """ + + def helper(self, value: str | None, expected: str) -> None: + """ + Test helper for `_mask()`. + + :param value: value to mask + :param expected: expected masked value + """ + # Run test. + actual = put._mask(value) + # Check outputs. + self.assert_equal(actual, expected) + + def test1(self) -> None: + """ + Test masking a missing value. + """ + # Prepare inputs. + value = None + # Prepare outputs. + expected = "" + # Run test. + self.helper(value, expected) + + def test2(self) -> None: + """ + Test masking an empty value. + """ + # Prepare inputs. + value = "" + # Prepare outputs. + expected = "" + # Run test. + self.helper(value, expected) + + def test3(self) -> None: + """ + Test masking a short value. + """ + # Prepare inputs. + value = "secret" + # Prepare outputs. + expected = "******" + # Run test. + self.helper(value, expected) + + def test4(self) -> None: + """ + Test masking a normal secret value. + """ + # Prepare inputs. + value = "sk-1234567890" + # Prepare outputs. + expected = "sk-...90" + # Run test. + self.helper(value, expected) + + +# ############################################################################# +# Test_get_weather +# ############################################################################# + + +class Test_get_weather(hunitest.TestCase): + """ + Test deterministic weather output. + """ + + def helper(self, city: str, expected: str) -> None: + """ + Test helper for `get_weather()`. + + :param city: city name + :param expected: expected weather response + """ + # Run test. + actual = put.get_weather(city) + # Check outputs. + self.assert_equal(actual, expected) + + def test1(self) -> None: + """ + Test weather output for a normal city. + """ + # Prepare inputs. + city = "Tokyo" + # Prepare outputs. + expected = "The weather in Tokyo is sunny." + # Run test. + self.helper(city, expected) + + def test2(self) -> None: + """ + Test weather output for an empty city. + """ + # Prepare inputs. + city = "" + # Prepare outputs. + expected = "The weather in is sunny." + # Run test. + self.helper(city, expected) + + +# ############################################################################# +# Test_build_missing_sources_retry +# ############################################################################# + + +class Test_build_missing_sources_retry(hunitest.TestCase): + """ + Test construction of the missing-sources retry exception. + """ + + def test1(self) -> None: + """ + Test that the helper builds a pydantic_ai.ModelRetry instance. + """ + # Prepare outputs. + expected = "Answer references documents but sources are empty." + # Run test. + actual = put.build_missing_sources_retry() + # Check outputs. + self.assertIsInstance(actual, pydantic_ai.ModelRetry) + self.assert_equal(str(actual), expected) + + +# ############################################################################# +# Test_validate_sources +# ############################################################################# + + +class Test_validate_sources(hunitest.TestCase): + """ + Test answer source validation. + """ + + def test1(self) -> None: + """ + Test an answer with no document claim and no sources. + """ + # Prepare inputs. + result = self._build_result("This answer is standalone.", []) + # Run test. + actual = put.validate_sources(result) + # Check outputs. + self.assertEqual(actual, result) + + def test2(self) -> None: + """ + Test an answer with document references and sources. + """ + # Prepare inputs. + sources = [self._build_source("doc1", "quoted text")] + result = self._build_result("According to the document.", sources) + # Run test. + actual = put.validate_sources(result) + # Check outputs. + self.assertEqual(actual, result) + + def test3(self) -> None: + """ + Test that duplicate sources raise pydantic_ai.ModelRetry. + """ + # Prepare inputs. + sources = [ + self._build_source("doc1", "quoted text"), + self._build_source("doc1", "quoted text"), + ] + result = self._build_result("Standalone answer.", sources) + # Run test and check output. + with self.assertRaises(pydantic_ai.ModelRetry) as cm: + put.validate_sources(result) + actual = str(cm.exception) + expected = "Duplicate sources found." + self.assert_equal(actual, expected) + + def test4(self) -> None: + """ + Test that too many sources raise pydantic_ai.ModelRetry. + """ + # Prepare inputs. + sources = [ + self._build_source("doc1", "quote1"), + self._build_source("doc2", "quote2"), + self._build_source("doc3", "quote3"), + self._build_source("doc4", "quote4"), + ] + result = self._build_result("Standalone answer.", sources) + # Run test and check output. + with self.assertRaises(pydantic_ai.ModelRetry) as cm: + put.validate_sources(result) + actual = str(cm.exception) + expected = "Too many sources. Maximum allowed is 3." + self.assert_equal(actual, expected) + + def test5(self) -> None: + """ + Test that document claims without sources raise pydantic_ai.ModelRetry. + """ + # Prepare inputs. + result = self._build_result("According to the documents.", []) + # Run test and check output. + with self.assertRaises(pydantic_ai.ModelRetry) as cm: + put.validate_sources(result) + actual = str(cm.exception) + expected = "Answer references documents but sources are empty." + self.assert_equal(actual, expected) + + @staticmethod + def _build_result( + answer: str, sources: list[types.SimpleNamespace] + ) -> types.SimpleNamespace: + """ + Build a validator input object. + + :param answer: answer text + :param sources: source references + :return: validator input + """ + result = types.SimpleNamespace(answer=answer, sources=sources) + return result + + @staticmethod + def _build_source(doc_id: str, quote: str) -> types.SimpleNamespace: + """ + Build a source reference object. + + :param doc_id: document identifier + :param quote: source quote + :return: source reference + """ + source = types.SimpleNamespace(doc_id=doc_id, quote=quote) + return source + + +# ############################################################################# +# Test_company_name +# ############################################################################# + + +class Test_company_name(hunitest.TestCase): + """ + Test dependency access for the company-name tool. + """ + + def test1(self) -> None: + """ + Test reading the company from a run context. + """ + # Prepare inputs. + ctx = types.SimpleNamespace(deps=types.SimpleNamespace(company="OpenAI")) + # Prepare outputs. + expected = "OpenAI" + # Run test. + actual = put.company_name(ctx) + # Check outputs. + self.assert_equal(actual, expected) + + +# ############################################################################# +# Test_load_example_documents +# ############################################################################# + + +class Test_load_example_documents(hunitest.TestCase): + """ + Test loading local example documents. + """ + + def test1(self) -> None: + """ + Test that tutorial documents are loaded. + """ + # Prepare inputs. + put._DOCUMENTS_CACHE = None + # Run test. + actual = put.load_example_documents() + # Check outputs. + self.assertIn("billing", actual) + self.assertIn("Starter: $20 per month", actual["billing"]) + + +# ############################################################################# +# Test_get_available_document_ids +# ############################################################################# + + +class Test_get_available_document_ids(hunitest.TestCase): + """ + Test document-id discovery. + """ + + def test1(self) -> None: + """ + Test that document ids are returned in sorted order. + """ + # Prepare outputs. + expected = sorted(put.load_example_documents()) + # Run test. + actual = put.get_available_document_ids() + # Check outputs. + self.assert_equal(str(actual), str(expected)) + + +# ############################################################################# +# Test_search_documents +# ############################################################################# + + +class Test_search_documents(hunitest.TestCase): + """ + Test local document search snippets. + """ + + def test1(self) -> None: + """ + Test a search query with matching snippets. + """ + # Prepare inputs. + query = "billing starter" + # Run test. + actual = put.search_documents(query, max_results=1) + # Check outputs. + self.assertIn("doc_id=billing", actual) + self.assertIn("Starter", actual) + + def test2(self) -> None: + """ + Test a search query with no matching snippets. + """ + # Prepare inputs. + query = "zzzzzz" + # Prepare outputs. + expected = "No matching snippets found." + # Run test. + actual = put.search_documents(query) + # Check outputs. + self.assert_equal(actual, expected) + + +# ############################################################################# +# Test_validate_document_sources +# ############################################################################# + + +class Test_validate_document_sources(hunitest.TestCase): + """ + Test source validation against local documents. + """ + + def test1(self) -> None: + """ + Test a valid source quote. + """ + # Prepare inputs. + sources = [ + self._build_source( + "billing", + "Starter: $20 per month, 5 data sources, email support.", + ) + ] + result = self._build_result("According to the documents.", sources) + # Run test. + actual = put.validate_document_sources(result) + # Check outputs. + self.assertEqual(actual, result) + + def test2(self) -> None: + """ + Test that an unknown document id raises pydantic_ai.ModelRetry. + """ + # Prepare inputs. + sources = [self._build_source("missing", "quoted text")] + result = self._build_result("According to the documents.", sources) + # Run test and check output. + with self.assertRaises(pydantic_ai.ModelRetry) as cm: + put.validate_document_sources(result) + actual = str(cm.exception) + expected = "Unknown doc_id 'missing'. Use ids from example_dataset." + self.assert_equal(actual, expected) + + def test3(self) -> None: + """ + Test that a quote mismatch raises pydantic_ai.ModelRetry. + """ + # Prepare inputs. + sources = [self._build_source("billing", "not present in billing")] + result = self._build_result("According to the documents.", sources) + # Run test and check output. + with self.assertRaises(pydantic_ai.ModelRetry) as cm: + put.validate_document_sources(result) + actual = str(cm.exception) + expected = "Quote not found in cited document 'billing'." + self.assert_equal(actual, expected) + + @staticmethod + def _build_result( + answer: str, sources: list[types.SimpleNamespace] + ) -> types.SimpleNamespace: + """ + Build a validator input object. + + :param answer: answer text + :param sources: source references + :return: validator input + """ + result = types.SimpleNamespace(answer=answer, sources=sources) + return result + + @staticmethod + def _build_source(doc_id: str, quote: str) -> types.SimpleNamespace: + """ + Build a source reference object. + + :param doc_id: document identifier + :param quote: source quote + :return: source reference + """ + source = types.SimpleNamespace(doc_id=doc_id, quote=quote) + return source + + +# ############################################################################# +# Test_run_agent +# ############################################################################# + + +class Test_run_agent(hunitest.TestCase): + """ + Test async agent helper execution. + """ + + class _Agent: + """ + Minimal async agent used by tests. + """ + + async def run(self, prompt: str) -> types.SimpleNamespace: + """ + Return a fake run result. + + :param prompt: prompt sent to the agent + :return: fake run result + """ + result = types.SimpleNamespace(output=f"answer: {prompt}") + return result + + def test1(self) -> None: + """ + Test running an async agent. + """ + # Prepare inputs. + agent = self._Agent() + prompt = "hello" + # Prepare outputs. + expected = "answer: hello" + # Run test. + actual = asyncio.run(put.run_agent(agent, prompt=prompt)) + # Check outputs. + self.assert_equal(actual, expected) + + +# ############################################################################# +# Test_run_validator_example +# ############################################################################# + + +class Test_run_validator_example(hunitest.TestCase): + """ + Test validator example helper execution. + """ + + class _Agent: + """ + Minimal async validator agent used by tests. + """ + + async def run(self, prompt: str) -> types.SimpleNamespace: + """ + Return a fake validator run result. + + :param prompt: prompt sent to the agent + :return: fake run result + """ + result = types.SimpleNamespace(output={"prompt": prompt}) + return result + + def test1(self) -> None: + """ + Test running the validator example helper. + """ + # Prepare inputs. + agent = self._Agent() + prompt = "cite docs" + # Prepare outputs. + expected = {"prompt": prompt} + # Run test. + actual = asyncio.run(put.run_validator_example(agent, prompt=prompt)) + # Check outputs. + self.assert_equal(str(actual), str(expected)) + + +# ############################################################################# +# Test_run_streaming_demo +# ############################################################################# + + +class Test_run_streaming_demo(hunitest.TestCase): + """ + Test streaming helper fallback behavior. + """ + + class _Agent: + """ + Minimal agent without streaming support. + """ + + async def run(self, prompt: str) -> types.SimpleNamespace: + """ + Return a fake fallback run result. + + :param prompt: prompt sent to the agent + :return: fake run result + """ + result = types.SimpleNamespace(output=f"fallback: {prompt}") + return result + + def test1(self) -> None: + """ + Test fallback execution when streaming is unavailable. + """ + # Prepare inputs. + agent = self._Agent() + # Run test. + actual = asyncio.run(put.run_streaming_demo(agent)) + # Check outputs. + self.assert_equal(actual.output, "fallback: What are unit tests?") + + +# ############################################################################# +# Test_get_openai_model_class +# ############################################################################# + + +class Test_get_openai_model_class(hunitest.TestCase): + """ + Test OpenAI model class discovery. + """ + + def test1(self) -> None: + """ + Test missing OpenAI model module. + """ + # Run test. + with unittest.mock.patch.object( + put.importlib.util, "find_spec", return_value=None + ): + actual = put._get_openai_model_class() + # Check outputs. + self.assertIsNone(actual) + + +# ############################################################################# +# Test_build_explicit_openai_model +# ############################################################################# + + +class Test_build_explicit_openai_model(hunitest.TestCase): + """ + Test explicit OpenAI model construction. + """ + + def test1(self) -> None: + """ + Test missing model class fallback. + """ + # Prepare inputs. + model_id = "openai:gpt-5-nano" + # Run test. + with unittest.mock.patch.object( + put, "_get_openai_model_class", return_value=None + ): + actual = put.build_explicit_openai_model(model_id) + # Check outputs. + self.assertIsNone(actual) + + +# ############################################################################# +# Test_get_settings_classes +# ############################################################################# + + +class Test_get_settings_classes(hunitest.TestCase): + """ + Test settings class discovery. + """ + + class _ModelSettings: + """ + Fake model settings class. + """ + + class _UsageLimits: + """ + Fake usage limits class. + """ + + def test1(self) -> None: + """ + Test direct class discovery from the pydantic_ai module. + """ + # Prepare inputs. + pydantic_ai_module: typing.Any = sys.modules["pydantic_ai"] + pydantic_ai_module.ModelSettings = self._ModelSettings + pydantic_ai_module.UsageLimits = self._UsageLimits + # Prepare outputs. + expected = (self._ModelSettings, self._UsageLimits) + # Run test. + actual = put.get_settings_classes() + # Check outputs. + self.assert_equal(str(actual), str(expected)) + del pydantic_ai_module.ModelSettings + del pydantic_ai_module.UsageLimits From a14ad850acf41c8b38ea01968a2c8dbf8e696ea3 Mon Sep 17 00:00:00 2001 From: Aayush Date: Tue, 21 Apr 2026 17:59:53 -0400 Subject: [PATCH 2/3] Updating API and example notebooks as per the SKILL files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-commit checks: All checks passed ✅ --- .../tutorial_pydanticAI/pydanticai.API.ipynb | 115 +++++++----------- .../tutorial_pydanticAI/pydanticai.API.py | 92 +++++--------- .../pydanticai_API_utils.py | 35 +++--- 3 files changed, 91 insertions(+), 151 deletions(-) diff --git a/tutorials/tutorial_pydanticAI/pydanticai.API.ipynb b/tutorials/tutorial_pydanticAI/pydanticai.API.ipynb index 39ae78362..ace6113d9 100644 --- a/tutorials/tutorial_pydanticAI/pydanticai.API.ipynb +++ b/tutorials/tutorial_pydanticAI/pydanticai.API.ipynb @@ -14,6 +14,10 @@ "import logging\n", "\n", "# Third party libraries.\n", + "import numpy as np\n", + "import pandas as pd\n", + "import seaborn as sns\n", + "import matplotlib.pyplot as plt\n", "\n", "# Common plotting and dataframe libraries are loaded for notebook exploration." ] @@ -27,14 +31,15 @@ "source": [ "# System libraries.\n", "import asyncio\n", - "import dataclasses\n", "import os\n", "\n", "# Third party libraries.\n", - "import dotenv\n", + "from dataclasses import dataclass\n", + "\n", "import nest_asyncio\n", - "import pydantic\n", - "import pydantic_ai\n", + "from dotenv import find_dotenv, load_dotenv\n", + "from pydantic import BaseModel\n", + "from pydantic_ai import Agent\n", "\n", "# Local utilities.\n", "import pydanticai_API_utils as utils\n", @@ -60,6 +65,11 @@ ], "source": [ "# Configure notebook logging.\n", + "import logging\n", + "\n", + "# Local utility.\n", + "import pydanticai_API_utils as utils\n", + "\n", "_LOG = logging.getLogger(__name__)\n", "utils.init_logger(_LOG)\n", "_LOG.info(\"Notebook logger initialized.\")\n", @@ -167,8 +177,8 @@ ], "source": [ "# Load environment variables from a local dotenv file if one exists.\n", - "env_path = dotenv.find_dotenv(usecwd=True)\n", - "dotenv.load_dotenv(env_path, override=True)\n", + "env_path = find_dotenv(usecwd=True)\n", + "load_dotenv(env_path, override=True)\n", "_LOG.info(\"dotenv path: %s\", env_path or \"\")\n", "env_path or \"\"\n", "# Environment variables are available to the model configuration cells." @@ -235,20 +245,13 @@ { "cell_type": "markdown", "id": "8569d597", - "metadata": { - "lines_to_next_cell": 2 - }, + "metadata": {}, "source": [ "# Minimal Example\n", "\n", "The quickest way to understand PydanticAI is through a small example.\n", "\n", - "We define a schema using Pydantic and instruct the agent to produce that structured output.\n", - "\n", - "\n", - "#############################################################################\n", - "City\n", - "#############################################################################" + "We define a schema using Pydantic and instruct the agent to produce that structured output." ] }, { @@ -261,7 +264,7 @@ "outputs": [], "source": [ "# Define the output schema for the minimal example.\n", - "class City(pydantic.BaseModel):\n", + "class City(BaseModel):\n", " name: str\n", " country: str\n", " population: int\n", @@ -290,7 +293,7 @@ ], "source": [ "# Create an agent that must return `City`.\n", - "agent = pydantic_ai.Agent(MODEL_ID, output_type=City)\n", + "agent = Agent(MODEL_ID, output_type=City)\n", "agent\n", "# The agent is configured to validate model output against class `City`." ] @@ -376,9 +379,7 @@ { "cell_type": "markdown", "id": "e0f3aa76", - "metadata": { - "lines_to_next_cell": 2 - }, + "metadata": {}, "source": [ "# Structured Outputs with Pydantic\n", "\n", @@ -386,12 +387,7 @@ "\n", "- Store validated outputs in databases\n", "- Feed typed objects into analytics\n", - "- Pass structured data downstream without brittle string parsing\n", - "\n", - "\n", - "#############################################################################\n", - "Product\n", - "#############################################################################" + "- Pass structured data downstream without brittle string parsing" ] }, { @@ -404,7 +400,7 @@ "outputs": [], "source": [ "# Define a product schema for structured extraction.\n", - "class Product(pydantic.BaseModel):\n", + "class Product(BaseModel):\n", " name: str\n", " price: float\n", " category: str\n", @@ -433,7 +429,7 @@ ], "source": [ "# Create an agent that must return `Product`.\n", - "agent = pydantic_ai.Agent(MODEL_ID, output_type=Product)\n", + "agent = Agent(MODEL_ID, output_type=Product)\n", "agent\n", "# The agent is configured to return product data with typed fields." ] @@ -464,9 +460,7 @@ { "cell_type": "markdown", "id": "5716df9d", - "metadata": { - "lines_to_next_cell": 2 - }, + "metadata": {}, "source": [ "# Validation and Retries\n", "\n", @@ -474,13 +468,7 @@ "\n", "- Schema validation checks the generated structure\n", "- Retries let `PydanticAI` ask the model to repair invalid output\n", - "- The notebook avoids custom parsing and retry logic in each prompt\n", - "\n", - "\n", - "\n", - "#############################################################################\n", - "Person\n", - "#############################################################################" + "- The notebook avoids custom parsing and retry logic in each prompt" ] }, { @@ -493,7 +481,7 @@ "outputs": [], "source": [ "# Define a schema that requires an integer age.\n", - "class Person(pydantic.BaseModel):\n", + "class Person(BaseModel):\n", " name: str\n", " age: int\n", "\n", @@ -521,7 +509,7 @@ ], "source": [ "# Configure retries so schema validation failures can be corrected.\n", - "agent = pydantic_ai.Agent(MODEL_ID, output_type=Person, retries=2)\n", + "agent = Agent(MODEL_ID, output_type=Person, retries=2)\n", "agent\n", "# The agent can retry when model output does not match `Person`." ] @@ -582,7 +570,7 @@ ], "source": [ "# Create an agent with a deterministic weather tool.\n", - "agent = pydantic_ai.Agent(MODEL_ID, tools=[utils.get_weather])\n", + "agent = Agent(MODEL_ID, tools=[utils.get_weather])\n", "agent\n", "# The agent can call `utils.get_weather()` while answering." ] @@ -613,21 +601,14 @@ { "cell_type": "markdown", "id": "6bbc710d", - "metadata": { - "lines_to_next_cell": 2 - }, + "metadata": {}, "source": [ "# Dependencies\n", "\n", "Dependencies inject runtime context into agents and tools.\n", "\n", "- Example values: tenant IDs, API clients, feature flags, and environment context\n", - "- Benefit: tools can access context without global variables or prompt string formatting\n", - "\n", - "\n", - "#############################################################################\n", - "Config\n", - "#############################################################################" + "- Benefit: tools can access context without global variables or prompt string formatting" ] }, { @@ -640,7 +621,7 @@ "outputs": [], "source": [ "# Define the dependency object passed into the agent at run time.\n", - "@dataclasses.dataclass\n", + "@dataclass\n", "class Config:\n", " company: str\n", "\n", @@ -669,7 +650,7 @@ "source": [ "# Create an agent that receives `Config` dependencies.\n", "# `deps_type=Config` declares the shape of runtime context the agent can receive.\n", - "agent = pydantic_ai.Agent(MODEL_ID, deps_type=Config, tools=[utils.company_name])\n", + "agent = Agent(MODEL_ID, deps_type=Config, tools=[utils.company_name])\n", "agent\n", "# Tools can access `Config` through the PydanticAI run context." ] @@ -738,9 +719,7 @@ { "cell_type": "markdown", "id": "6f49d16c-71a4-4d5b-9cfd-7149cdcad70f", - "metadata": { - "lines_to_next_cell": 2 - }, + "metadata": {}, "source": [ "## Validation Flow\n", "\n", @@ -754,12 +733,7 @@ "\n", "```text\n", "model output -> Pydantic schema validation -> output_validator -> final result\n", - "```\n", - "\n", - "\n", - "#############################################################################\n", - "SourceRef\n", - "#############################################################################" + "```" ] }, { @@ -789,17 +763,12 @@ ], "source": [ "# Define source citation schemas for validator examples.\n", - "class SourceRef(pydantic.BaseModel):\n", + "class SourceRef(BaseModel):\n", " doc_id: str\n", " quote: str\n", "\n", "\n", - "# #############################################################################\n", - "# AnswerWithSources\n", - "# #############################################################################\n", - "\n", - "\n", - "class AnswerWithSources(pydantic.BaseModel):\n", + "class AnswerWithSources(BaseModel):\n", " answer: str\n", " sources: list[SourceRef]\n", "\n", @@ -839,7 +808,7 @@ "outputs": [], "source": [ "# Create an agent that returns answers with source references.\n", - "validator_agent = pydantic_ai.Agent(\n", + "validator_agent = Agent(\n", " MODEL_ID,\n", " output_type=AnswerWithSources,\n", " instructions=validator_instructions,\n", @@ -1005,7 +974,7 @@ ], "source": [ "# Create an agent for the streaming example.\n", - "stream_agent = pydantic_ai.Agent(\n", + "stream_agent = Agent(\n", " MODEL_ID, instructions=\"Write one short paragraph about unit tests.\"\n", ")\n", "stream_agent\n", @@ -1102,7 +1071,7 @@ ], "source": [ "# Run an agent with the explicit provider model when available.\n", - "agent = pydantic_ai.Agent(explicit_model or MODEL_ID, instructions=\"Be concise.\")\n", + "agent = Agent(explicit_model or MODEL_ID, instructions=\"Be concise.\")\n", "result = asyncio.run(agent.run(\"Say hello in one sentence.\"))\n", "result\n", "# The result confirms that the provider configuration can execute a request." @@ -1154,7 +1123,7 @@ ], "source": [ "# Run an agent and collect execution metadata.\n", - "meta_agent = pydantic_ai.Agent(MODEL_ID, instructions=\"Answer in one sentence.\")\n", + "meta_agent = Agent(MODEL_ID, instructions=\"Answer in one sentence.\")\n", "result = asyncio.run(meta_agent.run(\"What is a unit test?\"))\n", "usage = getattr(result, \"usage\", None)\n", "message_count = len(result.new_messages())\n", @@ -1229,7 +1198,7 @@ ], "source": [ "# Create an agent with deterministic model settings.\n", - "settings_agent = pydantic_ai.Agent(\n", + "settings_agent = Agent(\n", " MODEL_ID,\n", " instructions=\"Answer in a single sentence.\",\n", " model_settings=ModelSettings(temperature=0.2),\n", diff --git a/tutorials/tutorial_pydanticAI/pydanticai.API.py b/tutorials/tutorial_pydanticAI/pydanticai.API.py index 5b47e1db1..fae060426 100644 --- a/tutorials/tutorial_pydanticAI/pydanticai.API.py +++ b/tutorials/tutorial_pydanticAI/pydanticai.API.py @@ -6,7 +6,7 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.17.1 +# jupytext_version: 1.19.1 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -21,20 +21,25 @@ import logging # Third party libraries. +import numpy as np +import pandas as pd +import seaborn as sns +import matplotlib.pyplot as plt # Common plotting and dataframe libraries are loaded for notebook exploration. # %% # System libraries. import asyncio -import dataclasses import os # Third party libraries. -import dotenv +from dataclasses import dataclass + import nest_asyncio -import pydantic -import pydantic_ai +from dotenv import find_dotenv, load_dotenv +from pydantic import BaseModel +from pydantic_ai import Agent # Local utilities. import pydanticai_API_utils as utils @@ -43,6 +48,11 @@ # %% # Configure notebook logging. +import logging + +# Local utility. +import pydanticai_API_utils as utils + _LOG = logging.getLogger(__name__) utils.init_logger(_LOG) _LOG.info("Notebook logger initialized.") @@ -115,8 +125,8 @@ # %% # Load environment variables from a local dotenv file if one exists. -env_path = dotenv.find_dotenv(usecwd=True) -dotenv.load_dotenv(env_path, override=True) +env_path = find_dotenv(usecwd=True) +load_dotenv(env_path, override=True) _LOG.info("dotenv path: %s", env_path or "") env_path or "" # Environment variables are available to the model configuration cells. @@ -164,16 +174,10 @@ # The quickest way to understand PydanticAI is through a small example. # # We define a schema using Pydantic and instruct the agent to produce that structured output. -# -# -# ############################################################################# -# City -# ############################################################################# - # %% # Define the output schema for the minimal example. -class City(pydantic.BaseModel): +class City(BaseModel): name: str country: str population: int @@ -185,7 +189,7 @@ class City(pydantic.BaseModel): # %% # Create an agent that must return `City`. -agent = pydantic_ai.Agent(MODEL_ID, output_type=City) +agent = Agent(MODEL_ID, output_type=City) agent # The agent is configured to validate model output against class `City`. @@ -223,16 +227,10 @@ class City(pydantic.BaseModel): # - Store validated outputs in databases # - Feed typed objects into analytics # - Pass structured data downstream without brittle string parsing -# -# -# ############################################################################# -# Product -# ############################################################################# - # %% # Define a product schema for structured extraction. -class Product(pydantic.BaseModel): +class Product(BaseModel): name: str price: float category: str @@ -244,7 +242,7 @@ class Product(pydantic.BaseModel): # %% # Create an agent that must return `Product`. -agent = pydantic_ai.Agent(MODEL_ID, output_type=Product) +agent = Agent(MODEL_ID, output_type=Product) agent # The agent is configured to return product data with typed fields. @@ -261,17 +259,10 @@ class Product(pydantic.BaseModel): # - Schema validation checks the generated structure # - Retries let `PydanticAI` ask the model to repair invalid output # - The notebook avoids custom parsing and retry logic in each prompt -# -# -# -# ############################################################################# -# Person -# ############################################################################# - # %% # Define a schema that requires an integer age. -class Person(pydantic.BaseModel): +class Person(BaseModel): name: str age: int @@ -282,7 +273,7 @@ class Person(pydantic.BaseModel): # %% # Configure retries so schema validation failures can be corrected. -agent = pydantic_ai.Agent(MODEL_ID, output_type=Person, retries=2) +agent = Agent(MODEL_ID, output_type=Person, retries=2) agent # The agent can retry when model output does not match `Person`. @@ -302,7 +293,7 @@ class Person(pydantic.BaseModel): # %% # Create an agent with a deterministic weather tool. -agent = pydantic_ai.Agent(MODEL_ID, tools=[utils.get_weather]) +agent = Agent(MODEL_ID, tools=[utils.get_weather]) agent # The agent can call `utils.get_weather()` while answering. @@ -318,16 +309,10 @@ class Person(pydantic.BaseModel): # # - Example values: tenant IDs, API clients, feature flags, and environment context # - Benefit: tools can access context without global variables or prompt string formatting -# -# -# ############################################################################# -# Config -# ############################################################################# - # %% # Define the dependency object passed into the agent at run time. -@dataclasses.dataclass +@dataclass class Config: company: str @@ -339,7 +324,7 @@ class Config: # %% # Create an agent that receives `Config` dependencies. # `deps_type=Config` declares the shape of runtime context the agent can receive. -agent = pydantic_ai.Agent(MODEL_ID, deps_type=Config, tools=[utils.company_name]) +agent = Agent(MODEL_ID, deps_type=Config, tools=[utils.company_name]) agent # Tools can access `Config` through the PydanticAI run context. @@ -390,26 +375,15 @@ class Config: # ```text # model output -> Pydantic schema validation -> output_validator -> final result # ``` -# -# -# ############################################################################# -# SourceRef -# ############################################################################# - # %% # Define source citation schemas for validator examples. -class SourceRef(pydantic.BaseModel): +class SourceRef(BaseModel): doc_id: str quote: str -# ############################################################################# -# AnswerWithSources -# ############################################################################# - - -class AnswerWithSources(pydantic.BaseModel): +class AnswerWithSources(BaseModel): answer: str sources: list[SourceRef] @@ -433,7 +407,7 @@ class AnswerWithSources(pydantic.BaseModel): # %% # Create an agent that returns answers with source references. -validator_agent = pydantic_ai.Agent( +validator_agent = Agent( MODEL_ID, output_type=AnswerWithSources, instructions=validator_instructions, @@ -521,7 +495,7 @@ def validate_output( # %% # Create an agent for the streaming example. -stream_agent = pydantic_ai.Agent( +stream_agent = Agent( MODEL_ID, instructions="Write one short paragraph about unit tests." ) stream_agent @@ -552,7 +526,7 @@ def validate_output( # %% # Run an agent with the explicit provider model when available. -agent = pydantic_ai.Agent(explicit_model or MODEL_ID, instructions="Be concise.") +agent = Agent(explicit_model or MODEL_ID, instructions="Be concise.") result = asyncio.run(agent.run("Say hello in one sentence.")) result # The result confirms that the provider configuration can execute a request. @@ -578,7 +552,7 @@ def validate_output( # %% # Run an agent and collect execution metadata. -meta_agent = pydantic_ai.Agent(MODEL_ID, instructions="Answer in one sentence.") +meta_agent = Agent(MODEL_ID, instructions="Answer in one sentence.") result = asyncio.run(meta_agent.run("What is a unit test?")) usage = getattr(result, "usage", None) message_count = len(result.new_messages()) @@ -615,7 +589,7 @@ def validate_output( # %% # Create an agent with deterministic model settings. -settings_agent = pydantic_ai.Agent( +settings_agent = Agent( MODEL_ID, instructions="Answer in a single sentence.", model_settings=ModelSettings(temperature=0.2), diff --git a/tutorials/tutorial_pydanticAI/pydanticai_API_utils.py b/tutorials/tutorial_pydanticAI/pydanticai_API_utils.py index e7a3cd381..b7a8e6f64 100644 --- a/tutorials/tutorial_pydanticAI/pydanticai_API_utils.py +++ b/tutorials/tutorial_pydanticAI/pydanticai_API_utils.py @@ -11,10 +11,10 @@ import inspect import logging import os -import pathlib +from pathlib import Path from typing import Any -import pydantic_ai # type: ignore[import-not-found] +from pydantic_ai import ModelRetry, RunContext import helpers.hdbg as hdbg import helpers.hnotebook as hnotebo @@ -32,10 +32,11 @@ def init_logger(notebook_log: logging.Logger) -> None: :param notebook_log: logger from the paired notebook """ + global _LOG hnotebo.config_notebook() hdbg.init_logger(verbosity=logging.INFO, use_exec_path=False) hnotebo.set_logger_to_print(notebook_log) - hnotebo.set_logger_to_print(_LOG) + _LOG = hnotebo.set_logger_to_print(_LOG) def _mask(value: str | None) -> str: @@ -78,14 +79,14 @@ def get_weather(city: str) -> str: return weather -def company_name(ctx: pydantic_ai.RunContext[Any]) -> str: +def company_name(ctx: RunContext[Any]) -> str: """ Get the configured company from an agent run context. :param ctx: PydanticAI run context :return: configured company name """ - company = str(ctx.deps.company) + company = ctx.deps.company return company @@ -101,7 +102,7 @@ def load_example_documents() -> dict[str, str]: global _DOCUMENTS_CACHE if _DOCUMENTS_CACHE is not None: return _DOCUMENTS_CACHE - dataset_dir = pathlib.Path(__file__).resolve().parent / "example_dataset" + dataset_dir = Path(__file__).resolve().parent / "example_dataset" documents = {} for path in sorted(dataset_dir.glob("*.md")): documents[path.stem] = path.read_text() @@ -175,16 +176,14 @@ def validate_sources(result: Any) -> Any: token in answer_l for token in ["doc", "document", "according", "source"] ) if mentions_docs and not result.sources: - raise pydantic_ai.ModelRetry( - "Answer references documents but sources are empty." - ) + raise ModelRetry("Answer references documents but sources are empty.") if len(result.sources) > 3: - raise pydantic_ai.ModelRetry("Too many sources. Maximum allowed is 3.") + raise ModelRetry("Too many sources. Maximum allowed is 3.") seen: set[tuple[str, str]] = set() for source in result.sources: key = (source.doc_id, source.quote) if key in seen: - raise pydantic_ai.ModelRetry("Duplicate sources found.") + raise ModelRetry("Duplicate sources found.") seen.add(key) return result @@ -200,27 +199,25 @@ def validate_document_sources(result: Any) -> Any: documents = load_example_documents() for source in result.sources: if source.doc_id not in documents: - raise pydantic_ai.ModelRetry( + raise ModelRetry( f"Unknown doc_id '{source.doc_id}'. Use ids from example_dataset." ) doc_text = " ".join(documents[source.doc_id].lower().split()) quote_text = " ".join(source.quote.lower().split()) if quote_text not in doc_text: - raise pydantic_ai.ModelRetry( + raise ModelRetry( f"Quote not found in cited document '{source.doc_id}'." ) return result -def build_missing_sources_retry() -> pydantic_ai.ModelRetry: +def build_missing_sources_retry() -> ModelRetry: """ Build the retry exception used by the missing-sources demo. :return: retry exception """ - retry = pydantic_ai.ModelRetry( - "Answer references documents but sources are empty." - ) + retry = ModelRetry("Answer references documents but sources are empty.") return retry @@ -310,8 +307,8 @@ def build_explicit_openai_model(model_id: str) -> Any | None: "api_key": os.getenv("OPENAI_API_KEY"), "base_url": os.getenv("OPENAI_BASE_URL"), } - args: list[Any] = [] - kwargs: dict[str, Any] = {} + args = [] + kwargs = {} if "model_name" in parameters: kwargs["model_name"] = model_name elif "model" in parameters: From 6933bbe262d9cc3df6bedaad6de954212dc64603 Mon Sep 17 00:00:00 2001 From: Aayush Date: Tue, 21 Apr 2026 23:01:16 -0400 Subject: [PATCH 3/3] Updated README file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-commit checks: All checks passed ✅ --- tutorials/tutorial_pydanticAI/README.md | 122 +++++++++++------------- 1 file changed, 54 insertions(+), 68 deletions(-) diff --git a/tutorials/tutorial_pydanticAI/README.md b/tutorials/tutorial_pydanticAI/README.md index 161343506..b2d402aa2 100644 --- a/tutorials/tutorial_pydanticAI/README.md +++ b/tutorials/tutorial_pydanticAI/README.md @@ -1,70 +1,56 @@ - - -- [Project files](#project-files) -- [Setup and Dependencies](#setup-and-dependencies) - * [Building and Running the Docker Container](#building-and-running-the-docker-container) - + [Environment Setup](#environment-setup) - - - -# Project files - -This project contains the following files. - -- `README.md`: This file -- `pydanticai.API.ipynb`: notebook describing core PydanticAI APIs -- `pydanticai.example.ipynb`: notebook with applied, end-to-end examples -- `requirements.txt`: Python dependencies used by this tutorial -- `example_dataset/`: supporting markdown files used in examples - - `api.md` - - `billing.md` - - `integrations.md` - - `limits.md` - - `overview.md` - - `security.md` - - `support.md` - - `troubleshooting.md` -- Docker/dev runtime files - - `Dockerfile` - - `docker_build.sh` - - `docker_bash.sh` - - `docker_jupyter.sh` - - `docker_exec.sh` - - `docker_cmd.sh` - - `docker_clean.sh` - - `docker_push.sh` - - `docker_name.sh` - - `version.sh` - - `run_jupyter.sh` - - `etc_sudoers` - -# Setup and Dependencies - -## Building and Running the Docker Container - -- Go to the project directory: - ```bash - > cd tutorials/tutorial_pydanticAI - ``` -- Build Docker image: - ```bash - > ./docker_build.sh - ``` -- Run container shell: - ```bash - > ./docker_bash.sh - ``` -- Launch Jupyter Notebook: - ```bash - > ./docker_jupyter.sh - ``` - -### Environment Setup - -Set the `OPENAI_API_KEY` environment variable for API access: - -```python -import os -os.environ["OPENAI_API_KEY"] = "" +# PydanticAI Tutorial + +This folder contains the setup for running PydanticAI tutorials within a +containerized environment. + +## Quick Start + +From the root of the repository, change your directory to the PydanticAI +tutorial folder: + +```bash +> cd tutorials/tutorial_pydanticAI ``` +Once the location has been changed to the repo run the command to build the +image to run dockers: + +```bash +> ./docker_build.sh +``` + +Once the docker has been built you can then go ahead and run the container and +launch jupyter notebook using the created image using the command: + +```bash +> ./docker_jupyter.sh +``` + +Once the `./docker_jupyter.sh` script is running, work through the following +notebooks in order. + +For more information on the Docker build system refer to [Project template +README](/class_project/project_template/README.md) + +## Tutorial Notebooks + +Work through the following notebooks in order: + +- [`pydanticai.API.ipynb`](pydanticai.API.ipynb): Core PydanticAI fundamentals + - Understanding the PydanticAI framework architecture + - Working with PydanticAI classes and methods + - Building basic agent configurations + - Integration with language models + +- [`pydanticai.example.ipynb`](pydanticai.example.ipynb): Real-world application + workflow + - End-to-end agentic application example + - Practical problem-solving with PydanticAI + - Advanced agent interactions and workflows + - Best practices and patterns + +- [`pydanticai_API_utils.py`](pydanticai_API_utils.py): Utility functions + supporting the API tutorial notebook + +- [`pydanticai_example_utils.py`](pydanticai_example_utils.py): Utility + functions supporting the example tutorial notebook