From 254f85fd629758400cd84f8096e44eaeea00ed09 Mon Sep 17 00:00:00 2001 From: AlanPonnachan Date: Sun, 30 Nov 2025 07:12:10 +0000 Subject: [PATCH 1/7] allow dynamic config of bulit in tools via runcontext --- pydantic_ai_slim/pydantic_ai/_agent_graph.py | 19 ++- .../pydantic_ai/agent/__init__.py | 13 +- .../pydantic_ai/agent/abstract.py | 37 ++--- pydantic_ai_slim/pydantic_ai/agent/wrapper.py | 7 +- .../pydantic_ai/durable_exec/dbos/_agent.py | 35 ++--- .../durable_exec/prefect/_agent.py | 31 ++--- .../durable_exec/temporal/_agent.py | 31 ++--- pydantic_ai_slim/pydantic_ai/tools.py | 13 ++ tests/test_dynamic_builtin_tools.py | 126 ++++++++++++++++++ 9 files changed, 236 insertions(+), 76 deletions(-) create mode 100644 tests/test_dynamic_builtin_tools.py diff --git a/pydantic_ai_slim/pydantic_ai/_agent_graph.py b/pydantic_ai_slim/pydantic_ai/_agent_graph.py index 6a14f8b350..043c27c4f5 100644 --- a/pydantic_ai_slim/pydantic_ai/_agent_graph.py +++ b/pydantic_ai_slim/pydantic_ai/_agent_graph.py @@ -30,6 +30,7 @@ from .output import OutputDataT, OutputSpec from .settings import ModelSettings from .tools import ( + BuiltinToolFunc, DeferredToolCallResult, DeferredToolResult, DeferredToolResults, @@ -148,7 +149,7 @@ class GraphAgentDeps(Generic[DepsT, OutputDataT]): history_processors: Sequence[HistoryProcessor[DepsT]] - builtin_tools: list[AbstractBuiltinTool] = dataclasses.field(repr=False) + builtin_tools: list[AbstractBuiltinTool | BuiltinToolFunc[DepsT]] = dataclasses.field(repr=False) tool_manager: ToolManager[DepsT] tracer: Tracer @@ -395,9 +396,23 @@ async def _prepare_request_parameters( else: function_tools.append(tool_def) + # resolve dynamic builtin tools + builtin_tools: list[AbstractBuiltinTool] = [] + if ctx.deps.builtin_tools: + run_context = build_run_context(ctx) + for tool in ctx.deps.builtin_tools: + if isinstance(tool, AbstractBuiltinTool): + builtin_tools.append(tool) + else: + t = tool(run_context) + if inspect.isawaitable(t): + t = await t + if t is not None: + builtin_tools.append(t) + return models.ModelRequestParameters( function_tools=function_tools, - builtin_tools=ctx.deps.builtin_tools, + builtin_tools=builtin_tools, output_mode=output_schema.mode, output_tools=output_tools, output_object=output_schema.object_def, diff --git a/pydantic_ai_slim/pydantic_ai/agent/__init__.py b/pydantic_ai_slim/pydantic_ai/agent/__init__.py index c8208ac9e6..19edb4a619 100644 --- a/pydantic_ai_slim/pydantic_ai/agent/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/agent/__init__.py @@ -43,6 +43,7 @@ from ..settings import ModelSettings, merge_model_settings from ..tools import ( AgentDepsT, + BuiltinToolFunc, DeferredToolResults, DocstringFormat, GenerateToolJsonSchema, @@ -170,7 +171,7 @@ def __init__( validation_context: Any | Callable[[RunContext[AgentDepsT]], Any] = None, output_retries: int | None = None, tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (), - builtin_tools: Sequence[AbstractBuiltinTool] = (), + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] = (), prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None, prepare_output_tools: ToolsPrepareFunc[AgentDepsT] | None = None, toolsets: Sequence[AbstractToolset[AgentDepsT] | ToolsetFunc[AgentDepsT]] | None = None, @@ -197,7 +198,7 @@ def __init__( validation_context: Any | Callable[[RunContext[AgentDepsT]], Any] = None, output_retries: int | None = None, tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (), - builtin_tools: Sequence[AbstractBuiltinTool] = (), + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] = (), prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None, prepare_output_tools: ToolsPrepareFunc[AgentDepsT] | None = None, mcp_servers: Sequence[MCPServer] = (), @@ -222,7 +223,7 @@ def __init__( validation_context: Any | Callable[[RunContext[AgentDepsT]], Any] = None, output_retries: int | None = None, tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] = (), - builtin_tools: Sequence[AbstractBuiltinTool] = (), + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] = (), prepare_tools: ToolsPrepareFunc[AgentDepsT] | None = None, prepare_output_tools: ToolsPrepareFunc[AgentDepsT] | None = None, toolsets: Sequence[AbstractToolset[AgentDepsT] | ToolsetFunc[AgentDepsT]] | None = None, @@ -427,7 +428,7 @@ def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AbstractAsyncContextManager[AgentRun[AgentDepsT, OutputDataT]]: ... @overload @@ -446,7 +447,7 @@ def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AbstractAsyncContextManager[AgentRun[AgentDepsT, RunOutputDataT]]: ... @asynccontextmanager @@ -465,7 +466,7 @@ async def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[AgentRun[AgentDepsT, Any]]: """A contextmanager which can be used to iterate over the agent graph's nodes as they are executed. diff --git a/pydantic_ai_slim/pydantic_ai/agent/abstract.py b/pydantic_ai_slim/pydantic_ai/agent/abstract.py index 567b61dff6..96d4d23766 100644 --- a/pydantic_ai_slim/pydantic_ai/agent/abstract.py +++ b/pydantic_ai_slim/pydantic_ai/agent/abstract.py @@ -31,6 +31,7 @@ from ..settings import ModelSettings from ..tools import ( AgentDepsT, + BuiltinToolFunc, DeferredToolResults, RunContext, Tool, @@ -138,7 +139,7 @@ async def run( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[OutputDataT]: ... @@ -158,7 +159,7 @@ async def run( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[RunOutputDataT]: ... @@ -177,7 +178,7 @@ async def run( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[Any]: """Run the agent with a user prompt in async mode. @@ -262,7 +263,7 @@ def run_sync( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[OutputDataT]: ... @@ -282,7 +283,7 @@ def run_sync( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[RunOutputDataT]: ... @@ -301,7 +302,7 @@ def run_sync( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[Any]: """Synchronously run the agent with a user prompt. @@ -378,7 +379,7 @@ def run_stream( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AbstractAsyncContextManager[result.StreamedRunResult[AgentDepsT, OutputDataT]]: ... @@ -398,7 +399,7 @@ def run_stream( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AbstractAsyncContextManager[result.StreamedRunResult[AgentDepsT, RunOutputDataT]]: ... @@ -418,7 +419,7 @@ async def run_stream( # noqa: C901 usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AsyncIterator[result.StreamedRunResult[AgentDepsT, Any]]: """Run the agent with a user prompt in async streaming mode. @@ -610,7 +611,7 @@ def run_stream_sync( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> result.StreamedRunResultSync[AgentDepsT, OutputDataT]: ... @@ -647,7 +648,7 @@ def run_stream_sync( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> result.StreamedRunResultSync[AgentDepsT, Any]: """Run the agent with a user prompt in sync streaming mode. @@ -738,7 +739,7 @@ def run_stream_events( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[OutputDataT]]: ... @overload @@ -757,7 +758,7 @@ def run_stream_events( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[RunOutputDataT]]: ... def run_stream_events( @@ -775,7 +776,7 @@ def run_stream_events( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[Any]]: """Run the agent with a user prompt in async mode and stream events from the run. @@ -866,7 +867,7 @@ async def _run_stream_events( usage_limits: _usage.UsageLimits | None = None, usage: _usage.RunUsage | None = None, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[Any]]: send_stream, receive_stream = anyio.create_memory_object_stream[ _messages.AgentStreamEvent | AgentRunResultEvent[Any] @@ -922,7 +923,7 @@ def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AbstractAsyncContextManager[AgentRun[AgentDepsT, OutputDataT]]: ... @overload @@ -941,7 +942,7 @@ def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AbstractAsyncContextManager[AgentRun[AgentDepsT, RunOutputDataT]]: ... @asynccontextmanager @@ -961,7 +962,7 @@ async def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[AgentRun[AgentDepsT, Any]]: """A contextmanager which can be used to iterate over the agent graph's nodes as they are executed. diff --git a/pydantic_ai_slim/pydantic_ai/agent/wrapper.py b/pydantic_ai_slim/pydantic_ai/agent/wrapper.py index 38e832fa2b..6e275b3f50 100644 --- a/pydantic_ai_slim/pydantic_ai/agent/wrapper.py +++ b/pydantic_ai_slim/pydantic_ai/agent/wrapper.py @@ -16,6 +16,7 @@ from ..settings import ModelSettings from ..tools import ( AgentDepsT, + BuiltinToolFunc, DeferredToolResults, Tool, ToolFuncEither, @@ -83,7 +84,7 @@ def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AbstractAsyncContextManager[AgentRun[AgentDepsT, OutputDataT]]: ... @overload @@ -102,7 +103,7 @@ def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AbstractAsyncContextManager[AgentRun[AgentDepsT, RunOutputDataT]]: ... @asynccontextmanager @@ -121,7 +122,7 @@ async def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[AgentRun[AgentDepsT, Any]]: """A contextmanager which can be used to iterate over the agent graph's nodes as they are executed. diff --git a/pydantic_ai_slim/pydantic_ai/durable_exec/dbos/_agent.py b/pydantic_ai_slim/pydantic_ai/durable_exec/dbos/_agent.py index ff6730f220..c5adf5221d 100644 --- a/pydantic_ai_slim/pydantic_ai/durable_exec/dbos/_agent.py +++ b/pydantic_ai_slim/pydantic_ai/durable_exec/dbos/_agent.py @@ -25,6 +25,7 @@ from pydantic_ai.settings import ModelSettings from pydantic_ai.tools import ( AgentDepsT, + BuiltinToolFunc, DeferredToolResults, RunContext, Tool, @@ -138,7 +139,7 @@ async def wrapped_run_workflow( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, **_deprecated_kwargs: Never, ) -> AgentRunResult[Any]: @@ -179,7 +180,7 @@ def wrapped_run_sync_workflow( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, **_deprecated_kwargs: Never, ) -> AgentRunResult[Any]: @@ -271,7 +272,7 @@ async def run( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[OutputDataT]: ... @@ -291,7 +292,7 @@ async def run( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[RunOutputDataT]: ... @@ -310,7 +311,7 @@ async def run( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, **_deprecated_kwargs: Never, ) -> AgentRunResult[Any]: @@ -389,7 +390,7 @@ def run_sync( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[OutputDataT]: ... @@ -409,7 +410,7 @@ def run_sync( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[RunOutputDataT]: ... @@ -428,7 +429,7 @@ def run_sync( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, **_deprecated_kwargs: Never, ) -> AgentRunResult[Any]: @@ -506,7 +507,7 @@ def run_stream( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AbstractAsyncContextManager[StreamedRunResult[AgentDepsT, OutputDataT]]: ... @@ -526,7 +527,7 @@ def run_stream( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AbstractAsyncContextManager[StreamedRunResult[AgentDepsT, RunOutputDataT]]: ... @@ -546,7 +547,7 @@ async def run_stream( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, **_deprecated_kwargs: Never, ) -> AsyncIterator[StreamedRunResult[AgentDepsT, Any]]: @@ -625,7 +626,7 @@ def run_stream_events( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[OutputDataT]]: ... @overload @@ -644,7 +645,7 @@ def run_stream_events( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[RunOutputDataT]]: ... def run_stream_events( @@ -662,7 +663,7 @@ def run_stream_events( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[Any]]: """Run the agent with a user prompt in async mode and stream events from the run. @@ -739,7 +740,7 @@ def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, **_deprecated_kwargs: Never, ) -> AbstractAsyncContextManager[AgentRun[AgentDepsT, OutputDataT]]: ... @@ -759,7 +760,7 @@ def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, **_deprecated_kwargs: Never, ) -> AbstractAsyncContextManager[AgentRun[AgentDepsT, RunOutputDataT]]: ... @@ -779,7 +780,7 @@ async def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, **_deprecated_kwargs: Never, ) -> AsyncIterator[AgentRun[AgentDepsT, Any]]: """A contextmanager which can be used to iterate over the agent graph's nodes as they are executed. diff --git a/pydantic_ai_slim/pydantic_ai/durable_exec/prefect/_agent.py b/pydantic_ai_slim/pydantic_ai/durable_exec/prefect/_agent.py index 8b1b6af44a..60c8122686 100644 --- a/pydantic_ai_slim/pydantic_ai/durable_exec/prefect/_agent.py +++ b/pydantic_ai_slim/pydantic_ai/durable_exec/prefect/_agent.py @@ -28,6 +28,7 @@ from pydantic_ai.settings import ModelSettings from pydantic_ai.tools import ( AgentDepsT, + BuiltinToolFunc, DeferredToolResults, RunContext, Tool, @@ -187,7 +188,7 @@ async def run( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[OutputDataT]: ... @@ -207,7 +208,7 @@ async def run( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[RunOutputDataT]: ... @@ -226,7 +227,7 @@ async def run( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, **_deprecated_kwargs: Never, ) -> AgentRunResult[Any]: @@ -311,7 +312,7 @@ def run_sync( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[OutputDataT]: ... @@ -331,7 +332,7 @@ def run_sync( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[RunOutputDataT]: ... @@ -350,7 +351,7 @@ def run_sync( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, **_deprecated_kwargs: Never, ) -> AgentRunResult[Any]: @@ -437,7 +438,7 @@ def run_stream( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AbstractAsyncContextManager[StreamedRunResult[AgentDepsT, OutputDataT]]: ... @@ -457,7 +458,7 @@ def run_stream( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AbstractAsyncContextManager[StreamedRunResult[AgentDepsT, RunOutputDataT]]: ... @@ -477,7 +478,7 @@ async def run_stream( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, **_deprecated_kwargs: Never, ) -> AsyncIterator[StreamedRunResult[AgentDepsT, Any]]: @@ -556,7 +557,7 @@ def run_stream_events( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[OutputDataT]]: ... @overload @@ -575,7 +576,7 @@ def run_stream_events( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[RunOutputDataT]]: ... def run_stream_events( @@ -593,7 +594,7 @@ def run_stream_events( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[Any]]: """Run the agent with a user prompt in async mode and stream events from the run. @@ -687,7 +688,7 @@ def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AbstractAsyncContextManager[AgentRun[AgentDepsT, OutputDataT]]: ... @overload @@ -706,7 +707,7 @@ def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AbstractAsyncContextManager[AgentRun[AgentDepsT, RunOutputDataT]]: ... @asynccontextmanager @@ -725,7 +726,7 @@ async def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[AgentRun[AgentDepsT, Any]]: """A contextmanager which can be used to iterate over the agent graph's nodes as they are executed. diff --git a/pydantic_ai_slim/pydantic_ai/durable_exec/temporal/_agent.py b/pydantic_ai_slim/pydantic_ai/durable_exec/temporal/_agent.py index 6e964c8d08..42fc2a872e 100644 --- a/pydantic_ai_slim/pydantic_ai/durable_exec/temporal/_agent.py +++ b/pydantic_ai_slim/pydantic_ai/durable_exec/temporal/_agent.py @@ -33,6 +33,7 @@ from pydantic_ai.settings import ModelSettings from pydantic_ai.tools import ( AgentDepsT, + BuiltinToolFunc, DeferredToolResults, RunContext, Tool, @@ -270,7 +271,7 @@ async def run( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[OutputDataT]: ... @@ -290,7 +291,7 @@ async def run( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[RunOutputDataT]: ... @@ -309,7 +310,7 @@ async def run( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, **_deprecated_kwargs: Never, ) -> AgentRunResult[Any]: @@ -390,7 +391,7 @@ def run_sync( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[OutputDataT]: ... @@ -410,7 +411,7 @@ def run_sync( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AgentRunResult[RunOutputDataT]: ... @@ -429,7 +430,7 @@ def run_sync( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, **_deprecated_kwargs: Never, ) -> AgentRunResult[Any]: @@ -508,7 +509,7 @@ def run_stream( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AbstractAsyncContextManager[StreamedRunResult[AgentDepsT, OutputDataT]]: ... @@ -528,7 +529,7 @@ def run_stream( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, ) -> AbstractAsyncContextManager[StreamedRunResult[AgentDepsT, RunOutputDataT]]: ... @@ -548,7 +549,7 @@ async def run_stream( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, event_stream_handler: EventStreamHandler[AgentDepsT] | None = None, **_deprecated_kwargs: Never, ) -> AsyncIterator[StreamedRunResult[AgentDepsT, Any]]: @@ -627,7 +628,7 @@ def run_stream_events( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[OutputDataT]]: ... @overload @@ -646,7 +647,7 @@ def run_stream_events( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[RunOutputDataT]]: ... def run_stream_events( @@ -664,7 +665,7 @@ def run_stream_events( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, ) -> AsyncIterator[_messages.AgentStreamEvent | AgentRunResultEvent[Any]]: """Run the agent with a user prompt in async mode and stream events from the run. @@ -757,7 +758,7 @@ def iter( usage_limits: _usage.UsageLimits | None = None, usage: _usage.RunUsage | None = None, infer_name: bool = True, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, **_deprecated_kwargs: Never, ) -> AbstractAsyncContextManager[AgentRun[AgentDepsT, OutputDataT]]: ... @@ -778,7 +779,7 @@ def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, **_deprecated_kwargs: Never, ) -> AbstractAsyncContextManager[AgentRun[AgentDepsT, RunOutputDataT]]: ... @@ -798,7 +799,7 @@ async def iter( usage: _usage.RunUsage | None = None, infer_name: bool = True, toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None, - builtin_tools: Sequence[AbstractBuiltinTool] | None = None, + builtin_tools: Sequence[AbstractBuiltinTool | BuiltinToolFunc[AgentDepsT]] | None = None, **_deprecated_kwargs: Never, ) -> AsyncIterator[AgentRun[AgentDepsT, Any]]: """A contextmanager which can be used to iterate over the agent graph's nodes as they are executed. diff --git a/pydantic_ai_slim/pydantic_ai/tools.py b/pydantic_ai_slim/pydantic_ai/tools.py index e54b829bfb..17ca3e37e6 100644 --- a/pydantic_ai_slim/pydantic_ai/tools.py +++ b/pydantic_ai_slim/pydantic_ai/tools.py @@ -11,6 +11,7 @@ from . import _function_schema, _utils from ._run_context import AgentDepsT, RunContext +from .builtin_tools import AbstractBuiltinTool from .exceptions import ModelRetry from .messages import RetryPromptPart, ToolCallPart, ToolReturn @@ -25,6 +26,7 @@ 'ToolParams', 'ToolPrepareFunc', 'ToolsPrepareFunc', + 'BuiltinToolFunc', 'Tool', 'ObjectJsonSchema', 'ToolDefinition', @@ -122,6 +124,17 @@ async def turn_on_strict_if_openai( Usage `ToolsPrepareFunc[AgentDepsT]`. """ +BuiltinToolFunc: TypeAlias = Callable[ + [RunContext[AgentDepsT]], Awaitable[AbstractBuiltinTool | None] | AbstractBuiltinTool | None +] +"""Definition of a function that can prepare a builtin tool at call time. + +This is useful if you want to customize the builtin tool based on the run context (e.g. user dependencies), +or omit it completely from a step. + +Usage `BuiltinToolFunc[AgentDepsT]`. +""" + DocstringFormat: TypeAlias = Literal['google', 'numpy', 'sphinx', 'auto'] """Supported docstring formats. diff --git a/tests/test_dynamic_builtin_tools.py b/tests/test_dynamic_builtin_tools.py new file mode 100644 index 0000000000..032dd60551 --- /dev/null +++ b/tests/test_dynamic_builtin_tools.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from pydantic_ai import Agent, RunContext +from pydantic_ai.builtin_tools import AbstractBuiltinTool, WebSearchTool, WebSearchUserLocation +from pydantic_ai.messages import ModelMessage, ModelResponse, TextPart +from pydantic_ai.models import Model, ModelRequestParameters +from pydantic_ai.settings import ModelSettings + + +@dataclass +class UserContext: + location: str | None + + +async def prepared_web_search(ctx: RunContext[UserContext]) -> WebSearchTool | None: + if not ctx.deps.location: + return None + + return WebSearchTool( + search_context_size='medium', + user_location=WebSearchUserLocation(city=ctx.deps.location), + ) + + +class InspectToolsModel(Model): + def __init__(self): + self.captured_tools: list[AbstractBuiltinTool] = [] + + @property + def model_name(self) -> str: + return 'inspect-tools-model' + + @property + def system(self) -> str: + return 'test' + + async def request( + self, + messages: list[ModelMessage], + model_settings: ModelSettings | None, + model_request_parameters: ModelRequestParameters, + ) -> ModelResponse: + self.captured_tools = model_request_parameters.builtin_tools + return ModelResponse(parts=[TextPart('OK')]) + + +async def test_dynamic_builtin_tool_configured(): + model = InspectToolsModel() + agent = Agent(model, builtin_tools=[prepared_web_search], deps_type=UserContext) + + user_context = UserContext(location='London') + await agent.run('Hello', deps=user_context) + + tools = model.captured_tools + assert len(tools) == 1 + tool = tools[0] + assert isinstance(tool, WebSearchTool) + assert tool.user_location is not None + assert tool.user_location.get('city') == 'London' + assert tool.search_context_size == 'medium' + + +async def test_dynamic_builtin_tool_omitted(): + model = InspectToolsModel() + agent = Agent(model, builtin_tools=[prepared_web_search], deps_type=UserContext) + + user_context = UserContext(location=None) + await agent.run('Hello', deps=user_context) + + tools = model.captured_tools + assert len(tools) == 0 + + +async def test_mixed_static_and_dynamic_builtin_tools(): + model = InspectToolsModel() + + static_tool = WebSearchTool(search_context_size='low') + agent = Agent(model, builtin_tools=[static_tool, prepared_web_search], deps_type=UserContext) + + # Case 1: Dynamic tool returns None + await agent.run('Hello', deps=UserContext(location=None)) + assert len(model.captured_tools) == 1 + assert model.captured_tools[0] == static_tool + + # Case 2: Dynamic tool returns a tool + await agent.run('Hello', deps=UserContext(location='Paris')) + assert len(model.captured_tools) == 2 + assert model.captured_tools[0] == static_tool + dynamic_tool = model.captured_tools[1] + assert isinstance(dynamic_tool, WebSearchTool) + assert dynamic_tool.user_location is not None + assert dynamic_tool.user_location.get('city') == 'Paris' + + +def sync_dynamic_tool(ctx: RunContext[UserContext]) -> WebSearchTool: + """Verify that synchronous functions work.""" + return WebSearchTool(search_context_size='low') + + +async def test_sync_dynamic_tool(): + model = InspectToolsModel() + agent = Agent(model, builtin_tools=[sync_dynamic_tool], deps_type=UserContext) + + await agent.run('Hello', deps=UserContext(location='London')) + + tools = model.captured_tools + assert len(tools) == 1 + assert isinstance(tools[0], WebSearchTool) + assert tools[0].search_context_size == 'low' + + +async def test_dynamic_tool_in_run_call(): + """Verify dynamic tools can be passed to agent.run().""" + model = InspectToolsModel() + agent = Agent(model, deps_type=UserContext) + + await agent.run('Hello', deps=UserContext(location='Berlin'), builtin_tools=[prepared_web_search]) + + tools = model.captured_tools + assert len(tools) == 1 + tool = tools[0] + assert isinstance(tool, WebSearchTool) + assert tool.user_location is not None + assert tool.user_location.get('city') == 'Berlin' From e8107a2f2544fe667def67b63ecde72a0b59d972 Mon Sep 17 00:00:00 2001 From: AlanPonnachan Date: Sun, 30 Nov 2025 07:51:30 +0000 Subject: [PATCH 2/7] test coverage --- tests/test_dynamic_builtin_tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_dynamic_builtin_tools.py b/tests/test_dynamic_builtin_tools.py index 032dd60551..3a61033306 100644 --- a/tests/test_dynamic_builtin_tools.py +++ b/tests/test_dynamic_builtin_tools.py @@ -48,6 +48,7 @@ async def request( async def test_dynamic_builtin_tool_configured(): model = InspectToolsModel() + assert model.system == 'test' agent = Agent(model, builtin_tools=[prepared_web_search], deps_type=UserContext) user_context = UserContext(location='London') From 289f8d0eac1d56b38fa2ef4f76f4733984d183f5 Mon Sep 17 00:00:00 2001 From: AlanPonnachan Date: Tue, 2 Dec 2025 13:15:16 +0000 Subject: [PATCH 3/7] add docs for built-in tools --- docs/builtin-tools.md | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/builtin-tools.md b/docs/builtin-tools.md index c61fa8b51d..92778e8535 100644 --- a/docs/builtin-tools.md +++ b/docs/builtin-tools.md @@ -20,6 +20,51 @@ These tools are passed to the agent via the `builtin_tools` parameter and are ex If a provider supports a built-in tool that is not currently supported by Pydantic AI, please file an issue. +## Dynamic Configuration + +Sometimes you need to configure a built-in tool dynamically based on the [run context](api/tools.md#pydantic_ai.tools.RunContext) (e.g., user dependencies). You can achieve this by passing a function to `builtin_tools` that takes [`RunContext`][pydantic_ai.tools.RunContext] as an argument and returns an [`AbstractBuiltinTool`][pydantic_ai.builtin_tools.AbstractBuiltinTool] or `None`. + +This is particularly useful for tools like [`WebSearchTool`][pydantic_ai.builtin_tools.WebSearchTool] where you might want to set the user's location based on the current request. + +```python {title="dynamic_builtin_tool.py"} +from dataclasses import dataclass +from pydantic_ai import Agent, RunContext, WebSearchTool, WebSearchUserLocation + +@dataclass +class UserContext: + location: str | None + +async def prepared_web_search(ctx: RunContext[UserContext]) -> WebSearchTool | None: + if not ctx.deps.location: + return None + + return WebSearchTool( + user_location=WebSearchUserLocation(city=ctx.deps.location), + ) + +agent = Agent( + 'openai-responses:gpt-5', + builtin_tools=[prepared_web_search], + deps_type=UserContext, +) + +# Run with location +result = agent.run_sync( + 'What is the weather like?', + deps=UserContext(location='London'), +) +print(result.output) +#> It's currently raining in London. + +# Run without location (tool will be omitted) +result = agent.run_sync( + 'What is the capital of France?', + deps=UserContext(location=None), +) +print(result.output) +#> The capital of France is Paris. +``` + ## Web Search Tool The [`WebSearchTool`][pydantic_ai.builtin_tools.WebSearchTool] allows your agent to search the web, From 04680d5e62c93f4be42bea186c78c4ca02464b46 Mon Sep 17 00:00:00 2001 From: AlanPonnachan Date: Tue, 2 Dec 2025 14:51:11 +0000 Subject: [PATCH 4/7] fix format errors --- docs/builtin-tools.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/builtin-tools.md b/docs/builtin-tools.md index 92778e8535..13c925f018 100644 --- a/docs/builtin-tools.md +++ b/docs/builtin-tools.md @@ -28,7 +28,13 @@ This is particularly useful for tools like [`WebSearchTool`][pydantic_ai.builtin ```python {title="dynamic_builtin_tool.py"} from dataclasses import dataclass -from pydantic_ai import Agent, RunContext, WebSearchTool, WebSearchUserLocation + +from pydantic_ai import ( + Agent, + RunContext, + WebSearchTool, + WebSearchUserLocation, +) @dataclass class UserContext: From 1ee5918c66f0b9c3922db2be3efcd87ae14b5085 Mon Sep 17 00:00:00 2001 From: AlanPonnachan Date: Tue, 2 Dec 2025 16:14:31 +0000 Subject: [PATCH 5/7] add examples to fix the error --- docs/builtin-tools.md | 24 +++++++----------------- tests/test_examples.py | 1 + 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/docs/builtin-tools.md b/docs/builtin-tools.md index 13c925f018..4f4ffa3857 100644 --- a/docs/builtin-tools.md +++ b/docs/builtin-tools.md @@ -27,37 +27,27 @@ Sometimes you need to configure a built-in tool dynamically based on the [run co This is particularly useful for tools like [`WebSearchTool`][pydantic_ai.builtin_tools.WebSearchTool] where you might want to set the user's location based on the current request. ```python {title="dynamic_builtin_tool.py"} -from dataclasses import dataclass +from pydantic_ai import Agent, RunContext, WebSearchTool -from pydantic_ai import ( - Agent, - RunContext, - WebSearchTool, - WebSearchUserLocation, -) - -@dataclass -class UserContext: - location: str | None -async def prepared_web_search(ctx: RunContext[UserContext]) -> WebSearchTool | None: - if not ctx.deps.location: +async def prepared_web_search(ctx: RunContext[dict]) -> WebSearchTool | None: + if not ctx.deps.get('location'): return None return WebSearchTool( - user_location=WebSearchUserLocation(city=ctx.deps.location), + user_location={'city': ctx.deps['location']}, ) agent = Agent( 'openai-responses:gpt-5', builtin_tools=[prepared_web_search], - deps_type=UserContext, + deps_type=dict, ) # Run with location result = agent.run_sync( 'What is the weather like?', - deps=UserContext(location='London'), + deps={'location': 'London'}, ) print(result.output) #> It's currently raining in London. @@ -65,7 +55,7 @@ print(result.output) # Run without location (tool will be omitted) result = agent.run_sync( 'What is the capital of France?', - deps=UserContext(location=None), + deps={'location': None}, ) print(result.output) #> The capital of France is Paris. diff --git a/tests/test_examples.py b/tests/test_examples.py index 3490f0dd3e..f5cf196b7c 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -328,6 +328,7 @@ async def call_tool( 'Give me a sentence with the biggest news in AI this week.': 'Scientists have developed a universal AI detector that can identify deepfake videos.', 'How many days between 2000-01-01 and 2025-03-18?': 'There are 9,208 days between January 1, 2000, and March 18, 2025.', 'What is 7 plus 5?': 'The answer is 12.', + 'What is the weather like?': "It's currently raining in London.", 'What is the weather like in West London and in Wiltshire?': ( 'The weather in West London is raining, while in Wiltshire it is sunny.' ), From c161d0193b8f813cfc77302a60e25d703e901fc8 Mon Sep 17 00:00:00 2001 From: AlanPonnachan Date: Wed, 3 Dec 2025 13:33:31 +0000 Subject: [PATCH 6/7] apply suggestions --- docs/builtin-tools.md | 4 +- pydantic_ai_slim/pydantic_ai/tools.py | 2 - tests/test_agent.py | 119 +++++++++++++++++++++++++- 3 files changed, 120 insertions(+), 5 deletions(-) diff --git a/docs/builtin-tools.md b/docs/builtin-tools.md index 4f4ffa3857..86f41b5c60 100644 --- a/docs/builtin-tools.md +++ b/docs/builtin-tools.md @@ -22,9 +22,9 @@ These tools are passed to the agent via the `builtin_tools` parameter and are ex ## Dynamic Configuration -Sometimes you need to configure a built-in tool dynamically based on the [run context](api/tools.md#pydantic_ai.tools.RunContext) (e.g., user dependencies). You can achieve this by passing a function to `builtin_tools` that takes [`RunContext`][pydantic_ai.tools.RunContext] as an argument and returns an [`AbstractBuiltinTool`][pydantic_ai.builtin_tools.AbstractBuiltinTool] or `None`. +Sometimes you need to configure a built-in tool dynamically based on the [run context][pydantic_ai.tools.RunContext] (e.g., user dependencies), or conditionally omit it. You can achieve this by passing a function to `builtin_tools` that takes [`RunContext`][pydantic_ai.tools.RunContext] as an argument and returns an [`AbstractBuiltinTool`][pydantic_ai.builtin_tools.AbstractBuiltinTool] or `None`. -This is particularly useful for tools like [`WebSearchTool`][pydantic_ai.builtin_tools.WebSearchTool] where you might want to set the user's location based on the current request. +This is particularly useful for tools like [`WebSearchTool`][pydantic_ai.builtin_tools.WebSearchTool] where you might want to set the user's location based on the current request, or disable the tool if the user provides no location. ```python {title="dynamic_builtin_tool.py"} from pydantic_ai import Agent, RunContext, WebSearchTool diff --git a/pydantic_ai_slim/pydantic_ai/tools.py b/pydantic_ai_slim/pydantic_ai/tools.py index 17ca3e37e6..dcd860b019 100644 --- a/pydantic_ai_slim/pydantic_ai/tools.py +++ b/pydantic_ai_slim/pydantic_ai/tools.py @@ -131,8 +131,6 @@ async def turn_on_strict_if_openai( This is useful if you want to customize the builtin tool based on the run context (e.g. user dependencies), or omit it completely from a step. - -Usage `BuiltinToolFunc[AgentDepsT]`. """ DocstringFormat: TypeAlias = Literal['google', 'numpy', 'sphinx', 'auto'] diff --git a/tests/test_agent.py b/tests/test_agent.py index c912334434..9c44f4adfb 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -57,7 +57,12 @@ TextOutput, ) from pydantic_ai.agent import AgentRunResult, WrapperAgent -from pydantic_ai.builtin_tools import CodeExecutionTool, MCPServerTool, WebSearchTool +from pydantic_ai.builtin_tools import ( + CodeExecutionTool, + MCPServerTool, + WebSearchTool, + WebSearchUserLocation, +) from pydantic_ai.models.function import AgentInfo, DeltaToolCall, DeltaToolCalls, FunctionModel from pydantic_ai.models.test import TestModel from pydantic_ai.output import OutputObjectDefinition, StructuredDict, ToolOutput @@ -6313,3 +6318,115 @@ def llm(messages: list[ModelMessage], _info: AgentInfo) -> ModelResponse: ] ) assert run.all_messages_json().startswith(b'[{"parts":[{"content":"Hello",') + + +@dataclass +class UserContext: + location: str | None + + +async def prepared_web_search(ctx: RunContext[UserContext]) -> WebSearchTool | None: + if not ctx.deps.location: + return None + + return WebSearchTool( + search_context_size='medium', + user_location=WebSearchUserLocation(city=ctx.deps.location), + ) + + +async def test_dynamic_builtin_tool_configured(): + model = TestModel() + agent = Agent(model, builtin_tools=[prepared_web_search], deps_type=UserContext) + + user_context = UserContext(location='London') + + with pytest.raises(UserError, match='TestModel does not support built-in tools'): + await agent.run('Hello', deps=user_context) + + assert model.last_model_request_parameters is not None + tools = model.last_model_request_parameters.builtin_tools + assert len(tools) == 1 + tool = tools[0] + assert isinstance(tool, WebSearchTool) + assert tool.user_location is not None + assert tool.user_location.get('city') == 'London' + assert tool.search_context_size == 'medium' + + +async def test_dynamic_builtin_tool_omitted(): + model = TestModel() + agent = Agent(model, builtin_tools=[prepared_web_search], deps_type=UserContext) + + user_context = UserContext(location=None) + + await agent.run('Hello', deps=user_context) + + assert model.last_model_request_parameters is not None + tools = model.last_model_request_parameters.builtin_tools + assert len(tools) == 0 + + +async def test_mixed_static_and_dynamic_builtin_tools(): + model = TestModel() + + static_tool = CodeExecutionTool() + agent = Agent(model, builtin_tools=[static_tool, prepared_web_search], deps_type=UserContext) + + # Case 1: Dynamic tool returns None + with pytest.raises(UserError, match='TestModel does not support built-in tools'): + await agent.run('Hello', deps=UserContext(location=None)) + + assert model.last_model_request_parameters is not None + tools = model.last_model_request_parameters.builtin_tools + assert len(tools) == 1 + assert tools[0] == static_tool + + # Case 2: Dynamic tool returns a tool + with pytest.raises(UserError, match='TestModel does not support built-in tools'): + await agent.run('Hello', deps=UserContext(location='Paris')) + + assert model.last_model_request_parameters is not None + tools = model.last_model_request_parameters.builtin_tools + assert len(tools) == 2 + assert tools[0] == static_tool + dynamic_tool = tools[1] + assert isinstance(dynamic_tool, WebSearchTool) + assert dynamic_tool.user_location is not None + assert dynamic_tool.user_location.get('city') == 'Paris' + + +def sync_dynamic_tool(ctx: RunContext[UserContext]) -> WebSearchTool: + """Verify that synchronous functions work.""" + return WebSearchTool(search_context_size='low') + + +async def test_sync_dynamic_tool(): + model = TestModel() + agent = Agent(model, builtin_tools=[sync_dynamic_tool], deps_type=UserContext) + + with pytest.raises(UserError, match='TestModel does not support built-in tools'): + await agent.run('Hello', deps=UserContext(location='London')) + + assert model.last_model_request_parameters is not None + tools = model.last_model_request_parameters.builtin_tools + assert len(tools) == 1 + assert isinstance(tools[0], WebSearchTool) + assert tools[0].search_context_size == 'low' + + +async def test_dynamic_tool_in_run_call(): + """Verify dynamic tools can be passed to agent.run().""" + model = TestModel() + agent = Agent(model, deps_type=UserContext) + + with pytest.raises(UserError, match='TestModel does not support built-in tools'): + await agent.run('Hello', deps=UserContext(location='Berlin'), builtin_tools=[prepared_web_search]) + + assert model.last_model_request_parameters is not None + tools = model.last_model_request_parameters.builtin_tools + assert len(tools) == 1 + tool = tools[0] + assert isinstance(tool, WebSearchTool) + assert tool.user_location is not None + assert tool.user_location.get('city') == 'Berlin' From 78e8e2a954af667e9283f361c4324dd96eb69645 Mon Sep 17 00:00:00 2001 From: AlanPonnachan Date: Wed, 3 Dec 2025 13:52:08 +0000 Subject: [PATCH 7/7] remove outdated dynamic_builtin_tools.py file --- tests/test_dynamic_builtin_tools.py | 127 ---------------------------- 1 file changed, 127 deletions(-) delete mode 100644 tests/test_dynamic_builtin_tools.py diff --git a/tests/test_dynamic_builtin_tools.py b/tests/test_dynamic_builtin_tools.py deleted file mode 100644 index 3a61033306..0000000000 --- a/tests/test_dynamic_builtin_tools.py +++ /dev/null @@ -1,127 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - -from pydantic_ai import Agent, RunContext -from pydantic_ai.builtin_tools import AbstractBuiltinTool, WebSearchTool, WebSearchUserLocation -from pydantic_ai.messages import ModelMessage, ModelResponse, TextPart -from pydantic_ai.models import Model, ModelRequestParameters -from pydantic_ai.settings import ModelSettings - - -@dataclass -class UserContext: - location: str | None - - -async def prepared_web_search(ctx: RunContext[UserContext]) -> WebSearchTool | None: - if not ctx.deps.location: - return None - - return WebSearchTool( - search_context_size='medium', - user_location=WebSearchUserLocation(city=ctx.deps.location), - ) - - -class InspectToolsModel(Model): - def __init__(self): - self.captured_tools: list[AbstractBuiltinTool] = [] - - @property - def model_name(self) -> str: - return 'inspect-tools-model' - - @property - def system(self) -> str: - return 'test' - - async def request( - self, - messages: list[ModelMessage], - model_settings: ModelSettings | None, - model_request_parameters: ModelRequestParameters, - ) -> ModelResponse: - self.captured_tools = model_request_parameters.builtin_tools - return ModelResponse(parts=[TextPart('OK')]) - - -async def test_dynamic_builtin_tool_configured(): - model = InspectToolsModel() - assert model.system == 'test' - agent = Agent(model, builtin_tools=[prepared_web_search], deps_type=UserContext) - - user_context = UserContext(location='London') - await agent.run('Hello', deps=user_context) - - tools = model.captured_tools - assert len(tools) == 1 - tool = tools[0] - assert isinstance(tool, WebSearchTool) - assert tool.user_location is not None - assert tool.user_location.get('city') == 'London' - assert tool.search_context_size == 'medium' - - -async def test_dynamic_builtin_tool_omitted(): - model = InspectToolsModel() - agent = Agent(model, builtin_tools=[prepared_web_search], deps_type=UserContext) - - user_context = UserContext(location=None) - await agent.run('Hello', deps=user_context) - - tools = model.captured_tools - assert len(tools) == 0 - - -async def test_mixed_static_and_dynamic_builtin_tools(): - model = InspectToolsModel() - - static_tool = WebSearchTool(search_context_size='low') - agent = Agent(model, builtin_tools=[static_tool, prepared_web_search], deps_type=UserContext) - - # Case 1: Dynamic tool returns None - await agent.run('Hello', deps=UserContext(location=None)) - assert len(model.captured_tools) == 1 - assert model.captured_tools[0] == static_tool - - # Case 2: Dynamic tool returns a tool - await agent.run('Hello', deps=UserContext(location='Paris')) - assert len(model.captured_tools) == 2 - assert model.captured_tools[0] == static_tool - dynamic_tool = model.captured_tools[1] - assert isinstance(dynamic_tool, WebSearchTool) - assert dynamic_tool.user_location is not None - assert dynamic_tool.user_location.get('city') == 'Paris' - - -def sync_dynamic_tool(ctx: RunContext[UserContext]) -> WebSearchTool: - """Verify that synchronous functions work.""" - return WebSearchTool(search_context_size='low') - - -async def test_sync_dynamic_tool(): - model = InspectToolsModel() - agent = Agent(model, builtin_tools=[sync_dynamic_tool], deps_type=UserContext) - - await agent.run('Hello', deps=UserContext(location='London')) - - tools = model.captured_tools - assert len(tools) == 1 - assert isinstance(tools[0], WebSearchTool) - assert tools[0].search_context_size == 'low' - - -async def test_dynamic_tool_in_run_call(): - """Verify dynamic tools can be passed to agent.run().""" - model = InspectToolsModel() - agent = Agent(model, deps_type=UserContext) - - await agent.run('Hello', deps=UserContext(location='Berlin'), builtin_tools=[prepared_web_search]) - - tools = model.captured_tools - assert len(tools) == 1 - tool = tools[0] - assert isinstance(tool, WebSearchTool) - assert tool.user_location is not None - assert tool.user_location.get('city') == 'Berlin'