diff --git a/pydantic_ai_slim/pydantic_ai/agent/__init__.py b/pydantic_ai_slim/pydantic_ai/agent/__init__.py index c8208ac9e6..c4bbba46cb 100644 --- a/pydantic_ai_slim/pydantic_ai/agent/__init__.py +++ b/pydantic_ai_slim/pydantic_ai/agent/__init__.py @@ -8,7 +8,7 @@ from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Sequence from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager, contextmanager from contextvars import ContextVar -from typing import TYPE_CHECKING, Any, ClassVar, overload +from typing import TYPE_CHECKING, Any, ClassVar, Literal, overload from opentelemetry.trace import NoOpTracer, use_span from pydantic.json_schema import GenerateJsonSchema @@ -1031,6 +1031,7 @@ def tool( sequential: bool = False, requires_approval: bool = False, metadata: dict[str, Any] | None = None, + programmatically_callable: bool | Literal['only'] = False, ) -> Callable[[ToolFuncContext[AgentDepsT, ToolParams]], ToolFuncContext[AgentDepsT, ToolParams]]: ... def tool( @@ -1049,6 +1050,7 @@ def tool( sequential: bool = False, requires_approval: bool = False, metadata: dict[str, Any] | None = None, + programmatically_callable: bool | Literal['only'] = False, ) -> Any: """Decorator to register a tool function which takes [`RunContext`][pydantic_ai.tools.RunContext] as its first argument. @@ -1098,6 +1100,10 @@ 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. + programmatically_callable: Whether this tool can be called from code execution. + Set to `True` to allow both direct model calls and calls from code execution. + Set to `'only'` to only allow calls from code execution. + Defaults to `False`. See [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] for more info. """ def tool_decorator( @@ -1118,6 +1124,7 @@ def tool_decorator( sequential=sequential, requires_approval=requires_approval, metadata=metadata, + programmatically_callable=programmatically_callable, ) return func_ @@ -1142,6 +1149,7 @@ def tool_plain( sequential: bool = False, requires_approval: bool = False, metadata: dict[str, Any] | None = None, + programmatically_callable: bool | Literal['only'] = False, ) -> Callable[[ToolFuncPlain[ToolParams]], ToolFuncPlain[ToolParams]]: ... def tool_plain( @@ -1160,6 +1168,7 @@ def tool_plain( sequential: bool = False, requires_approval: bool = False, metadata: dict[str, Any] | None = None, + programmatically_callable: bool | Literal['only'] = False, ) -> Any: """Decorator to register a tool function which DOES NOT take `RunContext` as an argument. @@ -1209,6 +1218,10 @@ 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. + programmatically_callable: Whether this tool can be called from code execution. + Set to `True` to allow both direct model calls and calls from code execution. + Set to `'only'` to only allow calls from code execution. + Defaults to `False`. See [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] for more info. """ def tool_decorator(func_: ToolFuncPlain[ToolParams]) -> ToolFuncPlain[ToolParams]: @@ -1227,6 +1240,7 @@ def tool_decorator(func_: ToolFuncPlain[ToolParams]) -> ToolFuncPlain[ToolParams sequential=sequential, requires_approval=requires_approval, metadata=metadata, + programmatically_callable=programmatically_callable, ) return func_ diff --git a/pydantic_ai_slim/pydantic_ai/models/anthropic.py b/pydantic_ai_slim/pydantic_ai/models/anthropic.py index ecdb9fe61f..40fed3929b 100644 --- a/pydantic_ai_slim/pydantic_ai/models/anthropic.py +++ b/pydantic_ai_slim/pydantic_ai/models/anthropic.py @@ -70,6 +70,7 @@ BetaCitationsConfigParam, BetaCitationsDelta, BetaCodeExecutionTool20250522Param, + BetaCodeExecutionTool20250825Param, BetaCodeExecutionToolResultBlock, BetaCodeExecutionToolResultBlockContent, BetaCodeExecutionToolResultBlockParam, @@ -510,11 +511,19 @@ def _process_response(self, response: BetaMessage) -> ModelResponse: items.append(_map_mcp_server_result_block(item, call_part, self.system)) else: assert isinstance(item, BetaToolUseBlock), f'unexpected item type {type(item)}' + # Include caller info in provider_details if tool was called programmatically + tool_provider_details: dict[str, Any] | None = None + if item.caller is not None: + tool_provider_details = {'caller': item.caller.type} + # If called from code execution, include the container_id (tool_id) + if hasattr(item.caller, 'tool_id'): + tool_provider_details['container_id'] = item.caller.tool_id items.append( ToolCallPart( tool_name=item.name, args=cast(dict[str, Any], item.input), tool_call_id=item.id, + provider_details=tool_provider_details, ) ) @@ -574,7 +583,24 @@ def _add_builtin_tools( ) -> tuple[list[BetaToolUnionParam], list[BetaRequestMCPServerURLDefinitionParam], set[str]]: beta_features: set[str] = set() mcp_servers: list[BetaRequestMCPServerURLDefinitionParam] = [] - for tool in model_request_parameters.builtin_tools: + + # Check if any tool has programmatically_callable set - if so, we need newer code execution + has_programmatically_callable = any( + t.programmatically_callable for t in model_request_parameters.tool_defs.values() + ) + # Check if CodeExecutionTool is in builtin_tools + has_code_execution = any(isinstance(t, CodeExecutionTool) for t in model_request_parameters.builtin_tools) + # Use newer code execution (20250825) when programmatically_callable is used + use_newer_code_execution = has_programmatically_callable + + # If any tool has programmatically_callable but CodeExecutionTool is not present, auto-add it + # (tools can only be called programmatically from code execution) + builtin_tools_to_process = list(model_request_parameters.builtin_tools) + if has_programmatically_callable and not has_code_execution: + builtin_tools_to_process.append(CodeExecutionTool()) + has_code_execution = True + + for tool in builtin_tools_to_process: if isinstance(tool, WebSearchTool): user_location = UserLocation(type='approximate', **tool.user_location) if tool.user_location else None tools.append( @@ -588,8 +614,17 @@ def _add_builtin_tools( ) ) elif isinstance(tool, CodeExecutionTool): # pragma: no branch - tools.append(BetaCodeExecutionTool20250522Param(name='code_execution', type='code_execution_20250522')) - beta_features.add('code-execution-2025-05-22') + if use_newer_code_execution: + # Use newer code execution tool that supports programmatic tool calling + tools.append( + BetaCodeExecutionTool20250825Param(name='code_execution', type='code_execution_20250825') + ) + beta_features.add('code-execution-2025-08-25') + else: + tools.append( + BetaCodeExecutionTool20250522Param(name='code_execution', type='code_execution_20250522') + ) + beta_features.add('code-execution-2025-05-22') elif isinstance(tool, WebFetchTool): # pragma: no branch citations = BetaCitationsConfigParam(enabled=tool.enable_citations) if tool.enable_citations else None tools.append( @@ -1041,6 +1076,14 @@ def _map_tool_definition(self, f: ToolDefinition) -> BetaToolParam: } if f.strict and self.profile.supports_json_schema_output: tool_param['strict'] = f.strict + # Handle programmatically_callable - maps to Anthropic's allowed_callers + if f.programmatically_callable: + if f.programmatically_callable == 'only': + # Only callable from code execution, not directly by the model + tool_param['allowed_callers'] = ['code_execution_20250825'] + else: + # Callable both directly and from code execution + tool_param['allowed_callers'] = ['direct', 'code_execution_20250825'] return tool_param @staticmethod @@ -1126,11 +1169,19 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]: provider_name=self.provider_name, ) elif isinstance(current_block, BetaToolUseBlock): + # Include caller info in provider_details if tool was called programmatically + tool_provider_details: dict[str, Any] | None = None + if current_block.caller is not None: + tool_provider_details = {'caller': current_block.caller.type} + # If called from code execution, include the container_id (tool_id) + if hasattr(current_block.caller, 'tool_id'): + tool_provider_details['container_id'] = current_block.caller.tool_id maybe_event = self._parts_manager.handle_tool_call_delta( vendor_part_id=event.index, tool_name=current_block.name, args=cast(dict[str, Any], current_block.input) or None, tool_call_id=current_block.id, + provider_details=tool_provider_details, ) if maybe_event is not None: # pragma: no branch yield maybe_event diff --git a/pydantic_ai_slim/pydantic_ai/tools.py b/pydantic_ai_slim/pydantic_ai/tools.py index e54b829bfb..cfb5be84a1 100644 --- a/pydantic_ai_slim/pydantic_ai/tools.py +++ b/pydantic_ai_slim/pydantic_ai/tools.py @@ -262,6 +262,7 @@ class Tool(Generic[ToolAgentDepsT]): sequential: bool requires_approval: bool metadata: dict[str, Any] | None + programmatically_callable: bool | Literal['only'] function_schema: _function_schema.FunctionSchema """ The base JSON schema for the tool's parameters. @@ -285,6 +286,7 @@ def __init__( sequential: bool = False, requires_approval: bool = False, metadata: dict[str, Any] | None = None, + programmatically_callable: bool | Literal['only'] = False, function_schema: _function_schema.FunctionSchema | None = None, ): """Create a new tool instance. @@ -341,6 +343,10 @@ 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. + programmatically_callable: Whether this tool can be called from code execution. + Set to `True` to allow both direct model calls and calls from code execution. + Set to `'only'` to only allow calls from code execution. + Defaults to `False`. See [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] 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 +368,7 @@ async def prep_my_tool( self.sequential = sequential self.requires_approval = requires_approval self.metadata = metadata + self.programmatically_callable = programmatically_callable @classmethod def from_schema( @@ -418,6 +425,7 @@ def tool_def(self): sequential=self.sequential, metadata=self.metadata, kind='unapproved' if self.requires_approval else 'function', + programmatically_callable=self.programmatically_callable, ) async def prepare_tool_def(self, ctx: RunContext[ToolAgentDepsT]) -> ToolDefinition | None: @@ -503,6 +511,16 @@ class ToolDefinition: For MCP tools, this contains the `meta`, `annotations`, and `output_schema` fields from the tool definition. """ + programmatically_callable: bool | Literal['only'] = False + """Whether this tool can be called programmatically from code execution. + + - `False` (default): The tool can only be called directly by the model. + - `True`: The tool can be called both directly by the model and from code execution. + - `'only'`: The tool can only be called from code execution, not directly by the model. + + Note: this is currently only supported by Anthropic models with the code execution feature. + """ + @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..93a52ae9ed 100644 --- a/pydantic_ai_slim/pydantic_ai/toolsets/function.py +++ b/pydantic_ai_slim/pydantic_ai/toolsets/function.py @@ -2,7 +2,7 @@ from collections.abc import Awaitable, Callable, Sequence from dataclasses import dataclass, replace -from typing import Any, overload +from typing import Any, Literal, overload from pydantic.json_schema import GenerateJsonSchema @@ -52,6 +52,7 @@ def __init__( sequential: bool = False, requires_approval: bool = False, metadata: dict[str, Any] | None = None, + programmatically_callable: bool | Literal['only'] = False, id: str | None = None, ): """Build a new function toolset. @@ -76,6 +77,10 @@ def __init__( Applies to all tools, unless overridden when adding a tool. metadata: Optional metadata for the tool. This is not sent to the model but can be used for filtering and tool behavior customization. Applies to all tools, unless overridden when adding a tool, which will be merged with the toolset's metadata. + programmatically_callable: Whether this tool can be called from code execution. + Set to `True` to allow both direct model calls and calls from code execution. + Set to `'only'` to only allow calls from code execution. + Defaults to `False`. Applies to all tools, unless overridden when adding a tool. id: An optional unique ID for the toolset. A toolset needs to have an ID in order to be used in a durable execution environment like Temporal, in which case the ID will be used to identify the toolset's activities within the workflow. """ @@ -88,6 +93,7 @@ def __init__( self.sequential = sequential self.requires_approval = requires_approval self.metadata = metadata + self.programmatically_callable = programmatically_callable self.tools = {} for tool in tools: @@ -119,6 +125,7 @@ def tool( sequential: bool | None = None, requires_approval: bool | None = None, metadata: dict[str, Any] | None = None, + programmatically_callable: bool | Literal['only'] | None = None, ) -> Callable[[ToolFuncEither[AgentDepsT, ToolParams]], ToolFuncEither[AgentDepsT, ToolParams]]: ... def tool( @@ -137,6 +144,7 @@ def tool( sequential: bool | None = None, requires_approval: bool | None = None, metadata: dict[str, Any] | None = None, + programmatically_callable: bool | Literal['only'] | None = None, ) -> Any: """Decorator to register a tool function which takes [`RunContext`][pydantic_ai.tools.RunContext] as its first argument. @@ -193,6 +201,10 @@ 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. + programmatically_callable: Whether this tool can be called from code execution. + Set to `True` to allow both direct model calls and calls from code execution. + Set to `'only'` to only allow calls from code execution. + If `None`, the default value is determined by the toolset. """ def tool_decorator( @@ -213,6 +225,7 @@ def tool_decorator( sequential=sequential, requires_approval=requires_approval, metadata=metadata, + programmatically_callable=programmatically_callable, ) return func_ @@ -233,6 +246,7 @@ def add_function( sequential: bool | None = None, requires_approval: bool | None = None, metadata: dict[str, Any] | None = None, + programmatically_callable: bool | Literal['only'] | None = None, ) -> None: """Add a function as a tool to the toolset. @@ -267,6 +281,10 @@ 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. + programmatically_callable: Whether this tool can be called from code execution. + Set to `True` to allow both direct model calls and calls from code execution. + Set to `'only'` to only allow calls from code execution. + If `None`, the default value is determined by the toolset. """ if docstring_format is None: docstring_format = self.docstring_format @@ -280,6 +298,8 @@ def add_function( sequential = self.sequential if requires_approval is None: requires_approval = self.requires_approval + if programmatically_callable is None: + programmatically_callable = self.programmatically_callable tool = Tool[AgentDepsT]( func, @@ -295,6 +315,7 @@ def add_function( sequential=sequential, requires_approval=requires_approval, metadata=metadata, + programmatically_callable=programmatically_callable, ) self.add_tool(tool) diff --git a/tests/models/anthropic/test_programmatically_callable.py b/tests/models/anthropic/test_programmatically_callable.py new file mode 100644 index 0000000000..be3149a518 --- /dev/null +++ b/tests/models/anthropic/test_programmatically_callable.py @@ -0,0 +1,204 @@ +"""Tests for Anthropic programmatically_callable (Programmatic Tool Calling) feature.""" + +from __future__ import annotations as _annotations + +import pytest + +from ...conftest import try_import + +with try_import() as imports_successful: + from anthropic.types.beta import BetaTextBlock, BetaUsage + + from pydantic_ai import Agent + from pydantic_ai.builtin_tools import CodeExecutionTool + from pydantic_ai.models.anthropic import AnthropicModel + from pydantic_ai.providers.anthropic import AnthropicProvider + + from ..test_anthropic import MockAnthropic, completion_message + +pytestmark = [ + pytest.mark.skipif(not imports_successful(), reason='anthropic not installed'), + pytest.mark.anyio, +] + + +async def test_programmatically_callable_true(allow_model_requests: None): + """Test that programmatically_callable=True maps to allowed_callers with both direct and code_execution.""" + c = completion_message( + [BetaTextBlock(text='Done', type='text')], + BetaUsage(input_tokens=5, output_tokens=10), + ) + mock_client = MockAnthropic.create_mock(c) + model = AnthropicModel('claude-sonnet-4-5', provider=AnthropicProvider(anthropic_client=mock_client)) + + agent = Agent(model) + + @agent.tool_plain(programmatically_callable=True) + def my_tool(x: int) -> int: + """A tool that can be called from code execution.""" + return x * 2 + + await agent.run('test') + + # Check that the tool was configured with allowed_callers + assert len(mock_client.chat_completion_kwargs) == 1 + tools = mock_client.chat_completion_kwargs[0]['tools'] + + # Find the my_tool definition + my_tool_def = next((t for t in tools if t.get('name') == 'my_tool'), None) + assert my_tool_def is not None + assert my_tool_def.get('allowed_callers') == ['direct', 'code_execution_20250825'] + + +async def test_programmatically_callable_only(allow_model_requests: None): + """Test that programmatically_callable='only' maps to allowed_callers with only code_execution.""" + c = completion_message( + [BetaTextBlock(text='Done', type='text')], + BetaUsage(input_tokens=5, output_tokens=10), + ) + mock_client = MockAnthropic.create_mock(c) + model = AnthropicModel('claude-sonnet-4-5', provider=AnthropicProvider(anthropic_client=mock_client)) + + agent = Agent(model) + + @agent.tool_plain(programmatically_callable='only') + def my_tool(x: int) -> int: + """A tool that can only be called from code execution.""" + return x * 2 + + await agent.run('test') + + # Check that the tool was configured with allowed_callers + assert len(mock_client.chat_completion_kwargs) == 1 + tools = mock_client.chat_completion_kwargs[0]['tools'] + + # Find the my_tool definition + my_tool_def = next((t for t in tools if t.get('name') == 'my_tool'), None) + assert my_tool_def is not None + assert my_tool_def.get('allowed_callers') == ['code_execution_20250825'] + + +async def test_programmatically_callable_false(allow_model_requests: None): + """Test that programmatically_callable=False (default) doesn't add allowed_callers.""" + c = completion_message( + [BetaTextBlock(text='Done', type='text')], + BetaUsage(input_tokens=5, output_tokens=10), + ) + mock_client = MockAnthropic.create_mock(c) + model = AnthropicModel('claude-sonnet-4-5', provider=AnthropicProvider(anthropic_client=mock_client)) + + agent = Agent(model) + + @agent.tool_plain + def my_tool(x: int) -> int: + """A regular tool.""" + return x * 2 + + await agent.run('test') + + # Check that the tool was NOT configured with allowed_callers + assert len(mock_client.chat_completion_kwargs) == 1 + tools = mock_client.chat_completion_kwargs[0]['tools'] + + # Find the my_tool definition + my_tool_def = next((t for t in tools if t.get('name') == 'my_tool'), None) + assert my_tool_def is not None + assert 'allowed_callers' not in my_tool_def + + +async def test_auto_adds_code_execution_tool(allow_model_requests: None): + """Test that CodeExecutionTool is auto-added when programmatically_callable is used.""" + c = completion_message( + [BetaTextBlock(text='Done', type='text')], + BetaUsage(input_tokens=5, output_tokens=10), + ) + mock_client = MockAnthropic.create_mock(c) + model = AnthropicModel('claude-sonnet-4-5', provider=AnthropicProvider(anthropic_client=mock_client)) + + # No explicit CodeExecutionTool in builtin_tools + agent = Agent(model) + + @agent.tool_plain(programmatically_callable=True) + def my_tool(x: int) -> int: + """A tool that can be called from code execution.""" + return x * 2 + + await agent.run('test') + + # Check that code_execution tool was auto-added + assert len(mock_client.chat_completion_kwargs) == 1 + tools = mock_client.chat_completion_kwargs[0]['tools'] + + # Should have my_tool and code_execution + tool_names = [t.get('name') for t in tools] + assert 'my_tool' in tool_names + assert 'code_execution' in tool_names + + # Check that the newer code execution type is used + code_exec_tool = next((t for t in tools if t.get('name') == 'code_execution'), None) + assert code_exec_tool is not None + assert code_exec_tool.get('type') == 'code_execution_20250825' + + +async def test_uses_newer_code_execution_with_ptc(allow_model_requests: None): + """Test that the newer code execution tool is used when PTC is enabled.""" + c = completion_message( + [BetaTextBlock(text='Done', type='text')], + BetaUsage(input_tokens=5, output_tokens=10), + ) + mock_client = MockAnthropic.create_mock(c) + model = AnthropicModel('claude-sonnet-4-5', provider=AnthropicProvider(anthropic_client=mock_client)) + + # Explicitly add CodeExecutionTool + agent = Agent(model, builtin_tools=[CodeExecutionTool()]) + + @agent.tool_plain(programmatically_callable=True) + def my_tool(x: int) -> int: + """A tool that can be called from code execution.""" + return x * 2 + + await agent.run('test') + + # Check that the newer code execution type is used + assert len(mock_client.chat_completion_kwargs) == 1 + tools = mock_client.chat_completion_kwargs[0]['tools'] + + code_exec_tool = next((t for t in tools if t.get('name') == 'code_execution'), None) + assert code_exec_tool is not None + assert code_exec_tool.get('type') == 'code_execution_20250825' + + # Also check that the newer beta is used + betas = mock_client.chat_completion_kwargs[0].get('betas', []) + assert 'code-execution-2025-08-25' in betas + + +async def test_uses_older_code_execution_without_ptc(allow_model_requests: None): + """Test that the older code execution tool is used when PTC is not enabled.""" + c = completion_message( + [BetaTextBlock(text='Done', type='text')], + BetaUsage(input_tokens=5, output_tokens=10), + ) + mock_client = MockAnthropic.create_mock(c) + model = AnthropicModel('claude-sonnet-4-5', provider=AnthropicProvider(anthropic_client=mock_client)) + + # Add CodeExecutionTool but no programmatically_callable tools + agent = Agent(model, builtin_tools=[CodeExecutionTool()]) + + @agent.tool_plain + def my_tool(x: int) -> int: + """A regular tool.""" + return x * 2 + + await agent.run('test') + + # Check that the older code execution type is used + assert len(mock_client.chat_completion_kwargs) == 1 + tools = mock_client.chat_completion_kwargs[0]['tools'] + + code_exec_tool = next((t for t in tools if t.get('name') == 'code_execution'), None) + assert code_exec_tool is not None + assert code_exec_tool.get('type') == 'code_execution_20250522' + + # Also check that the older beta is used + betas = mock_client.chat_completion_kwargs[0].get('betas', []) + assert 'code-execution-2025-05-22' in betas diff --git a/tests/test_tools.py b/tests/test_tools.py index bcdf537994..ee4df91501 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -150,6 +150,7 @@ def test_docstring_google(docstring_format: Literal['google', 'auto']): 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, } ) @@ -184,6 +185,7 @@ def test_docstring_sphinx(docstring_format: Literal['sphinx', 'auto']): 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, } ) @@ -226,6 +228,7 @@ def test_docstring_numpy(docstring_format: Literal['numpy', 'auto']): 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, } ) @@ -268,6 +271,7 @@ def my_tool(x: int) -> str: # pragma: no cover 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, } ) @@ -308,6 +312,7 @@ def my_tool(x: int) -> str: # pragma: no cover 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, } ) @@ -354,6 +359,7 @@ def my_tool(x: int) -> str: # pragma: no cover 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, } ) @@ -388,6 +394,7 @@ def test_only_returns_type(): 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, } ) @@ -413,6 +420,7 @@ def test_docstring_unknown(): 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, } ) @@ -456,6 +464,7 @@ def test_docstring_google_no_body(docstring_format: Literal['google', 'auto']): 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, } ) @@ -492,6 +501,7 @@ def takes_just_model(model: Foo) -> str: 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, } ) @@ -537,6 +547,7 @@ def takes_just_model(model: Foo, z: int) -> str: 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, } ) @@ -902,6 +913,7 @@ def test_suppress_griffe_logging(caplog: LogCaptureFixture): 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, } ) @@ -974,6 +986,7 @@ def my_tool_plain(*, a: int = 1, b: int) -> int: 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, }, { 'description': None, @@ -989,6 +1002,7 @@ def my_tool_plain(*, a: int = 1, b: int) -> int: 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, }, ] ) @@ -1077,6 +1091,7 @@ def my_tool(x: Annotated[str | None, WithJsonSchema({'type': 'string'})] = None, 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, }, { 'description': None, @@ -1090,6 +1105,7 @@ def my_tool(x: Annotated[str | None, WithJsonSchema({'type': 'string'})] = None, 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, }, ] ) @@ -1127,6 +1143,7 @@ def get_score(data: Data) -> int: ... # pragma: no branch 'kind': 'function', 'sequential': False, 'metadata': None, + 'programmatically_callable': False, } )