Skip to content

Commit dfc919c

Browse files
authored
Add GraphRun object to make use of next more ergonomic (#833)
1 parent 1887280 commit dfc919c

31 files changed

+1717
-908
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ testcov: test ## Run tests and generate a coverage report
6464

6565
.PHONY: update-examples
6666
update-examples: ## Update documentation examples
67-
uv run -m pytest --update-examples
67+
uv run -m pytest --update-examples tests/test_examples.py
6868

6969
# `--no-strict` so you can build the docs without insiders packages
7070
.PHONY: docs

docs/agents.md

Lines changed: 133 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,14 @@ print(result.data)
6262

6363
## Running Agents
6464

65-
There are three ways to run an agent:
65+
There are four ways to run an agent:
6666

67-
1. [`agent.run()`][pydantic_ai.Agent.run] — a coroutine which returns a [`RunResult`][pydantic_ai.result.RunResult] containing a completed response
68-
2. [`agent.run_sync()`][pydantic_ai.Agent.run_sync] — a plain, synchronous function which returns a [`RunResult`][pydantic_ai.result.RunResult] containing a completed response (internally, this just calls `loop.run_until_complete(self.run())`)
69-
3. [`agent.run_stream()`][pydantic_ai.Agent.run_stream] — a coroutine which returns a [`StreamedRunResult`][pydantic_ai.result.StreamedRunResult], which contains methods to stream a response as an async iterable
67+
1. [`agent.run()`][pydantic_ai.Agent.run] — a coroutine which returns a [`RunResult`][pydantic_ai.agent.AgentRunResult] containing a completed response.
68+
2. [`agent.run_sync()`][pydantic_ai.Agent.run_sync] — a plain, synchronous function which returns a [`RunResult`][pydantic_ai.agent.AgentRunResult] containing a completed response (internally, this just calls `loop.run_until_complete(self.run())`).
69+
3. [`agent.run_stream()`][pydantic_ai.Agent.run_stream] — a coroutine which returns a [`StreamedRunResult`][pydantic_ai.result.StreamedRunResult], which contains methods to stream a response as an async iterable.
70+
4. [`agent.iter()`][pydantic_ai.Agent.iter] — a context manager which returns an [`AgentRun`][pydantic_ai.agent.AgentRun], an async-iterable over the nodes of the agent's underlying [`Graph`][pydantic_graph.graph.Graph].
7071

71-
Here's a simple example demonstrating all three:
72+
Here's a simple example demonstrating the first three:
7273

7374
```python {title="run_agent.py"}
7475
from pydantic_ai import Agent
@@ -94,6 +95,131 @@ _(This example is complete, it can be run "as is" — you'll need to add `asynci
9495
You can also pass messages from previous runs to continue a conversation or provide context, as described in [Messages and Chat History](message-history.md).
9596

9697

98+
### Iterating Over an Agent's Graph
99+
100+
Under the hood, each `Agent` in PydanticAI uses **pydantic-graph** to manage its execution flow. **pydantic-graph** is a generic, type-centric library for building and running finite state machines in Python. It doesn't actually depend on PydanticAI — you can use it standalone for workflows that have nothing to do with GenAI — but PydanticAI makes use of it to orchestrate the handling of model requests and model responses in an agent's run.
101+
102+
In many scenarios, you don't need to worry about pydantic-graph at all; calling `agent.run(...)` simply traverses the underlying graph from start to finish. However, if you need deeper insight or control — for example to capture each tool invocation, or to inject your own logic at specific stages — PydanticAI exposes the lower-level iteration process via [`Agent.iter`][pydantic_ai.Agent.iter]. This method returns an [`AgentRun`][pydantic_ai.agent.AgentRun], which you can async-iterate over, or manually drive node-by-node via the [`next`][pydantic_ai.agent.AgentRun.next] method. Once the agent's graph returns an [`End`][pydantic_graph.nodes.End], you have the final result along with a detailed history of all steps.
103+
104+
#### `async for` iteration
105+
106+
Here's an example of using `async for` with `iter` to record each node the agent executes:
107+
108+
```python {title="agent_iter_async_for.py"}
109+
from pydantic_ai import Agent
110+
111+
agent = Agent('openai:gpt-4o')
112+
113+
114+
async def main():
115+
nodes = []
116+
# Begin an AgentRun, which is an async-iterable over the nodes of the agent's graph
117+
with agent.iter('What is the capital of France?') as agent_run:
118+
async for node in agent_run:
119+
# Each node represents a step in the agent's execution
120+
nodes.append(node)
121+
print(nodes)
122+
"""
123+
[
124+
ModelRequestNode(
125+
request=ModelRequest(
126+
parts=[
127+
UserPromptPart(
128+
content='What is the capital of France?',
129+
timestamp=datetime.datetime(...),
130+
part_kind='user-prompt',
131+
)
132+
],
133+
kind='request',
134+
)
135+
),
136+
HandleResponseNode(
137+
model_response=ModelResponse(
138+
parts=[TextPart(content='Paris', part_kind='text')],
139+
model_name='function:model_logic',
140+
timestamp=datetime.datetime(...),
141+
kind='response',
142+
)
143+
),
144+
End(data=FinalResult(data='Paris', tool_name=None)),
145+
]
146+
"""
147+
print(agent_run.result.data)
148+
#> Paris
149+
```
150+
151+
- The `AgentRun` is an async iterator that yields each node (`BaseNode` or `End`) in the flow.
152+
- The run ends when an `End` node is returned.
153+
154+
#### Using `.next(...)` manually
155+
156+
You can also drive the iteration manually by passing the node you want to run next to the `AgentRun.next(...)` method. This allows you to inspect or modify the node before it executes or skip nodes based on your own logic, and to catch errors in `next()` more easily:
157+
158+
```python {title="agent_iter_next.py"}
159+
from pydantic_ai import Agent
160+
from pydantic_graph import End
161+
162+
agent = Agent('openai:gpt-4o')
163+
164+
165+
async def main():
166+
with agent.iter('What is the capital of France?') as agent_run:
167+
node = agent_run.next_node # (1)!
168+
169+
all_nodes = [node]
170+
171+
# Drive the iteration manually:
172+
while not isinstance(node, End): # (2)!
173+
node = await agent_run.next(node) # (3)!
174+
all_nodes.append(node) # (4)!
175+
176+
print(all_nodes)
177+
"""
178+
[
179+
UserPromptNode(
180+
user_prompt='What is the capital of France?',
181+
system_prompts=(),
182+
system_prompt_functions=[],
183+
system_prompt_dynamic_functions={},
184+
),
185+
ModelRequestNode(
186+
request=ModelRequest(
187+
parts=[
188+
UserPromptPart(
189+
content='What is the capital of France?',
190+
timestamp=datetime.datetime(...),
191+
part_kind='user-prompt',
192+
)
193+
],
194+
kind='request',
195+
)
196+
),
197+
HandleResponseNode(
198+
model_response=ModelResponse(
199+
parts=[TextPart(content='Paris', part_kind='text')],
200+
model_name='function:model_logic',
201+
timestamp=datetime.datetime(...),
202+
kind='response',
203+
)
204+
),
205+
End(data=FinalResult(data='Paris', tool_name=None)),
206+
]
207+
"""
208+
```
209+
210+
1. We start by grabbing the first node that will be run in the agent's graph.
211+
2. The agent run is finished once an `End` node has been produced; instances of `End` cannot be passed to `next`.
212+
3. When you call `await agent_run.next(node)`, it executes that node in the agent's graph, updates the run's history, and returns the *next* node to run.
213+
4. You could also inspect or mutate the new `node` here as needed.
214+
215+
#### Accessing usage and the final result
216+
217+
You can retrieve usage statistics (tokens, requests, etc.) at any time from the [`AgentRun`][pydantic_ai.agent.AgentRun] object via `agent_run.usage()`. This method returns a [`Usage`][pydantic_ai.usage.Usage] object containing the usage data.
218+
219+
Once the run finishes, `agent_run.final_result` becomes a [`AgentRunResult`][pydantic_ai.agent.AgentRunResult] object containing the final output (and related metadata).
220+
221+
---
222+
97223
### Additional Configuration
98224

99225
#### Usage Limits
@@ -177,7 +303,7 @@ except UsageLimitExceeded as e:
177303
2. This run will error after 3 requests, preventing the infinite tool calling.
178304

179305
!!! note
180-
This is especially relevant if you're registered a lot of tools, `request_limit` can be used to prevent the model from choosing to make too many of these calls.
306+
This is especially relevant if you've registered many tools. The `request_limit` can be used to prevent the model from calling them in a loop too many times.
181307

182308
#### Model (Run) Settings
183309

@@ -441,7 +567,7 @@ If models behave unexpectedly (e.g., the retry limit is exceeded, or their API r
441567

442568
In these cases, [`capture_run_messages`][pydantic_ai.capture_run_messages] can be used to access the messages exchanged during the run to help diagnose the issue.
443569

444-
```python
570+
```python {title="agent_model_errors.py"}
445571
from pydantic_ai import Agent, ModelRetry, UnexpectedModelBehavior, capture_run_messages
446572

447573
agent = Agent('openai:gpt-4o')

docs/api/agent.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
options:
55
members:
66
- Agent
7+
- AgentRun
8+
- AgentRunResult
79
- EndStrategy
8-
- RunResultData
10+
- RunResultDataT
911
- capture_run_messages

docs/api/result.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@
22

33
::: pydantic_ai.result
44
options:
5-
inherited_members: true
5+
inherited_members: true
6+
members:
7+
- ResultDataT
8+
- StreamedRunResult

0 commit comments

Comments
 (0)