Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 50 additions & 17 deletions python/packages/anthropic/agent_framework_anthropic/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,20 +324,23 @@ class MyOptions(AnthropicChatOptions, total=False):
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

@staticmethod
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.
Expand All @@ -350,17 +353,19 @@ def get_code_interpreter_tool(
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.
Expand All @@ -373,7 +378,7 @@ def get_web_search_tool(
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(
Expand Down Expand Up @@ -661,8 +666,27 @@ def _prepare_message_for_anthropic(self, message: Message) -> dict[str, Any]:
"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")

Expand Down Expand Up @@ -866,12 +890,13 @@ def _parse_contents_from_anthropic(
)
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,
)
Expand Down Expand Up @@ -1129,24 +1154,32 @@ def _parse_contents_from_anthropic(
)
)
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 ("", "")
# 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_function_call(
call_id=call_id,
name="",
arguments=content_block.partial_json,
Content.from_text_reasoning(
text=content_block.thinking,
protected_data=getattr(content_block, "signature", None),
raw_representation=content_block,
)
)
case "thinking" | "thinking_delta":
case "signature_delta":
contents.append(
Content.from_text_reasoning(
text=content_block.thinking,
text=None,
protected_data=content_block.signature,
raw_representation=content_block,
)
)
Expand Down
134 changes: 134 additions & 0 deletions python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
10 changes: 5 additions & 5 deletions python/samples/02-agents/providers/anthropic/anthropic_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}]},
},
)
Expand All @@ -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] = []
Expand Down Expand Up @@ -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__":
Expand Down
Loading