Skip to content

Commit fcc3f1b

Browse files
Python: fix anthropic code interpreter tool repr (microsoft#2244)
* fix anthropic code interpreter tool repr * fixes * added skills and sample * test fix * add new sample to readme * fixes tests
1 parent 45dba6b commit fcc3f1b

4 files changed

Lines changed: 148 additions & 18 deletions

File tree

python/packages/anthropic/agent_framework_anthropic/_chat_client.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
FunctionCallContent,
1919
FunctionResultContent,
2020
HostedCodeInterpreterTool,
21+
HostedFileContent,
2122
HostedMCPTool,
2223
HostedWebSearchTool,
2324
Role,
@@ -122,6 +123,7 @@ def __init__(
122123
api_key: str | None = None,
123124
model_id: str | None = None,
124125
anthropic_client: AsyncAnthropic | None = None,
126+
additional_beta_flags: list[str] | None = None,
125127
env_file_path: str | None = None,
126128
env_file_encoding: str | None = None,
127129
**kwargs: Any,
@@ -134,6 +136,8 @@ def __init__(
134136
anthropic_client: An existing Anthropic client to use. If not provided, one will be created.
135137
This can be used to further configure the client before passing it in.
136138
For instance if you need to set a different base_url for testing or private deployments.
139+
additional_beta_flags: Additional beta flags to enable on the client.
140+
Default flags are: "mcp-client-2025-04-04", "code-execution-2025-08-25".
137141
env_file_path: Path to environment file for loading settings.
138142
env_file_encoding: Encoding of the environment file.
139143
kwargs: Additional keyword arguments passed to the parent class.
@@ -196,6 +200,7 @@ def __init__(
196200

197201
# Initialize instance variables
198202
self.anthropic_client = anthropic_client
203+
self.additional_beta_flags = additional_beta_flags or []
199204
self.model_id = anthropic_settings.chat_model_id
200205
# streaming requires tracking the last function call ID and name
201206
self._last_call_id_name: tuple[str, str] | None = None
@@ -246,12 +251,16 @@ def _create_run_options(
246251
Returns:
247252
A dictionary of run options for the Anthropic client.
248253
"""
254+
if chat_options.additional_properties and "additional_beta_flags" in chat_options.additional_properties:
255+
betas = chat_options.additional_properties.pop("additional_beta_flags")
256+
else:
257+
betas = []
249258
run_options: dict[str, Any] = {
250259
"model": chat_options.model_id or self.model_id,
251260
"messages": self._convert_messages_to_anthropic_format(messages),
252261
"max_tokens": chat_options.max_tokens or ANTHROPIC_DEFAULT_MAX_TOKENS,
253262
"extra_headers": {"User-Agent": AGENT_FRAMEWORK_USER_AGENT},
254-
"betas": BETA_FLAGS,
263+
"betas": {*BETA_FLAGS, *self.additional_beta_flags, *betas},
255264
}
256265

257266
# Add any additional options from chat_options or kwargs
@@ -396,7 +405,7 @@ def _convert_tools_to_anthropic_format(
396405
case HostedCodeInterpreterTool():
397406
code_tool: dict[str, Any] = {
398407
"type": "code_execution_20250825",
399-
"name": "code_interpreter",
408+
"name": "code_execution",
400409
}
401410
tool_list.append(code_tool)
402411
case HostedMCPTool():
@@ -524,17 +533,7 @@ def _parse_message_contents(
524533
annotations=self._parse_citations(content_block),
525534
)
526535
)
527-
case "tool_use":
528-
self._last_call_id_name = (content_block.id, content_block.name)
529-
contents.append(
530-
FunctionCallContent(
531-
call_id=content_block.id,
532-
name=content_block.name,
533-
arguments=content_block.input,
534-
raw_representation=content_block,
535-
)
536-
)
537-
case "mcp_tool_use" | "server_tool_use":
536+
case "tool_use" | "mcp_tool_use" | "server_tool_use":
538537
self._last_call_id_name = (content_block.id, content_block.name)
539538
contents.append(
540539
FunctionCallContent(
@@ -572,6 +571,19 @@ def _parse_message_contents(
572571
| "text_editor_code_execution_tool_result"
573572
):
574573
call_id, name = self._last_call_id_name or (None, None)
574+
if (
575+
content_block.content
576+
and (
577+
content_block.content.type == "bash_code_execution_result"
578+
or content_block.content.type == "code_execution_result"
579+
)
580+
and content_block.content.content
581+
):
582+
for result_content in content_block.content.content:
583+
if hasattr(result_content, "file_id"):
584+
contents.append(
585+
HostedFileContent(file_id=result_content.file_id, raw_representation=result_content)
586+
)
575587
contents.append(
576588
FunctionResultContent(
577589
call_id=content_block.tool_use_id,

python/packages/anthropic/tests/test_anthropic_client.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ def create_test_anthropic_client(
5050
) -> AnthropicClient:
5151
"""Helper function to create AnthropicClient instances for testing, bypassing normal validation."""
5252
if anthropic_settings is None:
53-
anthropic_settings = AnthropicSettings(api_key="test-api-key-12345", chat_model_id="claude-3-5-sonnet-20241022")
53+
anthropic_settings = AnthropicSettings(
54+
api_key="test-api-key-12345", chat_model_id="claude-3-5-sonnet-20241022", env_file_path="test.env"
55+
)
5456

5557
# Create client instance directly
5658
client = object.__new__(AnthropicClient)
@@ -61,6 +63,7 @@ def create_test_anthropic_client(
6163
client._last_call_id_name = None
6264
client.additional_properties = {}
6365
client.middleware = None
66+
client.additional_beta_flags = []
6467

6568
return client
6669

@@ -70,7 +73,7 @@ def create_test_anthropic_client(
7073

7174
def test_anthropic_settings_init(anthropic_unit_test_env: dict[str, str]) -> None:
7275
"""Test AnthropicSettings initialization."""
73-
settings = AnthropicSettings()
76+
settings = AnthropicSettings(env_file_path="test.env")
7477

7578
assert settings.api_key is not None
7679
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
8083
def test_anthropic_settings_init_with_explicit_values() -> None:
8184
"""Test AnthropicSettings initialization with explicit values."""
8285
settings = AnthropicSettings(
83-
api_key="custom-api-key",
84-
chat_model_id="claude-3-opus-20240229",
86+
api_key="custom-api-key", chat_model_id="claude-3-opus-20240229", env_file_path="test.env"
8587
)
8688

8789
assert settings.api_key is not None
@@ -114,6 +116,7 @@ def test_anthropic_client_init_auto_create_client(anthropic_unit_test_env: dict[
114116
client = AnthropicClient(
115117
api_key=anthropic_unit_test_env["ANTHROPIC_API_KEY"],
116118
model_id=anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"],
119+
env_file_path="test.env",
117120
)
118121

119122
assert client.anthropic_client is not None
@@ -307,7 +310,7 @@ def test_convert_tools_to_anthropic_format_code_interpreter(mock_anthropic_clien
307310
assert "tools" in result
308311
assert len(result["tools"]) == 1
309312
assert result["tools"][0]["type"] == "code_execution_20250825"
310-
assert result["tools"][0]["name"] == "code_interpreter"
313+
assert result["tools"][0]["name"] == "code_execution"
311314

312315

313316
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:
725728
assert has_function_call
726729

727730

731+
@pytest.mark.flaky
732+
@skip_if_anthropic_integration_tests_disabled
733+
async def test_anthropic_client_integration_hosted_tools() -> None:
734+
"""Integration test for hosted tools."""
735+
client = AnthropicClient()
736+
737+
messages = [ChatMessage(role=Role.USER, text="What tools do you have available?")]
738+
tools = [
739+
HostedWebSearchTool(),
740+
HostedCodeInterpreterTool(),
741+
HostedMCPTool(
742+
name="example-mcp",
743+
url="https://learn.microsoft.com/api/mcp",
744+
approval_mode="never_require",
745+
),
746+
]
747+
748+
response = await client.get_response(
749+
messages=messages,
750+
chat_options=ChatOptions(tools=tools, max_tokens=100),
751+
)
752+
753+
assert response is not None
754+
assert response.text is not None
755+
756+
728757
@pytest.mark.flaky
729758
@skip_if_anthropic_integration_tests_disabled
730759
async def test_anthropic_client_integration_with_system_message() -> None:

python/samples/getting_started/agents/anthropic/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This folder contains examples demonstrating how to use Anthropic's Claude models
88
|------|-------------|
99
| [`anthropic_basic.py`](anthropic_basic.py) | Demonstrates how to setup a simple agent using the AnthropicClient, with both streaming and non-streaming responses. |
1010
| [`anthropic_advanced.py`](anthropic_advanced.py) | Shows advanced usage of the AnthropicClient, including hosted tools and `thinking`. |
11+
| [`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. |
1112

1213
## Environment Variables
1314

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Copyright (c) Microsoft. All rights reserved.
2+
3+
import asyncio
4+
import logging
5+
from pathlib import Path
6+
7+
from agent_framework import HostedCodeInterpreterTool, HostedFileContent
8+
from agent_framework.anthropic import AnthropicClient
9+
10+
logger = logging.getLogger(__name__)
11+
"""
12+
Anthropic Skills Agent Example
13+
14+
This sample demonstrates using Anthropic with:
15+
- Listing and using Anthropic-managed Skills.
16+
- One approach to add additional beta flags.
17+
You can also set additonal_chat_options with "additional_beta_flags" per request.
18+
- Creating an agent with the Code Interpreter tool and a Skill.
19+
- Catching and downloading generated files from the agent.
20+
"""
21+
22+
23+
async def main() -> None:
24+
"""Example of streaming response (get results as they are generated)."""
25+
client = AnthropicClient(additional_beta_flags=["skills-2025-10-02"])
26+
27+
# List Anthropic-managed Skills
28+
skills = await client.anthropic_client.beta.skills.list(source="anthropic", betas=["skills-2025-10-02"])
29+
for skill in skills.data:
30+
print(f"{skill.source}: {skill.id} (version: {skill.latest_version})")
31+
32+
# Create a agent with the pptx skill enabled
33+
# Skills also need the code interpreter tool to function
34+
agent = client.create_agent(
35+
name="DocsAgent",
36+
instructions="You are a helpful agent for creating powerpoint presentations.",
37+
tools=HostedCodeInterpreterTool(),
38+
max_tokens=20000,
39+
additional_chat_options={
40+
"thinking": {"type": "enabled", "budget_tokens": 10000},
41+
"container": {"skills": [{"type": "anthropic", "skill_id": "pptx", "version": "latest"}]},
42+
},
43+
)
44+
45+
print(
46+
"The agent output will use the following colors:\n"
47+
"\033[0mUser: (default)\033[0m\n"
48+
"\033[0mAgent: (default)\033[0m\n"
49+
"\033[32mAgent Reasoning: (green)\033[0m\n"
50+
"\033[34mUsage: (blue)\033[0m\n"
51+
)
52+
query = "Create a presentation about renewable energy with 5 slides"
53+
print(f"User: {query}")
54+
print("Agent: ", end="", flush=True)
55+
files: list[HostedFileContent] = []
56+
async for chunk in agent.run_stream(query):
57+
for content in chunk.contents:
58+
match content.type:
59+
case "text":
60+
print(content.text, end="", flush=True)
61+
case "text_reasoning":
62+
print(f"\033[32m{content.text}\033[0m", end="", flush=True)
63+
case "usage":
64+
print(f"\n\033[34m[Usage so far: {content.details}]\033[0m\n", end="", flush=True)
65+
case "hosted_file":
66+
# Catch generated files
67+
files.append(content)
68+
case _:
69+
logger.debug("Unhandled content type: %s", content.type)
70+
pass
71+
72+
print("\n")
73+
if files:
74+
# Save to a new file (will be in the folder where you are running this script)
75+
# When running this sample multiple times, the files will be overritten
76+
# Since I'm using the pptx skill, the files will be PowerPoint presentations
77+
print("Generated files:")
78+
for idx, file in enumerate(files):
79+
file_content = await client.anthropic_client.beta.files.download(
80+
file_id=file.file_id, betas=["files-api-2025-04-14"]
81+
)
82+
with open(Path(__file__).parent / f"renewable_energy-{idx}.pptx", "wb") as f:
83+
await file_content.write_to_file(f.name)
84+
print(f"File {idx}: renewable_energy-{idx}.pptx saved to disk.")
85+
86+
87+
if __name__ == "__main__":
88+
asyncio.run(main())

0 commit comments

Comments
 (0)