mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Add file_ids and data_sources support to get_code_interpreter_tool() (#4201)
* 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> * addressed comments * addressed comments * 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> * Address PR review: validation, fix assertions, remove MessageAttachment - Add empty string validation in resolve_file_ids() - Add test for Content with file_id=None - Add test for empty string file_ids - Revert MessageAttachment/hosted_file handling from _prepare_messages() (moved to separate issue #4352 for proper design) - Remove per-message file upload sample and employees.csv - Keep data_sources assertion as-is (dict keyed by asset_identifier) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
1a8729d5a7
commit
7135ed13eb
@@ -87,10 +87,11 @@ from azure.ai.agents.models import (
|
||||
ToolApproval,
|
||||
ToolDefinition,
|
||||
ToolOutput,
|
||||
VectorStoreDataSource,
|
||||
)
|
||||
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
|
||||
@@ -219,9 +220,21 @@ class AzureAIAgentClient(
|
||||
# region Hosted Tool Factory Methods
|
||||
|
||||
@staticmethod
|
||||
def get_code_interpreter_tool() -> CodeInterpreterTool:
|
||||
def get_code_interpreter_tool(
|
||||
*,
|
||||
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 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.
|
||||
|
||||
Returns:
|
||||
A CodeInterpreterTool instance ready to pass to ChatAgent.
|
||||
|
||||
@@ -230,10 +243,21 @@ class AzureAIAgentClient(
|
||||
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
|
||||
# Basic code interpreter
|
||||
tool = AzureAIAgentClient.get_code_interpreter_tool()
|
||||
|
||||
# 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()
|
||||
resolved = resolve_file_ids(file_ids)
|
||||
return CodeInterpreterTool(file_ids=resolved, data_sources=data_sources)
|
||||
|
||||
@staticmethod
|
||||
def get_file_search_tool(
|
||||
|
||||
@@ -50,7 +50,7 @@ from azure.ai.projects.models import (
|
||||
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 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
|
||||
@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 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
|
||||
# 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
|
||||
|
||||
@@ -8,6 +8,7 @@ from collections.abc import Mapping, MutableMapping, Sequence
|
||||
from typing import Any, cast
|
||||
|
||||
from agent_framework import (
|
||||
Content,
|
||||
FunctionTool,
|
||||
)
|
||||
from agent_framework.exceptions import IntegrationInvalidRequestException
|
||||
@@ -109,6 +110,47 @@ 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):
|
||||
if not item:
|
||||
raise ValueError("file_ids must not contain empty strings.")
|
||||
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,
|
||||
|
||||
@@ -855,6 +855,110 @@ 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_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_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_get_code_interpreter_tool_content_missing_file_id() -> None:
|
||||
"""Test get_code_interpreter_tool raises ValueError when Content.file_id is None."""
|
||||
from agent_framework import Content
|
||||
|
||||
content = Content(type="hosted_file")
|
||||
with pytest.raises(ValueError, match="missing a file_id"):
|
||||
AzureAIAgentClient.get_code_interpreter_tool(file_ids=[content])
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_get_code_interpreter_tool_empty_string_file_id() -> None:
|
||||
"""Test get_code_interpreter_tool raises ValueError for empty string file_ids."""
|
||||
with pytest.raises(ValueError, match="must not contain empty strings"):
|
||||
AzureAIAgentClient.get_code_interpreter_tool(file_ids=[""])
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_create_agent_stream_submit_tool_approvals(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
|
||||
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user