Pydantic AI

Pydantic AI is a library for building AI agents. Unlike the OpenAI SDK, which is specific to OpenAI models, this library can work with a wide range of different LLMs.

Documentation for Pydantic AI is available at ai.pydantic.dev.

These notes will show you some examples to illustrate the basics of how the library works.

To install the library, simply run the command

pip install pydantic-ai

in a terminal window.

Setting up an agent

All work in Pydantic AI is done through agents.

Our first example show how to set up a single agent and run a prompt.

from openai import AsyncOpenAI
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider

client = AsyncOpenAI(
            base_url="http://143.44.10.188:1234/v1",
            api_key="not-needed"
        )
model = OpenAIChatModel(
    'gpt-oss',
    provider=OpenAIProvider(openai_client=client),
)
agent = Agent(model)

result = agent.run_sync('What is the capital of Italy?')
print(result.output)

The run_sync() method is one of several methods for running a prompt on an agent. There is also an asynchronous run() method that you can use.

Building a simple chatbot

When you run a prompt through an agent the result object that gets returned contains a lot of useful information. The most commonly used information is in the output property, but there are other useful things in the result object. For example, the result also contains a message history. By pulling the message history from one result and passing it along with the next prompt we can build a simple chat memory system.

The next example illustrates this technique.

from openai import AsyncOpenAI
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider

client = AsyncOpenAI(
            base_url="http://143.44.10.188:1234/v1",
            api_key="not-needed"
        )
model = OpenAIChatModel(
    'gpt-oss',
    provider=OpenAIProvider(openai_client=client),
)
agent = Agent(model,
             instructions="You are a helpful assistant. Limit your replies to one sentence, and be concise." )

# Initial user query
initial_query = 'Tell me about the Eiffel Tower.'

# Run the agent synchronously
initial_response = agent.run_sync(initial_query)
print(initial_response.output)

# Follow-up query
follow_up_query = 'How tall is it?'

# Run the agent with the follow-up query
follow_up_response = agent.run_sync(follow_up_query, message_history=initial_response.new_messages())
print(follow_up_response.output)

Tools

Pydantic AI makes it easy to provide tools to agents. We do this simply by decorating a tool function. The next example demonstrates the basic technique using the simplest type of tool, the plain tool.

import random
from openai import AsyncOpenAI
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider

client = AsyncOpenAI(
            base_url="http://143.44.10.188:1234/v1",
            api_key="not-needed"
        )
model = OpenAIChatModel(
    'gpt-oss',
    provider=OpenAIProvider(openai_client=client),
)
agent = Agent(model,
              system_prompt=
        """ 
        You're a dice game, you should roll the die and see if the number 
        you get back matches the user's guess. If so, tell them they're a 
        winner. 
        """
)

@agent.tool_plain  
def roll_dice() -> str:
    """Roll a six-sided die and return the result."""
    return str(random.randint(1, 6))

dice_result = agent.run_sync('My guess is 4')  
print(dice_result.output)

The part of the decoration between the @ and .tool_plain must match the name of the agent you want to provide the tool to. If you have multiple agents in an application each using different tools, each tool has to be decorated with the name of the agent the tool is intended for.

Contexts and dependencies

Pydantic AI uses a construct called a RunContext to pass additional information to tools. The RunContext is essentially a container for global state information that the agent is maintaining. One of the most important things you can place in the RunContext is a dependency object. This object provides resources such as database connections, API keys, and application state information for use by tools. The next example illustrates how to use a dependency to pass important state information to a tool.

from datetime import datetime
from zoneinfo import ZoneInfo
from dataclasses import dataclass
from openai import AsyncOpenAI
from pydantic_ai import Agent, RunContext
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider

@dataclass
class User:
    name: str
    zone: str

client = AsyncOpenAI(
            base_url="http://143.44.10.188:1234/v1",
            api_key="not-needed"
        )
model = OpenAIChatModel(
    'gpt-oss',
    provider=OpenAIProvider(openai_client=client),
)

agent = Agent(model,deps_type=User)

@agent.system_prompt  
def get_system_prompt(ctx: RunContext[User]) -> str:  
    return f"You are a helpful assistant. Address the user {ctx.deps.name} by name in each reply."

@agent.tool  
def get_time(ctx: RunContext[User]) -> str:
    """ Return the current time. """
    tz = ZoneInfo(ctx.deps.zone)
    now = datetime.now(tz)
    return now

deps = User("Joe", "America/New_York")
result = agent.run_sync('What time is it now?', deps=deps)
print(result.output)

Most commonly dependencies are a single object. We usually start by settting up a class for that object. In this example that is the User class, which contains information about a user's name and their time zone.

The tool in this example is a function that can tell the correct time in the user's time zone. To get access to the RunContext we have to declare this function to be a tool instead of a plain_tool, and we have to provide the context as a parameter to the function. The function can then access the deps property of the context to get the dependencies object, which in this case is a User containing a time zone name.

Another technique illustrated in this example is the use of a custom system prompt for the agent. To construct this system prompt we set up a function that receives the RunContext as a parameter. The function pulls the user's name from the context and instructs the agent to address that user by name.

Before we run the agent we set up a User object, and then pass that object as a dependency to the agent in the run_sync() method.

Structured output

Another strength of the Pydantic AI system is its mechanism for returning structured information from a query.

Here is a basic example.

from openai import AsyncOpenAI
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider

client = AsyncOpenAI(
            base_url="http://143.44.10.188:1234/v1",
            api_key="not-needed"
        )

model = OpenAIChatModel(
    'gpt-oss',
    provider=OpenAIProvider(openai_client=client),
)

agent = Agent(model,output_type=list[str])

result = agent.run_sync('Generate three jokes.')
print(result.output)

When setting up the agent for this example we specify an output_type. Here specifically we are telling the agent to return its results in the form of a Python list containing strings.

Here is a more extensive example involving a pair of agents and passing structured information from one agent to a different agent.

from openai import AsyncOpenAI
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
from pydantic import BaseModel

client = AsyncOpenAI(
            base_url="http://143.44.10.188:1234/v1",
            api_key="not-needed"
        )

model = OpenAIChatModel(
    'gpt-oss',
    provider=OpenAIProvider(openai_client=client),
)

class ApplicantProfile(BaseModel):
    first_name: str
    last_name: str
    experience_years: int
    primary_skill: list[str]

agent = Agent(
    model,
    system_prompt='Extract name, years of experience, and primary skill from the job applicant description.',
    output_type=ApplicantProfile, 
)

applicant_description = 'Joe Gregg is a programmer with 25 years of experience, skilled in development of algorithms in C++.'
first_result = agent.run_sync(applicant_description)
print(first_result.output)

class InterviewDecision(BaseModel):
    decision: str 
    reasons: list[str]

recruiter_agent = Agent(
    model,
    deps_type=ApplicantProfile,
    output_type=InterviewDecision,
    system_prompt="""
    You are an AI recruiter looking for skilled C++ programmers.
    Based on the candidate's profile, decide if they should move to the next interview round.
    Only say 'yes' or 'no' as the decision, and explain your reasoning.
    """
)

result = recruiter_agent.run_sync(deps=first_result.output)

print(f"Decision: {result.output.decision}")
print(f"Reasons: {result.output.reasons}")

In this case we set up a ApplicantProfile class to specify the structure the first agent should use to return its result. We then pass that structured information as a dependency to the second agent. The second agent has its own output structure format specified by a separate InterviewDecision class.

Handoffs in Pydantic AI

Pydantic AI suppports multi-agent systems, including systems where agents automatically invoke other agents.

The simplest way to set up an automated handoff is to supply an agent with a tool that ends up invoking another agent.

The documentation for Pydantic AI shows several examples of this technique.