-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: Add Anthropic advanced tool use features #3550
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1c40f26
7894a30
5b5e2fe
537d7a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,10 +13,12 @@ | |
| 'WebSearchTool', | ||
| 'WebSearchUserLocation', | ||
| 'CodeExecutionTool', | ||
| 'ProgrammaticCodeExecutionTool', | ||
| 'UrlContextTool', | ||
| 'ImageGenerationTool', | ||
| 'MemoryTool', | ||
| 'MCPServerTool', | ||
| 'ToolSearchTool', | ||
| ) | ||
|
|
||
| _BUILTIN_TOOL_TYPES: dict[str, type[AbstractBuiltinTool]] = {} | ||
|
|
@@ -334,6 +336,55 @@ 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 hundreds of tool definitions, Claude 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 (with `advanced-tool-use-2025-11-20` beta) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need to mention the beta, as we'd enable it automatically if this is used |
||
|
|
||
| See https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/tool-search-tool for more info. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This link should be on the same line as Anthropic, as we may add more providers in the future |
||
| """ | ||
|
|
||
| search_type: Literal['regex', 'bm25'] = 'regex' | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have to build this builtin tool class with the expectation that other providers will support this in the future as well. But we don't know what values they'll support here, or whether they'll support regex at all. So the default value here should always be a "neutral" value. I.e., let's add |
||
| """The type of search to use for tool discovery. | ||
|
|
||
| - `'regex'`: Constructs Python `re.search()` patterns. Max 200 characters per query. Case-sensitive by default. | ||
| - `'bm25'`: Uses natural language queries with semantic matching across tool metadata. | ||
| """ | ||
|
|
||
| kind: str = 'tool_search' | ||
| """The kind of tool.""" | ||
|
|
||
|
|
||
| @dataclass(kw_only=True) | ||
| class ProgrammaticCodeExecutionTool(AbstractBuiltinTool): | ||
| """A builtin tool that enables programmatic tool calling via code execution. | ||
|
|
||
| This is an enhanced version of CodeExecutionTool that allows Claude to write Python code | ||
| that calls your custom tools programmatically within the execution container. | ||
|
|
||
| Tools that should be callable from code must have `allowed_callers=['code_execution_20250825']` | ||
| set in their ToolDefinition. | ||
|
|
||
| Supported by: | ||
|
|
||
| * Anthropic (with `advanced-tool-use-2025-11-20` beta) | ||
|
|
||
| See https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/code-execution-tool for more info. | ||
| """ | ||
|
|
||
| kind: str = 'programmatic_code_execution' | ||
| """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) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,14 @@ | |
| 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, WebSearchTool | ||
| from ..builtin_tools import ( | ||
| CodeExecutionTool, | ||
| MCPServerTool, | ||
| MemoryTool, | ||
| ProgrammaticCodeExecutionTool, | ||
| ToolSearchTool, | ||
| WebSearchTool, | ||
| ) | ||
| from ..exceptions import ModelAPIError, UserError | ||
| from ..messages import ( | ||
| BinaryContent, | ||
|
|
@@ -502,6 +509,15 @@ def _add_builtin_tools( | |
| ) -> tuple[list[BetaToolUnionParam], list[BetaRequestMCPServerURLDefinitionParam], list[str]]: | ||
| beta_features: list[str] = [] | ||
| mcp_servers: list[BetaRequestMCPServerURLDefinitionParam] = [] | ||
|
|
||
| # Check if any tools use advanced tool use features (defer_loading, allowed_callers, input_examples) | ||
| uses_advanced_tool_use = any( | ||
| tool_def.defer_loading or tool_def.allowed_callers or tool_def.input_examples | ||
| for tool_def in model_request_parameters.tool_defs.values() | ||
| ) | ||
| if uses_advanced_tool_use: | ||
| beta_features.append('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 | ||
|
|
@@ -515,6 +531,46 @@ def _add_builtin_tools( | |
| user_location=user_location, | ||
| ) | ||
| ) | ||
| elif isinstance(tool, ToolSearchTool): | ||
| # Tool Search Tool for dynamic tool discovery | ||
| # Note: These tool types are new in Anthropic's advanced-tool-use beta | ||
| # and may not yet be in the SDK's type definitions | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please update the SDK |
||
| if tool.search_type == 'regex': | ||
| tools.append( | ||
| cast( | ||
| BetaToolUnionParam, | ||
| { | ||
| 'type': 'tool_search_tool_regex_20251119', | ||
| 'name': 'tool_search_tool_regex', | ||
| }, | ||
| ) | ||
| ) | ||
| else: # bm25 | ||
| tools.append( | ||
| cast( | ||
| BetaToolUnionParam, | ||
| { | ||
| 'type': 'tool_search_tool_bm25_20251119', | ||
| 'name': 'tool_search_tool_bm25', | ||
| }, | ||
| ) | ||
| ) | ||
| if 'advanced-tool-use-2025-11-20' not in beta_features: | ||
| beta_features.append('advanced-tool-use-2025-11-20') | ||
| elif isinstance(tool, ProgrammaticCodeExecutionTool): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should not introduce a new tool type just for this window of time where Anthropic has multiple versions of code-execution. Instead, we should make our logic smarter to automatically use the newer code_execution version when advanced-tool-use is enabled / when |
||
| # Programmatic Code Execution Tool (newer version that supports allowed_callers) | ||
| # Note: This tool type is new in Anthropic's advanced-tool-use beta | ||
| tools.append( | ||
| cast( | ||
| BetaToolUnionParam, | ||
| { | ||
| 'type': 'code_execution_20250825', | ||
| 'name': 'code_execution', | ||
| }, | ||
| ) | ||
| ) | ||
| if 'advanced-tool-use-2025-11-20' not in beta_features: | ||
| beta_features.append('advanced-tool-use-2025-11-20') | ||
| elif isinstance(tool, CodeExecutionTool): # pragma: no branch | ||
| tools.append(BetaCodeExecutionTool20250522Param(name='code_execution', type='code_execution_20250522')) | ||
| beta_features.append('code-execution-2025-05-22') | ||
|
|
@@ -848,11 +904,19 @@ async def _map_user_prompt( | |
|
|
||
| @staticmethod | ||
| def _map_tool_definition(f: ToolDefinition) -> BetaToolParam: | ||
| return { | ||
| tool_param: BetaToolParam = { | ||
| 'name': f.name, | ||
| 'description': f.description or '', | ||
| 'input_schema': f.parameters_json_schema, | ||
| } | ||
| # Add advanced tool use fields (Anthropic beta: advanced-tool-use-2025-11-20) | ||
| if f.defer_loading: | ||
| tool_param['defer_loading'] = True # type: ignore[typeddict-unknown-key] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please update the SDK and drop these type ignores |
||
| if f.allowed_callers: | ||
| tool_param['allowed_callers'] = f.allowed_callers # type: ignore[typeddict-unknown-key] | ||
| if f.input_examples: | ||
| tool_param['input_examples'] = f.input_examples # type: ignore[typeddict-unknown-key] | ||
| return tool_param | ||
|
|
||
|
|
||
| def _map_usage( | ||
|
|
@@ -1061,10 +1125,26 @@ def _map_server_tool_use_block(item: BetaServerToolUseBlock, provider_name: str) | |
| args=cast(dict[str, Any], item.input) or None, | ||
| tool_call_id=item.id, | ||
| ) | ||
| elif item.name in ('web_fetch', 'bash_code_execution', 'text_editor_code_execution'): # pragma: no cover | ||
| # Note: Tool search tool names are new in Anthropic's advanced-tool-use beta | ||
| # and may not yet be in the SDK's type definitions for item.name | ||
| elif cast(str, item.name) in ('tool_search_tool_regex', 'tool_search_tool_bm25'): | ||
| # Tool Search Tool for dynamic tool discovery | ||
| return BuiltinToolCallPart( | ||
| provider_name=provider_name, | ||
| tool_name=ToolSearchTool.kind, | ||
| args=cast(dict[str, Any], item.input) or None, | ||
| tool_call_id=item.id, | ||
| ) | ||
| elif cast(str, item.name) in ('web_fetch', 'bash_code_execution', 'text_editor_code_execution'): # pragma: no cover | ||
| raise NotImplementedError(f'Anthropic built-in tool {item.name!r} is not currently supported.') | ||
| else: | ||
| assert_never(item.name) | ||
| # For new server tools we don't recognize yet, return a generic BuiltinToolCallPart | ||
| return BuiltinToolCallPart( | ||
| provider_name=provider_name, | ||
| tool_name=item.name, | ||
| args=cast(dict[str, Any], item.input) or None, | ||
| tool_call_id=item.id, | ||
| ) | ||
|
|
||
|
|
||
| web_search_tool_result_content_ta: TypeAdapter[BetaWebSearchToolResultBlockContent] = TypeAdapter( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -262,6 +262,9 @@ class Tool(Generic[ToolAgentDepsT]): | |
| sequential: bool | ||
| requires_approval: bool | ||
| metadata: dict[str, Any] | None | ||
| defer_loading: bool | ||
| allowed_callers: list[str] | None | ||
| input_examples: list[dict[str, Any]] | None | ||
| function_schema: _function_schema.FunctionSchema | ||
| """ | ||
| The base JSON schema for the tool's parameters. | ||
|
|
@@ -285,6 +288,9 @@ def __init__( | |
| sequential: bool = False, | ||
| requires_approval: bool = False, | ||
| metadata: dict[str, Any] | None = None, | ||
| defer_loading: bool = False, | ||
| allowed_callers: list[str] | None = None, | ||
| input_examples: list[dict[str, Any]] | None = None, | ||
| function_schema: _function_schema.FunctionSchema | None = None, | ||
| ): | ||
| """Create a new tool instance. | ||
|
|
@@ -341,6 +347,12 @@ 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. | ||
| allowed_callers: List of tool types that can call this tool programmatically. Defaults to None. | ||
| See [`ToolDefinition.allowed_callers`][pydantic_ai.tools.ToolDefinition.allowed_callers] for more info. | ||
| input_examples: Example inputs demonstrating correct tool usage. Defaults to None. | ||
| See [`ToolDefinition.input_examples`][pydantic_ai.tools.ToolDefinition.input_examples] for more info. | ||
| function_schema: The function schema to use for the tool. If not provided, it will be generated. | ||
| """ | ||
| self.function = function | ||
|
|
@@ -362,6 +374,9 @@ async def prep_my_tool( | |
| self.sequential = sequential | ||
| self.requires_approval = requires_approval | ||
| self.metadata = metadata | ||
| self.defer_loading = defer_loading | ||
| self.allowed_callers = allowed_callers | ||
| self.input_examples = input_examples | ||
|
|
||
| @classmethod | ||
| def from_schema( | ||
|
|
@@ -418,6 +433,9 @@ def tool_def(self): | |
| sequential=self.sequential, | ||
| metadata=self.metadata, | ||
| kind='unapproved' if self.requires_approval else 'function', | ||
| defer_loading=self.defer_loading, | ||
| allowed_callers=self.allowed_callers, | ||
| input_examples=self.input_examples, | ||
| ) | ||
|
|
||
| async def prepare_tool_def(self, ctx: RunContext[ToolAgentDepsT]) -> ToolDefinition | None: | ||
|
|
@@ -503,6 +521,39 @@ class ToolDefinition: | |
| For MCP tools, this contains the `meta`, `annotations`, and `output_schema` fields from the tool definition. | ||
| """ | ||
|
|
||
| defer_loading: bool = False | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the comment below (in the code, above in the chronological feed), I'm a little uncomfortable adding a field for an Anthropic-only feature that will likely come to other providers as well, but we don't know yet in what form, nor if Anthropic keeps this form since it's still in beta. Generally I'd say it's too early to add this field on But on the other hand, So I'm OK with this field, but I'd want it to automatically add the |
||
| """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, Claude will | ||
| use the Tool Search tool to discover it on-demand when needed, reducing token usage. | ||
|
|
||
| Requires the `ToolSearchTool` builtin tool to be enabled. | ||
|
|
||
| Note: this is currently only supported by Anthropic models with the `advanced-tool-use-2025-11-20` beta. | ||
| See https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/tool-search-tool for more info. | ||
| """ | ||
|
|
||
| allowed_callers: list[str] | None = None | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is way too Anthropic-specific to be on the generic The goal is for Pydantic AI's representations to be generic so that we can make the same agent definition work with different providers, no matter how differently they may implement a feature. Ideally it'd also be something that we can make work for providers that don't natively support the feature, for example by implementing our own "programmatic tool calling" using a So ideally we'd have a generically named boolean field like In Anthropic's case, just setting that field on one of the tool definitions should then result in the code execution tool and beta header to be added.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A few more things:
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also have to consider these limitations from https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling#feature-incompatibilities:
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Per https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling#the-allowed-callers-field,
Still, |
||
| """List of tool types that are allowed to call this tool programmatically. | ||
|
|
||
| When set to `['code_execution_20250825']`, Claude can call this tool from within code execution, | ||
| enabling programmatic tool calling where Claude writes Python code that invokes your tools. | ||
|
|
||
| Note: this is currently only supported by Anthropic models with the `advanced-tool-use-2025-11-20` beta. | ||
| See https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/code-execution-tool for more info. | ||
| """ | ||
|
|
||
| input_examples: list[dict[str, Any]] | None = None | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this just be |
||
| """Example inputs demonstrating correct tool usage patterns. | ||
|
|
||
| Provide 1-5 realistic examples showing parameter conventions, optional field patterns, | ||
| nested structures, and API-specific conventions. Each example must validate against | ||
| the tool's `parameters_json_schema`. | ||
|
|
||
| Note: this is currently only supported by Anthropic models with the `advanced-tool-use-2025-11-20` beta. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above; don't mention the beta header, just enable it automatically |
||
| See https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/tool-use-examples for more info. | ||
| """ | ||
|
|
||
| @property | ||
| def defer(self) -> bool: | ||
| """Whether calls to this tool will be deferred. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This description should be generic, not Claude specific. If the details are Claude specific, the link to the docs below suffices.