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:
Dmytro Struk
2026-02-18 01:23:35 -05:00
committed by GitHub
Unverified
parent 28e3fc308b
commit f900febb6f
9 changed files with 203 additions and 37 deletions
@@ -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