mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Fixed Anthropic and GitHub Copilot samples (#4025)
* Fixed Anthropic advanced example * Small improvement * Simplified skills sample * Fixed custom agent sample * Added service_session_id parameter * Added tests * Resolved comments
This commit is contained in:
committed by
GitHub
Unverified
parent
28e3fc308b
commit
f900febb6f
@@ -324,8 +324,9 @@ class AnthropicClient(
|
||||
self.anthropic_client = anthropic_client
|
||||
self.additional_beta_flags = additional_beta_flags or []
|
||||
self.model_id = anthropic_settings["chat_model_id"]
|
||||
# streaming requires tracking the last function call ID and name
|
||||
# streaming requires tracking the last function call ID, name, and content type
|
||||
self._last_call_id_name: tuple[str, str] | None = None
|
||||
self._last_call_content_type: str | None = None
|
||||
|
||||
# region Static factory methods for hosted tools
|
||||
|
||||
@@ -333,11 +334,13 @@ class AnthropicClient(
|
||||
def get_code_interpreter_tool(
|
||||
*,
|
||||
type_name: str | None = None,
|
||||
name: str = "code_execution",
|
||||
) -> 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".
|
||||
name: The name for this tool. Defaults to "code_execution".
|
||||
|
||||
Returns:
|
||||
A dict-based tool configuration ready to pass to ChatAgent.
|
||||
@@ -350,17 +353,19 @@ class AnthropicClient(
|
||||
tool = AnthropicClient.get_code_interpreter_tool()
|
||||
agent = AnthropicClient().as_agent(tools=[tool])
|
||||
"""
|
||||
return {"type": type_name or "code_execution_20250825"}
|
||||
return {"type": type_name or "code_execution_20250825", "name": name}
|
||||
|
||||
@staticmethod
|
||||
def get_web_search_tool(
|
||||
*,
|
||||
type_name: str | None = None,
|
||||
name: str = "web_search",
|
||||
) -> 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".
|
||||
name: The name for this tool. Defaults to "web_search".
|
||||
|
||||
Returns:
|
||||
A dict-based tool configuration ready to pass to ChatAgent.
|
||||
@@ -373,7 +378,7 @@ class AnthropicClient(
|
||||
tool = AnthropicClient.get_web_search_tool()
|
||||
agent = AnthropicClient().as_agent(tools=[tool])
|
||||
"""
|
||||
return {"type": type_name or "web_search_20250305"}
|
||||
return {"type": type_name or "web_search_20250305", "name": name}
|
||||
|
||||
@staticmethod
|
||||
def get_mcp_tool(
|
||||
@@ -661,8 +666,27 @@ class AnthropicClient(
|
||||
"content": content.result if content.result is not None else "",
|
||||
"is_error": content.exception is not None,
|
||||
})
|
||||
case "mcp_server_tool_call":
|
||||
mcp_call: dict[str, Any] = {
|
||||
"type": "mcp_tool_use",
|
||||
"id": content.call_id,
|
||||
"name": content.tool_name,
|
||||
"server_name": content.server_name or "",
|
||||
"input": content.parse_arguments() or {},
|
||||
}
|
||||
a_content.append(mcp_call)
|
||||
case "mcp_server_tool_result":
|
||||
mcp_result: dict[str, Any] = {
|
||||
"type": "mcp_tool_result",
|
||||
"tool_use_id": content.call_id,
|
||||
"content": content.output if content.output is not None else "",
|
||||
}
|
||||
a_content.append(mcp_result)
|
||||
case "text_reasoning":
|
||||
a_content.append({"type": "thinking", "thinking": content.text})
|
||||
thinking_block: dict[str, Any] = {"type": "thinking", "thinking": content.text}
|
||||
if content.protected_data:
|
||||
thinking_block["signature"] = content.protected_data
|
||||
a_content.append(thinking_block)
|
||||
case _:
|
||||
logger.debug(f"Ignoring unsupported content type: {content.type} for now")
|
||||
|
||||
@@ -866,12 +890,13 @@ class AnthropicClient(
|
||||
)
|
||||
case "tool_use" | "mcp_tool_use" | "server_tool_use":
|
||||
self._last_call_id_name = (content_block.id, content_block.name)
|
||||
self._last_call_content_type = content_block.type
|
||||
if content_block.type == "mcp_tool_use":
|
||||
contents.append(
|
||||
Content.from_mcp_server_tool_call(
|
||||
call_id=content_block.id,
|
||||
tool_name=content_block.name,
|
||||
server_name=None,
|
||||
server_name=getattr(content_block, "server_name", None),
|
||||
arguments=content_block.input,
|
||||
raw_representation=content_block,
|
||||
)
|
||||
@@ -1129,24 +1154,32 @@ class AnthropicClient(
|
||||
)
|
||||
)
|
||||
case "input_json_delta":
|
||||
# For streaming argument deltas, only pass call_id and arguments.
|
||||
# Pass empty string for name - it causes ag-ui to emit duplicate ToolCallStartEvents
|
||||
# since it triggers on `if content.name:`. The initial tool_use event already
|
||||
# provides the name, so deltas should only carry incremental arguments.
|
||||
# This matches OpenAI's behavior where streaming chunks have name="".
|
||||
call_id, _name = self._last_call_id_name if self._last_call_id_name else ("", "")
|
||||
contents.append(
|
||||
Content.from_function_call(
|
||||
call_id=call_id,
|
||||
name="",
|
||||
arguments=content_block.partial_json,
|
||||
raw_representation=content_block,
|
||||
# Skip argument deltas for MCP tools — execution is handled server-side.
|
||||
if self._last_call_content_type == "mcp_tool_use":
|
||||
pass
|
||||
else:
|
||||
call_id = self._last_call_id_name[0] if self._last_call_id_name else ""
|
||||
contents.append(
|
||||
Content.from_function_call(
|
||||
call_id=call_id,
|
||||
name="",
|
||||
arguments=content_block.partial_json,
|
||||
raw_representation=content_block,
|
||||
)
|
||||
)
|
||||
)
|
||||
case "thinking" | "thinking_delta":
|
||||
contents.append(
|
||||
Content.from_text_reasoning(
|
||||
text=content_block.thinking,
|
||||
protected_data=getattr(content_block, "signature", None),
|
||||
raw_representation=content_block,
|
||||
)
|
||||
)
|
||||
case "signature_delta":
|
||||
contents.append(
|
||||
Content.from_text_reasoning(
|
||||
text=None,
|
||||
protected_data=content_block.signature,
|
||||
raw_representation=content_block,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -220,6 +220,119 @@ def test_prepare_message_for_anthropic_text_reasoning(mock_anthropic_client: Mag
|
||||
assert len(result["content"]) == 1
|
||||
assert result["content"][0]["type"] == "thinking"
|
||||
assert result["content"][0]["thinking"] == "Let me think about this..."
|
||||
assert "signature" not in result["content"][0]
|
||||
|
||||
|
||||
def test_prepare_message_for_anthropic_text_reasoning_with_signature(mock_anthropic_client: MagicMock) -> None:
|
||||
"""Test converting text reasoning message with signature to Anthropic format."""
|
||||
client = create_test_anthropic_client(mock_anthropic_client)
|
||||
message = Message(
|
||||
role="assistant",
|
||||
contents=[Content.from_text_reasoning(text="Let me think about this...", protected_data="sig_abc123")],
|
||||
)
|
||||
|
||||
result = client._prepare_message_for_anthropic(message)
|
||||
|
||||
assert result["role"] == "assistant"
|
||||
assert len(result["content"]) == 1
|
||||
assert result["content"][0]["type"] == "thinking"
|
||||
assert result["content"][0]["thinking"] == "Let me think about this..."
|
||||
assert result["content"][0]["signature"] == "sig_abc123"
|
||||
|
||||
|
||||
def test_prepare_message_for_anthropic_mcp_server_tool_call(mock_anthropic_client: MagicMock) -> None:
|
||||
"""Test converting MCP server tool call message to Anthropic format."""
|
||||
client = create_test_anthropic_client(mock_anthropic_client)
|
||||
message = Message(
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_mcp_server_tool_call(
|
||||
call_id="mcp_call_123",
|
||||
tool_name="search_docs",
|
||||
server_name="microsoft-learn",
|
||||
arguments={"query": "Azure Functions"},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = client._prepare_message_for_anthropic(message)
|
||||
|
||||
assert result["role"] == "assistant"
|
||||
assert len(result["content"]) == 1
|
||||
assert result["content"][0]["type"] == "mcp_tool_use"
|
||||
assert result["content"][0]["id"] == "mcp_call_123"
|
||||
assert result["content"][0]["name"] == "search_docs"
|
||||
assert result["content"][0]["server_name"] == "microsoft-learn"
|
||||
assert result["content"][0]["input"] == {"query": "Azure Functions"}
|
||||
|
||||
|
||||
def test_prepare_message_for_anthropic_mcp_server_tool_call_no_server_name(mock_anthropic_client: MagicMock) -> None:
|
||||
"""Test converting MCP server tool call with no server name defaults to empty string."""
|
||||
client = create_test_anthropic_client(mock_anthropic_client)
|
||||
message = Message(
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_mcp_server_tool_call(
|
||||
call_id="mcp_call_456",
|
||||
tool_name="list_files",
|
||||
arguments=None,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = client._prepare_message_for_anthropic(message)
|
||||
|
||||
assert result["role"] == "assistant"
|
||||
assert len(result["content"]) == 1
|
||||
assert result["content"][0]["type"] == "mcp_tool_use"
|
||||
assert result["content"][0]["id"] == "mcp_call_456"
|
||||
assert result["content"][0]["name"] == "list_files"
|
||||
assert result["content"][0]["server_name"] == ""
|
||||
assert result["content"][0]["input"] == {}
|
||||
|
||||
|
||||
def test_prepare_message_for_anthropic_mcp_server_tool_result(mock_anthropic_client: MagicMock) -> None:
|
||||
"""Test converting MCP server tool result message to Anthropic format."""
|
||||
client = create_test_anthropic_client(mock_anthropic_client)
|
||||
message = Message(
|
||||
role="tool",
|
||||
contents=[
|
||||
Content.from_mcp_server_tool_result(
|
||||
call_id="mcp_call_123",
|
||||
output="Found 3 results for Azure Functions.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = client._prepare_message_for_anthropic(message)
|
||||
|
||||
assert result["role"] == "user"
|
||||
assert len(result["content"]) == 1
|
||||
assert result["content"][0]["type"] == "mcp_tool_result"
|
||||
assert result["content"][0]["tool_use_id"] == "mcp_call_123"
|
||||
assert result["content"][0]["content"] == "Found 3 results for Azure Functions."
|
||||
|
||||
|
||||
def test_prepare_message_for_anthropic_mcp_server_tool_result_none_output(mock_anthropic_client: MagicMock) -> None:
|
||||
"""Test converting MCP server tool result with None output defaults to empty string."""
|
||||
client = create_test_anthropic_client(mock_anthropic_client)
|
||||
message = Message(
|
||||
role="tool",
|
||||
contents=[
|
||||
Content.from_mcp_server_tool_result(
|
||||
call_id="mcp_call_789",
|
||||
output=None,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
result = client._prepare_message_for_anthropic(message)
|
||||
|
||||
assert result["role"] == "user"
|
||||
assert len(result["content"]) == 1
|
||||
assert result["content"][0]["type"] == "mcp_tool_result"
|
||||
assert result["content"][0]["tool_use_id"] == "mcp_call_789"
|
||||
assert result["content"][0]["content"] == ""
|
||||
|
||||
|
||||
def test_prepare_messages_for_anthropic_with_system(mock_anthropic_client: MagicMock) -> None:
|
||||
@@ -287,6 +400,7 @@ 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:
|
||||
@@ -300,6 +414,7 @@ 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:
|
||||
@@ -1764,11 +1879,13 @@ def test_parse_thinking_block(mock_anthropic_client: MagicMock) -> None:
|
||||
mock_block = MagicMock()
|
||||
mock_block.type = "thinking"
|
||||
mock_block.thinking = "Let me think about this..."
|
||||
mock_block.signature = "sig_abc123"
|
||||
|
||||
result = client._parse_contents_from_anthropic([mock_block])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].type == "text_reasoning"
|
||||
assert result[0].protected_data == "sig_abc123"
|
||||
|
||||
|
||||
def test_parse_thinking_delta_block(mock_anthropic_client: MagicMock) -> None:
|
||||
@@ -1786,6 +1903,23 @@ def test_parse_thinking_delta_block(mock_anthropic_client: MagicMock) -> None:
|
||||
assert result[0].type == "text_reasoning"
|
||||
|
||||
|
||||
def test_parse_signature_delta_block(mock_anthropic_client: MagicMock) -> None:
|
||||
"""Test parsing signature delta content block."""
|
||||
client = create_test_anthropic_client(mock_anthropic_client)
|
||||
|
||||
# Create mock signature delta block
|
||||
mock_block = MagicMock()
|
||||
mock_block.type = "signature_delta"
|
||||
mock_block.signature = "sig_xyz789"
|
||||
|
||||
result = client._parse_contents_from_anthropic([mock_block])
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].type == "text_reasoning"
|
||||
assert result[0].text is None
|
||||
assert result[0].protected_data == "sig_xyz789"
|
||||
|
||||
|
||||
# Citation Tests
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user