Python: [BREAKING] Replace Hosted*Tool classes with tool methods (#3634)

* Replace Hosted*Tool classes with client static factory methods

* fixed failing test

* mypy fix

* mypy fix 2

* declarative mypy fix

* addressed comments

* ToolProtocol removal

* fixed test

* agents mypy fix

* fix failing tests

* mypy fix

* addressed comments

* fixed tests

* addressed comments + added factory method overrides for azureai v2 client

* mypy fix

* added kwargs to azureai tool methods

* fixed in test

* _sessions fix

* test fix
This commit is contained in:
Giles Odigwe
2026-02-11 00:04:27 +00:00
committed by GitHub
parent d249473a6d
commit 7a88af0aef
133 changed files with 3018 additions and 2650 deletions
@@ -20,9 +20,6 @@ from agent_framework import (
FunctionInvocationConfiguration,
FunctionInvocationLayer,
FunctionTool,
HostedCodeInterpreterTool,
HostedMCPTool,
HostedWebSearchTool,
Message,
ResponseStream,
TextSpanRegion,
@@ -350,6 +347,109 @@ class AnthropicClient(
# streaming requires tracking the last function call ID and name
self._last_call_id_name: tuple[str, str] | None = None
# region Static factory methods for hosted tools
@staticmethod
def get_code_interpreter_tool(
*,
type_name: str | None = None,
) -> dict[str, Any]:
"""Create a code interpreter tool configuration for Anthropic.
Keyword Args:
type_name: Override the tool type name. Defaults to "code_execution_20250825".
Returns:
A dict-based tool configuration ready to pass to ChatAgent.
Examples:
.. code-block:: python
from agent_framework.anthropic import AnthropicClient
tool = AnthropicClient.get_code_interpreter_tool()
agent = AnthropicClient().as_agent(tools=[tool])
"""
return {"type": type_name or "code_execution_20250825"}
@staticmethod
def get_web_search_tool(
*,
type_name: str | None = None,
) -> dict[str, Any]:
"""Create a web search tool configuration for Anthropic.
Keyword Args:
type_name: Override the tool type name. Defaults to "web_search_20250305".
Returns:
A dict-based tool configuration ready to pass to ChatAgent.
Examples:
.. code-block:: python
from agent_framework.anthropic import AnthropicClient
tool = AnthropicClient.get_web_search_tool()
agent = AnthropicClient().as_agent(tools=[tool])
"""
return {"type": type_name or "web_search_20250305"}
@staticmethod
def get_mcp_tool(
*,
name: str,
url: str,
allowed_tools: list[str] | None = None,
authorization_token: str | None = None,
) -> dict[str, Any]:
"""Create a hosted MCP tool configuration for Anthropic.
This configures an MCP (Model Context Protocol) server that will be called
by Anthropic's service. The tools from this MCP server are executed remotely
by Anthropic, not locally by your application.
Note:
For local MCP execution where your application calls the MCP server
directly, use the MCP client tools instead of this method.
Keyword Args:
name: A label/name for the MCP server.
url: The URL of the MCP server.
allowed_tools: List of tool names that are allowed to be used from this MCP server.
authorization_token: Authorization token for the MCP server (e.g., Bearer token).
Returns:
A dict-based tool configuration ready to pass to ChatAgent.
Examples:
.. code-block:: python
from agent_framework.anthropic import AnthropicClient
tool = AnthropicClient.get_mcp_tool(
name="GitHub",
url="https://api.githubcopilot.com/mcp/",
authorization_token="Bearer ghp_xxx",
)
agent = AnthropicClient().as_agent(tools=[tool])
"""
result: dict[str, Any] = {
"type": "mcp",
"server_label": name.replace(" ", "_"),
"server_url": url,
}
if allowed_tools:
result["allowed_tools"] = allowed_tools
if authorization_token:
result["headers"] = {"authorization": authorization_token}
return result
# endregion
# region Get response methods
@override
@@ -590,6 +690,9 @@ class AnthropicClient(
def _prepare_tools_for_anthropic(self, options: Mapping[str, Any]) -> dict[str, Any] | None:
"""Prepare tools and tool choice configuration for the Anthropic API request.
Converts FunctionTool to Anthropic format. MCP tools are routed to separate
mcp_servers parameter. All other tools pass through unchanged.
Args:
options: The options dict containing tools and tool choice settings.
@@ -603,46 +706,32 @@ class AnthropicClient(
# Process tools
if tools:
tool_list: list[MutableMapping[str, Any]] = []
mcp_server_list: list[MutableMapping[str, Any]] = []
tool_list: list[Any] = []
mcp_server_list: list[Any] = []
for tool in tools:
match tool:
case MutableMapping():
tool_list.append(tool)
case FunctionTool():
tool_list.append({
"type": "custom",
"name": tool.name,
"description": tool.description,
"input_schema": tool.parameters(),
})
case HostedWebSearchTool():
search_tool: dict[str, Any] = {
"type": "web_search_20250305",
"name": "web_search",
}
if tool.additional_properties:
search_tool.update(tool.additional_properties)
tool_list.append(search_tool)
case HostedCodeInterpreterTool():
code_tool: dict[str, Any] = {
"type": "code_execution_20250825",
"name": "code_execution",
}
tool_list.append(code_tool)
case HostedMCPTool():
server_def: dict[str, Any] = {
"type": "url",
"name": tool.name,
"url": str(tool.url),
}
if tool.allowed_tools:
server_def["tool_configuration"] = {"allowed_tools": list(tool.allowed_tools)}
if tool.headers and (auth := tool.headers.get("authorization")):
server_def["authorization_token"] = auth
mcp_server_list.append(server_def)
case _:
logger.debug(f"Ignoring unsupported tool type: {type(tool)} for now")
if isinstance(tool, FunctionTool):
tool_list.append({
"type": "custom",
"name": tool.name,
"description": tool.description,
"input_schema": tool.parameters(),
})
elif isinstance(tool, MutableMapping) and tool.get("type") == "mcp":
# MCP servers must be routed to separate mcp_servers parameter
server_def: dict[str, Any] = {
"type": "url",
"name": tool.get("server_label", ""),
"url": tool.get("server_url", ""),
}
if allowed_tools := tool.get("allowed_tools"):
server_def["tool_configuration"] = {"allowed_tools": list(allowed_tools)}
headers = tool.get("headers")
if isinstance(headers, dict) and (auth := headers.get("authorization")):
server_def["authorization_token"] = auth
mcp_server_list.append(server_def)
else:
# Pass through all other tools (dicts, SDK types) unchanged
tool_list.append(tool)
if tool_list:
result["tools"] = tool_list
@@ -9,9 +9,6 @@ from agent_framework import (
ChatOptions,
ChatResponseUpdate,
Content,
HostedCodeInterpreterTool,
HostedMCPTool,
HostedWebSearchTool,
Message,
SupportsChatGetResponse,
tool,
@@ -278,9 +275,9 @@ def test_prepare_tools_for_anthropic_tool(mock_anthropic_client: MagicMock) -> N
def test_prepare_tools_for_anthropic_web_search(mock_anthropic_client: MagicMock) -> None:
"""Test converting HostedWebSearchTool to Anthropic format."""
"""Test converting web_search dict tool to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
chat_options = ChatOptions(tools=[HostedWebSearchTool()])
chat_options = ChatOptions(tools=[client.get_web_search_tool()])
result = client._prepare_tools_for_anthropic(chat_options)
@@ -288,13 +285,12 @@ def test_prepare_tools_for_anthropic_web_search(mock_anthropic_client: MagicMock
assert "tools" in result
assert len(result["tools"]) == 1
assert result["tools"][0]["type"] == "web_search_20250305"
assert result["tools"][0]["name"] == "web_search"
def test_prepare_tools_for_anthropic_code_interpreter(mock_anthropic_client: MagicMock) -> None:
"""Test converting HostedCodeInterpreterTool to Anthropic format."""
"""Test converting code_interpreter dict tool to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
chat_options = ChatOptions(tools=[HostedCodeInterpreterTool()])
chat_options = ChatOptions(tools=[client.get_code_interpreter_tool()])
result = client._prepare_tools_for_anthropic(chat_options)
@@ -302,13 +298,12 @@ def test_prepare_tools_for_anthropic_code_interpreter(mock_anthropic_client: Mag
assert "tools" in result
assert len(result["tools"]) == 1
assert result["tools"][0]["type"] == "code_execution_20250825"
assert result["tools"][0]["name"] == "code_execution"
def test_prepare_tools_for_anthropic_mcp_tool(mock_anthropic_client: MagicMock) -> None:
"""Test converting HostedMCPTool to Anthropic format."""
"""Test converting MCP dict tool to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
chat_options = ChatOptions(tools=[HostedMCPTool(name="test-mcp", url="https://example.com/mcp")])
chat_options = ChatOptions(tools=[client.get_mcp_tool(name="test-mcp", url="https://example.com/mcp")])
result = client._prepare_tools_for_anthropic(chat_options)
@@ -321,23 +316,21 @@ def test_prepare_tools_for_anthropic_mcp_tool(mock_anthropic_client: MagicMock)
def test_prepare_tools_for_anthropic_mcp_with_auth(mock_anthropic_client: MagicMock) -> None:
"""Test converting HostedMCPTool with authorization headers."""
"""Test converting MCP dict tool with authorization token."""
client = create_test_anthropic_client(mock_anthropic_client)
chat_options = ChatOptions(
tools=[
HostedMCPTool(
name="test-mcp",
url="https://example.com/mcp",
headers={"authorization": "Bearer token123"},
)
]
# Use the static method with authorization_token
mcp_tool = client.get_mcp_tool(
name="test-mcp",
url="https://example.com/mcp",
authorization_token="Bearer token123",
)
chat_options = ChatOptions(tools=[mcp_tool])
result = client._prepare_tools_for_anthropic(chat_options)
assert result is not None
assert "mcp_servers" in result
# The authorization header is converted to authorization_token
# The authorization_token should be passed through
assert "authorization_token" in result["mcp_servers"][0]
assert result["mcp_servers"][0]["authorization_token"] == "Bearer token123"
@@ -806,12 +799,11 @@ async def test_anthropic_client_integration_hosted_tools() -> None:
messages = [Message(role="user", text="What tools do you have available?")]
tools = [
HostedWebSearchTool(),
HostedCodeInterpreterTool(),
HostedMCPTool(
AnthropicClient.get_web_search_tool(),
AnthropicClient.get_code_interpreter_tool(),
AnthropicClient.get_mcp_tool(
name="example-mcp",
url="https://learn.microsoft.com/api/mcp",
approval_mode="never_require",
),
]