From 1c40f260172aa7acac717a4e72f7eb95110876b9 Mon Sep 17 00:00:00 2001 From: Raymond Xu Date: Tue, 25 Nov 2025 01:54:51 -0800 Subject: [PATCH 1/3] feat: Add Anthropic advanced tool use features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds support for Anthropic's new advanced tool use features from the `advanced-tool-use-2025-11-20` beta: ## Tool Search Tool - New `ToolSearchTool` builtin tool with regex and BM25 search types - Enables dynamic tool discovery without loading all definitions upfront - New `defer_loading` field on `ToolDefinition` to mark tools for on-demand loading ## Programmatic Tool Calling - New `ProgrammaticCodeExecutionTool` builtin tool - New `allowed_callers` field on `ToolDefinition` to allow tools to be called from within code execution ## Tool Use Examples - New `input_examples` field on `ToolDefinition` to provide example inputs that demonstrate correct tool usage patterns All new fields are supported on: - `ToolDefinition` dataclass - `Tool` class - `@agent.tool` and `@agent.tool_plain` decorators - `FunctionToolset.add_function` method The Anthropic model automatically adds the `advanced-tool-use-2025-11-20` beta header when any of these features are used. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- pydantic_ai_slim/pydantic_ai/__init__.py | 4 + .../pydantic_ai/agent/__init__.py | 30 ++ pydantic_ai_slim/pydantic_ai/builtin_tools.py | 51 ++ .../pydantic_ai/models/anthropic.py | 72 ++- pydantic_ai_slim/pydantic_ai/tools.py | 51 ++ .../pydantic_ai/toolsets/function.py | 12 + tests/models/test_anthropic_advanced_tools.py | 444 ++++++++++++++++++ 7 files changed, 661 insertions(+), 3 deletions(-) create mode 100644 tests/models/test_anthropic_advanced_tools.py diff --git a/pydantic_ai_slim/pydantic_ai/__init__.py b/pydantic_ai_slim/pydantic_ai/__init__.py index f33a0ad3ec..73df5b247d 100644 --- a/pydantic_ai_slim/pydantic_ai/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/__init__.py @@ -14,6 +14,8 @@ ImageGenerationTool, MCPServerTool, MemoryTool, + ProgrammaticCodeExecutionTool, + ToolSearchTool, UrlContextTool, WebSearchTool, WebSearchUserLocation, @@ -218,9 +220,11 @@ 'WebSearchUserLocation', 'UrlContextTool', 'CodeExecutionTool', + 'ProgrammaticCodeExecutionTool', 'ImageGenerationTool', 'MemoryTool', 'MCPServerTool', + 'ToolSearchTool', # output 'ToolOutput', 'NativeOutput', diff --git a/pydantic_ai_slim/pydantic_ai/agent/__init__.py b/pydantic_ai_slim/pydantic_ai/agent/__init__.py index 4cd353b44a..c9bf482a02 100644 --- a/pydantic_ai_slim/pydantic_ai/agent/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/agent/__init__.py @@ -1023,6 +1023,9 @@ def tool( 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, ) -> Callable[[ToolFuncContext[AgentDepsT, ToolParams]], ToolFuncContext[AgentDepsT, ToolParams]]: ... def tool( @@ -1041,6 +1044,9 @@ def tool( 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, ) -> Any: """Decorator to register a tool function which takes [`RunContext`][pydantic_ai.tools.RunContext] as its first argument. @@ -1090,6 +1096,12 @@ async def spam(ctx: RunContext[str], y: float) -> 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. + 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. """ def tool_decorator( @@ -1110,6 +1122,9 @@ def tool_decorator( sequential=sequential, requires_approval=requires_approval, metadata=metadata, + defer_loading=defer_loading, + allowed_callers=allowed_callers, + input_examples=input_examples, ) return func_ @@ -1134,6 +1149,9 @@ def tool_plain( 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, ) -> Callable[[ToolFuncPlain[ToolParams]], ToolFuncPlain[ToolParams]]: ... def tool_plain( @@ -1152,6 +1170,9 @@ def tool_plain( 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, ) -> Any: """Decorator to register a tool function which DOES NOT take `RunContext` as an argument. @@ -1201,6 +1222,12 @@ 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. + 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. """ def tool_decorator(func_: ToolFuncPlain[ToolParams]) -> ToolFuncPlain[ToolParams]: @@ -1219,6 +1246,9 @@ def tool_decorator(func_: ToolFuncPlain[ToolParams]) -> ToolFuncPlain[ToolParams sequential=sequential, requires_approval=requires_approval, metadata=metadata, + defer_loading=defer_loading, + allowed_callers=allowed_callers, + input_examples=input_examples, ) return func_ diff --git a/pydantic_ai_slim/pydantic_ai/builtin_tools.py b/pydantic_ai_slim/pydantic_ai/builtin_tools.py index 5559b3124a..51bc6fe719 100644 --- a/pydantic_ai_slim/pydantic_ai/builtin_tools.py +++ b/pydantic_ai_slim/pydantic_ai/builtin_tools.py @@ -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) + + See https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/tool-search-tool for more info. + """ + + search_type: Literal['regex', 'bm25'] = 'regex' + """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) diff --git a/pydantic_ai_slim/pydantic_ai/models/anthropic.py b/pydantic_ai_slim/pydantic_ai/models/anthropic.py index 342c141b9d..a08313eaf2 100644 --- a/pydantic_ai_slim/pydantic_ai/models/anthropic.py +++ b/pydantic_ai_slim/pydantic_ai/models/anthropic.py @@ -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,34 @@ def _add_builtin_tools( user_location=user_location, ) ) + elif isinstance(tool, ToolSearchTool): + # Tool Search Tool for dynamic tool discovery + if tool.search_type == 'regex': + tools.append( + { + 'type': 'tool_search_tool_regex_20251119', + 'name': 'tool_search_tool_regex', + } + ) # type: ignore[arg-type] + else: # bm25 + tools.append( + { + 'type': 'tool_search_tool_bm25_20251119', + 'name': 'tool_search_tool_bm25', + } + ) # type: ignore[arg-type] + if 'advanced-tool-use-2025-11-20' not in beta_features: + beta_features.append('advanced-tool-use-2025-11-20') + elif isinstance(tool, ProgrammaticCodeExecutionTool): + # Programmatic Code Execution Tool (newer version that supports allowed_callers) + tools.append( + { + 'type': 'code_execution_20250825', + 'name': 'code_execution', + } + ) # type: ignore[arg-type] + 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 +892,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] + 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 +1113,24 @@ 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 ('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 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( diff --git a/pydantic_ai_slim/pydantic_ai/tools.py b/pydantic_ai_slim/pydantic_ai/tools.py index ca72cafbb5..e0b7c89543 100644 --- a/pydantic_ai_slim/pydantic_ai/tools.py +++ b/pydantic_ai_slim/pydantic_ai/tools.py @@ -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 + """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 + """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 + """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. + 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. diff --git a/pydantic_ai_slim/pydantic_ai/toolsets/function.py b/pydantic_ai_slim/pydantic_ai/toolsets/function.py index e185ed0273..727c430a8e 100644 --- a/pydantic_ai_slim/pydantic_ai/toolsets/function.py +++ b/pydantic_ai_slim/pydantic_ai/toolsets/function.py @@ -233,6 +233,9 @@ def add_function( sequential: bool | None = None, requires_approval: bool | None = None, metadata: dict[str, Any] | None = None, + defer_loading: bool = False, + allowed_callers: list[str] | None = None, + input_examples: list[dict[str, Any]] | None = None, ) -> None: """Add a function as a tool to the toolset. @@ -267,6 +270,12 @@ 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. + 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. """ if docstring_format is None: docstring_format = self.docstring_format @@ -295,6 +304,9 @@ def add_function( sequential=sequential, requires_approval=requires_approval, metadata=metadata, + defer_loading=defer_loading, + allowed_callers=allowed_callers, + input_examples=input_examples, ) self.add_tool(tool) diff --git a/tests/models/test_anthropic_advanced_tools.py b/tests/models/test_anthropic_advanced_tools.py new file mode 100644 index 0000000000..e3d756e9cc --- /dev/null +++ b/tests/models/test_anthropic_advanced_tools.py @@ -0,0 +1,444 @@ +"""Tests for Anthropic advanced tool use features. + +Tests for: +- defer_loading on ToolDefinition +- allowed_callers for programmatic tool calling +- input_examples for tool use examples +- ToolSearchTool built-in tool +- ProgrammaticCodeExecutionTool built-in tool +""" + +from __future__ import annotations as _annotations + +from typing import Any, cast + +import pytest + +from pydantic_ai import Agent, Tool +from pydantic_ai.builtin_tools import ProgrammaticCodeExecutionTool, ToolSearchTool +from pydantic_ai.models import ModelRequestParameters +from pydantic_ai.tools import ToolDefinition + +from ..conftest import try_import + +with try_import() as imports_successful: + from anthropic import NOT_GIVEN, AsyncAnthropic + from anthropic.types.beta import BetaMessage, BetaTextBlock, BetaUsage + + from pydantic_ai.models.anthropic import AnthropicModel + from pydantic_ai.providers.anthropic import AnthropicProvider + + +pytestmark = [ + pytest.mark.skipif(not imports_successful(), reason='anthropic not installed'), + pytest.mark.anyio, +] + + +class TestToolDefinitionAdvancedFields: + """Tests for ToolDefinition advanced fields.""" + + def test_tool_definition_defer_loading_default(self): + """Test that defer_loading defaults to False.""" + tool_def = ToolDefinition(name='test_tool') + assert tool_def.defer_loading is False + + def test_tool_definition_defer_loading_true(self): + """Test setting defer_loading to True.""" + tool_def = ToolDefinition(name='test_tool', defer_loading=True) + assert tool_def.defer_loading is True + + def test_tool_definition_allowed_callers_default(self): + """Test that allowed_callers defaults to None.""" + tool_def = ToolDefinition(name='test_tool') + assert tool_def.allowed_callers is None + + def test_tool_definition_allowed_callers_set(self): + """Test setting allowed_callers.""" + tool_def = ToolDefinition( + name='test_tool', + allowed_callers=['code_execution_20250825'], + ) + assert tool_def.allowed_callers == ['code_execution_20250825'] + + def test_tool_definition_input_examples_default(self): + """Test that input_examples defaults to None.""" + tool_def = ToolDefinition(name='test_tool') + assert tool_def.input_examples is None + + def test_tool_definition_input_examples_set(self): + """Test setting input_examples.""" + examples = [ + {'param1': 'value1', 'param2': 123}, + {'param1': 'value2'}, + ] + tool_def = ToolDefinition(name='test_tool', input_examples=examples) + assert tool_def.input_examples == examples + + +class TestToolAdvancedFields: + """Tests for Tool class with advanced fields.""" + + def test_tool_defer_loading_default(self): + """Test that Tool.defer_loading defaults to False.""" + + def my_tool(x: int) -> str: + return str(x) + + tool = Tool(my_tool) + assert tool.defer_loading is False + assert tool.tool_def.defer_loading is False + + def test_tool_defer_loading_true(self): + """Test setting Tool.defer_loading to True.""" + + def my_tool(x: int) -> str: + return str(x) + + tool = Tool(my_tool, defer_loading=True) + assert tool.defer_loading is True + assert tool.tool_def.defer_loading is True + + def test_tool_allowed_callers(self): + """Test setting Tool.allowed_callers.""" + + def my_tool(x: int) -> str: + return str(x) + + tool = Tool(my_tool, allowed_callers=['code_execution_20250825']) + assert tool.allowed_callers == ['code_execution_20250825'] + assert tool.tool_def.allowed_callers == ['code_execution_20250825'] + + def test_tool_input_examples(self): + """Test setting Tool.input_examples.""" + + def my_tool(x: int) -> str: + return str(x) + + examples = [{'x': 1}, {'x': 42}] + tool = Tool(my_tool, input_examples=examples) + assert tool.input_examples == examples + assert tool.tool_def.input_examples == examples + + +class TestBuiltinTools: + """Tests for new builtin tools.""" + + def test_tool_search_tool_regex(self): + """Test ToolSearchTool with regex search type.""" + tool = ToolSearchTool(search_type='regex') + assert tool.search_type == 'regex' + assert tool.kind == 'tool_search' + + def test_tool_search_tool_bm25(self): + """Test ToolSearchTool with BM25 search type.""" + tool = ToolSearchTool(search_type='bm25') + assert tool.search_type == 'bm25' + assert tool.kind == 'tool_search' + + def test_tool_search_tool_default(self): + """Test ToolSearchTool default search type.""" + tool = ToolSearchTool() + assert tool.search_type == 'regex' + + def test_programmatic_code_execution_tool(self): + """Test ProgrammaticCodeExecutionTool.""" + tool = ProgrammaticCodeExecutionTool() + assert tool.kind == 'programmatic_code_execution' + + +class MockAnthropic: + """Mock Anthropic client for testing.""" + + def __init__(self): + self.chat_completion_kwargs: list[dict[str, Any]] = [] + self.base_url = 'https://api.anthropic.com' + + @property + def beta(self): + return self + + @property + def messages(self): + return self + + async def create(self, **kwargs: Any) -> BetaMessage: + self.chat_completion_kwargs.append({k: v for k, v in kwargs.items() if v is not NOT_GIVEN}) + return BetaMessage( + id='123', + content=[BetaTextBlock(text='Hello', type='text')], + model='claude-sonnet-4-5-20250929', + role='assistant', + stop_reason='end_turn', + type='message', + usage=BetaUsage(input_tokens=10, output_tokens=5), + ) + + +class TestAnthropicModelAdvancedToolUse: + """Tests for AnthropicModel with advanced tool use features.""" + + def test_map_tool_definition_basic(self): + """Test basic tool definition mapping.""" + tool_def = ToolDefinition( + name='test_tool', + description='A test tool', + parameters_json_schema={'type': 'object', 'properties': {'x': {'type': 'integer'}}}, + ) + result = AnthropicModel._map_tool_definition(tool_def) + assert result['name'] == 'test_tool' + assert result['description'] == 'A test tool' + assert 'defer_loading' not in result + assert 'allowed_callers' not in result + assert 'input_examples' not in result + + def test_map_tool_definition_with_defer_loading(self): + """Test tool definition mapping with defer_loading.""" + tool_def = ToolDefinition( + name='test_tool', + description='A test tool', + defer_loading=True, + ) + result = AnthropicModel._map_tool_definition(tool_def) + assert result['defer_loading'] is True # type: ignore + + def test_map_tool_definition_with_allowed_callers(self): + """Test tool definition mapping with allowed_callers.""" + tool_def = ToolDefinition( + name='test_tool', + description='A test tool', + allowed_callers=['code_execution_20250825'], + ) + result = AnthropicModel._map_tool_definition(tool_def) + assert result['allowed_callers'] == ['code_execution_20250825'] # type: ignore + + def test_map_tool_definition_with_input_examples(self): + """Test tool definition mapping with input_examples.""" + examples = [{'x': 1}, {'x': 2}] + tool_def = ToolDefinition( + name='test_tool', + description='A test tool', + input_examples=examples, + ) + result = AnthropicModel._map_tool_definition(tool_def) + assert result['input_examples'] == examples # type: ignore + + def test_map_tool_definition_all_advanced_fields(self): + """Test tool definition mapping with all advanced fields.""" + examples = [{'x': 1}] + tool_def = ToolDefinition( + name='test_tool', + description='A test tool', + defer_loading=True, + allowed_callers=['code_execution_20250825'], + input_examples=examples, + ) + result = AnthropicModel._map_tool_definition(tool_def) + assert result['defer_loading'] is True # type: ignore + assert result['allowed_callers'] == ['code_execution_20250825'] # type: ignore + assert result['input_examples'] == examples # type: ignore + + async def test_add_builtin_tools_tool_search_regex(self): + """Test adding ToolSearchTool with regex type.""" + mock_client = cast(AsyncAnthropic, MockAnthropic()) + model = AnthropicModel('claude-sonnet-4-5-20250929', provider=AnthropicProvider(anthropic_client=mock_client)) + + model_request_params = ModelRequestParameters( + function_tools=[], + builtin_tools=[ToolSearchTool(search_type='regex')], + ) + + tools, mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) + + assert len(tools) == 1 + assert tools[0]['type'] == 'tool_search_tool_regex_20251119' + assert tools[0]['name'] == 'tool_search_tool_regex' + assert 'advanced-tool-use-2025-11-20' in beta_features + + async def test_add_builtin_tools_tool_search_bm25(self): + """Test adding ToolSearchTool with BM25 type.""" + mock_client = cast(AsyncAnthropic, MockAnthropic()) + model = AnthropicModel('claude-sonnet-4-5-20250929', provider=AnthropicProvider(anthropic_client=mock_client)) + + model_request_params = ModelRequestParameters( + function_tools=[], + builtin_tools=[ToolSearchTool(search_type='bm25')], + ) + + tools, mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) + + assert len(tools) == 1 + assert tools[0]['type'] == 'tool_search_tool_bm25_20251119' + assert tools[0]['name'] == 'tool_search_tool_bm25' + assert 'advanced-tool-use-2025-11-20' in beta_features + + async def test_add_builtin_tools_programmatic_code_execution(self): + """Test adding ProgrammaticCodeExecutionTool.""" + mock_client = cast(AsyncAnthropic, MockAnthropic()) + model = AnthropicModel('claude-sonnet-4-5-20250929', provider=AnthropicProvider(anthropic_client=mock_client)) + + model_request_params = ModelRequestParameters( + function_tools=[], + builtin_tools=[ProgrammaticCodeExecutionTool()], + ) + + tools, mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) + + assert len(tools) == 1 + assert tools[0]['type'] == 'code_execution_20250825' + assert tools[0]['name'] == 'code_execution' + assert 'advanced-tool-use-2025-11-20' in beta_features + + async def test_beta_header_added_for_defer_loading(self): + """Test that beta header is added when tool uses defer_loading.""" + mock_client = cast(AsyncAnthropic, MockAnthropic()) + model = AnthropicModel('claude-sonnet-4-5-20250929', provider=AnthropicProvider(anthropic_client=mock_client)) + + tool_def = ToolDefinition( + name='deferred_tool', + description='A deferred tool', + defer_loading=True, + ) + model_request_params = ModelRequestParameters( + function_tools=[tool_def], + builtin_tools=[], + ) + + tools, mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) + + assert 'advanced-tool-use-2025-11-20' in beta_features + + async def test_beta_header_added_for_allowed_callers(self): + """Test that beta header is added when tool uses allowed_callers.""" + mock_client = cast(AsyncAnthropic, MockAnthropic()) + model = AnthropicModel('claude-sonnet-4-5-20250929', provider=AnthropicProvider(anthropic_client=mock_client)) + + tool_def = ToolDefinition( + name='callable_tool', + description='A callable tool', + allowed_callers=['code_execution_20250825'], + ) + model_request_params = ModelRequestParameters( + function_tools=[tool_def], + builtin_tools=[], + ) + + tools, mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) + + assert 'advanced-tool-use-2025-11-20' in beta_features + + async def test_beta_header_added_for_input_examples(self): + """Test that beta header is added when tool uses input_examples.""" + mock_client = cast(AsyncAnthropic, MockAnthropic()) + model = AnthropicModel('claude-sonnet-4-5-20250929', provider=AnthropicProvider(anthropic_client=mock_client)) + + tool_def = ToolDefinition( + name='example_tool', + description='A tool with examples', + input_examples=[{'x': 1}], + ) + model_request_params = ModelRequestParameters( + function_tools=[tool_def], + builtin_tools=[], + ) + + tools, mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) + + assert 'advanced-tool-use-2025-11-20' in beta_features + + +class TestAgentWithAdvancedToolUse: + """Tests for Agent with advanced tool use features.""" + + def test_agent_with_defer_loading_tool(self): + """Test creating an agent with a tool that has defer_loading.""" + + def my_tool(x: int) -> str: + """A test tool.""" + return str(x) + + agent = Agent( + 'test', + tools=[Tool(my_tool, defer_loading=True)], + ) + + # Verify the tool was registered with defer_loading + tool = agent._function_toolset.tools.get('my_tool') + assert tool is not None + assert tool.defer_loading is True + + def test_agent_with_allowed_callers_tool(self): + """Test creating an agent with a tool that has allowed_callers.""" + + def my_tool(x: int) -> str: + """A test tool.""" + return str(x) + + agent = Agent( + 'test', + tools=[Tool(my_tool, allowed_callers=['code_execution_20250825'])], + ) + + # Verify the tool was registered with allowed_callers + tool = agent._function_toolset.tools.get('my_tool') + assert tool is not None + assert tool.allowed_callers == ['code_execution_20250825'] + + def test_agent_with_input_examples_tool(self): + """Test creating an agent with a tool that has input_examples.""" + + def my_tool(x: int) -> str: + """A test tool.""" + return str(x) + + examples = [{'x': 1}, {'x': 42}] + agent = Agent( + 'test', + tools=[Tool(my_tool, input_examples=examples)], + ) + + # Verify the tool was registered with input_examples + tool = agent._function_toolset.tools.get('my_tool') + assert tool is not None + assert tool.input_examples == examples + + def test_agent_tool_plain_decorator_with_defer_loading(self): + """Test the @agent.tool_plain decorator with defer_loading.""" + agent: Agent[None, str] = Agent('test') + + @agent.tool_plain(defer_loading=True) + def my_deferred_tool(x: int) -> str: + """A deferred tool.""" + return str(x) + + tool = agent._function_toolset.tools.get('my_deferred_tool') + assert tool is not None + assert tool.defer_loading is True + + def test_agent_tool_plain_decorator_with_allowed_callers(self): + """Test the @agent.tool_plain decorator with allowed_callers.""" + agent: Agent[None, str] = Agent('test') + + @agent.tool_plain(allowed_callers=['code_execution_20250825']) + def my_callable_tool(x: int) -> str: + """A callable tool.""" + return str(x) + + tool = agent._function_toolset.tools.get('my_callable_tool') + assert tool is not None + assert tool.allowed_callers == ['code_execution_20250825'] + + def test_agent_tool_plain_decorator_with_input_examples(self): + """Test the @agent.tool_plain decorator with input_examples.""" + agent: Agent[None, str] = Agent('test') + + examples = [{'x': 1}, {'x': 42}] + + @agent.tool_plain(input_examples=examples) + def my_example_tool(x: int) -> str: + """A tool with examples.""" + return str(x) + + tool = agent._function_toolset.tools.get('my_example_tool') + assert tool is not None + assert tool.input_examples == examples From 5b5e2fea62e89b24161a757d95dd0807e4f0d3db Mon Sep 17 00:00:00 2001 From: Raymond Xu Date: Tue, 25 Nov 2025 02:06:23 -0800 Subject: [PATCH 2/3] Fix type errors and lint issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use cast() instead of type: ignore for new tool dict types in Anthropic model - Fix test type errors by casting results to dict[str, Any] - Add proper pyright ignore comments for private method access - Use underscore prefix for unused variables 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../pydantic_ai/models/anthropic.py | 48 +++++++----- tests/models/test_anthropic_advanced_tools.py | 76 ++++++++++--------- 2 files changed, 73 insertions(+), 51 deletions(-) diff --git a/pydantic_ai_slim/pydantic_ai/models/anthropic.py b/pydantic_ai_slim/pydantic_ai/models/anthropic.py index a08313eaf2..08dc645b10 100644 --- a/pydantic_ai_slim/pydantic_ai/models/anthropic.py +++ b/pydantic_ai_slim/pydantic_ai/models/anthropic.py @@ -533,30 +533,42 @@ def _add_builtin_tools( ) 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 if tool.search_type == 'regex': tools.append( - { - 'type': 'tool_search_tool_regex_20251119', - 'name': 'tool_search_tool_regex', - } - ) # type: ignore[arg-type] + cast( + BetaToolUnionParam, + { + 'type': 'tool_search_tool_regex_20251119', + 'name': 'tool_search_tool_regex', + }, + ) + ) else: # bm25 tools.append( - { - 'type': 'tool_search_tool_bm25_20251119', - 'name': 'tool_search_tool_bm25', - } - ) # type: ignore[arg-type] + 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): # 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( - { - 'type': 'code_execution_20250825', - 'name': 'code_execution', - } - ) # type: ignore[arg-type] + 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 @@ -1113,7 +1125,9 @@ 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 ('tool_search_tool_regex', 'tool_search_tool_bm25'): + # 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, @@ -1121,7 +1135,7 @@ 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 + 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: # For new server tools we don't recognize yet, return a generic BuiltinToolCallPart diff --git a/tests/models/test_anthropic_advanced_tools.py b/tests/models/test_anthropic_advanced_tools.py index e3d756e9cc..54ebd28e3a 100644 --- a/tests/models/test_anthropic_advanced_tools.py +++ b/tests/models/test_anthropic_advanced_tools.py @@ -185,12 +185,13 @@ def test_map_tool_definition_basic(self): description='A test tool', parameters_json_schema={'type': 'object', 'properties': {'x': {'type': 'integer'}}}, ) - result = AnthropicModel._map_tool_definition(tool_def) - assert result['name'] == 'test_tool' - assert result['description'] == 'A test tool' - assert 'defer_loading' not in result - assert 'allowed_callers' not in result - assert 'input_examples' not in result + result = AnthropicModel._map_tool_definition(tool_def) # pyright: ignore[reportPrivateUsage] + result_dict = cast(dict[str, Any], result) + assert result_dict['name'] == 'test_tool' + assert result_dict['description'] == 'A test tool' + assert 'defer_loading' not in result_dict + assert 'allowed_callers' not in result_dict + assert 'input_examples' not in result_dict def test_map_tool_definition_with_defer_loading(self): """Test tool definition mapping with defer_loading.""" @@ -199,8 +200,9 @@ def test_map_tool_definition_with_defer_loading(self): description='A test tool', defer_loading=True, ) - result = AnthropicModel._map_tool_definition(tool_def) - assert result['defer_loading'] is True # type: ignore + result = AnthropicModel._map_tool_definition(tool_def) # pyright: ignore[reportPrivateUsage] + result_dict = cast(dict[str, Any], result) + assert result_dict['defer_loading'] is True def test_map_tool_definition_with_allowed_callers(self): """Test tool definition mapping with allowed_callers.""" @@ -209,8 +211,9 @@ def test_map_tool_definition_with_allowed_callers(self): description='A test tool', allowed_callers=['code_execution_20250825'], ) - result = AnthropicModel._map_tool_definition(tool_def) - assert result['allowed_callers'] == ['code_execution_20250825'] # type: ignore + result = AnthropicModel._map_tool_definition(tool_def) # pyright: ignore[reportPrivateUsage] + result_dict = cast(dict[str, Any], result) + assert result_dict['allowed_callers'] == ['code_execution_20250825'] def test_map_tool_definition_with_input_examples(self): """Test tool definition mapping with input_examples.""" @@ -220,8 +223,9 @@ def test_map_tool_definition_with_input_examples(self): description='A test tool', input_examples=examples, ) - result = AnthropicModel._map_tool_definition(tool_def) - assert result['input_examples'] == examples # type: ignore + result = AnthropicModel._map_tool_definition(tool_def) # pyright: ignore[reportPrivateUsage] + result_dict = cast(dict[str, Any], result) + assert result_dict['input_examples'] == examples def test_map_tool_definition_all_advanced_fields(self): """Test tool definition mapping with all advanced fields.""" @@ -233,10 +237,11 @@ def test_map_tool_definition_all_advanced_fields(self): allowed_callers=['code_execution_20250825'], input_examples=examples, ) - result = AnthropicModel._map_tool_definition(tool_def) - assert result['defer_loading'] is True # type: ignore - assert result['allowed_callers'] == ['code_execution_20250825'] # type: ignore - assert result['input_examples'] == examples # type: ignore + result = AnthropicModel._map_tool_definition(tool_def) # pyright: ignore[reportPrivateUsage] + result_dict = cast(dict[str, Any], result) + assert result_dict['defer_loading'] is True + assert result_dict['allowed_callers'] == ['code_execution_20250825'] + assert result_dict['input_examples'] == examples async def test_add_builtin_tools_tool_search_regex(self): """Test adding ToolSearchTool with regex type.""" @@ -248,11 +253,12 @@ async def test_add_builtin_tools_tool_search_regex(self): builtin_tools=[ToolSearchTool(search_type='regex')], ) - tools, mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) + tools, _mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) # pyright: ignore[reportPrivateUsage] assert len(tools) == 1 - assert tools[0]['type'] == 'tool_search_tool_regex_20251119' - assert tools[0]['name'] == 'tool_search_tool_regex' + tool_dict = cast(dict[str, Any], tools[0]) + assert tool_dict['type'] == 'tool_search_tool_regex_20251119' + assert tool_dict['name'] == 'tool_search_tool_regex' assert 'advanced-tool-use-2025-11-20' in beta_features async def test_add_builtin_tools_tool_search_bm25(self): @@ -265,11 +271,12 @@ async def test_add_builtin_tools_tool_search_bm25(self): builtin_tools=[ToolSearchTool(search_type='bm25')], ) - tools, mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) + tools, _mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) # pyright: ignore[reportPrivateUsage] assert len(tools) == 1 - assert tools[0]['type'] == 'tool_search_tool_bm25_20251119' - assert tools[0]['name'] == 'tool_search_tool_bm25' + tool_dict = cast(dict[str, Any], tools[0]) + assert tool_dict['type'] == 'tool_search_tool_bm25_20251119' + assert tool_dict['name'] == 'tool_search_tool_bm25' assert 'advanced-tool-use-2025-11-20' in beta_features async def test_add_builtin_tools_programmatic_code_execution(self): @@ -282,11 +289,12 @@ async def test_add_builtin_tools_programmatic_code_execution(self): builtin_tools=[ProgrammaticCodeExecutionTool()], ) - tools, mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) + tools, _mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) # pyright: ignore[reportPrivateUsage] assert len(tools) == 1 - assert tools[0]['type'] == 'code_execution_20250825' - assert tools[0]['name'] == 'code_execution' + tool_dict = cast(dict[str, Any], tools[0]) + assert tool_dict['type'] == 'code_execution_20250825' + assert tool_dict['name'] == 'code_execution' assert 'advanced-tool-use-2025-11-20' in beta_features async def test_beta_header_added_for_defer_loading(self): @@ -304,7 +312,7 @@ async def test_beta_header_added_for_defer_loading(self): builtin_tools=[], ) - tools, mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) + _tools, _mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) # pyright: ignore[reportPrivateUsage] assert 'advanced-tool-use-2025-11-20' in beta_features @@ -323,7 +331,7 @@ async def test_beta_header_added_for_allowed_callers(self): builtin_tools=[], ) - tools, mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) + _tools, _mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) # pyright: ignore[reportPrivateUsage] assert 'advanced-tool-use-2025-11-20' in beta_features @@ -342,7 +350,7 @@ async def test_beta_header_added_for_input_examples(self): builtin_tools=[], ) - tools, mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) + _tools, _mcp_servers, beta_features = model._add_builtin_tools([], model_request_params) # pyright: ignore[reportPrivateUsage] assert 'advanced-tool-use-2025-11-20' in beta_features @@ -363,7 +371,7 @@ def my_tool(x: int) -> str: ) # Verify the tool was registered with defer_loading - tool = agent._function_toolset.tools.get('my_tool') + tool = agent._function_toolset.tools.get('my_tool') # pyright: ignore[reportPrivateUsage] assert tool is not None assert tool.defer_loading is True @@ -380,7 +388,7 @@ def my_tool(x: int) -> str: ) # Verify the tool was registered with allowed_callers - tool = agent._function_toolset.tools.get('my_tool') + tool = agent._function_toolset.tools.get('my_tool') # pyright: ignore[reportPrivateUsage] assert tool is not None assert tool.allowed_callers == ['code_execution_20250825'] @@ -398,7 +406,7 @@ def my_tool(x: int) -> str: ) # Verify the tool was registered with input_examples - tool = agent._function_toolset.tools.get('my_tool') + tool = agent._function_toolset.tools.get('my_tool') # pyright: ignore[reportPrivateUsage] assert tool is not None assert tool.input_examples == examples @@ -411,7 +419,7 @@ def my_deferred_tool(x: int) -> str: """A deferred tool.""" return str(x) - tool = agent._function_toolset.tools.get('my_deferred_tool') + tool = agent._function_toolset.tools.get('my_deferred_tool') # pyright: ignore[reportPrivateUsage] assert tool is not None assert tool.defer_loading is True @@ -424,7 +432,7 @@ def my_callable_tool(x: int) -> str: """A callable tool.""" return str(x) - tool = agent._function_toolset.tools.get('my_callable_tool') + tool = agent._function_toolset.tools.get('my_callable_tool') # pyright: ignore[reportPrivateUsage] assert tool is not None assert tool.allowed_callers == ['code_execution_20250825'] @@ -439,6 +447,6 @@ def my_example_tool(x: int) -> str: """A tool with examples.""" return str(x) - tool = agent._function_toolset.tools.get('my_example_tool') + tool = agent._function_toolset.tools.get('my_example_tool') # pyright: ignore[reportPrivateUsage] assert tool is not None assert tool.input_examples == examples From 537d7a9fdba08db62e5d7636e6ec07a679aac40f Mon Sep 17 00:00:00 2001 From: Raymond Xu Date: Tue, 25 Nov 2025 02:59:14 -0800 Subject: [PATCH 3/3] Update inline snapshots for new ToolDefinition fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new defer_loading, allowed_callers, and input_examples fields are now included in ToolDefinition serialization, so update all affected test snapshots. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tests/models/test_model_request_parameters.py | 6 +++ tests/test_logfire.py | 6 +++ tests/test_tools.py | 51 +++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/tests/models/test_model_request_parameters.py b/tests/models/test_model_request_parameters.py index 1c8d0780e5..1d5beb8b97 100644 --- a/tests/models/test_model_request_parameters.py +++ b/tests/models/test_model_request_parameters.py @@ -68,6 +68,9 @@ def test_model_request_parameters_are_serializable(): 'sequential': False, 'kind': 'function', 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ], 'builtin_tools': [ @@ -124,6 +127,9 @@ def test_model_request_parameters_are_serializable(): 'sequential': False, 'kind': 'function', 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ], 'prompted_output_template': None, diff --git a/tests/test_logfire.py b/tests/test_logfire.py index dadb930dd0..4101efa5c5 100644 --- a/tests/test_logfire.py +++ b/tests/test_logfire.py @@ -547,6 +547,9 @@ async def my_ret(x: int) -> str: 'sequential': False, 'kind': 'function', 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ], 'builtin_tools': [], @@ -994,6 +997,9 @@ class MyOutput: 'sequential': False, 'kind': 'output', 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ], 'prompted_output_template': None, diff --git a/tests/test_tools.py b/tests/test_tools.py index f65105b4e6..d0e686baed 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -150,6 +150,9 @@ def test_docstring_google(docstring_format: Literal['google', 'auto']): 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ) @@ -184,6 +187,9 @@ def test_docstring_sphinx(docstring_format: Literal['sphinx', 'auto']): 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ) @@ -226,6 +232,9 @@ def test_docstring_numpy(docstring_format: Literal['numpy', 'auto']): 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ) @@ -268,6 +277,9 @@ def my_tool(x: int) -> str: # pragma: no cover 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ) @@ -308,6 +320,9 @@ def my_tool(x: int) -> str: # pragma: no cover 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ) @@ -354,6 +369,9 @@ def my_tool(x: int) -> str: # pragma: no cover 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ) @@ -388,6 +406,9 @@ def test_only_returns_type(): 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ) @@ -413,6 +434,9 @@ def test_docstring_unknown(): 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ) @@ -456,6 +480,9 @@ def test_docstring_google_no_body(docstring_format: Literal['google', 'auto']): 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ) @@ -492,6 +519,9 @@ def takes_just_model(model: Foo) -> str: 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ) @@ -537,6 +567,9 @@ def takes_just_model(model: Foo, z: int) -> str: 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ) @@ -902,6 +935,9 @@ def test_suppress_griffe_logging(caplog: LogCaptureFixture): 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } ) @@ -974,6 +1010,9 @@ def my_tool_plain(*, a: int = 1, b: int) -> int: 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, }, { 'description': None, @@ -989,6 +1028,9 @@ def my_tool_plain(*, a: int = 1, b: int) -> int: 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, }, ] ) @@ -1077,6 +1119,9 @@ def my_tool(x: Annotated[str | None, WithJsonSchema({'type': 'string'})] = None, 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, }, { 'description': None, @@ -1090,6 +1135,9 @@ def my_tool(x: Annotated[str | None, WithJsonSchema({'type': 'string'})] = None, 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, }, ] ) @@ -1127,6 +1175,9 @@ def get_score(data: Data) -> int: ... # pragma: no branch 'kind': 'function', 'sequential': False, 'metadata': None, + 'defer_loading': False, + 'allowed_callers': None, + 'input_examples': None, } )