Skip to content

Commit 490c3b4

Browse files
authored
Toolsets public interface and docs tweaks (#2241)
1 parent 0b3bf86 commit 490c3b4

File tree

7 files changed

+27
-31
lines changed

7 files changed

+27
-31
lines changed

docs/toolsets.md

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -334,9 +334,7 @@ print(test_model.last_model_request_parameters.function_tools)
334334

335335
[`WrapperToolset`][pydantic_ai.toolsets.WrapperToolset] wraps another toolset and delegates all responsibility to it.
336336

337-
To easily chain different modifications, you can also call [`wrap()`][pydantic_ai.toolsets.AbstractToolset.wrap] on any toolset instead of directly constructing an instance of (a subclass of) `WrapperToolset`.
338-
339-
`WrapperToolset` is a no-op by default, but enables some useful abilities:
337+
It is is a no-op by default, but enables some useful abilities:
340338

341339
#### Changing Tool Execution
342340

@@ -367,7 +365,7 @@ class LoggingToolset(WrapperToolset):
367365
return result
368366

369367

370-
logging_toolset = prepared_toolset.wrap(LoggingToolset)
368+
logging_toolset = LoggingToolset(prepared_toolset)
371369

372370
agent = Agent(TestModel(), toolsets=[logging_toolset]) # (1)!
373371
result = agent.run_sync('Call all the tools')
@@ -438,14 +436,17 @@ If you want to reuse a network connection or session across tool listings and ca
438436

439437
### Deferred Toolset
440438

441-
A deferred tool is one that will be executed not by Pydantic AI, but by the upstream service that called the agent, such as a web application that supports frontend-defined tools provided to Pydantic AI via a protocol like [AG-UI](https://docs.ag-ui.com/concepts/tools#frontend-defined-tools).
439+
A deferred tool is one whose result will be produced outside of the Pydantic AI agent run in which it was called, because it depends on an upstream service (or user) or could take longer to generate than it's reasonable to keep the agent process running.
440+
441+
Deferred tools enable various use cases:
442442

443-
!!! note
444-
This is not typically something you need to bother with, unless you are implementing support for such a protocol between an upstream tool provider and Pydantic AI.
443+
- Support client-side tools implemented by a web or app frontend
444+
- Implement a Human-in-the-Loop flow where the user needs to explicitly provide an "answer" before the run can continue
445+
- Pass slow tasks off to a background worker or external service that will send a (webhook) notification when the result is ready and the agent run can be continued.
445446

446-
When the model calls a deferred tool, the agent run ends with a [`DeferredToolCalls`][pydantic_ai.output.DeferredToolCalls] object containing the deferred tool call names and arguments, which is expected to be returned to the upstream tool provider. This upstream service is then expected to generate a response for each tool call and start a new Pydantic AI agent run with the message history and new [`ToolReturnPart`s][pydantic_ai.messages.ToolReturnPart] corresponding to each deferred call, after which the run will continue.
447+
When the model calls a deferred tool, the agent run ends with a [`DeferredToolCalls`][pydantic_ai.output.DeferredToolCalls] object containing the deferred tool call names and arguments, which are expected to be returned to the service that will (eventually) produce the result(s). Once all the results are ready, a new Pydantic AI agent run can then be started with the original run's message history plus new [`ToolReturnPart`s][pydantic_ai.messages.ToolReturnPart] (or [`RetryPromptPart`s][pydantic_ai.messages.RetryPromptPart] in case of failure) corresponding to each deferred call, after which the run will continue.
447448

448-
To enable an agent to call deferred tools, you create a [`DeferredToolset`][pydantic_ai.toolsets.DeferredToolset], pass it a list of [`ToolDefinition`s][pydantic_ai.tools.ToolDefinition], and provide it to the agent using one of the methods described above. Additionally, you need to add `DeferredToolCalls` to the `Agent`'s [output types](output.md#structured-output) so that the agent run's output type is correctly inferred. Finally, you should handle the possible `DeferredToolCalls` result by returning it to the upstream tool provider.
449+
To enable an agent to call deferred tools, you create a [`DeferredToolset`][pydantic_ai.toolsets.DeferredToolset], pass it a list of [`ToolDefinition`s][pydantic_ai.tools.ToolDefinition], and provide it to the agent using one of the methods described above. Additionally, you need to add `DeferredToolCalls` to the `Agent`'s [`output_type`](output.md#structured-output) so that the possible types of the agent run output are correctly inferred. Finally, you should handle the possible `DeferredToolCalls` output by passing it to the service that will produce the results.
449450

450451
If your agent can also be used in a context where no deferred tools are available, you will not want to include `DeferredToolCalls` in the `output_type` passed to the `Agent` constructor as you'd have to deal with that type everywhere you use the agent. Instead, you can pass the `toolsets` and `output_type` keyword arguments when you run the agent using [`agent.run()`][pydantic_ai.Agent.run], [`agent.run_sync()`][pydantic_ai.Agent.run_sync], [`agent.run_stream()`][pydantic_ai.Agent.run_stream], or [`agent.iter()`][pydantic_ai.Agent.iter]. Note that while `toolsets` provided at this stage are additional to the toolsets provided to the constructor, the `output_type` overrides the one specified at construction time (for type inference reasons), so you'll need to include the original output types explicitly.
451452

@@ -482,7 +483,7 @@ print(repr(result.output))
482483
#> PersonalizedGreeting(greeting='Hello, David!', language_code='en-US')
483484
```
484485

485-
Next, let's define an function for a hypothetical "run agent" API endpoint that can be called by the frontend and takes a list of messages to send to the model plus a dict of frontend tool names and descriptions. This is where `DeferredToolset` and `DeferredToolCalls` come in:
486+
Next, let's define a function that represents a hypothetical "run agent" API endpoint that can be called by the frontend and takes a list of messages to send to the model plus a list of frontend tool definitions. This is where `DeferredToolset` and `DeferredToolCalls` come in:
486487

487488
```python {title="deferred_toolset_api.py" requires="deferred_toolset_agent.py"}
488489
from deferred_toolset_agent import agent, PersonalizedGreeting
@@ -526,8 +527,10 @@ frontend_tool_definitions = [
526527
description="Get the user's preferred language from their browser",
527528
)
528529
]
530+
529531
def get_preferred_language(default_language: str) -> str:
530532
return 'es-MX' # (1)!
533+
531534
frontend_tool_functions = {'get_preferred_language': get_preferred_language}
532535

533536
messages: list[ModelMessage] = [
@@ -578,7 +581,7 @@ PersonalizedGreeting(greeting='Hola, David! Espero que tengas un gran día!', la
578581
"""
579582
```
580583

581-
1. Imagine that this returns [`navigator.language`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language)
584+
1. Imagine that this returns the frontend [`navigator.language`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language).
582585

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

pydantic_ai_slim/pydantic_ai/output.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,10 @@ def __get_pydantic_json_schema__(
361361

362362
@dataclass
363363
class DeferredToolCalls:
364-
"""Container for calls of deferred tools. This can be used as an agent's `output_type` and will be used as the output of the agent run if the model called any deferred tools."""
364+
"""Container for calls of deferred tools. This can be used as an agent's `output_type` and will be used as the output of the agent run if the model called any deferred tools.
365+
366+
See [deferred toolset docs](../toolsets.md#deferred-toolset) for more information.
367+
"""
365368

366369
tool_calls: list[ToolCallPart]
367370
tool_defs: dict[str, ToolDefinition]

pydantic_ai_slim/pydantic_ai/tools.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,9 +365,9 @@ class ToolDefinition:
365365
kind: ToolKind = field(default='function')
366366
"""The kind of tool:
367367
368-
- `'function'`: a tool that can be executed by Pydantic AI and has its result returned to the model
368+
- `'function'`: a tool that will be executed by Pydantic AI during an agent run and has its result returned to the model
369369
- `'output'`: a tool that passes through an output value that ends the run
370-
- `'deferred'`: a tool that will be executed not by Pydantic AI, but by the upstream service that called the agent, such as a web application that supports frontend-defined tools provided to Pydantic AI via e.g. [AG-UI](https://docs.ag-ui.com/concepts/tools#frontend-defined-tools).
370+
- `'deferred'`: a tool whose result will be produced outside of the Pydantic AI agent run in which it was called, because it depends on an upstream service (or user) or could take longer to generate than it's reasonable to keep the agent process running.
371371
When the model calls a deferred tool, the agent run ends with a `DeferredToolCalls` object and a new run is expected to be started at a later point with the message history and new `ToolReturnPart`s corresponding to each deferred call.
372372
"""
373373

pydantic_ai_slim/pydantic_ai/toolsets/abstract.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from abc import ABC, abstractmethod
44
from dataclasses import dataclass
5-
from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, Protocol, TypeVar
5+
from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, Protocol
66

77
from pydantic_core import SchemaValidator
88
from typing_extensions import Self
@@ -15,9 +15,6 @@
1515
from .prefixed import PrefixedToolset
1616
from .prepared import PreparedToolset
1717
from .renamed import RenamedToolset
18-
from .wrapper import WrapperToolset
19-
20-
WrapperT = TypeVar('WrapperT', bound='WrapperToolset[Any]')
2118

2219

2320
class SchemaValidatorProt(Protocol):
@@ -115,9 +112,9 @@ async def call_tool(
115112
"""
116113
raise NotImplementedError()
117114

118-
def apply(self, visitor: Callable[[AbstractToolset[AgentDepsT]], Any]) -> Any:
115+
def apply(self, visitor: Callable[[AbstractToolset[AgentDepsT]], None]) -> None:
119116
"""Run a visitor function on all concrete toolsets that are not wrappers (i.e. they implement their own tool listing and calling)."""
120-
return visitor(self)
117+
visitor(self)
121118

122119
def filtered(
123120
self, filter_func: Callable[[RunContext[AgentDepsT], ToolDefinition], bool]
@@ -156,10 +153,3 @@ def renamed(self, name_map: dict[str, str]) -> RenamedToolset[AgentDepsT]:
156153
from .renamed import RenamedToolset
157154

158155
return RenamedToolset(self, name_map)
159-
160-
def wrap(self, wrapper_cls: type[WrapperT], *args: Any, **kwargs: Any) -> WrapperT:
161-
"""Returns an instance of the provided wrapper class wrapping this toolset, with all arguments passed to the wrapper class constructor.
162-
163-
See [toolset docs](../toolsets.md#wrapping-a-toolset) for more information.
164-
"""
165-
return wrapper_cls(self, *args, **kwargs)

pydantic_ai_slim/pydantic_ai/toolsets/combined.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,6 @@ async def call_tool(
8383
assert isinstance(tool, _CombinedToolsetTool)
8484
return await tool.source_toolset.call_tool(name, tool_args, ctx, tool.source_tool)
8585

86-
def apply(self, visitor: Callable[[AbstractToolset[AgentDepsT]], Any]) -> Any:
86+
def apply(self, visitor: Callable[[AbstractToolset[AgentDepsT]], None]) -> None:
8787
for toolset in self.toolsets:
8888
toolset.apply(visitor)

pydantic_ai_slim/pydantic_ai/toolsets/deferred.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
@dataclass
1616
class DeferredToolset(AbstractToolset[AgentDepsT]):
17-
"""A toolset that holds deferred tools that will be called by the upstream service that called the agent.
17+
"""A toolset that holds deferred tools whose results will be produced outside of the Pydantic AI agent run in which they were called.
1818
1919
See [toolset docs](../toolsets.md#deferred-toolset), [`ToolDefinition.kind`][pydantic_ai.tools.ToolDefinition.kind], and [`DeferredToolCalls`][pydantic_ai.output.DeferredToolCalls] for more information.
2020
"""

pydantic_ai_slim/pydantic_ai/toolsets/wrapper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ async def call_tool(
3333
) -> Any:
3434
return await self.wrapped.call_tool(name, tool_args, ctx, tool)
3535

36-
def apply(self, visitor: Callable[[AbstractToolset[AgentDepsT]], Any]) -> Any:
37-
return self.wrapped.apply(visitor)
36+
def apply(self, visitor: Callable[[AbstractToolset[AgentDepsT]], None]) -> None:
37+
self.wrapped.apply(visitor)

0 commit comments

Comments
 (0)