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
@@ -44,7 +44,7 @@ async def main() -> None:
print("Agent: ", end="", flush=True)
async for chunk in agent.run(query, stream=True):
for content in chunk.contents:
if content.type == "text_reasoning":
if content.type == "text_reasoning" and content.text:
print(f"\033[32m{content.text}\033[0m", end="", flush=True)
if content.type == "usage":
print(f"\n\033[34m[Usage so far: {content.usage_details}]\033[0m\n", end="", flush=True)
@@ -123,8 +123,8 @@ async def example_with_existing_session_id() -> None:
)
async with agent2:
# Create session with existing session ID
session = agent2.create_session(service_session_id=existing_session_id)
# Get session with existing session ID
session = agent2.get_session(service_session_id=existing_session_id)
query2 = "What was the last city I asked about?"
print(f"User: {query2}")
@@ -47,7 +47,7 @@ async def main() -> None:
)
async with agent:
query = "List the first 3 Python files in the current directory"
query = "List the first 3 markdown (.md) files in the current directory"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}\n")
@@ -36,8 +36,8 @@ async def main() -> None:
instructions="You are a helpful agent for creating powerpoint presentations.",
tools=client.get_code_interpreter_tool(),
default_options={
"max_tokens": 20000,
"thinking": {"type": "enabled", "budget_tokens": 10000},
"max_tokens": 4096,
"thinking": {"type": "enabled", "budget_tokens": 2000},
"container": {"skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}]},
},
)
@@ -49,7 +49,7 @@ async def main() -> None:
"\033[32mAgent Reasoning: (green)\033[0m\n"
"\033[34mUsage: (blue)\033[0m\n"
)
query = "Create a presentation about renewable energy with 5 slides"
query = "Create a simple presentation with 2 slides about Python programming"
print(f"User: {query}")
print("Agent: ", end="", flush=True)
files: list[Content] = []
@@ -79,9 +79,9 @@ async def main() -> None:
file_content = await client.anthropic_client.beta.files.download(
file_id=file.file_id, betas=["files-api-2025-04-14"]
)
with open(Path(__file__).parent / f"renewable_energy-{idx}.pptx", "wb") as f:
with open(Path(__file__).parent / f"python_programming-{idx}.pptx", "wb") as f:
await file_content.write_to_file(f.name)
print(f"File {idx}: renewable_energy-{idx}.pptx saved to disk.")
print(f"File {idx}: python_programming-{idx}.pptx saved to disk.")
if __name__ == "__main__":
@@ -12,7 +12,6 @@ from agent_framework import (
Content,
InMemoryHistoryProvider,
Message,
Role,
normalize_messages,
)
@@ -93,7 +92,7 @@ class EchoAgent(BaseAgent):
if not normalized_messages:
response_message = Message(
role=Role.ASSISTANT,
role="assistant",
contents=[
Content.from_text(text="Hello! I'm a custom echo agent. Send me a message and I'll echo it back.")
],
@@ -106,11 +105,11 @@ class EchoAgent(BaseAgent):
else:
echo_text = f"{self.echo_prefix}[Non-text message received]"
response_message = Message(role=Role.ASSISTANT, contents=[Content.from_text(text=echo_text)])
response_message = Message(role="assistant", contents=[Content.from_text(text=echo_text)])
# Store messages in session state if provided
if session is not None:
stored = session.state.setdefault("memory", {}).setdefault("messages", [])
stored = session.state.setdefault(InMemoryHistoryProvider.DEFAULT_SOURCE_ID, {}).setdefault("messages", [])
stored.extend(normalized_messages)
stored.append(response_message)
@@ -145,7 +144,7 @@ class EchoAgent(BaseAgent):
yield AgentResponseUpdate(
contents=[Content.from_text(text=chunk_text)],
role=Role.ASSISTANT,
role="assistant",
)
# Small delay to simulate streaming
@@ -153,8 +152,8 @@ class EchoAgent(BaseAgent):
# Store messages in session state if provided
if session is not None:
complete_response = Message(role=Role.ASSISTANT, contents=[Content.from_text(text=response_text)])
stored = session.state.setdefault("memory", {}).setdefault("messages", [])
complete_response = Message(role="assistant", contents=[Content.from_text(text=response_text)])
stored = session.state.setdefault(InMemoryHistoryProvider.DEFAULT_SOURCE_ID, {}).setdefault("messages", [])
stored.extend(normalized_messages)
stored.append(complete_response)
@@ -118,8 +118,8 @@ async def example_with_existing_session_id() -> None:
)
async with agent2:
# Create session with existing session ID
session = agent2.create_session(service_session_id=existing_session_id)
# Get session with existing session ID
session = agent2.get_session(service_session_id=existing_session_id)
query2 = "What was the last city I asked about?"
print(f"User: {query2}")
@@ -32,8 +32,8 @@ from typing import Any
from agent_framework import (
AgentExecutorRequest,
AgentExecutorResponse,
Message,
Executor,
Message,
Workflow,
WorkflowBuilder,
WorkflowContext,