Skip to content

Commit e854f98

Browse files
authored
Add support for human-in-the-loop tool call approval (#2581)
1 parent 69a8476 commit e854f98

Some content is hidden

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

41 files changed

+3212
-566
lines changed

docs/api/output.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@
1010
- PromptedOutput
1111
- TextOutput
1212
- StructuredDict
13-
- DeferredToolCalls
13+
- DeferredToolRequests

docs/api/toolsets.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
members:
66
- AbstractToolset
77
- CombinedToolset
8-
- DeferredToolset
8+
- ExternalToolset
9+
- ApprovalRequiredToolset
910
- FilteredToolset
1011
- FunctionToolset
1112
- PrefixedToolset

docs/output.md

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Structured outputs (like tools) use Pydantic to build the JSON schema used for t
4949

5050
Specifically, there are three valid uses of `output_type` where you'll need to do this:
5151

52-
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`. Alternatively, you can use a list: `output_type=[Foo, Bar]`.
52+
1. When using a union of types, e.g. `output_type=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`. Alternatively, you can use a list: `output_type=[Foo, Bar]`.
5353
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.
5454
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.
5555

@@ -87,20 +87,18 @@ print(result.output)
8787
#> width=10 height=20 depth=30 units='cm'
8888
```
8989

90-
1. This could also have been a union: `output_type=Box | str` (or in older Python, `output_type=Union[Box, str]`). However, as explained in the "Type checking considerations" section above, that would've required explicitly specifying the generic parameters on the `Agent` constructor and adding `# type: ignore` to this line in order to be type checked correctly.
90+
1. This could also have been a union: `output_type=Box | str`. However, as explained in the "Type checking considerations" section above, that would've required explicitly specifying the generic parameters on the `Agent` constructor and adding `# type: ignore` to this line in order to be type checked correctly.
9191

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

9494
Here's an example of using a union return type, which will register multiple output tools and wrap non-object schemas in an object:
9595

9696
```python {title="colors_or_sizes.py"}
97-
from typing import Union
98-
9997
from pydantic_ai import Agent
10098

101-
agent = Agent[None, Union[list[str], list[int]]](
99+
agent = Agent[None, list[str] | list[int]](
102100
'openai:gpt-4o-mini',
103-
output_type=Union[list[str], list[int]], # type: ignore # (1)!
101+
output_type=list[str] | list[int], # type: ignore # (1)!
104102
system_prompt='Extract either colors or sizes from the shapes provided.',
105103
)
106104

@@ -132,7 +130,6 @@ Here's an example of all of these features in action:
132130

133131
```python {title="output_functions.py"}
134132
import re
135-
from typing import Union
136133

137134
from pydantic import BaseModel
138135

@@ -179,7 +176,7 @@ def run_sql_query(query: str) -> list[Row]:
179176
raise ModelRetry(f"Unsupported query: '{query}'.")
180177

181178

182-
sql_agent = Agent[None, Union[list[Row], SQLFailure]](
179+
sql_agent = Agent[None, list[Row] | SQLFailure](
183180
'openai:gpt-4o',
184181
output_type=[run_sql_query, SQLFailure],
185182
instructions='You are a SQL agent that can run SQL queries on a database.',
@@ -211,7 +208,7 @@ class RouterFailure(BaseModel):
211208
explanation: str
212209

213210

214-
router_agent = Agent[None, Union[list[Row], RouterFailure]](
211+
router_agent = Agent[None, list[Row] | RouterFailure](
215212
'openai:gpt-4o',
216213
output_type=[hand_off_to_sql_agent, RouterFailure],
217214
instructions='You are a router to other agents. Never try to solve a problem yourself, just pass it on.',
@@ -306,7 +303,7 @@ print(repr(result.output))
306303
#> Fruit(name='banana', color='yellow')
307304
```
308305

309-
1. If we were passing just `Fruit` and `Vehicle` without custom tool names, we could have used a union: `output_type=Fruit | Vehicle` (or in older Python, `output_type=Union[Fruit | Vehicle]`). However, as `ToolOutput` is an object rather than a type, we have to use a list.
306+
1. If we were passing just `Fruit` and `Vehicle` without custom tool names, we could have used a union: `output_type=Fruit | Vehicle`. However, as `ToolOutput` is an object rather than a type, we have to use a list.
310307

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

@@ -334,7 +331,7 @@ print(repr(result.output))
334331
#> Vehicle(name='Ford Explorer', wheels=4)
335332
```
336333

337-
1. This could also have been a union: `output_type=Fruit | Vehicle` (or in older Python, `output_type=Union[Fruit, Vehicle]`). However, as explained in the "Type checking considerations" section above, that would've required explicitly specifying the generic parameters on the `Agent` constructor and adding `# type: ignore` to this line in order to be type checked correctly.
334+
1. This could also have been a union: `output_type=Fruit | Vehicle`. However, as explained in the "Type checking considerations" section above, that would've required explicitly specifying the generic parameters on the `Agent` constructor and adding `# type: ignore` to this line in order to be type checked correctly.
338335

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

@@ -384,7 +381,7 @@ print(repr(result.output))
384381
#> Vehicle(name='Ford Explorer', wheels=4)
385382
```
386383

387-
1. This could also have been a union: `output_type=Vehicle | Device` (or in older Python, `output_type=Union[Vehicle, Device]`). However, as explained in the "Type checking considerations" section above, that would've required explicitly specifying the generic parameters on the `Agent` constructor and adding `# type: ignore` to this line in order to be type checked correctly.
384+
1. This could also have been a union: `output_type=Vehicle | Device`. However, as explained in the "Type checking considerations" section above, that would've required explicitly specifying the generic parameters on the `Agent` constructor and adding `# type: ignore` to this line in order to be type checked correctly.
388385

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

@@ -429,8 +426,6 @@ If you want the model to output plain text, do your own processing or validation
429426
Here's a simplified variant of the [SQL Generation example](examples/sql-gen.md):
430427

431428
```python {title="sql_gen.py"}
432-
from typing import Union
433-
434429
from fake_database import DatabaseConn, QueryError
435430
from pydantic import BaseModel
436431

@@ -445,7 +440,7 @@ class InvalidRequest(BaseModel):
445440
error_message: str
446441

447442

448-
Output = Union[Success, InvalidRequest]
443+
Output = Success | InvalidRequest
449444
agent = Agent[DatabaseConn, Output](
450445
'google-gla:gemini-1.5-flash',
451446
output_type=Output, # type: ignore

docs/temporal.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ As workflows and activities run in separate processes, any values passed between
168168

169169
To account for these limitations, tool functions and the [event stream handler](#streaming) running inside activities receive a limited version of the agent's [`RunContext`][pydantic_ai.tools.RunContext], and it's your responsibility to make sure that the [dependencies](dependencies.md) object provided to [`TemporalAgent.run()`][pydantic_ai.durable_exec.temporal.TemporalAgent.run] can be serialized using Pydantic.
170170

171-
Specifically, only the `deps`, `retries`, `tool_call_id`, `tool_name`, `retry`, and `run_step` fields are available by default, and trying to access `model`, `usage`, `prompt`, `messages`, or `tracer` will raise an error.
171+
Specifically, only the `deps`, `retries`, `tool_call_id`, `tool_name`, `tool_call_approved`, `retry`, and `run_step` fields are available by default, and trying to access `model`, `usage`, `prompt`, `messages`, or `tracer` will raise an error.
172172
If you need one or more of these attributes to be available inside activities, you can create a [`TemporalRunContext`][pydantic_ai.durable_exec.temporal.TemporalRunContext] subclass with custom `serialize_run_context` and `deserialize_run_context` class methods and pass it to [`TemporalAgent`][pydantic_ai.durable_exec.temporal.TemporalAgent] as `run_context_type`.
173173

174174
### Streaming

0 commit comments

Comments
 (0)