Structured output: a typed ticket
Give an agent an output_type and the run ends with an instance of it, checked by Pydantic, instead of text you would have to parse.
from typing import Literal
from pydantic import BaseModel, Field
class Ticket(BaseModel):
category: Literal["billing", "shipping", "other"]
priority: int = Field(ge=1, le=5, description="1 is low, 5 is urgent")agent = Agent(shop_model, output_type=Ticket)
result = agent.run_sync("I was charged twice for one order")
print(result.output)
print(type(result.output).__name__)
print(result.output.priority + 1)result.output is a Ticket, so result.output.priority is an int you can do arithmetic with, and your editor knows its fields.
How the model is asked for a Ticket
A model only produces text and tool calls. Pydantic AI turns Ticket into a tool the model must call to finish, and the tool's arguments are the ticket's fields. A model function can print what it was given:
def peek(messages, info):
tool = info.output_tools[0]
print("output tool:", tool.name)
print("text allowed:", info.allow_text_output)
print(json.dumps(tool.parameters_json_schema, indent=2))
return ModelResponse(parts=[ToolCallPart(tool.name, {"category": "billing", "priority": 4})])
Agent(FunctionModel(peek), output_type=Ticket).run_sync("I was charged twice")- The output tool is called
final_result. Calling it ends the run. text allowed: False: with anoutput_typethat is notstr, a plain text answer does not end the run.- The schema is Pydantic's JSON Schema for
Ticket: theLiteralbecame anenum, andField's limits and description are there for the model to read.
This way of getting data, through a tool call, is the default because almost every model supports tools. NativeOutput and PromptedOutput from pydantic_ai use a provider's JSON mode or plain instructions instead, for models where that works better.
Not only models
agent = Agent(shop_model, output_type=list[str])
print(agent.run_sync("hello").output)Any type Pydantic can validate works: int, list[str], a TypedDict. A type that is not an object, like list[str], is wrapped in an object with one field named response. The stand-in knows nothing about that and filled in category and priority, so validation failed. What happened next is lesson 7.
- Add
summary: strtoTicket. What does the stand-in's output fail on? - Change
categoryto a plainstrand print the schema again. - Print
result.all_messages()[-1]after a typed run.
Little by little, you're building something great.