Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pydantic_ai_slim/pydantic_ai/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,7 @@ def tool_plain(
sequential: bool = False,
requires_approval: bool = False,
metadata: dict[str, Any] | None = None,
defer_loading: bool = False,
) -> Callable[[ToolFuncPlain[ToolParams]], ToolFuncPlain[ToolParams]]: ...

def tool_plain(
Expand All @@ -1160,6 +1161,7 @@ def tool_plain(
sequential: bool = False,
requires_approval: bool = False,
metadata: dict[str, Any] | None = None,
defer_loading: bool = False,
) -> Any:
"""Decorator to register a tool function which DOES NOT take `RunContext` as an argument.

Expand Down Expand Up @@ -1209,6 +1211,8 @@ async def spam(ctx: RunContext[str]) -> float:
requires_approval: Whether this tool requires human-in-the-loop approval. Defaults to False.
See the [tools documentation](../deferred-tools.md#human-in-the-loop-tool-approval) for more info.
metadata: Optional metadata for the tool. This is not sent to the model but can be used for filtering and tool behavior customization.
defer_loading: Whether to defer loading this tool until discovered via tool search. Defaults to False.
See [`ToolDefinition.defer_loading`][pydantic_ai.tools.ToolDefinition.defer_loading] for more info.
"""

def tool_decorator(func_: ToolFuncPlain[ToolParams]) -> ToolFuncPlain[ToolParams]:
Expand All @@ -1227,6 +1231,7 @@ def tool_decorator(func_: ToolFuncPlain[ToolParams]) -> ToolFuncPlain[ToolParams
sequential=sequential,
requires_approval=requires_approval,
metadata=metadata,
defer_loading=defer_loading,
)
return func_

Expand Down
27 changes: 27 additions & 0 deletions pydantic_ai_slim/pydantic_ai/builtin_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
'ImageGenerationTool',
'MemoryTool',
'MCPServerTool',
'ToolSearchTool',
)

_BUILTIN_TOOL_TYPES: dict[str, type[AbstractBuiltinTool]] = {}
Expand Down Expand Up @@ -395,6 +396,32 @@ def unique_id(self) -> str:
return ':'.join([self.kind, self.id])


@dataclass(kw_only=True)
class ToolSearchTool(AbstractBuiltinTool):
"""A builtin tool that enables dynamic tool discovery without loading all definitions upfront.

Instead of consuming tokens on many tool definitions, the model searches for relevant tools
on-demand. Only matching tools get expanded into full definitions in the model's context.

You should mark tools with `defer_loading=True` to make them discoverable on-demand while
keeping critical tools always loaded (with `defer_loading=False`).

Supported by:

* [Anthropic](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/tool-search-tool)
"""

search_type: Literal['regex', 'bm25'] | None = None
"""The type of search to use for tool discovery. If None, uses the provider's default.

- `'regex'`: Constructs regex patterns (for Anthropic: Python `re.search()`). Case-sensitive by default.
- `'bm25'`: Uses natural language queries with semantic matching across tool metadata.
"""

kind: str = 'tool_search'
"""The kind of tool."""


def _tool_discriminator(tool_data: dict[str, Any] | AbstractBuiltinTool) -> str:
if isinstance(tool_data, dict):
return tool_data.get('kind', AbstractBuiltinTool.kind)
Expand Down
40 changes: 39 additions & 1 deletion pydantic_ai_slim/pydantic_ai/models/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
from .._run_context import RunContext
from .._utils import guard_tool_call_id as _guard_tool_call_id
from ..builtin_tools import CodeExecutionTool, MCPServerTool, MemoryTool, WebFetchTool, WebSearchTool
from ..builtin_tools import CodeExecutionTool, MCPServerTool, MemoryTool, ToolSearchTool, WebFetchTool, WebSearchTool
from ..exceptions import ModelAPIError, UserError
from ..messages import (
BinaryContent,
Expand Down Expand Up @@ -114,6 +114,9 @@
BetaToolParam,
BetaToolResultBlockParam,
BetaToolUnionParam,
BetaToolSearchToolBm25_20251119Param,
BetaToolSearchToolRegex20251119Param,
BetaToolSearchToolResultBlock,
BetaToolUseBlock,
BetaToolUseBlockParam,
BetaWebFetchTool20250910Param,
Expand Down Expand Up @@ -574,6 +577,22 @@ def _add_builtin_tools(
) -> tuple[list[BetaToolUnionParam], list[BetaRequestMCPServerURLDefinitionParam], set[str]]:
beta_features: set[str] = set()
mcp_servers: list[BetaRequestMCPServerURLDefinitionParam] = []

# Check if any tools use defer_loading and auto-add ToolSearchTool if needed
has_defer_loading = any(
tool_def.defer_loading for tool_def in model_request_parameters.tool_defs.values()
)
has_tool_search = any(isinstance(tool, ToolSearchTool) for tool in model_request_parameters.builtin_tools)
if has_defer_loading and not has_tool_search:
# Auto-add ToolSearchTool with default (regex) when defer_loading is used
tools.append(
BetaToolSearchToolRegex20251119Param(
name='tool_search_tool_regex',
type='tool_search_tool_regex_20251119',
)
)
beta_features.add('advanced-tool-use-2025-11-20')

for tool in model_request_parameters.builtin_tools:
if isinstance(tool, WebSearchTool):
user_location = UserLocation(type='approximate', **tool.user_location) if tool.user_location else None
Expand Down Expand Up @@ -626,6 +645,23 @@ def _add_builtin_tools(
mcp_server_url_definition_param['authorization_token'] = tool.authorization_token
mcp_servers.append(mcp_server_url_definition_param)
beta_features.add('mcp-client-2025-04-04')
elif isinstance(tool, ToolSearchTool): # pragma: no branch
if tool.search_type == 'bm25':
tools.append(
BetaToolSearchToolBm25_20251119Param(
name='tool_search_tool_bm25',
type='tool_search_tool_bm25_20251119',
)
)
else:
# Default to regex if search_type is None or 'regex'
tools.append(
BetaToolSearchToolRegex20251119Param(
name='tool_search_tool_regex',
type='tool_search_tool_regex_20251119',
)
)
beta_features.add('advanced-tool-use-2025-11-20')
else: # pragma: no cover
raise UserError(
f'`{tool.__class__.__name__}` is not supported by `AnthropicModel`. If it should be, please file an issue.'
Expand Down Expand Up @@ -1041,6 +1077,8 @@ def _map_tool_definition(self, f: ToolDefinition) -> BetaToolParam:
}
if f.strict and self.profile.supports_json_schema_output:
tool_param['strict'] = f.strict
if f.defer_loading:
tool_param['defer_loading'] = True
return tool_param

@staticmethod
Expand Down
19 changes: 19 additions & 0 deletions pydantic_ai_slim/pydantic_ai/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ class Tool(Generic[ToolAgentDepsT]):
sequential: bool
requires_approval: bool
metadata: dict[str, Any] | None
defer_loading: bool
function_schema: _function_schema.FunctionSchema
"""
The base JSON schema for the tool's parameters.
Expand All @@ -285,6 +286,7 @@ def __init__(
sequential: bool = False,
requires_approval: bool = False,
metadata: dict[str, Any] | None = None,
defer_loading: bool = False,
function_schema: _function_schema.FunctionSchema | None = None,
):
"""Create a new tool instance.
Expand Down Expand Up @@ -341,6 +343,8 @@ async def prep_my_tool(
requires_approval: Whether this tool requires human-in-the-loop approval. Defaults to False.
See the [tools documentation](../deferred-tools.md#human-in-the-loop-tool-approval) for more info.
metadata: Optional metadata for the tool. This is not sent to the model but can be used for filtering and tool behavior customization.
defer_loading: Whether to defer loading this tool until discovered via tool search. Defaults to False.
See [`ToolDefinition.defer_loading`][pydantic_ai.tools.ToolDefinition.defer_loading] for more info.
function_schema: The function schema to use for the tool. If not provided, it will be generated.
"""
self.function = function
Expand All @@ -362,6 +366,7 @@ async def prep_my_tool(
self.sequential = sequential
self.requires_approval = requires_approval
self.metadata = metadata
self.defer_loading = defer_loading

@classmethod
def from_schema(
Expand Down Expand Up @@ -418,6 +423,7 @@ def tool_def(self):
sequential=self.sequential,
metadata=self.metadata,
kind='unapproved' if self.requires_approval else 'function',
defer_loading=self.defer_loading,
)

async def prepare_tool_def(self, ctx: RunContext[ToolAgentDepsT]) -> ToolDefinition | None:
Expand Down Expand Up @@ -503,6 +509,19 @@ class ToolDefinition:
For MCP tools, this contains the `meta`, `annotations`, and `output_schema` fields from the tool definition.
"""

defer_loading: bool = False
"""Whether to defer loading this tool until it's discovered via tool search.

When `True`, this tool will not be loaded into the model's context initially. Instead, the model
will use the Tool Search tool to discover it on-demand when needed, reducing token usage.

Requires the [`ToolSearchTool`][pydantic_ai.builtin_tools.ToolSearchTool] builtin tool to be enabled.

Supported by:

* [Anthropic](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/tool-search-tool)
"""

@property
def defer(self) -> bool:
"""Whether calls to this tool will be deferred.
Expand Down
9 changes: 9 additions & 0 deletions pydantic_ai_slim/pydantic_ai/toolsets/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def tool(
sequential: bool | None = None,
requires_approval: bool | None = None,
metadata: dict[str, Any] | None = None,
defer_loading: bool | None = None,
) -> Callable[[ToolFuncEither[AgentDepsT, ToolParams]], ToolFuncEither[AgentDepsT, ToolParams]]: ...

def tool(
Expand All @@ -137,6 +138,7 @@ def tool(
sequential: bool | None = None,
requires_approval: bool | None = None,
metadata: dict[str, Any] | None = None,
defer_loading: bool | None = None,
) -> Any:
"""Decorator to register a tool function which takes [`RunContext`][pydantic_ai.tools.RunContext] as its first argument.

Expand Down Expand Up @@ -193,6 +195,8 @@ async def spam(ctx: RunContext[str], y: float) -> float:
If `None`, the default value is determined by the toolset.
metadata: Optional metadata for the tool. This is not sent to the model but can be used for filtering and tool behavior customization.
If `None`, the default value is determined by the toolset. If provided, it will be merged with the toolset's metadata.
defer_loading: Whether to defer loading this tool until discovered via tool search. Defaults to False.
See [`ToolDefinition.defer_loading`][pydantic_ai.tools.ToolDefinition.defer_loading] for more info.
"""

def tool_decorator(
Expand All @@ -213,6 +217,7 @@ def tool_decorator(
sequential=sequential,
requires_approval=requires_approval,
metadata=metadata,
defer_loading=defer_loading,
)
return func_

Expand All @@ -233,6 +238,7 @@ def add_function(
sequential: bool | None = None,
requires_approval: bool | None = None,
metadata: dict[str, Any] | None = None,
defer_loading: bool | None = None,
) -> None:
"""Add a function as a tool to the toolset.

Expand Down Expand Up @@ -267,6 +273,8 @@ def add_function(
If `None`, the default value is determined by the toolset.
metadata: Optional metadata for the tool. This is not sent to the model but can be used for filtering and tool behavior customization.
If `None`, the default value is determined by the toolset. If provided, it will be merged with the toolset's metadata.
defer_loading: Whether to defer loading this tool until discovered via tool search. Defaults to False.
See [`ToolDefinition.defer_loading`][pydantic_ai.tools.ToolDefinition.defer_loading] for more info.
"""
if docstring_format is None:
docstring_format = self.docstring_format
Expand Down Expand Up @@ -295,6 +303,7 @@ def add_function(
sequential=sequential,
requires_approval=requires_approval,
metadata=metadata,
defer_loading=defer_loading or False,
)
self.add_tool(tool)

Expand Down
Loading
Loading