From 35a9726a94d067dab842f05e55b6c28faaa6814b Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Mon, 23 Feb 2026 15:33:01 -0800 Subject: [PATCH 1/4] Python: Add file_ids and data_sources support to AzureAIAgentClient.get_code_interpreter_tool() Update the factory method to accept file_ids and data_sources keyword arguments, matching the underlying azure.ai.agents SDK CodeInterpreterTool constructor. This enables users to attach uploaded files for code interpreter analysis. Fixes #4050 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework_azure_ai/_chat_client.py | 19 +++++++++- .../tests/test_azure_ai_agent_client.py | 38 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py index 7590111bac..81c1e74610 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py @@ -87,6 +87,7 @@ ToolApproval, ToolDefinition, ToolOutput, + VectorStoreDataSource, ) from pydantic import BaseModel @@ -219,9 +220,18 @@ class AzureAIAgentClient( # region Hosted Tool Factory Methods @staticmethod - def get_code_interpreter_tool() -> CodeInterpreterTool: + def get_code_interpreter_tool( + *, + file_ids: list[str] | None = None, + data_sources: list[VectorStoreDataSource] | None = None, + ) -> CodeInterpreterTool: """Create a code interpreter tool configuration for Azure AI Agents. + Keyword Args: + file_ids: List of uploaded file IDs to make available to the code interpreter. + data_sources: List of vector store data sources for enterprise file search. + Mutually exclusive with file_ids. + Returns: A CodeInterpreterTool instance ready to pass to ChatAgent. @@ -230,10 +240,15 @@ def get_code_interpreter_tool() -> CodeInterpreterTool: from agent_framework.azure import AzureAIAgentClient + # Basic code interpreter tool = AzureAIAgentClient.get_code_interpreter_tool() + + # With uploaded files + tool = AzureAIAgentClient.get_code_interpreter_tool(file_ids=["file-abc123"]) + agent = ChatAgent(client, tools=[tool]) """ - return CodeInterpreterTool() + return CodeInterpreterTool(file_ids=file_ids, data_sources=data_sources) @staticmethod def get_file_search_tool( diff --git a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py index 1daf611bef..0cd6403d9e 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py @@ -858,6 +858,44 @@ async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_file_search_with_ assert run_options["tool_resources"] == {"file_search": {"vector_store_ids": ["vs-123"]}} +async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_code_interpreter_with_file_ids( + mock_agents_client: MagicMock, +) -> None: + """Test _prepare_tools_for_azure_ai with CodeInterpreterTool with file_ids from get_code_interpreter_tool().""" + + client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent") + + code_interpreter_tool = client.get_code_interpreter_tool(file_ids=["file-123", "file-456"]) + + run_options: dict[str, Any] = {} + result = await client._prepare_tools_for_azure_ai([code_interpreter_tool], run_options) # type: ignore + + assert len(result) == 1 + assert result[0] == {"type": "code_interpreter"} + assert "tool_resources" in run_options + assert "code_interpreter" in run_options["tool_resources"] + assert sorted(run_options["tool_resources"]["code_interpreter"]["file_ids"]) == ["file-123", "file-456"] + + +async def test_azure_ai_chat_client_get_code_interpreter_tool_basic() -> None: + """Test get_code_interpreter_tool returns CodeInterpreterTool without files.""" + from azure.ai.agents.models import CodeInterpreterTool + + tool = AzureAIAgentClient.get_code_interpreter_tool() + assert isinstance(tool, CodeInterpreterTool) + assert len(tool.file_ids) == 0 + + +async def test_azure_ai_chat_client_get_code_interpreter_tool_with_file_ids() -> None: + """Test get_code_interpreter_tool forwards file_ids to the SDK.""" + from azure.ai.agents.models import CodeInterpreterTool + + tool = AzureAIAgentClient.get_code_interpreter_tool(file_ids=["file-abc", "file-def"]) + assert isinstance(tool, CodeInterpreterTool) + assert "file-abc" in tool.file_ids + assert "file-def" in tool.file_ids + + async def test_azure_ai_chat_client_create_agent_stream_submit_tool_approvals( mock_agents_client: MagicMock, ) -> None: From 3d6c7e028f5b872143c8d8f21687a4dd75ea2eb9 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Mon, 23 Feb 2026 15:50:58 -0800 Subject: [PATCH 2/4] addressed comments --- .../tests/test_azure_ai_agent_client.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py index 0cd6403d9e..771dceae31 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py @@ -896,6 +896,25 @@ async def test_azure_ai_chat_client_get_code_interpreter_tool_with_file_ids() -> assert "file-def" in tool.file_ids +async def test_azure_ai_chat_client_get_code_interpreter_tool_with_data_sources() -> None: + """Test get_code_interpreter_tool forwards data_sources to the SDK.""" + from azure.ai.agents.models import CodeInterpreterTool, VectorStoreDataSource + + ds = VectorStoreDataSource(asset_identifier="test-asset-id", asset_type="id_asset") + tool = AzureAIAgentClient.get_code_interpreter_tool(data_sources=[ds]) + assert isinstance(tool, CodeInterpreterTool) + assert "test-asset-id" in tool.data_sources + + +async def test_azure_ai_chat_client_get_code_interpreter_tool_mutually_exclusive() -> None: + """Test get_code_interpreter_tool raises ValueError when both file_ids and data_sources are provided.""" + from azure.ai.agents.models import VectorStoreDataSource + + ds = VectorStoreDataSource(asset_identifier="test-asset-id", asset_type="id_asset") + with pytest.raises(ValueError, match="mutually exclusive"): + AzureAIAgentClient.get_code_interpreter_tool(file_ids=["file-abc"], data_sources=[ds]) + + async def test_azure_ai_chat_client_create_agent_stream_submit_tool_approvals( mock_agents_client: MagicMock, ) -> None: From bde61b563ea36f3e9c0c218bc2ac6ca13938a3ca Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Tue, 24 Feb 2026 14:16:50 -0800 Subject: [PATCH 3/4] addressed comments --- .../agent_framework_azure_ai/_chat_client.py | 19 ++++++--- .../agent_framework_azure_ai/_client.py | 11 +++-- .../agent_framework_azure_ai/_shared.py | 40 +++++++++++++++++++ .../tests/test_azure_ai_agent_client.py | 32 +++++++++++++++ .../azure-ai/tests/test_azure_ai_client.py | 29 ++++++++++++++ 5 files changed, 122 insertions(+), 9 deletions(-) diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py index 81c1e74610..2c0498b1e4 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py @@ -91,7 +91,7 @@ ) from pydantic import BaseModel -from ._shared import AzureAISettings, to_azure_ai_agent_tools +from ._shared import AzureAISettings, resolve_file_ids, to_azure_ai_agent_tools if sys.version_info >= (3, 13): from typing import TypeVar # type: ignore # pragma: no cover @@ -222,13 +222,16 @@ class AzureAIAgentClient( @staticmethod def get_code_interpreter_tool( *, - file_ids: list[str] | None = None, + file_ids: list[str | Content] | None = None, data_sources: list[VectorStoreDataSource] | None = None, ) -> CodeInterpreterTool: """Create a code interpreter tool configuration for Azure AI Agents. Keyword Args: - file_ids: List of uploaded file IDs to make available to the code interpreter. + file_ids: List of uploaded file IDs or Content objects to make available to + the code interpreter. Accepts plain strings or Content.from_hosted_file() + instances. The underlying SDK raises ValueError if both file_ids and + data_sources are provided. data_sources: List of vector store data sources for enterprise file search. Mutually exclusive with file_ids. @@ -243,12 +246,18 @@ def get_code_interpreter_tool( # Basic code interpreter tool = AzureAIAgentClient.get_code_interpreter_tool() - # With uploaded files + # With uploaded file IDs tool = AzureAIAgentClient.get_code_interpreter_tool(file_ids=["file-abc123"]) + # With Content objects + from agent_framework import Content + + tool = AzureAIAgentClient.get_code_interpreter_tool(file_ids=[Content.from_hosted_file("file-abc123")]) + agent = ChatAgent(client, tools=[tool]) """ - return CodeInterpreterTool(file_ids=file_ids, data_sources=data_sources) + resolved = resolve_file_ids(file_ids) + return CodeInterpreterTool(file_ids=resolved, data_sources=data_sources) @staticmethod def get_file_search_tool( diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_client.py index 7c698847cc..37e5e1fbcc 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_client.py @@ -50,7 +50,7 @@ from azure.ai.projects.models import FileSearchTool as ProjectsFileSearchTool from azure.core.exceptions import ResourceNotFoundError -from ._shared import AzureAISettings, create_text_format_config +from ._shared import AzureAISettings, create_text_format_config, resolve_file_ids if sys.version_info >= (3, 13): from typing import TypeVar # type: ignore # pragma: no cover @@ -830,14 +830,16 @@ def _enrich_update(update: ChatResponseUpdate) -> ChatResponseUpdate: @staticmethod def get_code_interpreter_tool( # type: ignore[override] *, - file_ids: list[str] | None = None, + file_ids: list[str | Content] | None = None, container: Literal["auto"] | dict[str, Any] = "auto", **kwargs: Any, ) -> CodeInterpreterTool: """Create a code interpreter tool configuration for Azure AI Projects. Keyword Args: - file_ids: Optional list of file IDs to make available to the code interpreter. + file_ids: Optional list of file IDs or Content objects to make available to + the code interpreter. Accepts plain strings or Content.from_hosted_file() + instances. container: Container configuration. Use "auto" for automatic container management. Note: Custom container settings from this parameter are not used by Azure AI Projects; use file_ids instead. @@ -857,7 +859,8 @@ def get_code_interpreter_tool( # type: ignore[override] # Extract file_ids from container if provided as dict and file_ids not explicitly set if file_ids is None and isinstance(container, dict): file_ids = container.get("file_ids") - tool_container = CodeInterpreterToolAuto(file_ids=file_ids if file_ids else None) + resolved = resolve_file_ids(file_ids) + tool_container = CodeInterpreterToolAuto(file_ids=resolved) return CodeInterpreterTool(container=tool_container, **kwargs) @staticmethod diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_shared.py b/python/packages/azure-ai/agent_framework_azure_ai/_shared.py index 7dd1064bda..996c68edbd 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_shared.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_shared.py @@ -8,6 +8,7 @@ from typing import Any, cast from agent_framework import ( + Content, FunctionTool, ) from agent_framework.exceptions import IntegrationInvalidRequestException @@ -109,6 +110,45 @@ def _extract_project_connection_id(additional_properties: dict[str, Any] | None) return None +def resolve_file_ids(file_ids: Sequence[str | Content] | None) -> list[str] | None: + """Resolve a list of file ID values that may include Content objects. + + Accepts plain strings and Content objects with type "hosted_file", extracting + the file_id from each. This enables users to pass Content.from_hosted_file() + alongside plain file ID strings. + + Args: + file_ids: Sequence of file ID strings or Content objects, or None. + + Returns: + A list of resolved file ID strings, or None if input is None or empty. + + Raises: + ValueError: If a Content object has an unsupported type (not "hosted_file"). + """ + if not file_ids: + return None + + resolved: list[str] = [] + for item in file_ids: + if isinstance(item, str): + resolved.append(item) + elif isinstance(item, Content): + if item.type != "hosted_file": + raise ValueError( + f"Unsupported Content type '{item.type}' for code interpreter file_ids. " + "Only Content.from_hosted_file() is supported." + ) + if item.file_id is None: + raise ValueError( + "Content.from_hosted_file() item is missing a file_id. " + "Ensure the Content object has a valid file_id before using it in file_ids." + ) + resolved.append(item.file_id) + + return resolved if resolved else None + + def to_azure_ai_agent_tools( tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None, run_options: dict[str, Any] | None = None, diff --git a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py index e54be382bb..8ae4d3f312 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py @@ -912,6 +912,38 @@ async def test_azure_ai_chat_client_get_code_interpreter_tool_mutually_exclusive AzureAIAgentClient.get_code_interpreter_tool(file_ids=["file-abc"], data_sources=[ds]) +async def test_azure_ai_chat_client_get_code_interpreter_tool_with_content() -> None: + """Test get_code_interpreter_tool accepts Content.from_hosted_file in file_ids.""" + from agent_framework import Content + from azure.ai.agents.models import CodeInterpreterTool + + content = Content.from_hosted_file("file-content-123") + tool = AzureAIAgentClient.get_code_interpreter_tool(file_ids=[content]) + assert isinstance(tool, CodeInterpreterTool) + assert "file-content-123" in tool.file_ids + + +async def test_azure_ai_chat_client_get_code_interpreter_tool_with_mixed_file_ids() -> None: + """Test get_code_interpreter_tool accepts a mix of strings and Content objects.""" + from agent_framework import Content + from azure.ai.agents.models import CodeInterpreterTool + + content = Content.from_hosted_file("file-from-content") + tool = AzureAIAgentClient.get_code_interpreter_tool(file_ids=["file-plain", content]) + assert isinstance(tool, CodeInterpreterTool) + assert "file-plain" in tool.file_ids + assert "file-from-content" in tool.file_ids + + +async def test_azure_ai_chat_client_get_code_interpreter_tool_content_unsupported_type() -> None: + """Test get_code_interpreter_tool raises ValueError for unsupported Content types.""" + from agent_framework import Content + + content = Content.from_hosted_vector_store("vs-123") + with pytest.raises(ValueError, match="Unsupported Content type"): + AzureAIAgentClient.get_code_interpreter_tool(file_ids=[content]) + + async def test_azure_ai_chat_client_create_agent_stream_submit_tool_approvals( mock_agents_client: MagicMock, ) -> None: diff --git a/python/packages/azure-ai/tests/test_azure_ai_client.py b/python/packages/azure-ai/tests/test_azure_ai_client.py index 4ec1b90971..5ddeae6783 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_client.py @@ -1685,6 +1685,35 @@ def test_get_code_interpreter_tool_with_file_ids() -> None: assert tool["container"]["file_ids"] == ["file-123", "file-456"] +def test_get_code_interpreter_tool_with_content() -> None: + """Test get_code_interpreter_tool accepts Content.from_hosted_file in file_ids.""" + from agent_framework import Content + + content = Content.from_hosted_file("file-content-123") + tool = AzureAIClient.get_code_interpreter_tool(file_ids=[content]) + assert isinstance(tool, CodeInterpreterTool) + assert tool["container"]["file_ids"] == ["file-content-123"] + + +def test_get_code_interpreter_tool_with_mixed_file_ids() -> None: + """Test get_code_interpreter_tool accepts a mix of strings and Content objects.""" + from agent_framework import Content + + content = Content.from_hosted_file("file-from-content") + tool = AzureAIClient.get_code_interpreter_tool(file_ids=["file-plain", content]) + assert isinstance(tool, CodeInterpreterTool) + assert sorted(tool["container"]["file_ids"]) == ["file-from-content", "file-plain"] + + +def test_get_code_interpreter_tool_content_unsupported_type() -> None: + """Test get_code_interpreter_tool raises ValueError for unsupported Content types.""" + from agent_framework import Content + + content = Content.from_hosted_vector_store("vs-123") + with pytest.raises(ValueError, match="Unsupported Content type"): + AzureAIClient.get_code_interpreter_tool(file_ids=[content]) + + def test_get_file_search_tool_basic() -> None: """Test get_file_search_tool returns FileSearchTool.""" tool = AzureAIClient.get_file_search_tool(vector_store_ids=["vs-123"]) From fa1a91172ec349fdeac0f1c621d323daf8e5801b Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Wed, 25 Feb 2026 11:20:10 -0800 Subject: [PATCH 4/4] Add per-message file attachment support for AzureAIAgentClient Add hosted_file handling in _prepare_messages() to convert Content.from_hosted_file() into MessageAttachment on ThreadMessageOptions. This enables per-message file scoping for code interpreter, matching the underlying Azure AI Agents SDK MessageAttachment pattern. - Add hosted_file case in _prepare_messages() match statement - Import MessageAttachment from azure.ai.agents.models - Add sample for per-message CSV file attachment with code interpreter - Add employees.csv test data file - Add 3 unit tests for hosted_file attachment conversion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework_azure_ai/_chat_client.py | 12 ++- .../tests/test_azure_ai_agent_client.py | 64 +++++++++++++ ...re_ai_with_code_interpreter_file_upload.py | 95 +++++++++++++++++++ python/samples/shared/resources/employees.csv | 11 +++ 4 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_code_interpreter_file_upload.py create mode 100644 python/samples/shared/resources/employees.csv diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py index 2c0498b1e4..f43b5d31bc 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py @@ -60,6 +60,7 @@ FunctionToolDefinition, ListSortOrder, McpTool, + MessageAttachment, MessageDeltaChunk, MessageDeltaTextContent, MessageDeltaTextFileCitationAnnotation, @@ -1315,11 +1316,19 @@ def _prepare_messages( continue message_contents: list[MessageInputContentBlock] = [] + attachments: list[MessageAttachment] = [] for content in chat_message.contents: match content.type: case "text": message_contents.append(MessageInputTextBlock(text=content.text)) # type: ignore[arg-type] + case "hosted_file": + attachments.append( + MessageAttachment( + file_id=content.file_id, + tools=CodeInterpreterTool().definitions, + ) + ) case "data" | "uri": if content.has_top_level_media_type("image"): message_contents.append( @@ -1334,13 +1343,14 @@ def _prepare_messages( if isinstance(content.raw_representation, MessageInputContentBlock): message_contents.append(content.raw_representation) - if message_contents: + if message_contents or attachments: if additional_messages is None: additional_messages = [] additional_messages.append( ThreadMessageOptions( role=MessageRole.AGENT if chat_message.role == "assistant" else MessageRole.USER, content=message_contents, + attachments=attachments if attachments else None, ) ) diff --git a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py index 8ae4d3f312..4b47a676a6 100644 --- a/python/packages/azure-ai/tests/test_azure_ai_agent_client.py +++ b/python/packages/azure-ai/tests/test_azure_ai_agent_client.py @@ -2210,6 +2210,70 @@ def test_azure_ai_chat_client_prepare_messages_with_raw_content_block( assert additional_messages[0].content[0] == raw_block +def test_azure_ai_chat_client_prepare_messages_with_hosted_file_attachment( + mock_agents_client: MagicMock, +) -> None: + """Test _prepare_messages converts hosted_file content to MessageAttachment.""" + client = create_test_azure_ai_chat_client(mock_agents_client) + + file_content = Content.from_hosted_file(file_id="file-abc123") + messages = [Message(role="user", contents=["Analyze this CSV.", file_content])] + + additional_messages, instructions, required_action_results = client._prepare_messages(messages) # type: ignore + + assert additional_messages is not None + assert len(additional_messages) == 1 + msg = additional_messages[0] + # Text content should be present + assert len(msg.content) == 1 + assert msg.content[0].text == "Analyze this CSV." # type: ignore[union-attr] + # Attachment should be created from hosted_file + assert msg.attachments is not None + assert len(msg.attachments) == 1 + assert msg.attachments[0]["file_id"] == "file-abc123" + assert msg.attachments[0]["tools"] == [{"type": "code_interpreter"}] + + +def test_azure_ai_chat_client_prepare_messages_with_multiple_hosted_files( + mock_agents_client: MagicMock, +) -> None: + """Test _prepare_messages handles multiple hosted_file contents as separate attachments.""" + client = create_test_azure_ai_chat_client(mock_agents_client) + + file1 = Content.from_hosted_file(file_id="file-001") + file2 = Content.from_hosted_file(file_id="file-002") + messages = [Message(role="user", contents=["Analyze both files.", file1, file2])] + + additional_messages, _, _ = client._prepare_messages(messages) # type: ignore + + assert additional_messages is not None + msg = additional_messages[0] + assert msg.attachments is not None + assert len(msg.attachments) == 2 + assert msg.attachments[0]["file_id"] == "file-001" + assert msg.attachments[1]["file_id"] == "file-002" + + +def test_azure_ai_chat_client_prepare_messages_hosted_file_only( + mock_agents_client: MagicMock, +) -> None: + """Test _prepare_messages creates a message when only hosted_file content is present (no text).""" + client = create_test_azure_ai_chat_client(mock_agents_client) + + file_content = Content.from_hosted_file(file_id="file-only") + messages = [Message(role="user", contents=[file_content])] + + additional_messages, _, _ = client._prepare_messages(messages) # type: ignore + + assert additional_messages is not None + assert len(additional_messages) == 1 + msg = additional_messages[0] + assert msg.content == [] + assert msg.attachments is not None + assert len(msg.attachments) == 1 + assert msg.attachments[0]["file_id"] == "file-only" + + async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_mcp_tool( mock_agents_client: MagicMock, ) -> None: diff --git a/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_code_interpreter_file_upload.py b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_code_interpreter_file_upload.py new file mode 100644 index 0000000000..7fbe1673f1 --- /dev/null +++ b/python/samples/02-agents/providers/azure_ai_agent/azure_ai_with_code_interpreter_file_upload.py @@ -0,0 +1,95 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +import contextlib +import os +from pathlib import Path + +from agent_framework import Content, Message +from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider +from azure.ai.agents.aio import AgentsClient +from azure.ai.agents.models import FilePurpose +from azure.identity.aio import AzureCliCredential +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +""" +Azure AI Agent Code Interpreter with Per-Message File Attachment + +This sample demonstrates how to upload a CSV file and attach it to a specific +message using Content.from_hosted_file(), rather than to the agent/thread. + +This uses the MessageAttachment pattern from the Azure AI Agents SDK, which +scopes the file to a single message — making it safe for multi-user scenarios +where the same agent definition is shared across users. + +The flow: +1. Create an agent with code interpreter tool (no file_ids) +2. Upload a CSV file via the agents client +3. Attach the file per-message using Content.from_hosted_file() +4. The framework converts this to a MessageAttachment automatically +5. Clean up the uploaded file +""" + +# Questions to ask the agent about the CSV data +USER_INPUTS = [ + "How many employees are in the dataset?", + "What is the average salary by department?", + "Who has the most years of experience?", +] + + +async def main() -> None: + """Example showing per-message file attachment with code interpreter.""" + print("=== Azure AI Code Interpreter with Per-Message File Attachment ===\n") + + async with ( + AzureCliCredential() as credential, + AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client, + AzureAIAgentsProvider(agents_client=agents_client) as provider, + ): + # 1. Upload the CSV file + csv_file_path = Path(__file__).parents[3] / "shared" / "resources" / "employees.csv" + print(f"Uploading file from: {csv_file_path}") + + uploaded = await agents_client.files.upload_and_poll( + file_path=str(csv_file_path), + purpose=FilePurpose.AGENTS, + ) + print(f"Uploaded file, file ID: {uploaded.id}\n") + + try: + # 2. Create a code interpreter tool (no file_ids — files attached per-message) + client = AzureAIAgentClient(credential=credential) + code_interpreter_tool = client.get_code_interpreter_tool() + + # 3. Create an agent with code interpreter + agent = await provider.create_agent( + name="DataAnalysisAgent", + instructions=( + "You are a data analyst assistant. Use the code interpreter to read " + "and analyze the attached CSV file. Always show your work by writing " + "Python code to answer questions about the data." + ), + tools=[code_interpreter_tool], + ) + + # 4. Ask questions — file is attached per-message via Content.from_hosted_file() + file_content = Content.from_hosted_file(file_id=uploaded.id) + for user_input in USER_INPUTS: + print(f"# User: '{user_input}'") + message = Message(role="user", contents=[user_input, file_content]) + response = await agent.run(message) + print(f"# Agent: {response.text}\n") + + finally: + # 5. Cleanup: Delete the uploaded file + with contextlib.suppress(Exception): + await agents_client.files.delete(uploaded.id) + print("Cleaned up uploaded file.") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/python/samples/shared/resources/employees.csv b/python/samples/shared/resources/employees.csv new file mode 100644 index 0000000000..373de4a3a3 --- /dev/null +++ b/python/samples/shared/resources/employees.csv @@ -0,0 +1,11 @@ +Name,Department,Salary,Years_Experience +Alice Johnson,Engineering,120000,8 +Bob Smith,Marketing,85000,5 +Carol Williams,Engineering,115000,7 +David Brown,Sales,90000,6 +Eva Martinez,Marketing,82000,4 +Frank Lee,Engineering,130000,10 +Grace Kim,Sales,95000,7 +Henry Chen,HR,78000,3 +Iris Patel,Engineering,125000,9 +Jack Wilson,Marketing,88000,5