mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user