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
38 changes: 25 additions & 13 deletions python/packages/anthropic/agent_framework_anthropic/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
FunctionCallContent,
FunctionResultContent,
HostedCodeInterpreterTool,
HostedFileContent,
HostedMCPTool,
HostedWebSearchTool,
Role,
Expand Down Expand Up @@ -122,6 +123,7 @@ def __init__(
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,
Expand All @@ -134,6 +136,8 @@ def __init__(
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.
Expand Down Expand Up @@ -196,6 +200,7 @@ def __init__(

# 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
Expand Down Expand Up @@ -246,12 +251,16 @@ def _create_run_options(
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
Expand Down Expand Up @@ -396,7 +405,7 @@ def _convert_tools_to_anthropic_format(
case HostedCodeInterpreterTool():
code_tool: dict[str, Any] = {
"type": "code_execution_20250825",
"name": "code_interpreter",
"name": "code_execution",
Comment thread
eavanvalkenburg marked this conversation as resolved.
}
tool_list.append(code_tool)
case HostedMCPTool():
Expand Down Expand Up @@ -524,17 +533,7 @@ def _parse_message_contents(
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(
Expand Down Expand Up @@ -572,6 +571,19 @@ def _parse_message_contents(
| "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,
Expand Down
39 changes: 34 additions & 5 deletions python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand All @@ -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"]
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions python/samples/getting_started/agents/anthropic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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())