diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 303ea0ee20..96a70bc4a0 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -18,6 +18,7 @@ from agent_framework import ( FunctionCallContent, FunctionResultContent, HostedCodeInterpreterTool, + HostedFileContent, HostedMCPTool, HostedWebSearchTool, Role, @@ -122,6 +123,7 @@ class AnthropicClient(BaseChatClient): api_key: str | None = None, model_id: str | None = None, anthropic_client: AsyncAnthropic | None = None, + additional_beta_flags: list[str] | None = None, env_file_path: str | None = None, env_file_encoding: str | None = None, **kwargs: Any, @@ -134,6 +136,8 @@ class AnthropicClient(BaseChatClient): anthropic_client: An existing Anthropic client to use. If not provided, one will be created. This can be used to further configure the client before passing it in. For instance if you need to set a different base_url for testing or private deployments. + additional_beta_flags: Additional beta flags to enable on the client. + Default flags are: "mcp-client-2025-04-04", "code-execution-2025-08-25". env_file_path: Path to environment file for loading settings. env_file_encoding: Encoding of the environment file. kwargs: Additional keyword arguments passed to the parent class. @@ -196,6 +200,7 @@ class AnthropicClient(BaseChatClient): # Initialize instance variables 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 self._last_call_id_name: tuple[str, str] | None = None @@ -246,12 +251,16 @@ class AnthropicClient(BaseChatClient): Returns: A dictionary of run options for the Anthropic client. """ + if chat_options.additional_properties and "additional_beta_flags" in chat_options.additional_properties: + betas = chat_options.additional_properties.pop("additional_beta_flags") + else: + betas = [] run_options: dict[str, Any] = { "model": chat_options.model_id or self.model_id, "messages": self._convert_messages_to_anthropic_format(messages), "max_tokens": chat_options.max_tokens or ANTHROPIC_DEFAULT_MAX_TOKENS, "extra_headers": {"User-Agent": AGENT_FRAMEWORK_USER_AGENT}, - "betas": BETA_FLAGS, + "betas": {*BETA_FLAGS, *self.additional_beta_flags, *betas}, } # Add any additional options from chat_options or kwargs @@ -396,7 +405,7 @@ class AnthropicClient(BaseChatClient): case HostedCodeInterpreterTool(): code_tool: dict[str, Any] = { "type": "code_execution_20250825", - "name": "code_interpreter", + "name": "code_execution", } tool_list.append(code_tool) case HostedMCPTool(): @@ -524,17 +533,7 @@ class AnthropicClient(BaseChatClient): annotations=self._parse_citations(content_block), ) ) - case "tool_use": - self._last_call_id_name = (content_block.id, content_block.name) - contents.append( - FunctionCallContent( - call_id=content_block.id, - name=content_block.name, - arguments=content_block.input, - raw_representation=content_block, - ) - ) - case "mcp_tool_use" | "server_tool_use": + case "tool_use" | "mcp_tool_use" | "server_tool_use": self._last_call_id_name = (content_block.id, content_block.name) contents.append( FunctionCallContent( @@ -572,6 +571,19 @@ class AnthropicClient(BaseChatClient): | "text_editor_code_execution_tool_result" ): call_id, name = self._last_call_id_name or (None, None) + if ( + content_block.content + and ( + content_block.content.type == "bash_code_execution_result" + or content_block.content.type == "code_execution_result" + ) + and content_block.content.content + ): + for result_content in content_block.content.content: + if hasattr(result_content, "file_id"): + contents.append( + HostedFileContent(file_id=result_content.file_id, raw_representation=result_content) + ) contents.append( FunctionResultContent( call_id=content_block.tool_use_id, diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 11498bfe59..fa6061a998 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -50,7 +50,9 @@ def create_test_anthropic_client( ) -> AnthropicClient: """Helper function to create AnthropicClient instances for testing, bypassing normal validation.""" if anthropic_settings is None: - anthropic_settings = AnthropicSettings(api_key="test-api-key-12345", chat_model_id="claude-3-5-sonnet-20241022") + anthropic_settings = AnthropicSettings( + api_key="test-api-key-12345", chat_model_id="claude-3-5-sonnet-20241022", env_file_path="test.env" + ) # Create client instance directly client = object.__new__(AnthropicClient) @@ -61,6 +63,7 @@ def create_test_anthropic_client( client._last_call_id_name = None client.additional_properties = {} client.middleware = None + client.additional_beta_flags = [] return client @@ -70,7 +73,7 @@ def create_test_anthropic_client( def test_anthropic_settings_init(anthropic_unit_test_env: dict[str, str]) -> None: """Test AnthropicSettings initialization.""" - settings = AnthropicSettings() + settings = AnthropicSettings(env_file_path="test.env") assert settings.api_key is not None assert settings.api_key.get_secret_value() == anthropic_unit_test_env["ANTHROPIC_API_KEY"] @@ -80,8 +83,7 @@ def test_anthropic_settings_init(anthropic_unit_test_env: dict[str, str]) -> Non def test_anthropic_settings_init_with_explicit_values() -> None: """Test AnthropicSettings initialization with explicit values.""" settings = AnthropicSettings( - api_key="custom-api-key", - chat_model_id="claude-3-opus-20240229", + api_key="custom-api-key", chat_model_id="claude-3-opus-20240229", env_file_path="test.env" ) assert settings.api_key is not None @@ -114,6 +116,7 @@ def test_anthropic_client_init_auto_create_client(anthropic_unit_test_env: dict[ client = AnthropicClient( api_key=anthropic_unit_test_env["ANTHROPIC_API_KEY"], model_id=anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"], + env_file_path="test.env", ) assert client.anthropic_client is not None @@ -307,7 +310,7 @@ def test_convert_tools_to_anthropic_format_code_interpreter(mock_anthropic_clien assert "tools" in result assert len(result["tools"]) == 1 assert result["tools"][0]["type"] == "code_execution_20250825" - assert result["tools"][0]["name"] == "code_interpreter" + assert result["tools"][0]["name"] == "code_execution" def test_convert_tools_to_anthropic_format_mcp_tool(mock_anthropic_client: MagicMock) -> None: @@ -725,6 +728,32 @@ async def test_anthropic_client_integration_function_calling() -> None: assert has_function_call +@pytest.mark.flaky +@skip_if_anthropic_integration_tests_disabled +async def test_anthropic_client_integration_hosted_tools() -> None: + """Integration test for hosted tools.""" + client = AnthropicClient() + + messages = [ChatMessage(role=Role.USER, text="What tools do you have available?")] + tools = [ + HostedWebSearchTool(), + HostedCodeInterpreterTool(), + HostedMCPTool( + name="example-mcp", + url="https://learn.microsoft.com/api/mcp", + approval_mode="never_require", + ), + ] + + response = await client.get_response( + messages=messages, + chat_options=ChatOptions(tools=tools, max_tokens=100), + ) + + assert response is not None + assert response.text is not None + + @pytest.mark.flaky @skip_if_anthropic_integration_tests_disabled async def test_anthropic_client_integration_with_system_message() -> None: diff --git a/python/samples/getting_started/agents/anthropic/README.md b/python/samples/getting_started/agents/anthropic/README.md index c0d15c3e02..a4f76d7225 100644 --- a/python/samples/getting_started/agents/anthropic/README.md +++ b/python/samples/getting_started/agents/anthropic/README.md @@ -8,6 +8,7 @@ This folder contains examples demonstrating how to use Anthropic's Claude models |------|-------------| | [`anthropic_basic.py`](anthropic_basic.py) | Demonstrates how to setup a simple agent using the AnthropicClient, with both streaming and non-streaming responses. | | [`anthropic_advanced.py`](anthropic_advanced.py) | Shows advanced usage of the AnthropicClient, including hosted tools and `thinking`. | +| [`anthropic_skills.py`](anthropic_skills.py) | Illustrates how to use Anthropic-managed Skills with an agent, including the Code Interpreter tool and file generation and saving. | ## Environment Variables diff --git a/python/samples/getting_started/agents/anthropic/anthropic_skills.py b/python/samples/getting_started/agents/anthropic/anthropic_skills.py new file mode 100644 index 0000000000..331b6405fb --- /dev/null +++ b/python/samples/getting_started/agents/anthropic/anthropic_skills.py @@ -0,0 +1,88 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import logging +from pathlib import Path + +from agent_framework import HostedCodeInterpreterTool, HostedFileContent +from agent_framework.anthropic import AnthropicClient + +logger = logging.getLogger(__name__) +""" +Anthropic Skills Agent Example + +This sample demonstrates using Anthropic with: +- Listing and using Anthropic-managed Skills. +- One approach to add additional beta flags. + You can also set additonal_chat_options with "additional_beta_flags" per request. +- Creating an agent with the Code Interpreter tool and a Skill. +- Catching and downloading generated files from the agent. +""" + + +async def main() -> None: + """Example of streaming response (get results as they are generated).""" + client = AnthropicClient(additional_beta_flags=["skills-2025-10-02"]) + + # List Anthropic-managed Skills + skills = await client.anthropic_client.beta.skills.list(source="anthropic", betas=["skills-2025-10-02"]) + for skill in skills.data: + print(f"{skill.source}: {skill.id} (version: {skill.latest_version})") + + # Create a agent with the pptx skill enabled + # Skills also need the code interpreter tool to function + agent = client.create_agent( + name="DocsAgent", + instructions="You are a helpful agent for creating powerpoint presentations.", + tools=HostedCodeInterpreterTool(), + max_tokens=20000, + additional_chat_options={ + "thinking": {"type": "enabled", "budget_tokens": 10000}, + "container": {"skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}]}, + }, + ) + + print( + "The agent output will use the following colors:\n" + "\033[0mUser: (default)\033[0m\n" + "\033[0mAgent: (default)\033[0m\n" + "\033[32mAgent Reasoning: (green)\033[0m\n" + "\033[34mUsage: (blue)\033[0m\n" + ) + query = "Create a presentation about renewable energy with 5 slides" + print(f"User: {query}") + print("Agent: ", end="", flush=True) + files: list[HostedFileContent] = [] + async for chunk in agent.run_stream(query): + for content in chunk.contents: + match content.type: + case "text": + print(content.text, end="", flush=True) + case "text_reasoning": + print(f"\033[32m{content.text}\033[0m", end="", flush=True) + case "usage": + print(f"\n\033[34m[Usage so far: {content.details}]\033[0m\n", end="", flush=True) + case "hosted_file": + # Catch generated files + files.append(content) + case _: + logger.debug("Unhandled content type: %s", content.type) + pass + + print("\n") + if files: + # Save to a new file (will be in the folder where you are running this script) + # When running this sample multiple times, the files will be overritten + # Since I'm using the pptx skill, the files will be PowerPoint presentations + print("Generated files:") + for idx, file in enumerate(files): + 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: + await file_content.write_to_file(f.name) + print(f"File {idx}: renewable_energy-{idx}.pptx saved to disk.") + + +if __name__ == "__main__": + asyncio.run(main())