Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b3f8bd2
Output handling refactoring borrowed from output_mode PR
DouweM May 20, 2025
2ebe6a9
Support functions as output_type, as well as lists of functions and o…
DouweM May 20, 2025
ab576d7
Fix tests
DouweM May 20, 2025
6c4fcec
Make Python 3.9 happy
DouweM May 20, 2025
1bd16dc
Support output_type = bound instance method
DouweM May 21, 2025
98e64d4
Support RunContext arg on output_type function using same logic as tools
DouweM May 21, 2025
60d789e
Improve test coverage
DouweM May 21, 2025
14e69d0
Start output tool name disambiguation counter at 2
DouweM May 22, 2025
ee95a80
Stop requiring explicitly specifying type_ kwarg name on ToolOutput
DouweM May 22, 2025
f1f093e
Remove runtime assertion from typed_agent.py as the file is only type…
DouweM May 22, 2025
66e5405
Add typing tests for Agent(output_type=)
DouweM May 22, 2025
53d25f9
Merge remote-tracking branch 'origin/main' into output-type-callable
DouweM May 22, 2025
3aad6fc
Update typing test for mypy
DouweM May 23, 2025
3ff6e74
Treat str in an output_type list the same as in a union
DouweM May 23, 2025
a7fd8ac
Set ToolRetryError as cause on UnexpectedModelBehavior when available
DouweM May 23, 2025
5399df2
Drop end line from example test parameterized test ID to make it easi…
DouweM May 23, 2025
e503edb
Document output functions
DouweM May 23, 2025
f436797
Fix docs
DouweM May 23, 2025
046b813
Update output_type typing tests
DouweM May 23, 2025
dc39e68
Update output_type typing tests
DouweM May 23, 2025
a578745
Document when automatic output_type type inference may fail
DouweM May 23, 2025
2d6cc1a
Drop unnecessary explicit generic parameter on constructor in typing …
DouweM May 26, 2025
0d360ed
Merge remote-tracking branch 'origin/main' into output-type-callable
DouweM May 27, 2025
e40ee67
Fix bug causing multiple tools to be registered for ToolOutput[Union[…
DouweM May 27, 2025
6111ad1
Test that ToolOutput(strict=False) makes it through to the ToolDefini…
DouweM May 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/multi-agent-applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Of course, you can combine multiple strategies in a single application.
## Agent delegation

"Agent delegation" refers to the scenario where an agent delegates work to another agent, then takes back control when the delegate agent (the agent called from within a tool) finishes.
If you want to hand off control to another agent completely, without coming back to the first agent, you can use an [output function](output.md#output-functions).

Since agents are stateless and designed to be global, you do not need to include the agent itself in agent [dependencies](dependencies.md).

Expand Down
168 changes: 151 additions & 17 deletions docs/output.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
"Output" refers to the final value returned from [running an agent](agents.md#running-agents) these can be either plain text or structured data.
"Output" refers to the final value returned from [running an agent](agents.md#running-agents). This can be either plain text, [structured data](#structured-output), or the result of a [function](#output-functions) called with arguments provided by the model.

The output is wrapped in [`AgentRunResult`][pydantic_ai.agent.AgentRunResult] or [`StreamedRunResult`][pydantic_ai.result.StreamedRunResult] so you can access other data like [usage][pydantic_ai.usage.Usage] of the run and [message history](message-history.md#accessing-messages-from-results)
The output is wrapped in [`AgentRunResult`][pydantic_ai.agent.AgentRunResult] or [`StreamedRunResult`][pydantic_ai.result.StreamedRunResult] so that you can access other data, like [usage][pydantic_ai.usage.Usage] of the run and [message history](message-history.md#accessing-messages-from-results).

Both `AgentRunResult` and `StreamedRunResult` are generic in the data they wrap, so typing information about the data returned by the agent is preserved.

A run ends when a plain text response is received (assuming no output type is specified or `str` is one of the allowed options), or when the model responds with one of the structured output types by calling a special output tool. A run can also be cancelled if usage limits are exceeded, see [Usage Limits](agents.md#usage-limits).

Here's an example using a Pydantic model as the `output_type`, forcing the model to respond with data matching our specification:

```python {title="olympics.py" line_length="90"}
from pydantic import BaseModel

Expand All @@ -25,27 +29,32 @@ print(result.usage())

_(This example is complete, it can be run "as is")_

Runs end when either a plain text response is received or the model calls a tool associated with one of the structured output types (run can also be cancelled if usage limits are exceeded, see [Usage Limits](agents.md#usage-limits)).

## Output data {#structured-output}

When the output type is `str`, or a union including `str`, plain text responses are enabled on the model, and the raw text response from the model is used as the response data.
The [`Agent`][pydantic_ai.Agent] class constructor takes an `output_type` argument that takes one or more types or [output functions](#output-functions). It supports both type unions and lists of types and functions.

When no output type is specified, or when the output type is `str` or a union or list of types including `str`, the model is allowed to respond with plain text, and this text is used as the output data.
If `str` is not among the allowed output types, the model is not allowed to respond with plain text and is forced to return structured data (or arguments to an output function).

If the output type is a union with multiple members (after removing `str` from the members), each member is registered as a separate tool with the model in order to reduce the complexity of the tool schemas and maximise the chances a model will respond correctly.
If the output type is a union or list with multiple members, each member (except for `str`, if it is a member) is registered with the model as a separate output tool in order to reduce the complexity of the tool schemas and maximise the chances a model will respond correctly.

If the output type schema is not of type `"object"` (e.g. it's `int` or `list[int]`), the output type is wrapped in a single element object, so the schema of all tools registered with the model are object schemas.

Structured outputs (like tools) use Pydantic to build the JSON schema used for the tool, and to validate the data returned by the model.

!!! note "Bring on PEP-747"
Until [PEP-747](https://peps.python.org/pep-0747/) "Annotating Type Forms" lands, unions are not valid as `type`s in Python.
!!! note "Type checking considerations"
The Agent class is generic in its output type, and this type is carried through to `AgentRunResult.output` and `StreamedRunResult.output` so that your IDE or static type checker can warn you when your code doesn't properly take into account all the possible values those outputs could have.

When creating the agent we need to `# type: ignore` the `output_type` argument, and add a type hint to tell type checkers about the type of the agent.
Static type checkers like pyright and mypy will do their best the infer the agent's output type from the `output_type` you've specified, but they're not always able to do so correctly when you provide functions or multiple types in a union or list, even though PydanticAI will behave correctly. When this happens, your type checker will complain even when you're confident you've passed a valid `output_type`, and you'll need to help the type checker by explicitly specifying the generic parameters on the `Agent` constructor. This is shown in the second example below and the output functions example further down.

Here's an example of returning either text or a structured value
Specifically, there are three valid uses of `output_type` where you'll need to do this:
1. When using a union of types, e.g. `output_type=Foo | Bar` or in older Python, `output_type=Union[Foo, Bar]`. Until [PEP-747](https://peps.python.org/pep-0747/) "Annotating Type Forms" lands in Python 3.15, type checkers do not consider these a valid value for `output_type`. In addition to the generic parameters on the `Agent` constructor, you'll need to add `# type: ignore` to the line that passes the union to `output_type`.
2. With mypy: When using a list, as a functionally equivalent alternative to a union, or because you're passing in [output functions](#output-functions). Pyright does handle this correctly, and we've filed [an issue](https://github.com/python/mypy/issues/19142) with mypy to try and get this fixed.
3. With mypy: when using an async output function. Pyright does handle this correctly, and we've filed [an issue](https://github.com/python/mypy/issues/19143) with mypy to try and get this fixed.

Here's an example of returning either text or structured data:

```python {title="box_or_error.py"}
from typing import Union

from pydantic import BaseModel

Expand All @@ -59,9 +68,9 @@ class Box(BaseModel):
units: str


agent: Agent[None, Union[Box, str]] = Agent(
agent = Agent(
'openai:gpt-4o-mini',
output_type=Union[Box, str], # type: ignore
output_type=[Box, str],
system_prompt=(
"Extract me the dimensions of a box, "
"if you can't extract all data, ask the user to try again."
Expand All @@ -79,14 +88,14 @@ print(result.output)

_(This example is complete, it can be run "as is")_

Here's an example of using a union return type which registers multiple tools, and wraps non-object schemas in an object:
Here's an example of using a union return type, for which PydanticAI will register multiple tools and wraps non-object schemas in an object:

```python {title="colors_or_sizes.py"}
from typing import Union

from pydantic_ai import Agent

agent: Agent[None, Union[list[str], list[int]]] = Agent(
agent = Agent[None, Union[list[str], list[int]]](
'openai:gpt-4o-mini',
output_type=Union[list[str], list[int]], # type: ignore
system_prompt='Extract either colors or sizes from the shapes provided.',
Expand All @@ -103,10 +112,135 @@ print(result.output)

_(This example is complete, it can be run "as is")_

### Output validator functions
### Output functions

Instead of plain text or structured data, you may want the output of your agent run to be the result of a function called with arguments provided by the model, for example to further process or validate the data provided through the arguments (with the option to tell the model to try again), or to hand off to another agent.

Output functions are similar to [function tools](tools.md), but the model is forced to call one of them, the call ends the agent run, and the result is not passed back to the model.

As with tool functions, output function arguments provided by the model are validated using Pydantic, they can optionally take [`RunContext`][pydantic_ai.tools.RunContext] as the first argument, and they can raise [`ModelRetry`][pydantic_ai.exceptions.ModelRetry] to ask the model to try again with modified arguments (or with a different output type).

To specify output functions, you set the agent's `output_type` to either a single function (or bound instance method), or a list of functions. The list can also contain other output types like simple scalars or entire Pydantic models.
You typically do not want to also register your output function as a tool (using the `@agent.tool` decorator or `tools` argument), as this could confuse the model about which it should be calling.

Here's an example of all of these features in action:

```python {title="output_functions.py"}
import re
from typing import Union

from pydantic import BaseModel

from pydantic_ai import Agent, ModelRetry, RunContext
from pydantic_ai._output import ToolRetryError
from pydantic_ai.exceptions import UnexpectedModelBehavior


class Row(BaseModel):
name: str
country: str


tables = {
'capital_cities': [
Row(name='Amsterdam', country='Netherlands'),
Row(name='Mexico City', country='Mexico'),
]
}


class SQLFailure(BaseModel):
"""An unrecoverable failure. Only use this when you can't change the query to make it work."""

explanation: str


def run_sql_query(query: str) -> list[Row]:
"""Run a SQL query on the database."""

select_table = re.match(r'SELECT (.+) FROM (\w+)', query)
if select_table:
column_names = select_table.group(1)
if column_names != '*':
raise ModelRetry("Only 'SELECT *' is supported, you'll have to do column filtering manually.")

table_name = select_table.group(2)
if table_name not in tables:
raise ModelRetry(
f"Unknown table '{table_name}' in query '{query}'. Available tables: {', '.join(tables.keys())}."
)

return tables[table_name]

raise ModelRetry(f"Unsupported query: '{query}'.")


sql_agent = Agent[None, Union[list[Row], SQLFailure]](
'openai:gpt-4o',
output_type=[run_sql_query, SQLFailure],
instructions='You are a SQL agent that can run SQL queries on a database.',
)


async def hand_off_to_sql_agent(ctx: RunContext, query: str) -> list[Row]:
"""I take natural language queries, turn them into SQL, and run them on a database."""

# Drop the final message with the output tool call, as it shouldn't be passed on to the SQL agent
messages = ctx.messages[:-1]
try:
result = await sql_agent.run(query, message_history=messages)
output = result.output
if isinstance(output, SQLFailure):
raise ModelRetry(f'SQL agent failed: {output.explanation}')
return output
except UnexpectedModelBehavior as e:
# Bubble up potentially retryable errors to the router agent
if (cause := e.__cause__) and isinstance(cause, ToolRetryError):
raise ModelRetry(f'SQL agent failed: {cause.tool_retry.content}') from e
else:
raise


class RouterFailure(BaseModel):
"""Use me when no appropriate agent is found or the used agent failed."""

explanation: str


router_agent = Agent[None, Union[list[Row], RouterFailure]](
'openai:gpt-4o',
output_type=[hand_off_to_sql_agent, RouterFailure],
instructions='You are a router to other agents. Never try to solve a problem yourself, just pass it on.',
)

result = router_agent.run_sync('Select the names and countries of all capitals')
print(result.output)
"""
[
Row(name='Amsterdam', country='Netherlands'),
Row(name='Mexico City', country='Mexico'),
]
"""

result = router_agent.run_sync('Select all pets')
print(result.output)
"""
explanation = "The requested table 'pets' does not exist in the database. The only available table is 'capital_cities', which does not contain data about pets."
"""

result = router_agent.run_sync('How do I fly from Amsterdam to Mexico City?')
print(result.output)
"""
explanation = 'I am not equipped to provide travel information, such as flights from Amsterdam to Mexico City.'
"""
```

### Output validators {#output-validator-functions}

Some validation is inconvenient or impossible to do in Pydantic validators, in particular when the validation requires IO and is asynchronous. PydanticAI provides a way to add validation functions via the [`agent.output_validator`][pydantic_ai.Agent.output_validator] decorator.

If you want to implement separate validation logic for different output types, it's recommended to use [output functions](#output-functions) instead, to save you from having to do `isinstance` checks inside the output validator.

Here's a simplified variant of the [SQL Generation example](examples/sql-gen.md):

```python {title="sql_gen.py"}
Expand All @@ -127,7 +261,7 @@ class InvalidRequest(BaseModel):


Output = Union[Success, InvalidRequest]
agent: Agent[DatabaseConn, Output] = Agent(
agent = Agent[DatabaseConn, Output](
'google-gla:gemini-1.5-flash',
output_type=Output, # type: ignore
deps_type=DatabaseConn,
Expand Down
4 changes: 3 additions & 1 deletion docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

Function tools provide a mechanism for models to retrieve extra information to help them generate a response.

They're useful when it is impractical or impossible to put all the context an agent might need into the system prompt, or when you want to make agents' behavior more deterministic or reliable by deferring some of the logic required to generate a response to another (not necessarily AI-powered) tool.
They're useful when you want to enable the model to take some action and use the result, when it is impractical or impossible to put all the context an agent might need into the system prompt, or when you want to make agents' behavior more deterministic or reliable by deferring some of the logic required to generate a response to another (not necessarily AI-powered) tool.

If you want a model to be able to call a function as its final action, without the result being sent back to the model, you can use an [output function](output.md#output-functions) instead.

!!! info "Function tools vs. RAG"
Function tools are basically the "R" of RAG (Retrieval-Augmented Generation) — they augment what the model can do by letting it request extra information.
Expand Down
2 changes: 1 addition & 1 deletion examples/pydantic_ai_examples/sql_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class InvalidRequest(BaseModel):


Response: TypeAlias = Union[Success, InvalidRequest]
agent: Agent[Deps, Response] = Agent(
agent = Agent[Deps, Response](
'google-gla:gemini-1.5-flash',
# Type ignore while we wait for PEP-0747, nonetheless unions will work fine everywhere else
output_type=Response, # type: ignore
Expand Down
Loading