diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 3df1279578..840aa8d7fd 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -324,8 +324,9 @@ 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 @@ -333,11 +334,13 @@ class MyOptions(AnthropicChatOptions, total=False): 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 @@ 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. @@ -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( @@ -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") @@ -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, ) @@ -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, ) ) diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 4f74c60308..4b164ce84d 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -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 diff --git a/python/samples/02-agents/providers/anthropic/anthropic_advanced.py b/python/samples/02-agents/providers/anthropic/anthropic_advanced.py index 3918005b5d..0ae241c8a4 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_advanced.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_advanced.py @@ -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) diff --git a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_session.py b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_session.py index 6ff1fff25a..8a022624b5 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_session.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_session.py @@ -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}") diff --git a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_shell.py b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_shell.py index 86f8be9794..eb8c0961fa 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_claude_with_shell.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_claude_with_shell.py @@ -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") diff --git a/python/samples/02-agents/providers/anthropic/anthropic_skills.py b/python/samples/02-agents/providers/anthropic/anthropic_skills.py index 3b014f9b6a..417feabb50 100644 --- a/python/samples/02-agents/providers/anthropic/anthropic_skills.py +++ b/python/samples/02-agents/providers/anthropic/anthropic_skills.py @@ -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__": diff --git a/python/samples/02-agents/providers/custom/custom_agent.py b/python/samples/02-agents/providers/custom/custom_agent.py index 603af97153..595e6b6d9d 100644 --- a/python/samples/02-agents/providers/custom/custom_agent.py +++ b/python/samples/02-agents/providers/custom/custom_agent.py @@ -12,7 +12,6 @@ Content, InMemoryHistoryProvider, Message, - Role, normalize_messages, ) @@ -93,7 +92,7 @@ async def _run( 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 @@ async def _run( 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 @@ async def _run_stream( yield AgentResponseUpdate( contents=[Content.from_text(text=chunk_text)], - role=Role.ASSISTANT, + role="assistant", ) # Small delay to simulate streaming @@ -153,8 +152,8 @@ async def _run_stream( # 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) diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_session.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_session.py index 4d386bfa19..4f15aeee34 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_session.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_session.py @@ -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}") diff --git a/python/samples/04-hosting/azure_functions/12_workflow_hitl/function_app.py b/python/samples/04-hosting/azure_functions/12_workflow_hitl/function_app.py index f747d60919..11d11b4a92 100644 --- a/python/samples/04-hosting/azure_functions/12_workflow_hitl/function_app.py +++ b/python/samples/04-hosting/azure_functions/12_workflow_hitl/function_app.py @@ -32,8 +32,8 @@ from agent_framework import ( AgentExecutorRequest, AgentExecutorResponse, - Message, Executor, + Message, Workflow, WorkflowBuilder, WorkflowContext,