Skip to content

Commit 44118b6

Browse files
authored
Merge branch 'main' into copilot/add-unit-tests-for-copyconversationmessagesexecuto
2 parents 214041d + 7a88af0 commit 44118b6

File tree

133 files changed

+3012
-2644
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

133 files changed

+3012
-2644
lines changed

python/packages/ag-ui/agent_framework_ag_ui/_utils.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from datetime import date, datetime
1313
from typing import Any
1414

15-
from agent_framework import AgentResponseUpdate, ChatResponseUpdate, FunctionTool, ToolProtocol
15+
from agent_framework import AgentResponseUpdate, ChatResponseUpdate, FunctionTool
1616

1717
# Role mapping constants
1818
AGUI_TO_FRAMEWORK_ROLE: dict[str, str] = {
@@ -200,10 +200,10 @@ def convert_agui_tools_to_agent_framework(
200200

201201
def convert_tools_to_agui_format(
202202
tools: (
203-
ToolProtocol
203+
FunctionTool
204204
| Callable[..., Any]
205205
| MutableMapping[str, Any]
206-
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
206+
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
207207
| None
208208
),
209209
) -> list[dict[str, Any]] | None:
@@ -225,7 +225,7 @@ def convert_tools_to_agui_format(
225225

226226
# Normalize to list
227227
if not isinstance(tools, list):
228-
tool_list: list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] = [tools] # type: ignore[list-item]
228+
tool_list: list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] = [tools] # type: ignore[list-item]
229229
else:
230230
tool_list = tools # type: ignore[assignment]
231231

@@ -256,12 +256,8 @@ def convert_tools_to_agui_format(
256256
"parameters": ai_func.parameters(),
257257
}
258258
)
259-
elif isinstance(tool_item, ToolProtocol):
260-
# Handle other ToolProtocol implementations
261-
# For now, we'll skip non-FunctionTool instances as they may not have
262-
# the parameters() method. This matches .NET behavior which only
263-
# converts FunctionToolDeclaration instances.
264-
continue
259+
# Note: dict-based hosted tools (CodeInterpreter, WebSearch, etc.) are passed through
260+
# as-is in the first branch. Non-FunctionTool, non-dict items are skipped.
265261

266262
return results if results else None
267263

python/packages/anthropic/agent_framework_anthropic/_chat_client.py

Lines changed: 131 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@
2020
FunctionInvocationConfiguration,
2121
FunctionInvocationLayer,
2222
FunctionTool,
23-
HostedCodeInterpreterTool,
24-
HostedMCPTool,
25-
HostedWebSearchTool,
2623
Message,
2724
ResponseStream,
2825
TextSpanRegion,
@@ -350,6 +347,109 @@ class MyOptions(AnthropicChatOptions, total=False):
350347
# streaming requires tracking the last function call ID and name
351348
self._last_call_id_name: tuple[str, str] | None = None
352349

350+
# region Static factory methods for hosted tools
351+
352+
@staticmethod
353+
def get_code_interpreter_tool(
354+
*,
355+
type_name: str | None = None,
356+
) -> dict[str, Any]:
357+
"""Create a code interpreter tool configuration for Anthropic.
358+
359+
Keyword Args:
360+
type_name: Override the tool type name. Defaults to "code_execution_20250825".
361+
362+
Returns:
363+
A dict-based tool configuration ready to pass to ChatAgent.
364+
365+
Examples:
366+
.. code-block:: python
367+
368+
from agent_framework.anthropic import AnthropicClient
369+
370+
tool = AnthropicClient.get_code_interpreter_tool()
371+
agent = AnthropicClient().as_agent(tools=[tool])
372+
"""
373+
return {"type": type_name or "code_execution_20250825"}
374+
375+
@staticmethod
376+
def get_web_search_tool(
377+
*,
378+
type_name: str | None = None,
379+
) -> dict[str, Any]:
380+
"""Create a web search tool configuration for Anthropic.
381+
382+
Keyword Args:
383+
type_name: Override the tool type name. Defaults to "web_search_20250305".
384+
385+
Returns:
386+
A dict-based tool configuration ready to pass to ChatAgent.
387+
388+
Examples:
389+
.. code-block:: python
390+
391+
from agent_framework.anthropic import AnthropicClient
392+
393+
tool = AnthropicClient.get_web_search_tool()
394+
agent = AnthropicClient().as_agent(tools=[tool])
395+
"""
396+
return {"type": type_name or "web_search_20250305"}
397+
398+
@staticmethod
399+
def get_mcp_tool(
400+
*,
401+
name: str,
402+
url: str,
403+
allowed_tools: list[str] | None = None,
404+
authorization_token: str | None = None,
405+
) -> dict[str, Any]:
406+
"""Create a hosted MCP tool configuration for Anthropic.
407+
408+
This configures an MCP (Model Context Protocol) server that will be called
409+
by Anthropic's service. The tools from this MCP server are executed remotely
410+
by Anthropic, not locally by your application.
411+
412+
Note:
413+
For local MCP execution where your application calls the MCP server
414+
directly, use the MCP client tools instead of this method.
415+
416+
Keyword Args:
417+
name: A label/name for the MCP server.
418+
url: The URL of the MCP server.
419+
allowed_tools: List of tool names that are allowed to be used from this MCP server.
420+
authorization_token: Authorization token for the MCP server (e.g., Bearer token).
421+
422+
Returns:
423+
A dict-based tool configuration ready to pass to ChatAgent.
424+
425+
Examples:
426+
.. code-block:: python
427+
428+
from agent_framework.anthropic import AnthropicClient
429+
430+
tool = AnthropicClient.get_mcp_tool(
431+
name="GitHub",
432+
url="https://api.githubcopilot.com/mcp/",
433+
authorization_token="Bearer ghp_xxx",
434+
)
435+
agent = AnthropicClient().as_agent(tools=[tool])
436+
"""
437+
result: dict[str, Any] = {
438+
"type": "mcp",
439+
"server_label": name.replace(" ", "_"),
440+
"server_url": url,
441+
}
442+
443+
if allowed_tools:
444+
result["allowed_tools"] = allowed_tools
445+
446+
if authorization_token:
447+
result["headers"] = {"authorization": authorization_token}
448+
449+
return result
450+
451+
# endregion
452+
353453
# region Get response methods
354454

355455
@override
@@ -590,6 +690,9 @@ def _prepare_message_for_anthropic(self, message: Message) -> dict[str, Any]:
590690
def _prepare_tools_for_anthropic(self, options: Mapping[str, Any]) -> dict[str, Any] | None:
591691
"""Prepare tools and tool choice configuration for the Anthropic API request.
592692
693+
Converts FunctionTool to Anthropic format. MCP tools are routed to separate
694+
mcp_servers parameter. All other tools pass through unchanged.
695+
593696
Args:
594697
options: The options dict containing tools and tool choice settings.
595698
@@ -603,46 +706,32 @@ def _prepare_tools_for_anthropic(self, options: Mapping[str, Any]) -> dict[str,
603706

604707
# Process tools
605708
if tools:
606-
tool_list: list[MutableMapping[str, Any]] = []
607-
mcp_server_list: list[MutableMapping[str, Any]] = []
709+
tool_list: list[Any] = []
710+
mcp_server_list: list[Any] = []
608711
for tool in tools:
609-
match tool:
610-
case MutableMapping():
611-
tool_list.append(tool)
612-
case FunctionTool():
613-
tool_list.append({
614-
"type": "custom",
615-
"name": tool.name,
616-
"description": tool.description,
617-
"input_schema": tool.parameters(),
618-
})
619-
case HostedWebSearchTool():
620-
search_tool: dict[str, Any] = {
621-
"type": "web_search_20250305",
622-
"name": "web_search",
623-
}
624-
if tool.additional_properties:
625-
search_tool.update(tool.additional_properties)
626-
tool_list.append(search_tool)
627-
case HostedCodeInterpreterTool():
628-
code_tool: dict[str, Any] = {
629-
"type": "code_execution_20250825",
630-
"name": "code_execution",
631-
}
632-
tool_list.append(code_tool)
633-
case HostedMCPTool():
634-
server_def: dict[str, Any] = {
635-
"type": "url",
636-
"name": tool.name,
637-
"url": str(tool.url),
638-
}
639-
if tool.allowed_tools:
640-
server_def["tool_configuration"] = {"allowed_tools": list(tool.allowed_tools)}
641-
if tool.headers and (auth := tool.headers.get("authorization")):
642-
server_def["authorization_token"] = auth
643-
mcp_server_list.append(server_def)
644-
case _:
645-
logger.debug(f"Ignoring unsupported tool type: {type(tool)} for now")
712+
if isinstance(tool, FunctionTool):
713+
tool_list.append({
714+
"type": "custom",
715+
"name": tool.name,
716+
"description": tool.description,
717+
"input_schema": tool.parameters(),
718+
})
719+
elif isinstance(tool, MutableMapping) and tool.get("type") == "mcp":
720+
# MCP servers must be routed to separate mcp_servers parameter
721+
server_def: dict[str, Any] = {
722+
"type": "url",
723+
"name": tool.get("server_label", ""),
724+
"url": tool.get("server_url", ""),
725+
}
726+
if allowed_tools := tool.get("allowed_tools"):
727+
server_def["tool_configuration"] = {"allowed_tools": list(allowed_tools)}
728+
headers = tool.get("headers")
729+
if isinstance(headers, dict) and (auth := headers.get("authorization")):
730+
server_def["authorization_token"] = auth
731+
mcp_server_list.append(server_def)
732+
else:
733+
# Pass through all other tools (dicts, SDK types) unchanged
734+
tool_list.append(tool)
646735

647736
if tool_list:
648737
result["tools"] = tool_list

python/packages/anthropic/tests/test_anthropic_client.py

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@
99
ChatOptions,
1010
ChatResponseUpdate,
1111
Content,
12-
HostedCodeInterpreterTool,
13-
HostedMCPTool,
14-
HostedWebSearchTool,
1512
Message,
1613
SupportsChatGetResponse,
1714
tool,
@@ -278,37 +275,35 @@ def get_weather(location: Annotated[str, Field(description="Location to get weat
278275

279276

280277
def test_prepare_tools_for_anthropic_web_search(mock_anthropic_client: MagicMock) -> None:
281-
"""Test converting HostedWebSearchTool to Anthropic format."""
278+
"""Test converting web_search dict tool to Anthropic format."""
282279
client = create_test_anthropic_client(mock_anthropic_client)
283-
chat_options = ChatOptions(tools=[HostedWebSearchTool()])
280+
chat_options = ChatOptions(tools=[client.get_web_search_tool()])
284281

285282
result = client._prepare_tools_for_anthropic(chat_options)
286283

287284
assert result is not None
288285
assert "tools" in result
289286
assert len(result["tools"]) == 1
290287
assert result["tools"][0]["type"] == "web_search_20250305"
291-
assert result["tools"][0]["name"] == "web_search"
292288

293289

294290
def test_prepare_tools_for_anthropic_code_interpreter(mock_anthropic_client: MagicMock) -> None:
295-
"""Test converting HostedCodeInterpreterTool to Anthropic format."""
291+
"""Test converting code_interpreter dict tool to Anthropic format."""
296292
client = create_test_anthropic_client(mock_anthropic_client)
297-
chat_options = ChatOptions(tools=[HostedCodeInterpreterTool()])
293+
chat_options = ChatOptions(tools=[client.get_code_interpreter_tool()])
298294

299295
result = client._prepare_tools_for_anthropic(chat_options)
300296

301297
assert result is not None
302298
assert "tools" in result
303299
assert len(result["tools"]) == 1
304300
assert result["tools"][0]["type"] == "code_execution_20250825"
305-
assert result["tools"][0]["name"] == "code_execution"
306301

307302

308303
def test_prepare_tools_for_anthropic_mcp_tool(mock_anthropic_client: MagicMock) -> None:
309-
"""Test converting HostedMCPTool to Anthropic format."""
304+
"""Test converting MCP dict tool to Anthropic format."""
310305
client = create_test_anthropic_client(mock_anthropic_client)
311-
chat_options = ChatOptions(tools=[HostedMCPTool(name="test-mcp", url="https://example.com/mcp")])
306+
chat_options = ChatOptions(tools=[client.get_mcp_tool(name="test-mcp", url="https://example.com/mcp")])
312307

313308
result = client._prepare_tools_for_anthropic(chat_options)
314309

@@ -321,23 +316,21 @@ def test_prepare_tools_for_anthropic_mcp_tool(mock_anthropic_client: MagicMock)
321316

322317

323318
def test_prepare_tools_for_anthropic_mcp_with_auth(mock_anthropic_client: MagicMock) -> None:
324-
"""Test converting HostedMCPTool with authorization headers."""
325-
client = create_test_anthropic_client(mock_anthropic_client)
326-
chat_options = ChatOptions(
327-
tools=[
328-
HostedMCPTool(
329-
name="test-mcp",
330-
url="https://example.com/mcp",
331-
headers={"authorization": "Bearer token123"},
332-
)
333-
]
319+
"""Test converting MCP dict tool with authorization token."""
320+
client = create_test_anthropic_client(mock_anthropic_client)
321+
# Use the static method with authorization_token
322+
mcp_tool = client.get_mcp_tool(
323+
name="test-mcp",
324+
url="https://example.com/mcp",
325+
authorization_token="Bearer token123",
334326
)
327+
chat_options = ChatOptions(tools=[mcp_tool])
335328

336329
result = client._prepare_tools_for_anthropic(chat_options)
337330

338331
assert result is not None
339332
assert "mcp_servers" in result
340-
# The authorization header is converted to authorization_token
333+
# The authorization_token should be passed through
341334
assert "authorization_token" in result["mcp_servers"][0]
342335
assert result["mcp_servers"][0]["authorization_token"] == "Bearer token123"
343336

@@ -806,12 +799,11 @@ async def test_anthropic_client_integration_hosted_tools() -> None:
806799

807800
messages = [Message(role="user", text="What tools do you have available?")]
808801
tools = [
809-
HostedWebSearchTool(),
810-
HostedCodeInterpreterTool(),
811-
HostedMCPTool(
802+
AnthropicClient.get_web_search_tool(),
803+
AnthropicClient.get_code_interpreter_tool(),
804+
AnthropicClient.get_mcp_tool(
812805
name="example-mcp",
813806
url="https://learn.microsoft.com/api/mcp",
814-
approval_mode="never_require",
815807
),
816808
]
817809

0 commit comments

Comments
 (0)