|
14 | 14 | from pydantic.json_schema import GenerateJsonSchema
|
15 | 15 | from typing_extensions import TypeVar, deprecated
|
16 | 16 |
|
17 |
| -from pydantic_ai.builtin_tools import AbstractBuiltinTool |
18 | 17 | from pydantic_graph import Graph
|
19 | 18 |
|
20 | 19 | from .. import (
|
|
30 | 29 | from .._agent_graph import HistoryProcessor
|
31 | 30 | from .._output import OutputToolset
|
32 | 31 | from .._tool_manager import ToolManager
|
| 32 | +from ..builtin_tools import AbstractBuiltinTool |
33 | 33 | from ..models.instrumented import InstrumentationSettings, InstrumentedModel, instrument_model
|
34 | 34 | from ..output import OutputDataT, OutputSpec
|
35 | 35 | from ..profiles import ModelProfile
|
|
50 | 50 | ToolsPrepareFunc,
|
51 | 51 | )
|
52 | 52 | from ..toolsets import AbstractToolset
|
| 53 | +from ..toolsets._dynamic import ( |
| 54 | + DynamicToolset, |
| 55 | + ToolsetFunc, |
| 56 | +) |
53 | 57 | from ..toolsets.combined import CombinedToolset
|
54 | 58 | from ..toolsets.function import FunctionToolset
|
55 | 59 | from ..toolsets.prepared import PreparedToolset
|
@@ -139,7 +143,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
|
139 | 143 | )
|
140 | 144 | _function_toolset: FunctionToolset[AgentDepsT] = dataclasses.field(repr=False)
|
141 | 145 | _output_toolset: OutputToolset[AgentDepsT] | None = dataclasses.field(repr=False)
|
142 |
| - _user_toolsets: Sequence[AbstractToolset[AgentDepsT]] = dataclasses.field(repr=False) |
| 146 | + _user_toolsets: list[AbstractToolset[AgentDepsT]] = dataclasses.field(repr=False) |
143 | 147 | _prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = dataclasses.field(repr=False)
|
144 | 148 | _prepare_output_tools: ToolsPrepareFunc[AgentDepsT] | None = dataclasses.field(repr=False)
|
145 | 149 | _max_result_retries: int = dataclasses.field(repr=False)
|
@@ -171,7 +175,7 @@ def __init__(
|
171 | 175 | builtin_tools: Sequence[AbstractBuiltinTool] = (),
|
172 | 176 | prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
|
173 | 177 | prepare_output_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
|
174 |
| - toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, |
| 178 | + toolsets: Sequence[AbstractToolset[AgentDepsT] | ToolsetFunc[AgentDepsT]] | None = None, |
175 | 179 | defer_model_check: bool = False,
|
176 | 180 | end_strategy: EndStrategy = 'early',
|
177 | 181 | instrument: InstrumentationSettings | bool | None = None,
|
@@ -227,7 +231,7 @@ def __init__(
|
227 | 231 | builtin_tools: Sequence[AbstractBuiltinTool] = (),
|
228 | 232 | prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
|
229 | 233 | prepare_output_tools: ToolsPrepareFunc[AgentDepsT] | None = None,
|
230 |
| - toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, |
| 234 | + toolsets: Sequence[AbstractToolset[AgentDepsT] | ToolsetFunc[AgentDepsT]] | None = None, |
231 | 235 | defer_model_check: bool = False,
|
232 | 236 | end_strategy: EndStrategy = 'early',
|
233 | 237 | instrument: InstrumentationSettings | bool | None = None,
|
@@ -265,7 +269,8 @@ def __init__(
|
265 | 269 | prepare_output_tools: Custom function to prepare the tool definition of all output tools for each step.
|
266 | 270 | This is useful if you want to customize the definition of multiple output tools or you want to register
|
267 | 271 | a subset of output tools for a given step. See [`ToolsPrepareFunc`][pydantic_ai.tools.ToolsPrepareFunc]
|
268 |
| - toolsets: Toolsets to register with the agent, including MCP servers. |
| 272 | + toolsets: Toolsets to register with the agent, including MCP servers and functions which take a run context |
| 273 | + and return a toolset. See [`ToolsetFunc`][pydantic_ai.toolsets.ToolsetFunc] for more information. |
269 | 274 | defer_model_check: by default, if you provide a [named][pydantic_ai.models.KnownModelName] model,
|
270 | 275 | it's evaluated to create a [`Model`][pydantic_ai.models.Model] instance immediately,
|
271 | 276 | which checks for the necessary environment variables. Set this to `false`
|
@@ -341,7 +346,12 @@ def __init__(
|
341 | 346 | self._output_toolset.max_retries = self._max_result_retries
|
342 | 347 |
|
343 | 348 | self._function_toolset = _AgentFunctionToolset(tools, max_retries=self._max_tool_retries)
|
344 |
| - self._user_toolsets = toolsets or () |
| 349 | + self._dynamic_toolsets = [ |
| 350 | + DynamicToolset[AgentDepsT](toolset_func=toolset) |
| 351 | + for toolset in toolsets or [] |
| 352 | + if not isinstance(toolset, AbstractToolset) |
| 353 | + ] |
| 354 | + self._user_toolsets = [toolset for toolset in toolsets or [] if isinstance(toolset, AbstractToolset)] |
345 | 355 |
|
346 | 356 | self.history_processors = history_processors or []
|
347 | 357 |
|
@@ -1138,6 +1148,53 @@ def tool_decorator(func_: ToolFuncPlain[ToolParams]) -> ToolFuncPlain[ToolParams
|
1138 | 1148 |
|
1139 | 1149 | return tool_decorator if func is None else tool_decorator(func)
|
1140 | 1150 |
|
| 1151 | + @overload |
| 1152 | + def toolset(self, func: ToolsetFunc[AgentDepsT], /) -> ToolsetFunc[AgentDepsT]: ... |
| 1153 | + |
| 1154 | + @overload |
| 1155 | + def toolset( |
| 1156 | + self, |
| 1157 | + /, |
| 1158 | + *, |
| 1159 | + per_run_step: bool = True, |
| 1160 | + ) -> Callable[[ToolsetFunc[AgentDepsT]], ToolsetFunc[AgentDepsT]]: ... |
| 1161 | + |
| 1162 | + def toolset( |
| 1163 | + self, |
| 1164 | + func: ToolsetFunc[AgentDepsT] | None = None, |
| 1165 | + /, |
| 1166 | + *, |
| 1167 | + per_run_step: bool = True, |
| 1168 | + ) -> Any: |
| 1169 | + """Decorator to register a toolset function which takes [`RunContext`][pydantic_ai.tools.RunContext] as its only argument. |
| 1170 | +
|
| 1171 | + Can decorate a sync or async functions. |
| 1172 | +
|
| 1173 | + The decorator can be used bare (`agent.toolset`). |
| 1174 | +
|
| 1175 | + Example: |
| 1176 | + ```python |
| 1177 | + from pydantic_ai import Agent, RunContext |
| 1178 | + from pydantic_ai.toolsets import AbstractToolset, FunctionToolset |
| 1179 | +
|
| 1180 | + agent = Agent('test', deps_type=str) |
| 1181 | +
|
| 1182 | + @agent.toolset |
| 1183 | + async def simple_toolset(ctx: RunContext[str]) -> AbstractToolset[str]: |
| 1184 | + return FunctionToolset() |
| 1185 | + ``` |
| 1186 | +
|
| 1187 | + Args: |
| 1188 | + func: The toolset function to register. |
| 1189 | + per_run_step: Whether to re-evaluate the toolset for each run step. Defaults to True. |
| 1190 | + """ |
| 1191 | + |
| 1192 | + def toolset_decorator(func_: ToolsetFunc[AgentDepsT]) -> ToolsetFunc[AgentDepsT]: |
| 1193 | + self._dynamic_toolsets.append(DynamicToolset(func_, per_run_step=per_run_step)) |
| 1194 | + return func_ |
| 1195 | + |
| 1196 | + return toolset_decorator if func is None else toolset_decorator(func) |
| 1197 | + |
1141 | 1198 | def _get_model(self, model: models.Model | models.KnownModelName | str | None) -> models.Model:
|
1142 | 1199 | """Create a model configured for this agent.
|
1143 | 1200 |
|
@@ -1197,10 +1254,10 @@ def _get_toolset(
|
1197 | 1254 |
|
1198 | 1255 | if some_user_toolsets := self._override_toolsets.get():
|
1199 | 1256 | user_toolsets = some_user_toolsets.value
|
1200 |
| - elif additional is not None: |
1201 |
| - user_toolsets = [*self._user_toolsets, *additional] |
1202 | 1257 | else:
|
1203 |
| - user_toolsets = self._user_toolsets |
| 1258 | + # Copy the dynamic toolsets to ensure each run has its own instances |
| 1259 | + dynamic_toolsets = [dataclasses.replace(toolset) for toolset in self._dynamic_toolsets] |
| 1260 | + user_toolsets = [*self._user_toolsets, *dynamic_toolsets, *(additional or [])] |
1204 | 1261 |
|
1205 | 1262 | if user_toolsets:
|
1206 | 1263 | toolset = CombinedToolset([function_toolset, *user_toolsets])
|
|
0 commit comments