Skip to content

Commit 5745402

Browse files
authored
Add AbstractAgent, WrapperAgent, Agent.event_stream_handler, Toolset.id, Agent.override(tools=...) in preparation for Temporal (#2458)
1 parent 174fc48 commit 5745402

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+2820
-1489
lines changed

docs/ag-ui.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ There are three ways to run a Pydantic AI agent based on AG-UI run input with st
3535

3636
1. [`run_ag_ui()`][pydantic_ai.ag_ui.run_ag_ui] takes an agent and an AG-UI [`RunAgentInput`](https://docs.ag-ui.com/sdk/python/core/types#runagentinput) object, and returns a stream of AG-UI events encoded as strings. It also takes optional [`Agent.iter()`][pydantic_ai.Agent.iter] arguments including `deps`. Use this if you're using a web framework not based on Starlette (e.g. Django or Flask) or want to modify the input or output some way.
3737
2. [`handle_ag_ui_request()`][pydantic_ai.ag_ui.handle_ag_ui_request] takes an agent and a Starlette request (e.g. from FastAPI) coming from an AG-UI frontend, and returns a streaming Starlette response of AG-UI events that you can return directly from your endpoint. It also takes optional [`Agent.iter()`][pydantic_ai.Agent.iter] arguments including `deps`, that you can vary for each request (e.g. based on the authenticated user).
38-
3. [`Agent.to_ag_ui()`][pydantic_ai.Agent.to_ag_ui] returns an ASGI application that handles every AG-UI request by running the agent. It also takes optional [`Agent.iter()`][pydantic_ai.Agent.iter] arguments including `deps`, but these will be the same for each request, with the exception of the AG-UI state that's injected as described under [state management](#state-management). This ASGI app can be [mounted](https://fastapi.tiangolo.com/advanced/sub-applications/) at a given path in an existing FastAPI app.
38+
3. [`Agent.to_ag_ui()`][pydantic_ai.agent.AbstractAgent.to_ag_ui] returns an ASGI application that handles every AG-UI request by running the agent. It also takes optional [`Agent.iter()`][pydantic_ai.Agent.iter] arguments including `deps`, but these will be the same for each request, with the exception of the AG-UI state that's injected as described under [state management](#state-management). This ASGI app can be [mounted](https://fastapi.tiangolo.com/advanced/sub-applications/) at a given path in an existing FastAPI app.
3939

4040
### Handle run input and output directly
4141

@@ -117,7 +117,7 @@ This will expose the agent as an AG-UI server, and your frontend can start sendi
117117

118118
### Stand-alone ASGI app
119119

120-
This example uses [`Agent.to_ag_ui()`][pydantic_ai.Agent.to_ag_ui] to turn the agent into a stand-alone ASGI application:
120+
This example uses [`Agent.to_ag_ui()`][pydantic_ai.agent.AbstractAgent.to_ag_ui] to turn the agent into a stand-alone ASGI application:
121121

122122
```py {title="agent_to_ag_ui.py" py="3.10" hl_lines="4"}
123123
from pydantic_ai import Agent
@@ -265,7 +265,7 @@ uvicorn ag_ui_tool_events:app --host 0.0.0.0 --port 9000
265265

266266
## Examples
267267

268-
For more examples of how to use [`to_ag_ui()`][pydantic_ai.Agent.to_ag_ui] see
268+
For more examples of how to use [`to_ag_ui()`][pydantic_ai.agent.AbstractAgent.to_ag_ui] see
269269
[`pydantic_ai_examples.ag_ui`](https://github.com/pydantic/pydantic-ai/tree/main/examples/pydantic_ai_examples/ag_ui),
270270
which includes a server for use with the
271271
[AG-UI Dojo](https://docs.ag-ui.com/tutorials/debugging#the-ag-ui-dojo).

docs/agents.md

Lines changed: 227 additions & 58 deletions
Large diffs are not rendered by default.

docs/api/agent.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
options:
55
members:
66
- Agent
7+
- AbstractAgent
8+
- WrapperAgent
79
- AgentRun
810
- AgentRunResult
911
- EndStrategy

docs/changelog.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ Pydantic AI is still pre-version 1, so breaking changes will occur, however:
1212
!!! note
1313
Here's a filtered list of the breaking changes for each version to help you upgrade Pydantic AI.
1414

15+
### v0.7.0 (2025-08-08)
16+
17+
See [#2458](https://github.com/pydantic/pydantic-ai/pull/2458) - `pydantic_ai.models.StreamedResponse` now yields a `FinalResultEvent` along with the existing `PartStartEvent` and `PartDeltaEvent`. If you're using `pydantic_ai.direct.model_request_stream` or `pydantic_ai.direct.model_request_stream_sync`, you may need to update your code to account for this.
18+
19+
See [#2458](https://github.com/pydantic/pydantic-ai/pull/2458) - `pydantic_ai.models.Model.request_stream` now receives a `run_context` argument. If you've implemented a custom `Model` subclass, you will need to account for this.
20+
21+
See [#2458](https://github.com/pydantic/pydantic-ai/pull/2458) - `pydantic_ai.models.StreamedResponse` now requires a `model_request_parameters` field and constructor argument. If you've implemented a custom `Model` subclass and implemented `request_stream`, you will need to account for this.
22+
1523
### v0.6.0 (2025-08-06)
1624

1725
This release was meant to clean some old deprecated code, so we can get a step closer to V1.

docs/direct.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ model_response = model_request_sync(
2626
)
2727

2828
print(model_response.parts[0].content)
29-
#> Paris
29+
#> The capital of France is Paris.
3030
print(model_response.usage)
31-
#> Usage(requests=1, request_tokens=56, response_tokens=1, total_tokens=57)
31+
#> Usage(requests=1, request_tokens=56, response_tokens=7, total_tokens=63)
3232
```
3333

3434
_(This example is complete, it can be run "as is")_
@@ -122,7 +122,7 @@ model_response = model_request_sync(
122122
)
123123

124124
print(model_response.parts[0].content)
125-
#> Paris
125+
#> The capital of France is Paris.
126126
```
127127

128128
_(This example is complete, it can be run "as is")_
@@ -145,7 +145,7 @@ model_response = model_request_sync(
145145
)
146146

147147
print(model_response.parts[0].content)
148-
#> Paris
148+
#> The capital of France is Paris.
149149
```
150150

151151
See [Debugging and Monitoring](logfire.md) for more details, including how to instrument with plain OpenTelemetry without Logfire.

docs/logfire.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ We can also query data with SQL in Logfire to monitor the performance of an appl
119119
agent = Agent('openai:gpt-4o')
120120
result = agent.run_sync('What is the capital of France?')
121121
print(result.output)
122-
#> Paris
122+
#> The capital of France is Paris.
123123
```
124124

125125
1. See the [`logfire.instrument_httpx` docs][logfire.Logfire.instrument_httpx] more details, `capture_all=True` means both headers and body are captured for both the request and response.
@@ -139,7 +139,7 @@ We can also query data with SQL in Logfire to monitor the performance of an appl
139139
agent = Agent('openai:gpt-4o')
140140
result = agent.run_sync('What is the capital of France?')
141141
print(result.output)
142-
#> Paris
142+
#> The capital of France is Paris.
143143
```
144144

145145
![Logfire without HTTPX instrumentation](img/logfire-without-httpx.png)
@@ -272,7 +272,7 @@ logfire.instrument_pydantic_ai(event_mode='logs')
272272
agent = Agent('openai:gpt-4o')
273273
result = agent.run_sync('What is the capital of France?')
274274
print(result.output)
275-
#> Paris
275+
#> The capital of France is Paris.
276276
```
277277

278278
For now, this won't look as good in the Logfire UI, but we're working on it.

docs/message-history.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ Pydantic AI provides access to messages exchanged during an agent run. These mes
77
After running an agent, you can access the messages exchanged during that run from the `result` object.
88

99
Both [`RunResult`][pydantic_ai.agent.AgentRunResult]
10-
(returned by [`Agent.run`][pydantic_ai.Agent.run], [`Agent.run_sync`][pydantic_ai.Agent.run_sync])
11-
and [`StreamedRunResult`][pydantic_ai.result.StreamedRunResult] (returned by [`Agent.run_stream`][pydantic_ai.Agent.run_stream]) have the following methods:
10+
(returned by [`Agent.run`][pydantic_ai.agent.AbstractAgent.run], [`Agent.run_sync`][pydantic_ai.agent.AbstractAgent.run_sync])
11+
and [`StreamedRunResult`][pydantic_ai.result.StreamedRunResult] (returned by [`Agent.run_stream`][pydantic_ai.agent.AbstractAgent.run_stream]) have the following methods:
1212

1313
- [`all_messages()`][pydantic_ai.agent.AgentRunResult.all_messages]: returns all messages, including messages from prior runs. There's also a variant that returns JSON bytes, [`all_messages_json()`][pydantic_ai.agent.AgentRunResult.all_messages_json].
1414
- [`new_messages()`][pydantic_ai.agent.AgentRunResult.new_messages]: returns only the messages from the current run. There's also a variant that returns JSON bytes, [`new_messages_json()`][pydantic_ai.agent.AgentRunResult.new_messages_json].
@@ -141,8 +141,8 @@ _(This example is complete, it can be run "as is" — you'll need to add `asynci
141141
The primary use of message histories in Pydantic AI is to maintain context across multiple agent runs.
142142

143143
To use existing messages in a run, pass them to the `message_history` parameter of
144-
[`Agent.run`][pydantic_ai.Agent.run], [`Agent.run_sync`][pydantic_ai.Agent.run_sync] or
145-
[`Agent.run_stream`][pydantic_ai.Agent.run_stream].
144+
[`Agent.run`][pydantic_ai.agent.AbstractAgent.run], [`Agent.run_sync`][pydantic_ai.agent.AbstractAgent.run_sync] or
145+
[`Agent.run_stream`][pydantic_ai.agent.AbstractAgent.run_stream].
146146

147147
If `message_history` is set and not empty, a new system prompt is not generated — we assume the existing message history includes a system prompt.
148148

docs/models/google.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,14 @@ You can supply a custom `GoogleProvider` instance using the `provider` argument
115115
This is useful if you're using a custom-compatible endpoint with the Google Generative Language API.
116116

117117
```python
118-
from google import genai
118+
from google.genai import Client
119119
from google.genai.types import HttpOptions
120120

121121
from pydantic_ai import Agent
122122
from pydantic_ai.models.google import GoogleModel
123123
from pydantic_ai.providers.google import GoogleProvider
124124

125-
client = genai.Client(
125+
client = Client(
126126
api_key='gemini-custom-api-key',
127127
http_options=HttpOptions(base_url='gemini-custom-base-url'),
128128
)

docs/multi-agent-applications.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ If you want to hand off control to another agent completely, without coming back
1616

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

19-
You'll generally want to pass [`ctx.usage`][pydantic_ai.RunContext.usage] to the [`usage`][pydantic_ai.Agent.run] keyword argument of the delegate agent run so usage within that run counts towards the total usage of the parent agent run.
19+
You'll generally want to pass [`ctx.usage`][pydantic_ai.RunContext.usage] to the [`usage`][pydantic_ai.agent.AbstractAgent.run] keyword argument of the delegate agent run so usage within that run counts towards the total usage of the parent agent run.
2020

2121
!!! note "Multiple models"
2222
Agent delegation doesn't need to use the same model for each agent. If you choose to use different models within a run, calculating the monetary cost from the final [`result.usage()`][pydantic_ai.agent.AgentRunResult.usage] of the run will not be possible, but you can still use [`UsageLimits`][pydantic_ai.usage.UsageLimits] to avoid unexpected costs.

docs/output.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,13 @@ There two main challenges with streamed results:
482482
1. Validating structured responses before they're complete, this is achieved by "partial validation" which was recently added to Pydantic in [pydantic/pydantic#10748](https://github.com/pydantic/pydantic/pull/10748).
483483
2. When receiving a response, we don't know if it's the final response without starting to stream it and peeking at the content. Pydantic AI streams just enough of the response to sniff out if it's a tool call or an output, then streams the whole thing and calls tools, or returns the stream as a [`StreamedRunResult`][pydantic_ai.result.StreamedRunResult].
484484

485+
!!! note
486+
As the `run_stream()` method will consider the first output matching the `output_type` to be the final output,
487+
it will stop running the agent graph and will not execute any tool calls made by the model after this "final" output.
488+
489+
If you want to always run the agent graph to completion and stream all events from the model's streaming response and the agent's execution of tools,
490+
use [`agent.run()`][pydantic_ai.agent.AbstractAgent.run] with an `event_stream_handler` ([docs](agents.md#streaming-all-events)) or [`agent.iter()`][pydantic_ai.agent.AbstractAgent.iter] ([docs](agents.md#streaming-all-events-and-output)) instead.
491+
485492
### Streaming Text
486493

487494
Example of streamed text output:
@@ -505,7 +512,7 @@ async def main():
505512
```
506513

507514
1. Streaming works with the standard [`Agent`][pydantic_ai.Agent] class, and doesn't require any special setup, just a model that supports streaming (currently all models support streaming).
508-
2. The [`Agent.run_stream()`][pydantic_ai.Agent.run_stream] method is used to start a streamed run, this method returns a context manager so the connection can be closed when the stream completes.
515+
2. The [`Agent.run_stream()`][pydantic_ai.agent.AbstractAgent.run_stream] method is used to start a streamed run, this method returns a context manager so the connection can be closed when the stream completes.
509516
3. Each item yield by [`StreamedRunResult.stream_text()`][pydantic_ai.result.StreamedRunResult.stream_text] is the complete text response, extended as new data is received.
510517

511518
_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_
@@ -540,22 +547,20 @@ _(This example is complete, it can be run "as is" — you'll need to add `asynci
540547

541548
### Streaming Structured Output
542549

543-
Not all types are supported with partial validation in Pydantic, see [pydantic/pydantic#10748](https://github.com/pydantic/pydantic/pull/10748), generally for model-like structures it's currently best to use `TypeDict`.
544-
545-
Here's an example of streaming a use profile as it's built:
550+
Here's an example of streaming a user profile as it's built:
546551

547552
```python {title="streamed_user_profile.py" line_length="120"}
548553
from datetime import date
549554

550-
from typing_extensions import TypedDict
555+
from typing_extensions import TypedDict, NotRequired
551556

552557
from pydantic_ai import Agent
553558

554559

555-
class UserProfile(TypedDict, total=False):
560+
class UserProfile(TypedDict):
556561
name: str
557-
dob: date
558-
bio: str
562+
dob: NotRequired[date]
563+
bio: NotRequired[str]
559564

560565

561566
agent = Agent(
@@ -581,7 +586,7 @@ async def main():
581586

582587
_(This example is complete, it can be run "as is" — you'll need to add `asyncio.run(main())` to run `main`)_
583588

584-
If you want fine-grained control of validation, particularly catching validation errors, you can use the following pattern:
589+
If you want fine-grained control of validation, you can use the following pattern to get the entire partial [`ModelResponse`][pydantic_ai.messages.ModelResponse]:
585590

586591
```python {title="streamed_user_profile.py" line_length="120"}
587592
from datetime import date

0 commit comments

Comments
 (0)