mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [BREAKING] Replace Hosted*Tool classes with tool methods (#3634)
* Replace Hosted*Tool classes with client static factory methods * fixed failing test * mypy fix * mypy fix 2 * declarative mypy fix * addressed comments * ToolProtocol removal * fixed test * agents mypy fix * fix failing tests * mypy fix * addressed comments * fixed tests * addressed comments + added factory method overrides for azureai v2 client * mypy fix * added kwargs to azureai tool methods * fixed in test * _sessions fix * test fix
This commit is contained in:
@@ -12,7 +12,6 @@ from agent_framework import (
|
||||
ContextProvider,
|
||||
FunctionTool,
|
||||
MiddlewareTypes,
|
||||
ToolProtocol,
|
||||
normalize_tools,
|
||||
)
|
||||
from agent_framework._mcp import MCPTool
|
||||
@@ -169,10 +168,10 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
|
||||
model: str | None = None,
|
||||
instructions: str | None = None,
|
||||
description: str | None = None,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
default_options: OptionsCoT | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
@@ -266,10 +265,10 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
|
||||
self,
|
||||
id: str,
|
||||
*,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
default_options: OptionsCoT | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
@@ -322,10 +321,10 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
|
||||
def as_agent(
|
||||
self,
|
||||
agent: AzureAgent,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
default_options: OptionsCoT | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
@@ -379,7 +378,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
|
||||
def _to_chat_agent_from_agent(
|
||||
self,
|
||||
agent: AzureAgent,
|
||||
provided_tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None = None,
|
||||
provided_tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None = None,
|
||||
default_options: OptionsCoT | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
context_provider: ContextProvider | None = None,
|
||||
@@ -422,8 +421,8 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
|
||||
def _merge_tools(
|
||||
self,
|
||||
agent_tools: Sequence[Any] | None,
|
||||
provided_tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None,
|
||||
) -> list[ToolProtocol | dict[str, Any]]:
|
||||
provided_tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None,
|
||||
) -> list[FunctionTool | dict[str, Any]]:
|
||||
"""Merge hosted tools from agent with user-provided function tools.
|
||||
|
||||
Args:
|
||||
@@ -433,7 +432,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
|
||||
Returns:
|
||||
Combined list of tools for the Agent.
|
||||
"""
|
||||
merged: list[ToolProtocol | dict[str, Any]] = []
|
||||
merged: list[FunctionTool | dict[str, Any]] = []
|
||||
|
||||
# Convert hosted tools from agent definition
|
||||
hosted_tools = from_azure_ai_agent_tools(agent_tools)
|
||||
@@ -459,7 +458,7 @@ class AzureAIAgentsProvider(Generic[OptionsCoT]):
|
||||
def _validate_function_tools(
|
||||
self,
|
||||
agent_tools: Sequence[Any] | None,
|
||||
provided_tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None,
|
||||
provided_tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None,
|
||||
) -> None:
|
||||
"""Validate that required function tools are provided.
|
||||
|
||||
|
||||
@@ -26,16 +26,11 @@ from agent_framework import (
|
||||
FunctionInvocationConfiguration,
|
||||
FunctionInvocationLayer,
|
||||
FunctionTool,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
Message,
|
||||
MiddlewareTypes,
|
||||
ResponseStream,
|
||||
Role,
|
||||
TextSpanRegion,
|
||||
ToolProtocol,
|
||||
UsageDetails,
|
||||
get_logger,
|
||||
prepare_function_call_results,
|
||||
@@ -55,7 +50,7 @@ from azure.ai.agents.models import (
|
||||
AsyncAgentRunStream,
|
||||
BingCustomSearchTool,
|
||||
BingGroundingTool,
|
||||
CodeInterpreterToolDefinition,
|
||||
CodeInterpreterTool,
|
||||
FileSearchTool,
|
||||
FunctionName,
|
||||
FunctionToolDefinition,
|
||||
@@ -217,6 +212,198 @@ class AzureAIAgentClient(
|
||||
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai" # type: ignore[reportIncompatibleVariableOverride, misc]
|
||||
|
||||
# region Hosted Tool Factory Methods
|
||||
|
||||
@staticmethod
|
||||
def get_code_interpreter_tool() -> CodeInterpreterTool:
|
||||
"""Create a code interpreter tool configuration for Azure AI Agents.
|
||||
|
||||
Returns:
|
||||
A CodeInterpreterTool instance ready to pass to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
|
||||
tool = AzureAIAgentClient.get_code_interpreter_tool()
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
return CodeInterpreterTool()
|
||||
|
||||
@staticmethod
|
||||
def get_file_search_tool(
|
||||
*,
|
||||
vector_store_ids: list[str],
|
||||
) -> FileSearchTool:
|
||||
"""Create a file search tool configuration for Azure AI Agents.
|
||||
|
||||
Keyword Args:
|
||||
vector_store_ids: List of vector store IDs to search within.
|
||||
|
||||
Returns:
|
||||
A FileSearchTool instance ready to pass to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
|
||||
tool = AzureAIAgentClient.get_file_search_tool(
|
||||
vector_store_ids=["vs_abc123"],
|
||||
)
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
return FileSearchTool(vector_store_ids=vector_store_ids)
|
||||
|
||||
@staticmethod
|
||||
def get_web_search_tool(
|
||||
*,
|
||||
bing_connection_id: str | None = None,
|
||||
bing_custom_connection_id: str | None = None,
|
||||
bing_custom_instance_id: str | None = None,
|
||||
) -> BingGroundingTool | BingCustomSearchTool:
|
||||
"""Create a web search tool configuration for Azure AI Agents.
|
||||
|
||||
For Azure AI Agents, web search uses Bing Grounding or Bing Custom Search.
|
||||
If no arguments are provided, attempts to read from environment variables.
|
||||
If no connection IDs are found, raises ValueError.
|
||||
|
||||
Keyword Args:
|
||||
bing_connection_id: The Bing Grounding connection ID for standard web search.
|
||||
Falls back to BING_CONNECTION_ID environment variable.
|
||||
bing_custom_connection_id: The Bing Custom Search connection ID.
|
||||
Falls back to BING_CUSTOM_CONNECTION_ID environment variable.
|
||||
bing_custom_instance_id: The Bing Custom Search instance ID.
|
||||
Falls back to BING_CUSTOM_INSTANCE_NAME environment variable.
|
||||
|
||||
Returns:
|
||||
A BingGroundingTool or BingCustomSearchTool instance ready to pass to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
|
||||
# Bing Grounding (explicit)
|
||||
tool = AzureAIAgentClient.get_web_search_tool(
|
||||
bing_connection_id="conn_bing_123",
|
||||
)
|
||||
|
||||
# Bing Grounding (from environment variable)
|
||||
tool = AzureAIAgentClient.get_web_search_tool()
|
||||
|
||||
# Bing Custom Search (explicit)
|
||||
tool = AzureAIAgentClient.get_web_search_tool(
|
||||
bing_custom_connection_id="conn_custom_123",
|
||||
bing_custom_instance_id="instance_456",
|
||||
)
|
||||
|
||||
# Bing Custom Search (from environment variables)
|
||||
# Set BING_CUSTOM_CONNECTION_ID and BING_CUSTOM_INSTANCE_NAME
|
||||
tool = AzureAIAgentClient.get_web_search_tool()
|
||||
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
# Try explicit Bing Custom Search parameters first, then environment variables
|
||||
resolved_custom_connection = bing_custom_connection_id or os.environ.get("BING_CUSTOM_CONNECTION_ID")
|
||||
resolved_custom_instance = bing_custom_instance_id or os.environ.get("BING_CUSTOM_INSTANCE_NAME")
|
||||
|
||||
if resolved_custom_connection and resolved_custom_instance:
|
||||
return BingCustomSearchTool(
|
||||
connection_id=resolved_custom_connection,
|
||||
instance_name=resolved_custom_instance,
|
||||
)
|
||||
|
||||
# Try explicit Bing Grounding parameter first, then environment variable
|
||||
resolved_connection_id = bing_connection_id or os.environ.get("BING_CONNECTION_ID")
|
||||
if resolved_connection_id:
|
||||
return BingGroundingTool(connection_id=resolved_connection_id)
|
||||
|
||||
# Azure AI Agents requires Bing connection for web search
|
||||
raise ValueError(
|
||||
"Azure AI Agents requires a Bing connection for web search. "
|
||||
"Provide bing_connection_id (or set BING_CONNECTION_ID env var) for Bing Grounding, "
|
||||
"or provide both bing_custom_connection_id and bing_custom_instance_id "
|
||||
"(or set BING_CUSTOM_CONNECTION_ID and BING_CUSTOM_INSTANCE_NAME env vars) for Bing Custom Search."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_mcp_tool(
|
||||
*,
|
||||
name: str,
|
||||
url: str | None = None,
|
||||
description: str | None = None,
|
||||
approval_mode: str | dict[str, list[str]] | None = None,
|
||||
allowed_tools: list[str] | None = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
) -> McpTool:
|
||||
"""Create a hosted MCP tool configuration for Azure AI Agents.
|
||||
|
||||
This configures an MCP (Model Context Protocol) server that will be called
|
||||
by Azure AI's service. The tools from this MCP server are executed remotely
|
||||
by Azure AI, not locally by your application.
|
||||
|
||||
Note:
|
||||
For local MCP execution where your application calls the MCP server
|
||||
directly, use the MCP client tools instead of this method.
|
||||
|
||||
Keyword Args:
|
||||
name: A label/name for the MCP server.
|
||||
url: The URL of the MCP server.
|
||||
description: A description of what the MCP server provides.
|
||||
approval_mode: Tool approval mode. Use "always_require" or "never_require" for all tools,
|
||||
or provide a dict with "always_require_approval" and/or "never_require_approval"
|
||||
keys mapping to lists of tool names.
|
||||
allowed_tools: List of tool names that are allowed to be used from this MCP server.
|
||||
headers: HTTP headers to include in requests to the MCP server.
|
||||
|
||||
Returns:
|
||||
An McpTool instance ready to pass to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
|
||||
tool = AzureAIAgentClient.get_mcp_tool(
|
||||
name="my_mcp",
|
||||
url="https://mcp.example.com",
|
||||
)
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
mcp_tool = McpTool(
|
||||
server_label=name.replace(" ", "_"),
|
||||
server_url=url or "",
|
||||
allowed_tools=list(allowed_tools) if allowed_tools else [],
|
||||
)
|
||||
|
||||
# Set approval mode if provided
|
||||
# The SDK's set_approval_mode() accepts dict at runtime even though type hints say str.
|
||||
if approval_mode:
|
||||
if isinstance(approval_mode, str):
|
||||
if approval_mode == "never_require":
|
||||
mcp_tool.set_approval_mode("never")
|
||||
elif approval_mode == "always_require":
|
||||
mcp_tool.set_approval_mode("always")
|
||||
else:
|
||||
mcp_tool.set_approval_mode(approval_mode)
|
||||
elif isinstance(approval_mode, dict):
|
||||
# Handle dict-based approval mode (per-tool approval settings)
|
||||
if "never_require_approval" in approval_mode:
|
||||
mcp_tool.set_approval_mode({"never": {"tool_names": approval_mode["never_require_approval"]}}) # type: ignore[arg-type]
|
||||
elif "always_require_approval" in approval_mode:
|
||||
mcp_tool.set_approval_mode({"always": {"tool_names": approval_mode["always_require_approval"]}}) # type: ignore[arg-type]
|
||||
|
||||
# Set headers if provided
|
||||
if headers:
|
||||
for key, value in headers.items():
|
||||
mcp_tool.update_headers(key, value)
|
||||
|
||||
return mcp_tool
|
||||
|
||||
# endregion
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
@@ -1051,38 +1238,25 @@ class AzureAIAgentClient(
|
||||
|
||||
return tool_definitions
|
||||
|
||||
def _prepare_mcp_resources(self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Prepare MCP tool resources for approval mode configuration."""
|
||||
mcp_tools = [tool for tool in tools if isinstance(tool, HostedMCPTool)]
|
||||
if not mcp_tools:
|
||||
return []
|
||||
def _prepare_mcp_resources(self, tools: Sequence[Any]) -> list[dict[str, Any]]:
|
||||
"""Prepare MCP tool resources for approval mode configuration.
|
||||
|
||||
Extracts MCP resources from McpTool instances including server_label,
|
||||
require_approval, and headers.
|
||||
"""
|
||||
mcp_resources: list[dict[str, Any]] = []
|
||||
for mcp_tool in mcp_tools:
|
||||
server_label = mcp_tool.name.replace(" ", "_")
|
||||
mcp_resource: dict[str, Any] = {"server_label": server_label}
|
||||
|
||||
if mcp_tool.headers:
|
||||
mcp_resource["headers"] = mcp_tool.headers
|
||||
|
||||
if mcp_tool.approval_mode is not None:
|
||||
match mcp_tool.approval_mode:
|
||||
case str():
|
||||
# Map agent framework approval modes to Azure AI approval modes
|
||||
approval_mode = "always" if mcp_tool.approval_mode == "always_require" else "never"
|
||||
mcp_resource["require_approval"] = approval_mode
|
||||
case _:
|
||||
if "always_require_approval" in mcp_tool.approval_mode:
|
||||
mcp_resource["require_approval"] = {
|
||||
"always": mcp_tool.approval_mode["always_require_approval"]
|
||||
}
|
||||
elif "never_require_approval" in mcp_tool.approval_mode:
|
||||
mcp_resource["require_approval"] = {
|
||||
"never": mcp_tool.approval_mode["never_require_approval"]
|
||||
}
|
||||
|
||||
mcp_resources.append(mcp_resource)
|
||||
|
||||
for tool in tools:
|
||||
if isinstance(tool, McpTool):
|
||||
# Use the resources property which includes all config (approval, headers)
|
||||
tool_resources = tool.resources
|
||||
if tool_resources and tool_resources.mcp:
|
||||
for mcp_resource in tool_resources.mcp:
|
||||
resource_dict: dict[str, Any] = {"server_label": mcp_resource.server_label}
|
||||
if mcp_resource.require_approval:
|
||||
resource_dict["require_approval"] = mcp_resource.require_approval
|
||||
if mcp_resource.headers:
|
||||
resource_dict["headers"] = mcp_resource.headers
|
||||
mcp_resources.append(resource_dict)
|
||||
return mcp_resources
|
||||
|
||||
def _prepare_messages(
|
||||
@@ -1144,79 +1318,40 @@ class AzureAIAgentClient(
|
||||
return additional_messages, instructions, required_action_results
|
||||
|
||||
async def _prepare_tools_for_azure_ai(
|
||||
self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]], run_options: dict[str, Any] | None = None
|
||||
) -> list[ToolDefinition | dict[str, Any]]:
|
||||
"""Prepare tool definitions for the Azure AI Agents API."""
|
||||
tool_definitions: list[ToolDefinition | dict[str, Any]] = []
|
||||
self, tools: Sequence[Any], run_options: dict[str, Any] | None = None
|
||||
) -> list[Any]:
|
||||
"""Prepare tool definitions for the Azure AI Agents API.
|
||||
|
||||
Converts FunctionTool to JSON schema format. SDK Tool wrappers with .definitions
|
||||
are unpacked. All other tools (ToolDefinition, dict, etc.) pass through unchanged.
|
||||
|
||||
Args:
|
||||
tools: Sequence of tools to prepare.
|
||||
run_options: Optional run options dict that may be updated with tool_resources.
|
||||
|
||||
Returns:
|
||||
List of tool definitions ready for the Azure AI API.
|
||||
"""
|
||||
tool_definitions: list[Any] = []
|
||||
for tool in tools:
|
||||
match tool:
|
||||
case FunctionTool():
|
||||
tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType]
|
||||
case HostedWebSearchTool():
|
||||
additional_props = tool.additional_properties or {}
|
||||
config_args: dict[str, Any] = {}
|
||||
if count := additional_props.get("count"):
|
||||
config_args["count"] = count
|
||||
if freshness := additional_props.get("freshness"):
|
||||
config_args["freshness"] = freshness
|
||||
if market := additional_props.get("market"):
|
||||
config_args["market"] = market
|
||||
if set_lang := additional_props.get("set_lang"):
|
||||
config_args["set_lang"] = set_lang
|
||||
# Bing Grounding
|
||||
connection_id = additional_props.get("connection_id") or os.getenv("BING_CONNECTION_ID")
|
||||
# Custom Bing Search
|
||||
custom_connection_id = additional_props.get("custom_connection_id") or os.getenv(
|
||||
"BING_CUSTOM_CONNECTION_ID"
|
||||
)
|
||||
custom_instance_name = additional_props.get("custom_instance_name") or os.getenv(
|
||||
"BING_CUSTOM_INSTANCE_NAME"
|
||||
)
|
||||
bing_search: BingGroundingTool | BingCustomSearchTool | None = None
|
||||
if (connection_id) and not custom_connection_id and not custom_instance_name:
|
||||
if connection_id:
|
||||
conn_id = connection_id
|
||||
else:
|
||||
raise ServiceInitializationError("Parameter connection_id is not provided.")
|
||||
bing_search = BingGroundingTool(connection_id=conn_id, **config_args)
|
||||
if custom_connection_id and custom_instance_name:
|
||||
bing_search = BingCustomSearchTool(
|
||||
connection_id=custom_connection_id,
|
||||
instance_name=custom_instance_name,
|
||||
**config_args,
|
||||
)
|
||||
if not bing_search:
|
||||
raise ServiceInitializationError(
|
||||
"Bing search tool requires either 'connection_id' for Bing Grounding "
|
||||
"or both 'custom_connection_id' and 'custom_instance_name' for Custom Bing Search. "
|
||||
"These can be provided via additional_properties or environment variables: "
|
||||
"'BING_CONNECTION_ID', 'BING_CUSTOM_CONNECTION_ID', "
|
||||
"'BING_CUSTOM_INSTANCE_NAME'"
|
||||
)
|
||||
tool_definitions.extend(bing_search.definitions)
|
||||
case HostedCodeInterpreterTool():
|
||||
tool_definitions.append(CodeInterpreterToolDefinition())
|
||||
case HostedMCPTool():
|
||||
mcp_tool = McpTool(
|
||||
server_label=tool.name.replace(" ", "_"),
|
||||
server_url=str(tool.url),
|
||||
allowed_tools=list(tool.allowed_tools) if tool.allowed_tools else [],
|
||||
)
|
||||
tool_definitions.extend(mcp_tool.definitions)
|
||||
case HostedFileSearchTool():
|
||||
vector_stores = [inp for inp in tool.inputs or [] if inp.type == "hosted_vector_store"]
|
||||
if vector_stores:
|
||||
file_search = FileSearchTool(vector_store_ids=[vs.vector_store_id for vs in vector_stores]) # type: ignore[misc]
|
||||
tool_definitions.extend(file_search.definitions)
|
||||
# Set tool_resources for file search to work properly with Azure AI
|
||||
if run_options is not None and "tool_resources" not in run_options:
|
||||
run_options["tool_resources"] = file_search.resources
|
||||
case ToolDefinition():
|
||||
tool_definitions.append(tool)
|
||||
case dict():
|
||||
tool_definitions.append(tool)
|
||||
case _:
|
||||
raise ServiceInitializationError(f"Unsupported tool type: {type(tool)}")
|
||||
if isinstance(tool, FunctionTool):
|
||||
tool_definitions.append(tool.to_json_schema_spec())
|
||||
elif hasattr(tool, "definitions") and not isinstance(tool, MutableMapping):
|
||||
# SDK Tool wrappers (McpTool, FileSearchTool, BingGroundingTool, etc.)
|
||||
tool_definitions.extend(tool.definitions)
|
||||
# Handle tool resources (MCP resources handled separately by _prepare_mcp_resources)
|
||||
if (
|
||||
run_options is not None
|
||||
and hasattr(tool, "resources")
|
||||
and tool.resources
|
||||
and "mcp" not in tool.resources
|
||||
):
|
||||
if "tool_resources" not in run_options:
|
||||
run_options["tool_resources"] = {}
|
||||
run_options["tool_resources"].update(tool.resources)
|
||||
else:
|
||||
# Pass through ToolDefinition, dict, and other types unchanged
|
||||
tool_definitions.append(tool)
|
||||
return tool_definitions
|
||||
|
||||
def _prepare_tool_outputs_for_azure_ai(
|
||||
@@ -1293,10 +1428,10 @@ class AzureAIAgentClient(
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
instructions: str | None = None,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
default_options: AzureAIAgentOptionsT | Mapping[str, Any] | None = None,
|
||||
chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None,
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import Callable, Mapping, MutableMapping, Sequence
|
||||
from typing import Any, ClassVar, Generic, TypedDict, TypeVar, cast
|
||||
from typing import Any, ClassVar, Generic, Literal, TypedDict, TypeVar, cast
|
||||
|
||||
from agent_framework import (
|
||||
AGENT_FRAMEWORK_USER_AGENT,
|
||||
@@ -15,10 +15,9 @@ from agent_framework import (
|
||||
ContextProvider,
|
||||
FunctionInvocationConfiguration,
|
||||
FunctionInvocationLayer,
|
||||
HostedMCPTool,
|
||||
FunctionTool,
|
||||
Message,
|
||||
MiddlewareTypes,
|
||||
ToolProtocol,
|
||||
get_logger,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
@@ -26,12 +25,24 @@ from agent_framework.observability import ChatTelemetryLayer
|
||||
from agent_framework.openai import OpenAIResponsesOptions
|
||||
from agent_framework.openai._responses_client import RawOpenAIResponsesClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import MCPTool, PromptAgentDefinition, PromptAgentDefinitionText, RaiConfig, Reasoning
|
||||
from azure.ai.projects.models import (
|
||||
ApproximateLocation,
|
||||
CodeInterpreterTool,
|
||||
CodeInterpreterToolAuto,
|
||||
ImageGenTool,
|
||||
MCPTool,
|
||||
PromptAgentDefinition,
|
||||
PromptAgentDefinitionText,
|
||||
RaiConfig,
|
||||
Reasoning,
|
||||
WebSearchPreviewTool,
|
||||
)
|
||||
from azure.ai.projects.models import FileSearchTool as ProjectsFileSearchTool
|
||||
from azure.core.credentials_async import AsyncTokenCredential
|
||||
from azure.core.exceptions import ResourceNotFoundError
|
||||
from pydantic import ValidationError
|
||||
|
||||
from ._shared import AzureAISettings, _extract_project_connection_id, create_text_format_config
|
||||
from ._shared import AzureAISettings, create_text_format_config
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
from typing import TypeVar # type: ignore # pragma: no cover
|
||||
@@ -526,37 +537,263 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
|
||||
if description and not self.agent_description:
|
||||
self.agent_description = description
|
||||
|
||||
# region Hosted Tool Factory Methods (Azure-specific overrides)
|
||||
|
||||
@staticmethod
|
||||
def _prepare_mcp_tool(tool: HostedMCPTool) -> MCPTool: # type: ignore[override]
|
||||
"""Get MCP tool from HostedMCPTool."""
|
||||
mcp = MCPTool(server_label=tool.name.replace(" ", "_"), server_url=str(tool.url))
|
||||
def get_code_interpreter_tool( # type: ignore[override]
|
||||
*,
|
||||
file_ids: list[str] | None = None,
|
||||
container: Literal["auto"] | dict[str, Any] = "auto",
|
||||
**kwargs: Any,
|
||||
) -> CodeInterpreterTool:
|
||||
"""Create a code interpreter tool configuration for Azure AI Projects.
|
||||
|
||||
if tool.description:
|
||||
mcp["server_description"] = tool.description
|
||||
Keyword Args:
|
||||
file_ids: Optional list of file IDs to make available to the code interpreter.
|
||||
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.
|
||||
**kwargs: Additional arguments passed to the SDK CodeInterpreterTool constructor.
|
||||
|
||||
Returns:
|
||||
A CodeInterpreterTool ready to pass to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.azure import AzureAIClient
|
||||
|
||||
tool = AzureAIClient.get_code_interpreter_tool()
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
# 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)
|
||||
return CodeInterpreterTool(container=tool_container, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def get_file_search_tool(
|
||||
*,
|
||||
vector_store_ids: list[str],
|
||||
max_num_results: int | None = None,
|
||||
ranking_options: dict[str, Any] | None = None,
|
||||
filters: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ProjectsFileSearchTool:
|
||||
"""Create a file search tool configuration for Azure AI Projects.
|
||||
|
||||
Keyword Args:
|
||||
vector_store_ids: List of vector store IDs to search.
|
||||
max_num_results: Maximum number of results to return (1-50).
|
||||
ranking_options: Ranking options for search results.
|
||||
filters: A filter to apply (ComparisonFilter or CompoundFilter).
|
||||
**kwargs: Additional arguments passed to the SDK FileSearchTool constructor.
|
||||
|
||||
Returns:
|
||||
A FileSearchTool ready to pass to ChatAgent.
|
||||
|
||||
Raises:
|
||||
ValueError: If vector_store_ids is empty.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.azure import AzureAIClient
|
||||
|
||||
tool = AzureAIClient.get_file_search_tool(
|
||||
vector_store_ids=["vs_abc123"],
|
||||
)
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
if not vector_store_ids:
|
||||
raise ValueError("File search tool requires 'vector_store_ids' to be specified.")
|
||||
return ProjectsFileSearchTool(
|
||||
vector_store_ids=vector_store_ids,
|
||||
max_num_results=max_num_results,
|
||||
ranking_options=ranking_options, # type: ignore[arg-type]
|
||||
filters=filters, # type: ignore[arg-type]
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_web_search_tool( # type: ignore[override]
|
||||
*,
|
||||
user_location: dict[str, str] | None = None,
|
||||
search_context_size: Literal["low", "medium", "high"] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> WebSearchPreviewTool:
|
||||
"""Create a web search preview tool configuration for Azure AI Projects.
|
||||
|
||||
Keyword Args:
|
||||
user_location: Location context for search results. Dict with keys like
|
||||
"city", "country", "region", "timezone".
|
||||
search_context_size: Amount of context to include from search results.
|
||||
One of "low", "medium", or "high". Defaults to "medium".
|
||||
**kwargs: Additional arguments passed to the SDK WebSearchPreviewTool constructor.
|
||||
|
||||
Returns:
|
||||
A WebSearchPreviewTool ready to pass to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.azure import AzureAIClient
|
||||
|
||||
tool = AzureAIClient.get_web_search_tool()
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
|
||||
# With location and context size
|
||||
tool = AzureAIClient.get_web_search_tool(
|
||||
user_location={"city": "Seattle", "country": "US"},
|
||||
search_context_size="high",
|
||||
)
|
||||
"""
|
||||
ws_tool = WebSearchPreviewTool(search_context_size=search_context_size, **kwargs)
|
||||
|
||||
if user_location:
|
||||
ws_tool.user_location = ApproximateLocation(
|
||||
city=user_location.get("city"),
|
||||
country=user_location.get("country"),
|
||||
region=user_location.get("region"),
|
||||
timezone=user_location.get("timezone"),
|
||||
)
|
||||
|
||||
return ws_tool
|
||||
|
||||
@staticmethod
|
||||
def get_image_generation_tool( # type: ignore[override]
|
||||
*,
|
||||
model: Literal["gpt-image-1"] | str | None = None,
|
||||
size: Literal["1024x1024", "1024x1536", "1536x1024", "auto"] | None = None,
|
||||
output_format: Literal["png", "webp", "jpeg"] | None = None,
|
||||
quality: Literal["low", "medium", "high", "auto"] | None = None,
|
||||
background: Literal["transparent", "opaque", "auto"] | None = None,
|
||||
partial_images: int | None = None,
|
||||
moderation: Literal["auto", "low"] | None = None,
|
||||
output_compression: int | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ImageGenTool:
|
||||
"""Create an image generation tool configuration for Azure AI Projects.
|
||||
|
||||
Keyword Args:
|
||||
model: The model to use for image generation.
|
||||
size: Output image size.
|
||||
output_format: Output image format.
|
||||
quality: Output image quality.
|
||||
background: Background transparency setting.
|
||||
partial_images: Number of partial images to return during generation.
|
||||
moderation: Moderation level.
|
||||
output_compression: Compression level.
|
||||
**kwargs: Additional arguments passed to the SDK ImageGenTool constructor.
|
||||
|
||||
Returns:
|
||||
An ImageGenTool ready to pass to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.azure import AzureAIClient
|
||||
|
||||
tool = AzureAIClient.get_image_generation_tool()
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
return ImageGenTool( # type: ignore[misc]
|
||||
model=model, # type: ignore[arg-type]
|
||||
size=size,
|
||||
output_format=output_format,
|
||||
quality=quality,
|
||||
background=background,
|
||||
partial_images=partial_images,
|
||||
moderation=moderation,
|
||||
output_compression=output_compression,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_mcp_tool(
|
||||
*,
|
||||
name: str,
|
||||
url: str | None = None,
|
||||
description: str | None = None,
|
||||
approval_mode: Literal["always_require", "never_require"] | dict[str, list[str]] | None = None,
|
||||
allowed_tools: list[str] | None = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
project_connection_id: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> MCPTool:
|
||||
"""Create a hosted MCP tool configuration for Azure AI.
|
||||
|
||||
This configures an MCP (Model Context Protocol) server that will be called
|
||||
by Azure AI's service. The tools from this MCP server are executed remotely
|
||||
by Azure AI, not locally by your application.
|
||||
|
||||
Note:
|
||||
For local MCP execution where your application calls the MCP server
|
||||
directly, use the MCP client tools instead of this method.
|
||||
|
||||
Keyword Args:
|
||||
name: A label/name for the MCP server.
|
||||
url: The URL of the MCP server. Required if project_connection_id is not provided.
|
||||
description: A description of what the MCP server provides.
|
||||
approval_mode: Tool approval mode. Use "always_require" or "never_require" for all tools,
|
||||
or provide a dict with "always_require_approval" and/or "never_require_approval"
|
||||
keys mapping to lists of tool names.
|
||||
allowed_tools: List of tool names that are allowed to be used from this MCP server.
|
||||
headers: HTTP headers to include in requests to the MCP server.
|
||||
project_connection_id: Azure AI Foundry connection ID for managed MCP connections.
|
||||
If provided, url and headers are not required.
|
||||
**kwargs: Additional arguments passed to the SDK MCPTool constructor.
|
||||
|
||||
Returns:
|
||||
An MCPTool configuration ready to pass to ChatAgent.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.azure import AzureAIClient
|
||||
|
||||
# With URL
|
||||
tool = AzureAIClient.get_mcp_tool(
|
||||
name="my_mcp",
|
||||
url="https://mcp.example.com",
|
||||
)
|
||||
|
||||
# With Azure AI Foundry connection
|
||||
tool = AzureAIClient.get_mcp_tool(
|
||||
name="github_mcp",
|
||||
project_connection_id="conn_abc123",
|
||||
description="GitHub MCP via Azure AI Foundry",
|
||||
)
|
||||
|
||||
agent = ChatAgent(client, tools=[tool])
|
||||
"""
|
||||
mcp = MCPTool(server_label=name.replace(" ", "_"), server_url=url or "", **kwargs)
|
||||
|
||||
if description:
|
||||
mcp["server_description"] = description
|
||||
|
||||
# Check for project_connection_id in additional_properties (for Azure AI Foundry connections)
|
||||
project_connection_id = _extract_project_connection_id(tool.additional_properties)
|
||||
if project_connection_id:
|
||||
mcp["project_connection_id"] = project_connection_id
|
||||
elif tool.headers:
|
||||
# Only use headers if no project_connection_id is available
|
||||
mcp["headers"] = tool.headers
|
||||
elif headers:
|
||||
mcp["headers"] = headers
|
||||
|
||||
if tool.allowed_tools:
|
||||
mcp["allowed_tools"] = list(tool.allowed_tools)
|
||||
if allowed_tools:
|
||||
mcp["allowed_tools"] = allowed_tools
|
||||
|
||||
if tool.approval_mode:
|
||||
match tool.approval_mode:
|
||||
case str():
|
||||
mcp["require_approval"] = "always" if tool.approval_mode == "always_require" else "never"
|
||||
case _:
|
||||
if always_require_approvals := tool.approval_mode.get("always_require_approval"):
|
||||
mcp["require_approval"] = {"always": {"tool_names": list(always_require_approvals)}}
|
||||
if never_require_approvals := tool.approval_mode.get("never_require_approval"):
|
||||
mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}}
|
||||
if approval_mode:
|
||||
if isinstance(approval_mode, str):
|
||||
mcp["require_approval"] = "always" if approval_mode == "always_require" else "never"
|
||||
else:
|
||||
if always_require := approval_mode.get("always_require_approval"):
|
||||
mcp["require_approval"] = {"always": {"tool_names": always_require}}
|
||||
if never_require := approval_mode.get("never_require_approval"):
|
||||
mcp["require_approval"] = {"never": {"tool_names": never_require}}
|
||||
|
||||
return mcp
|
||||
|
||||
# endregion
|
||||
|
||||
@override
|
||||
def as_agent(
|
||||
self,
|
||||
@@ -565,10 +802,10 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
instructions: str | None = None,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
default_options: AzureAIClientOptionsT | Mapping[str, Any] | None = None,
|
||||
chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None,
|
||||
|
||||
@@ -12,7 +12,6 @@ from agent_framework import (
|
||||
ContextProvider,
|
||||
FunctionTool,
|
||||
MiddlewareTypes,
|
||||
ToolProtocol,
|
||||
get_logger,
|
||||
normalize_tools,
|
||||
)
|
||||
@@ -162,10 +161,10 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
|
||||
model: str | None = None,
|
||||
instructions: str | None = None,
|
||||
description: str | None = None,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
default_options: OptionsCoT | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
@@ -221,7 +220,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
|
||||
# Normalize tools and separate MCP tools from other tools
|
||||
normalized_tools = normalize_tools(tools)
|
||||
mcp_tools: list[MCPTool] = []
|
||||
non_mcp_tools: list[ToolProtocol | MutableMapping[str, Any]] = []
|
||||
non_mcp_tools: list[FunctionTool | MutableMapping[str, Any]] = []
|
||||
|
||||
if normalized_tools:
|
||||
for tool in normalized_tools:
|
||||
@@ -239,7 +238,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
|
||||
mcp_discovered_functions.extend(mcp_tool.functions)
|
||||
|
||||
# Combine non-MCP tools with discovered MCP functions for Azure AI
|
||||
all_tools_for_azure: list[ToolProtocol | MutableMapping[str, Any]] = list(non_mcp_tools)
|
||||
all_tools_for_azure: list[FunctionTool | MutableMapping[str, Any]] = list(non_mcp_tools)
|
||||
all_tools_for_azure.extend(mcp_discovered_functions)
|
||||
|
||||
if all_tools_for_azure:
|
||||
@@ -264,10 +263,10 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
|
||||
*,
|
||||
name: str | None = None,
|
||||
reference: AgentReference | None = None,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
default_options: OptionsCoT | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
@@ -324,10 +323,10 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
|
||||
def as_agent(
|
||||
self,
|
||||
details: AgentVersionDetails,
|
||||
tools: ToolProtocol
|
||||
tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
default_options: OptionsCoT | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
@@ -368,7 +367,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
|
||||
def _to_chat_agent_from_details(
|
||||
self,
|
||||
details: AgentVersionDetails,
|
||||
provided_tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None = None,
|
||||
provided_tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None = None,
|
||||
default_options: OptionsCoT | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
context_provider: ContextProvider | None = None,
|
||||
@@ -416,8 +415,8 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
|
||||
def _merge_tools(
|
||||
self,
|
||||
definition_tools: Sequence[Any] | None,
|
||||
provided_tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None,
|
||||
) -> list[ToolProtocol | dict[str, Any]]:
|
||||
provided_tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None,
|
||||
) -> list[FunctionTool | dict[str, Any]]:
|
||||
"""Merge hosted tools from definition with user-provided function tools.
|
||||
|
||||
Args:
|
||||
@@ -427,7 +426,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
|
||||
Returns:
|
||||
Combined list of tools for the Agent.
|
||||
"""
|
||||
merged: list[ToolProtocol | dict[str, Any]] = []
|
||||
merged: list[FunctionTool | dict[str, Any]] = []
|
||||
|
||||
# Convert hosted tools from definition (MCP, code interpreter, file search, web search)
|
||||
# Function tools from the definition are skipped - we use user-provided implementations instead
|
||||
@@ -451,10 +450,10 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
|
||||
def _validate_function_tools(
|
||||
self,
|
||||
agent_tools: Sequence[Any] | None,
|
||||
provided_tools: ToolProtocol
|
||||
provided_tools: FunctionTool
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None,
|
||||
) -> None:
|
||||
"""Validate that required function tools are provided."""
|
||||
|
||||
@@ -2,37 +2,21 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import Mapping, MutableMapping, Sequence
|
||||
from typing import Any, ClassVar, Literal, cast
|
||||
from typing import Any, ClassVar, cast
|
||||
|
||||
from agent_framework import (
|
||||
Content,
|
||||
FunctionTool,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedImageGenerationTool,
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
ToolProtocol,
|
||||
get_logger,
|
||||
)
|
||||
from agent_framework._pydantic import AFBaseSettings
|
||||
from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError
|
||||
from agent_framework.exceptions import ServiceInvalidRequestError
|
||||
from azure.ai.agents.models import (
|
||||
BingCustomSearchTool,
|
||||
BingGroundingTool,
|
||||
CodeInterpreterToolDefinition,
|
||||
McpTool,
|
||||
ToolDefinition,
|
||||
)
|
||||
from azure.ai.agents.models import FileSearchTool as AgentsFileSearchTool
|
||||
from azure.ai.projects.models import (
|
||||
ApproximateLocation,
|
||||
CodeInterpreterTool,
|
||||
CodeInterpreterToolAuto,
|
||||
ImageGenTool,
|
||||
ImageGenToolInputImageMask,
|
||||
MCPTool,
|
||||
ResponseTextFormatConfigurationJsonObject,
|
||||
ResponseTextFormatConfigurationJsonSchema,
|
||||
@@ -93,13 +77,13 @@ class AzureAISettings(AFBaseSettings):
|
||||
|
||||
|
||||
def _extract_project_connection_id(additional_properties: dict[str, Any] | None) -> str | None:
|
||||
"""Extract project_connection_id from HostedMCPTool additional_properties.
|
||||
"""Extract project_connection_id from tool additional_properties.
|
||||
|
||||
Checks for both direct 'project_connection_id' key (programmatic usage)
|
||||
and 'connection.name' structure (declarative/YAML usage).
|
||||
|
||||
Args:
|
||||
additional_properties: The additional_properties dict from a HostedMCPTool.
|
||||
additional_properties: The additional_properties dict from a tool.
|
||||
|
||||
Returns:
|
||||
The project_connection_id if found, None otherwise.
|
||||
@@ -124,11 +108,13 @@ def _extract_project_connection_id(additional_properties: dict[str, Any] | None)
|
||||
|
||||
|
||||
def to_azure_ai_agent_tools(
|
||||
tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None,
|
||||
tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None,
|
||||
run_options: dict[str, Any] | None = None,
|
||||
) -> list[ToolDefinition | dict[str, Any]]:
|
||||
"""Convert Agent Framework tools to Azure AI V1 SDK tool definitions.
|
||||
|
||||
Handles FunctionTool instances and dict-based tools from static factory methods.
|
||||
|
||||
Args:
|
||||
tools: Sequence of Agent Framework tools to convert.
|
||||
run_options: Optional dict with run options.
|
||||
@@ -144,91 +130,53 @@ def to_azure_ai_agent_tools(
|
||||
|
||||
tool_definitions: list[ToolDefinition | dict[str, Any]] = []
|
||||
for tool in tools:
|
||||
match tool:
|
||||
case FunctionTool():
|
||||
tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType]
|
||||
case HostedWebSearchTool():
|
||||
additional_props = tool.additional_properties or {}
|
||||
config_args: dict[str, Any] = {}
|
||||
if count := additional_props.get("count"):
|
||||
config_args["count"] = count
|
||||
if freshness := additional_props.get("freshness"):
|
||||
config_args["freshness"] = freshness
|
||||
if market := additional_props.get("market"):
|
||||
config_args["market"] = market
|
||||
if set_lang := additional_props.get("set_lang"):
|
||||
config_args["set_lang"] = set_lang
|
||||
# Bing Grounding
|
||||
connection_id = additional_props.get("connection_id") or os.getenv("BING_CONNECTION_ID")
|
||||
# Custom Bing Search
|
||||
custom_connection_id = additional_props.get("custom_connection_id") or os.getenv(
|
||||
"BING_CUSTOM_CONNECTION_ID"
|
||||
)
|
||||
custom_instance_name = additional_props.get("custom_instance_name") or os.getenv(
|
||||
"BING_CUSTOM_INSTANCE_NAME"
|
||||
)
|
||||
bing_search: BingGroundingTool | BingCustomSearchTool | None = None
|
||||
if connection_id and not custom_connection_id and not custom_instance_name:
|
||||
bing_search = BingGroundingTool(connection_id=connection_id, **config_args)
|
||||
if custom_connection_id and custom_instance_name:
|
||||
bing_search = BingCustomSearchTool(
|
||||
connection_id=custom_connection_id,
|
||||
instance_name=custom_instance_name,
|
||||
**config_args,
|
||||
)
|
||||
if not bing_search:
|
||||
raise ServiceInitializationError(
|
||||
"Bing search tool requires either 'connection_id' for Bing Grounding "
|
||||
"or both 'custom_connection_id' and 'custom_instance_name' for Custom Bing Search. "
|
||||
"These can be provided via additional_properties or environment variables: "
|
||||
"'BING_CONNECTION_ID', 'BING_CUSTOM_CONNECTION_ID', 'BING_CUSTOM_INSTANCE_NAME'"
|
||||
)
|
||||
tool_definitions.extend(bing_search.definitions)
|
||||
case HostedCodeInterpreterTool():
|
||||
tool_definitions.append(CodeInterpreterToolDefinition())
|
||||
case HostedMCPTool():
|
||||
mcp_tool = McpTool(
|
||||
server_label=tool.name.replace(" ", "_"),
|
||||
server_url=str(tool.url),
|
||||
allowed_tools=list(tool.allowed_tools) if tool.allowed_tools else [],
|
||||
)
|
||||
tool_definitions.extend(mcp_tool.definitions)
|
||||
case HostedFileSearchTool():
|
||||
vector_stores = [inp for inp in tool.inputs or [] if inp.type == "hosted_vector_store"]
|
||||
if vector_stores:
|
||||
file_search = AgentsFileSearchTool(vector_store_ids=[vs.vector_store_id for vs in vector_stores]) # type: ignore[misc]
|
||||
tool_definitions.extend(file_search.definitions)
|
||||
# Set tool_resources for file search to work properly with Azure AI
|
||||
if run_options is not None and "tool_resources" not in run_options:
|
||||
run_options["tool_resources"] = file_search.resources
|
||||
case ToolDefinition():
|
||||
tool_definitions.append(tool)
|
||||
case dict():
|
||||
tool_definitions.append(tool)
|
||||
case _:
|
||||
raise ServiceInitializationError(f"Unsupported tool type: {type(tool)}")
|
||||
if isinstance(tool, FunctionTool):
|
||||
tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType]
|
||||
elif isinstance(tool, ToolDefinition):
|
||||
# Pass through ToolDefinition subclasses unchanged (includes CodeInterpreterToolDefinition, etc.)
|
||||
tool_definitions.append(tool)
|
||||
elif hasattr(tool, "definitions") and not isinstance(tool, (dict, MutableMapping)):
|
||||
# SDK Tool wrappers (McpTool, FileSearchTool, BingGroundingTool, etc.)
|
||||
tool_definitions.extend(tool.definitions)
|
||||
# Handle tool resources (MCP resources handled separately)
|
||||
if (
|
||||
run_options is not None
|
||||
and hasattr(tool, "resources")
|
||||
and tool.resources
|
||||
and "mcp" not in tool.resources
|
||||
):
|
||||
if "tool_resources" not in run_options:
|
||||
run_options["tool_resources"] = {}
|
||||
run_options["tool_resources"].update(tool.resources)
|
||||
elif isinstance(tool, (dict, MutableMapping)):
|
||||
# Handle dict-based tools - pass through directly
|
||||
tool_dict = tool if isinstance(tool, dict) else dict(tool)
|
||||
tool_definitions.append(tool_dict)
|
||||
else:
|
||||
# Pass through other types unchanged
|
||||
tool_definitions.append(tool)
|
||||
return tool_definitions
|
||||
|
||||
|
||||
def from_azure_ai_agent_tools(
|
||||
tools: Sequence[ToolDefinition | dict[str, Any]] | None,
|
||||
) -> list[ToolProtocol | dict[str, Any]]:
|
||||
"""Convert Azure AI V1 SDK tool definitions to Agent Framework tools.
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Convert Azure AI V1 SDK tool definitions to dict-based tools.
|
||||
|
||||
Args:
|
||||
tools: Sequence of Azure AI V1 SDK tool definitions.
|
||||
|
||||
Returns:
|
||||
List of Agent Framework tools.
|
||||
List of dict-based tool definitions.
|
||||
"""
|
||||
if not tools:
|
||||
return []
|
||||
|
||||
result: list[ToolProtocol | dict[str, Any]] = []
|
||||
result: list[dict[str, Any]] = []
|
||||
for tool in tools:
|
||||
# Handle SDK objects
|
||||
if isinstance(tool, CodeInterpreterToolDefinition):
|
||||
result.append(HostedCodeInterpreterTool())
|
||||
result.append({"type": "code_interpreter"})
|
||||
elif isinstance(tool, dict):
|
||||
# Handle dict format
|
||||
converted = _convert_dict_tool(tool)
|
||||
@@ -242,35 +190,38 @@ def from_azure_ai_agent_tools(
|
||||
return result
|
||||
|
||||
|
||||
def _convert_dict_tool(tool: dict[str, Any]) -> ToolProtocol | dict[str, Any] | None:
|
||||
"""Convert a dict-format Azure AI tool to Agent Framework tool."""
|
||||
def _convert_dict_tool(tool: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""Convert a dict-format Azure AI tool to dict-based tool format."""
|
||||
tool_type = tool.get("type")
|
||||
|
||||
if tool_type == "code_interpreter":
|
||||
return HostedCodeInterpreterTool()
|
||||
return {"type": "code_interpreter"}
|
||||
|
||||
if tool_type == "file_search":
|
||||
file_search_config = tool.get("file_search", {})
|
||||
vector_store_ids = file_search_config.get("vector_store_ids", [])
|
||||
inputs = [Content.from_hosted_vector_store(vector_store_id=vs_id) for vs_id in vector_store_ids]
|
||||
return HostedFileSearchTool(inputs=inputs if inputs else None) # type: ignore
|
||||
return {"type": "file_search", "vector_store_ids": vector_store_ids}
|
||||
|
||||
if tool_type == "bing_grounding":
|
||||
bing_config = tool.get("bing_grounding", {})
|
||||
connection_id = bing_config.get("connection_id")
|
||||
return HostedWebSearchTool(additional_properties={"connection_id": connection_id} if connection_id else None)
|
||||
return {"type": "bing_grounding", "connection_id": connection_id} if connection_id else None
|
||||
|
||||
if tool_type == "bing_custom_search":
|
||||
bing_config = tool.get("bing_custom_search", {})
|
||||
return HostedWebSearchTool(
|
||||
additional_properties={
|
||||
"custom_connection_id": bing_config.get("connection_id"),
|
||||
"custom_instance_name": bing_config.get("instance_name"),
|
||||
connection_id = bing_config.get("connection_id")
|
||||
instance_name = bing_config.get("instance_name")
|
||||
# Only return if both required fields are present
|
||||
if connection_id and instance_name:
|
||||
return {
|
||||
"type": "bing_custom_search",
|
||||
"connection_id": connection_id,
|
||||
"instance_name": instance_name,
|
||||
}
|
||||
)
|
||||
return None
|
||||
|
||||
if tool_type == "mcp":
|
||||
# Hosted MCP tools are defined on the Azure agent, no local handling needed
|
||||
# MCP tools are defined on the Azure agent, no local handling needed
|
||||
# Azure may not return full server_url, so skip conversion
|
||||
return None
|
||||
|
||||
@@ -282,35 +233,38 @@ def _convert_dict_tool(tool: dict[str, Any]) -> ToolProtocol | dict[str, Any] |
|
||||
return tool
|
||||
|
||||
|
||||
def _convert_sdk_tool(tool: ToolDefinition) -> ToolProtocol | dict[str, Any] | None:
|
||||
"""Convert an SDK-object Azure AI tool to Agent Framework tool."""
|
||||
def _convert_sdk_tool(tool: ToolDefinition) -> dict[str, Any] | None:
|
||||
"""Convert an SDK-object Azure AI tool to dict-based tool format."""
|
||||
tool_type = getattr(tool, "type", None)
|
||||
|
||||
if tool_type == "code_interpreter":
|
||||
return HostedCodeInterpreterTool()
|
||||
return {"type": "code_interpreter"}
|
||||
|
||||
if tool_type == "file_search":
|
||||
file_search_config = getattr(tool, "file_search", None)
|
||||
vector_store_ids = getattr(file_search_config, "vector_store_ids", []) if file_search_config else []
|
||||
inputs = [Content.from_hosted_vector_store(vector_store_id=vs_id) for vs_id in vector_store_ids]
|
||||
return HostedFileSearchTool(inputs=inputs if inputs else None) # type: ignore
|
||||
return {"type": "file_search", "vector_store_ids": vector_store_ids}
|
||||
|
||||
if tool_type == "bing_grounding":
|
||||
bing_config = getattr(tool, "bing_grounding", None)
|
||||
connection_id = getattr(bing_config, "connection_id", None) if bing_config else None
|
||||
return HostedWebSearchTool(additional_properties={"connection_id": connection_id} if connection_id else None)
|
||||
return {"type": "bing_grounding", "connection_id": connection_id} if connection_id else None
|
||||
|
||||
if tool_type == "bing_custom_search":
|
||||
bing_config = getattr(tool, "bing_custom_search", None)
|
||||
return HostedWebSearchTool(
|
||||
additional_properties={
|
||||
"custom_connection_id": getattr(bing_config, "connection_id", None) if bing_config else None,
|
||||
"custom_instance_name": getattr(bing_config, "instance_name", None) if bing_config else None,
|
||||
connection_id = getattr(bing_config, "connection_id", None) if bing_config else None
|
||||
instance_name = getattr(bing_config, "instance_name", None) if bing_config else None
|
||||
# Only return if both required fields are present
|
||||
if connection_id and instance_name:
|
||||
return {
|
||||
"type": "bing_custom_search",
|
||||
"connection_id": connection_id,
|
||||
"instance_name": instance_name,
|
||||
}
|
||||
)
|
||||
return None
|
||||
|
||||
if tool_type == "mcp":
|
||||
# Hosted MCP tools are defined on the Azure agent, no local handling needed
|
||||
# MCP tools are defined on the Azure agent, no local handling needed
|
||||
# Azure may not return full server_url, so skip conversion
|
||||
return None
|
||||
|
||||
@@ -324,18 +278,17 @@ def _convert_sdk_tool(tool: ToolDefinition) -> ToolProtocol | dict[str, Any] | N
|
||||
return {"type": tool_type} if tool_type else {}
|
||||
|
||||
|
||||
def from_azure_ai_tools(tools: Sequence[Tool | dict[str, Any]] | None) -> list[ToolProtocol | dict[str, Any]]:
|
||||
"""Parses and converts a sequence of Azure AI tools into Agent Framework compatible tools.
|
||||
def from_azure_ai_tools(tools: Sequence[Tool | dict[str, Any]] | None) -> list[dict[str, Any]]:
|
||||
"""Parses and converts a sequence of Azure AI tools into dict-based tools.
|
||||
|
||||
Args:
|
||||
tools: A sequence of tool objects or dictionaries
|
||||
defining the tools to be parsed. Can be None.
|
||||
|
||||
Returns:
|
||||
list[ToolProtocol | dict[str, Any]]: A list of converted tools compatible with the
|
||||
Agent Framework.
|
||||
list[dict[str, Any]]: A list of dict-based tool definitions.
|
||||
"""
|
||||
agent_tools: list[ToolProtocol | dict[str, Any]] = []
|
||||
agent_tools: list[dict[str, Any]] = []
|
||||
if not tools:
|
||||
return agent_tools
|
||||
for tool in tools:
|
||||
@@ -345,81 +298,62 @@ def from_azure_ai_tools(tools: Sequence[Tool | dict[str, Any]] | None) -> list[T
|
||||
|
||||
if tool_type == "mcp":
|
||||
mcp_tool = cast(MCPTool, tool_dict)
|
||||
approval_mode: Literal["always_require", "never_require"] | dict[str, set[str]] | None = None
|
||||
result: dict[str, Any] = {
|
||||
"type": "mcp",
|
||||
"server_label": mcp_tool.get("server_label", ""),
|
||||
"server_url": mcp_tool.get("server_url", ""),
|
||||
}
|
||||
if description := mcp_tool.get("server_description"):
|
||||
result["server_description"] = description
|
||||
if headers := mcp_tool.get("headers"):
|
||||
result["headers"] = headers
|
||||
if allowed_tools := mcp_tool.get("allowed_tools"):
|
||||
result["allowed_tools"] = allowed_tools
|
||||
if require_approval := mcp_tool.get("require_approval"):
|
||||
if require_approval == "always":
|
||||
approval_mode = "always_require"
|
||||
elif require_approval == "never":
|
||||
approval_mode = "never_require"
|
||||
elif isinstance(require_approval, dict):
|
||||
approval_mode = {}
|
||||
if "always" in require_approval:
|
||||
approval_mode["always_require_approval"] = set(require_approval["always"].get("tool_names", [])) # type: ignore
|
||||
if "never" in require_approval:
|
||||
approval_mode["never_require_approval"] = set(require_approval["never"].get("tool_names", [])) # type: ignore
|
||||
|
||||
# Preserve project_connection_id in additional_properties
|
||||
additional_props: dict[str, Any] | None = None
|
||||
result["require_approval"] = require_approval
|
||||
if project_connection_id := mcp_tool.get("project_connection_id"):
|
||||
additional_props = {"connection": {"name": project_connection_id}}
|
||||
|
||||
agent_tools.append(
|
||||
HostedMCPTool(
|
||||
name=mcp_tool.get("server_label", "").replace("_", " "),
|
||||
url=mcp_tool.get("server_url", ""),
|
||||
description=mcp_tool.get("server_description"),
|
||||
headers=mcp_tool.get("headers"),
|
||||
allowed_tools=mcp_tool.get("allowed_tools"),
|
||||
approval_mode=approval_mode, # type: ignore
|
||||
additional_properties=additional_props,
|
||||
)
|
||||
)
|
||||
result["project_connection_id"] = project_connection_id
|
||||
agent_tools.append(result)
|
||||
elif tool_type == "code_interpreter":
|
||||
ci_tool = cast(CodeInterpreterTool, tool_dict)
|
||||
container = ci_tool.get("container", {})
|
||||
ci_inputs: list[Content] = []
|
||||
result = {"type": "code_interpreter"}
|
||||
if "file_ids" in container:
|
||||
for file_id in container["file_ids"]:
|
||||
ci_inputs.append(Content.from_hosted_file(file_id=file_id))
|
||||
|
||||
agent_tools.append(HostedCodeInterpreterTool(inputs=ci_inputs if ci_inputs else None)) # type: ignore
|
||||
result["file_ids"] = container["file_ids"]
|
||||
agent_tools.append(result)
|
||||
elif tool_type == "file_search":
|
||||
fs_tool = cast(ProjectsFileSearchTool, tool_dict)
|
||||
fs_inputs: list[Content] = []
|
||||
result = {"type": "file_search"}
|
||||
if "vector_store_ids" in fs_tool:
|
||||
for vs_id in fs_tool["vector_store_ids"]:
|
||||
fs_inputs.append(Content.from_hosted_vector_store(vector_store_id=vs_id))
|
||||
|
||||
agent_tools.append(
|
||||
HostedFileSearchTool(
|
||||
inputs=fs_inputs if fs_inputs else None, # type: ignore
|
||||
max_results=fs_tool.get("max_num_results"),
|
||||
)
|
||||
)
|
||||
result["vector_store_ids"] = fs_tool["vector_store_ids"]
|
||||
if max_results := fs_tool.get("max_num_results"):
|
||||
result["max_num_results"] = max_results
|
||||
agent_tools.append(result)
|
||||
elif tool_type == "web_search_preview":
|
||||
ws_tool = cast(WebSearchPreviewTool, tool_dict)
|
||||
additional_properties: dict[str, Any] = {}
|
||||
result = {"type": "web_search_preview"}
|
||||
if user_location := ws_tool.get("user_location"):
|
||||
additional_properties["user_location"] = {
|
||||
result["user_location"] = {
|
||||
"city": user_location.get("city"),
|
||||
"country": user_location.get("country"),
|
||||
"region": user_location.get("region"),
|
||||
"timezone": user_location.get("timezone"),
|
||||
}
|
||||
|
||||
agent_tools.append(HostedWebSearchTool(additional_properties=additional_properties))
|
||||
agent_tools.append(result)
|
||||
else:
|
||||
agent_tools.append(tool_dict)
|
||||
return agent_tools
|
||||
|
||||
|
||||
def to_azure_ai_tools(
|
||||
tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None,
|
||||
tools: Sequence[FunctionTool | MutableMapping[str, Any] | Tool] | None,
|
||||
) -> list[Tool | dict[str, Any]]:
|
||||
"""Converts Agent Framework tools into Azure AI compatible tools.
|
||||
|
||||
Handles FunctionTool instances and passes through SDK Tool types directly.
|
||||
|
||||
Args:
|
||||
tools: A sequence of Agent Framework tool objects or dictionaries
|
||||
tools: A sequence of Agent Framework tool objects, SDK Tool types, or dictionaries
|
||||
defining the tools to be converted. Can be None.
|
||||
|
||||
Returns:
|
||||
@@ -430,133 +364,54 @@ def to_azure_ai_tools(
|
||||
return azure_tools
|
||||
|
||||
for tool in tools:
|
||||
if isinstance(tool, ToolProtocol):
|
||||
match tool:
|
||||
case HostedMCPTool():
|
||||
azure_tools.append(_prepare_mcp_tool_for_azure_ai(tool))
|
||||
case HostedCodeInterpreterTool():
|
||||
file_ids: list[str] = []
|
||||
if tool.inputs:
|
||||
for tool_input in tool.inputs:
|
||||
if tool_input.type == "hosted_file":
|
||||
file_ids.append(tool_input.file_id) # type: ignore[misc, arg-type]
|
||||
container = CodeInterpreterToolAuto(file_ids=file_ids if file_ids else None)
|
||||
ci_tool: CodeInterpreterTool = CodeInterpreterTool(container=container)
|
||||
azure_tools.append(ci_tool)
|
||||
case FunctionTool():
|
||||
params = tool.parameters()
|
||||
params["additionalProperties"] = False
|
||||
azure_tools.append(
|
||||
AzureFunctionTool(
|
||||
name=tool.name,
|
||||
parameters=params,
|
||||
strict=False,
|
||||
description=tool.description,
|
||||
)
|
||||
)
|
||||
case HostedFileSearchTool():
|
||||
if not tool.inputs:
|
||||
raise ValueError("HostedFileSearchTool requires inputs to be specified.")
|
||||
vector_store_ids: list[str] = [
|
||||
inp.vector_store_id # type: ignore[misc]
|
||||
for inp in tool.inputs
|
||||
if inp.type == "hosted_vector_store"
|
||||
]
|
||||
if not vector_store_ids:
|
||||
raise ValueError(
|
||||
"HostedFileSearchTool requires inputs to be of type `Content` with "
|
||||
"type 'hosted_vector_store'."
|
||||
)
|
||||
fs_tool: ProjectsFileSearchTool = ProjectsFileSearchTool(vector_store_ids=vector_store_ids)
|
||||
if tool.max_results:
|
||||
fs_tool["max_num_results"] = tool.max_results
|
||||
azure_tools.append(fs_tool)
|
||||
case HostedWebSearchTool():
|
||||
ws_tool: WebSearchPreviewTool = WebSearchPreviewTool()
|
||||
if tool.additional_properties:
|
||||
location: dict[str, str] | None = (
|
||||
tool.additional_properties.get("user_location", None)
|
||||
if tool.additional_properties
|
||||
else None
|
||||
)
|
||||
if location:
|
||||
ws_tool.user_location = ApproximateLocation(
|
||||
city=location.get("city"),
|
||||
country=location.get("country"),
|
||||
region=location.get("region"),
|
||||
timezone=location.get("timezone"),
|
||||
)
|
||||
azure_tools.append(ws_tool)
|
||||
case HostedImageGenerationTool():
|
||||
opts = tool.options or {}
|
||||
addl = tool.additional_properties or {}
|
||||
# Azure ImageGenTool requires the constant model "gpt-image-1"
|
||||
ig_tool: ImageGenTool = ImageGenTool(
|
||||
model=opts.get("model_id", "gpt-image-1"), # type: ignore
|
||||
size=cast(
|
||||
Literal["1024x1024", "1024x1536", "1536x1024", "auto"] | None, opts.get("image_size")
|
||||
),
|
||||
output_format=cast(Literal["png", "webp", "jpeg"] | None, opts.get("media_type")),
|
||||
input_image_mask=(
|
||||
ImageGenToolInputImageMask(
|
||||
image_url=addl.get("input_image_mask", {}).get("image_url"),
|
||||
file_id=addl.get("input_image_mask", {}).get("file_id"),
|
||||
)
|
||||
if isinstance(addl.get("input_image_mask"), dict)
|
||||
else None
|
||||
),
|
||||
quality=cast(Literal["low", "medium", "high", "auto"] | None, addl.get("quality")),
|
||||
background=cast(Literal["transparent", "opaque", "auto"] | None, addl.get("background")),
|
||||
output_compression=cast(int | None, addl.get("output_compression")),
|
||||
moderation=cast(Literal["auto", "low"] | None, addl.get("moderation")),
|
||||
partial_images=opts.get("streaming_count"),
|
||||
)
|
||||
azure_tools.append(ig_tool)
|
||||
case _:
|
||||
logger.debug("Unsupported tool passed (type: %s)", type(tool))
|
||||
if isinstance(tool, FunctionTool):
|
||||
params = tool.parameters()
|
||||
params["additionalProperties"] = False
|
||||
azure_tools.append(
|
||||
AzureFunctionTool(
|
||||
name=tool.name,
|
||||
parameters=params,
|
||||
strict=False,
|
||||
description=tool.description,
|
||||
)
|
||||
)
|
||||
elif isinstance(tool, Tool):
|
||||
# Pass through SDK Tool types directly (CodeInterpreterTool, FileSearchTool, etc.)
|
||||
azure_tools.append(tool)
|
||||
else:
|
||||
# Handle raw dictionary tools
|
||||
tool_dict = tool if isinstance(tool, dict) else dict(tool)
|
||||
azure_tools.append(tool_dict)
|
||||
# Pass through dict-based tools directly
|
||||
azure_tools.append(dict(tool) if isinstance(tool, MutableMapping) else tool) # type: ignore[arg-type]
|
||||
|
||||
return azure_tools
|
||||
|
||||
|
||||
def _prepare_mcp_tool_for_azure_ai(tool: HostedMCPTool) -> MCPTool:
|
||||
"""Convert HostedMCPTool to Azure AI MCPTool format.
|
||||
def _prepare_mcp_tool_dict_for_azure_ai(tool_dict: dict[str, Any]) -> MCPTool:
|
||||
"""Convert dict-based MCP tool to Azure AI MCPTool format.
|
||||
|
||||
Args:
|
||||
tool: The HostedMCPTool to convert.
|
||||
tool_dict: The dict-based MCP tool configuration.
|
||||
|
||||
Returns:
|
||||
MCPTool: The converted Azure AI MCPTool.
|
||||
"""
|
||||
mcp: MCPTool = MCPTool(server_label=tool.name.replace(" ", "_"), server_url=str(tool.url))
|
||||
server_label = tool_dict.get("server_label", "")
|
||||
server_url = tool_dict.get("server_url", "")
|
||||
mcp: MCPTool = MCPTool(server_label=server_label, server_url=server_url)
|
||||
|
||||
if tool.description:
|
||||
mcp["server_description"] = tool.description
|
||||
if description := tool_dict.get("server_description"):
|
||||
mcp["server_description"] = description
|
||||
|
||||
# Check for project_connection_id in additional_properties (for Azure AI Foundry connections)
|
||||
project_connection_id = _extract_project_connection_id(tool.additional_properties)
|
||||
if project_connection_id:
|
||||
# Check for project_connection_id
|
||||
if project_connection_id := tool_dict.get("project_connection_id"):
|
||||
mcp["project_connection_id"] = project_connection_id
|
||||
elif tool.headers:
|
||||
# Only use headers if no project_connection_id is available
|
||||
# Note: Azure AI Agent Service may reject headers with sensitive info
|
||||
mcp["headers"] = tool.headers
|
||||
elif headers := tool_dict.get("headers"):
|
||||
mcp["headers"] = headers
|
||||
|
||||
if tool.allowed_tools:
|
||||
mcp["allowed_tools"] = list(tool.allowed_tools)
|
||||
if allowed_tools := tool_dict.get("allowed_tools"):
|
||||
mcp["allowed_tools"] = list(allowed_tools)
|
||||
|
||||
if tool.approval_mode:
|
||||
match tool.approval_mode:
|
||||
case str():
|
||||
mcp["require_approval"] = "always" if tool.approval_mode == "always_require" else "never"
|
||||
case _:
|
||||
if always_require_approvals := tool.approval_mode.get("always_require_approval"):
|
||||
mcp["require_approval"] = {"always": {"tool_names": list(always_require_approvals)}}
|
||||
if never_require_approvals := tool.approval_mode.get("never_require_approval"):
|
||||
mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}}
|
||||
if require_approval := tool_dict.get("require_approval"):
|
||||
mcp["require_approval"] = require_approval
|
||||
|
||||
return mcp
|
||||
|
||||
|
||||
@@ -7,11 +7,6 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
from agent_framework import (
|
||||
Agent,
|
||||
Content,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
tool,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
@@ -25,6 +20,7 @@ from azure.identity.aio import AzureCliCredential
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agent_framework_azure_ai import (
|
||||
AzureAIAgentClient,
|
||||
AzureAIAgentsProvider,
|
||||
AzureAISettings,
|
||||
)
|
||||
@@ -466,8 +462,9 @@ def test_as_agent_with_hosted_tools(
|
||||
agent = provider.as_agent(mock_agent)
|
||||
|
||||
assert isinstance(agent, Agent)
|
||||
# Should have HostedCodeInterpreterTool in the default_options tools
|
||||
assert any(isinstance(t, HostedCodeInterpreterTool) for t in (agent.default_options.get("tools") or [])) # type: ignore
|
||||
# Should have code_interpreter dict tool in the default_options tools
|
||||
tools = agent.default_options.get("tools") or []
|
||||
assert any(isinstance(t, dict) and t.get("type") == "code_interpreter" for t in tools)
|
||||
|
||||
|
||||
def test_as_agent_with_dict_function_tools_validates(
|
||||
@@ -571,8 +568,8 @@ def test_to_azure_ai_agent_tools_function() -> None:
|
||||
|
||||
|
||||
def test_to_azure_ai_agent_tools_code_interpreter() -> None:
|
||||
"""Test converting HostedCodeInterpreterTool."""
|
||||
tool = HostedCodeInterpreterTool()
|
||||
"""Test converting code_interpreter dict tool."""
|
||||
tool = AzureAIAgentClient.get_code_interpreter_tool()
|
||||
|
||||
result = to_azure_ai_agent_tools([tool])
|
||||
|
||||
@@ -581,8 +578,8 @@ def test_to_azure_ai_agent_tools_code_interpreter() -> None:
|
||||
|
||||
|
||||
def test_to_azure_ai_agent_tools_file_search() -> None:
|
||||
"""Test converting HostedFileSearchTool with vector stores."""
|
||||
tool = HostedFileSearchTool(inputs=[Content.from_hosted_vector_store(vector_store_id="vs-123")])
|
||||
"""Test converting file_search dict tool with vector stores."""
|
||||
tool = AzureAIAgentClient.get_file_search_tool(vector_store_ids=["vs-123"])
|
||||
run_options: dict[str, Any] = {}
|
||||
|
||||
result = to_azure_ai_agent_tools([tool], run_options)
|
||||
@@ -592,15 +589,14 @@ def test_to_azure_ai_agent_tools_file_search() -> None:
|
||||
|
||||
|
||||
def test_to_azure_ai_agent_tools_web_search_bing_grounding(monkeypatch: Any) -> None:
|
||||
"""Test converting HostedWebSearchTool for Bing Grounding."""
|
||||
"""Test converting web_search dict tool for Bing Grounding."""
|
||||
# Use a properly formatted connection ID as required by Azure SDK
|
||||
valid_conn_id = (
|
||||
"/subscriptions/test-sub/resourceGroups/test-rg/"
|
||||
"providers/Microsoft.CognitiveServices/accounts/test-account/"
|
||||
"projects/test-project/connections/test-connection"
|
||||
)
|
||||
monkeypatch.setenv("BING_CONNECTION_ID", valid_conn_id)
|
||||
tool = HostedWebSearchTool()
|
||||
tool = AzureAIAgentClient.get_web_search_tool(bing_connection_id=valid_conn_id)
|
||||
|
||||
result = to_azure_ai_agent_tools([tool])
|
||||
|
||||
@@ -608,10 +604,11 @@ def test_to_azure_ai_agent_tools_web_search_bing_grounding(monkeypatch: Any) ->
|
||||
|
||||
|
||||
def test_to_azure_ai_agent_tools_web_search_custom(monkeypatch: Any) -> None:
|
||||
"""Test converting HostedWebSearchTool for Custom Bing Search."""
|
||||
monkeypatch.setenv("BING_CUSTOM_CONNECTION_ID", "custom-conn-id")
|
||||
monkeypatch.setenv("BING_CUSTOM_INSTANCE_NAME", "my-instance")
|
||||
tool = HostedWebSearchTool()
|
||||
"""Test converting web_search dict tool for Custom Bing Search."""
|
||||
tool = AzureAIAgentClient.get_web_search_tool(
|
||||
bing_custom_connection_id="custom-conn-id",
|
||||
bing_custom_instance_id="my-instance",
|
||||
)
|
||||
|
||||
result = to_azure_ai_agent_tools([tool])
|
||||
|
||||
@@ -619,22 +616,23 @@ def test_to_azure_ai_agent_tools_web_search_custom(monkeypatch: Any) -> None:
|
||||
|
||||
|
||||
def test_to_azure_ai_agent_tools_web_search_missing_config(monkeypatch: Any) -> None:
|
||||
"""Test converting HostedWebSearchTool raises error when config is missing."""
|
||||
"""Test converting web_search dict tool without bing config returns empty."""
|
||||
monkeypatch.delenv("BING_CONNECTION_ID", raising=False)
|
||||
monkeypatch.delenv("BING_CUSTOM_CONNECTION_ID", raising=False)
|
||||
monkeypatch.delenv("BING_CUSTOM_INSTANCE_NAME", raising=False)
|
||||
tool = HostedWebSearchTool()
|
||||
tool = {"type": "web_search"}
|
||||
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
to_azure_ai_agent_tools([tool])
|
||||
result = to_azure_ai_agent_tools([tool])
|
||||
|
||||
# web_search without bing connection is passed through as dict
|
||||
assert len(result) == 1
|
||||
|
||||
|
||||
def test_to_azure_ai_agent_tools_mcp() -> None:
|
||||
"""Test converting HostedMCPTool."""
|
||||
tool = HostedMCPTool(
|
||||
"""Test converting MCP dict tool."""
|
||||
tool = AzureAIAgentClient.get_mcp_tool(
|
||||
name="my mcp server",
|
||||
url="https://mcp.example.com",
|
||||
allowed_tools=["tool1", "tool2"],
|
||||
)
|
||||
|
||||
result = to_azure_ai_agent_tools([tool])
|
||||
@@ -653,13 +651,15 @@ def test_to_azure_ai_agent_tools_dict_passthrough() -> None:
|
||||
|
||||
|
||||
def test_to_azure_ai_agent_tools_unsupported_type() -> None:
|
||||
"""Test that unsupported tool types raise error."""
|
||||
"""Test that unsupported tool types pass through unchanged."""
|
||||
|
||||
class UnsupportedTool:
|
||||
pass
|
||||
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
to_azure_ai_agent_tools([UnsupportedTool()]) # type: ignore
|
||||
unsupported = UnsupportedTool()
|
||||
result = to_azure_ai_agent_tools([unsupported]) # type: ignore
|
||||
assert len(result) == 1
|
||||
assert result[0] is unsupported # Passed through unchanged
|
||||
|
||||
|
||||
# endregion
|
||||
@@ -684,7 +684,7 @@ def test_from_azure_ai_agent_tools_code_interpreter() -> None:
|
||||
result = from_azure_ai_agent_tools([tool])
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], HostedCodeInterpreterTool)
|
||||
assert result[0] == {"type": "code_interpreter"}
|
||||
|
||||
|
||||
def test_from_azure_ai_agent_tools_code_interpreter_dict() -> None:
|
||||
@@ -694,7 +694,7 @@ def test_from_azure_ai_agent_tools_code_interpreter_dict() -> None:
|
||||
result = from_azure_ai_agent_tools([tool])
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], HostedCodeInterpreterTool)
|
||||
assert result[0] == {"type": "code_interpreter"}
|
||||
|
||||
|
||||
def test_from_azure_ai_agent_tools_file_search_dict() -> None:
|
||||
@@ -707,8 +707,8 @@ def test_from_azure_ai_agent_tools_file_search_dict() -> None:
|
||||
result = from_azure_ai_agent_tools([tool])
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], HostedFileSearchTool)
|
||||
assert len(result[0].inputs or []) == 2
|
||||
assert result[0]["type"] == "file_search"
|
||||
assert result[0]["vector_store_ids"] == ["vs-123", "vs-456"]
|
||||
|
||||
|
||||
def test_from_azure_ai_agent_tools_bing_grounding_dict() -> None:
|
||||
@@ -721,12 +721,8 @@ def test_from_azure_ai_agent_tools_bing_grounding_dict() -> None:
|
||||
result = from_azure_ai_agent_tools([tool])
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], HostedWebSearchTool)
|
||||
|
||||
additional_properties = result[0].additional_properties
|
||||
|
||||
assert additional_properties
|
||||
assert additional_properties.get("connection_id") == "conn-123"
|
||||
assert result[0]["type"] == "bing_grounding"
|
||||
assert result[0]["connection_id"] == "conn-123"
|
||||
|
||||
|
||||
def test_from_azure_ai_agent_tools_bing_custom_search_dict() -> None:
|
||||
@@ -742,11 +738,9 @@ def test_from_azure_ai_agent_tools_bing_custom_search_dict() -> None:
|
||||
result = from_azure_ai_agent_tools([tool])
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], HostedWebSearchTool)
|
||||
additional_properties = result[0].additional_properties
|
||||
|
||||
assert additional_properties
|
||||
assert additional_properties.get("custom_connection_id") == "custom-conn"
|
||||
assert result[0]["type"] == "bing_custom_search"
|
||||
assert result[0]["connection_id"] == "custom-conn"
|
||||
assert result[0]["instance_name"] == "my-instance"
|
||||
|
||||
|
||||
def test_from_azure_ai_agent_tools_mcp_dict() -> None:
|
||||
|
||||
@@ -16,10 +16,6 @@ from agent_framework import (
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
Message,
|
||||
SupportsChatGetResponse,
|
||||
tool,
|
||||
@@ -721,147 +717,130 @@ def test_azure_ai_chat_client_service_url_method(mock_agents_client: MagicMock)
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prepare_options_mcp_never_require(mock_agents_client: MagicMock) -> None:
|
||||
"""Test _prepare_options with HostedMCPTool having never_require approval mode."""
|
||||
"""Test _prepare_options with MCP dict tool having never_require approval mode."""
|
||||
client = create_test_azure_ai_chat_client(mock_agents_client)
|
||||
|
||||
mcp_tool = HostedMCPTool(name="Test MCP Tool", url="https://example.com/mcp", approval_mode="never_require")
|
||||
|
||||
messages = [Message(role="user", text="Hello")]
|
||||
chat_options: ChatOptions = {"tools": [mcp_tool], "tool_choice": "auto"}
|
||||
|
||||
with patch("agent_framework_azure_ai._shared.McpTool") as mock_mcp_tool_class:
|
||||
mock_mcp_tool_instance = MagicMock()
|
||||
mock_mcp_tool_instance.definitions = [{"type": "mcp", "name": "test_mcp"}]
|
||||
mock_mcp_tool_class.return_value = mock_mcp_tool_instance
|
||||
|
||||
run_options, _ = await client._prepare_options(messages, chat_options) # type: ignore
|
||||
|
||||
# Verify tool_resources is created with correct MCP approval structure
|
||||
assert "tool_resources" in run_options, (
|
||||
f"Expected 'tool_resources' in run_options keys: {list(run_options.keys())}"
|
||||
)
|
||||
assert "mcp" in run_options["tool_resources"]
|
||||
assert len(run_options["tool_resources"]["mcp"]) == 1
|
||||
|
||||
mcp_resource = run_options["tool_resources"]["mcp"][0]
|
||||
assert mcp_resource["server_label"] == "Test_MCP_Tool"
|
||||
assert mcp_resource["require_approval"] == "never"
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prepare_options_mcp_with_headers(mock_agents_client: MagicMock) -> None:
|
||||
"""Test _prepare_options with HostedMCPTool having headers."""
|
||||
client = create_test_azure_ai_chat_client(mock_agents_client)
|
||||
|
||||
# Test with headers
|
||||
headers = {"Authorization": "Bearer DUMMY_TOKEN", "X-API-Key": "DUMMY_KEY"}
|
||||
mcp_tool = HostedMCPTool(
|
||||
name="Test MCP Tool", url="https://example.com/mcp", headers=headers, approval_mode="never_require"
|
||||
# Create MCP tool with approval_mode parameter
|
||||
mcp_tool = AzureAIAgentClient.get_mcp_tool(
|
||||
name="Test MCP Tool", url="https://example.com/mcp", approval_mode="never_require"
|
||||
)
|
||||
|
||||
messages = [Message(role="user", text="Hello")]
|
||||
chat_options: ChatOptions = {"tools": [mcp_tool], "tool_choice": "auto"}
|
||||
|
||||
with patch("agent_framework_azure_ai._shared.McpTool") as mock_mcp_tool_class:
|
||||
mock_mcp_tool_instance = MagicMock()
|
||||
mock_mcp_tool_instance.definitions = [{"type": "mcp", "name": "test_mcp"}]
|
||||
mock_mcp_tool_class.return_value = mock_mcp_tool_instance
|
||||
run_options, _ = await client._prepare_options(messages, chat_options) # type: ignore
|
||||
|
||||
run_options, _ = await client._prepare_options(messages, chat_options) # type: ignore
|
||||
# Verify tool_resources is created with correct MCP approval structure
|
||||
assert "tool_resources" in run_options, f"Expected 'tool_resources' in run_options keys: {list(run_options.keys())}"
|
||||
assert "mcp" in run_options["tool_resources"]
|
||||
assert len(run_options["tool_resources"]["mcp"]) == 1
|
||||
|
||||
# Verify tool_resources is created with headers
|
||||
assert "tool_resources" in run_options
|
||||
assert "mcp" in run_options["tool_resources"]
|
||||
assert len(run_options["tool_resources"]["mcp"]) == 1
|
||||
mcp_resource = run_options["tool_resources"]["mcp"][0]
|
||||
assert mcp_resource["server_label"] == "Test_MCP_Tool"
|
||||
assert mcp_resource["require_approval"] == "never"
|
||||
|
||||
mcp_resource = run_options["tool_resources"]["mcp"][0]
|
||||
assert mcp_resource["server_label"] == "Test_MCP_Tool"
|
||||
assert mcp_resource["require_approval"] == "never"
|
||||
assert mcp_resource["headers"] == headers
|
||||
|
||||
async def test_azure_ai_chat_client_prepare_options_mcp_with_headers(mock_agents_client: MagicMock) -> None:
|
||||
"""Test _prepare_options with MCP dict tool having headers."""
|
||||
client = create_test_azure_ai_chat_client(mock_agents_client)
|
||||
|
||||
# Test with headers - create MCP tool with all options
|
||||
headers = {"Authorization": "Bearer DUMMY_TOKEN", "X-API-Key": "DUMMY_KEY"}
|
||||
mcp_tool = AzureAIAgentClient.get_mcp_tool(
|
||||
name="Test MCP Tool",
|
||||
url="https://example.com/mcp",
|
||||
headers=headers,
|
||||
approval_mode="never_require",
|
||||
)
|
||||
|
||||
messages = [Message(role="user", text="Hello")]
|
||||
chat_options: ChatOptions = {"tools": [mcp_tool], "tool_choice": "auto"}
|
||||
|
||||
run_options, _ = await client._prepare_options(messages, chat_options) # type: ignore
|
||||
|
||||
# Verify tool_resources is created with headers
|
||||
assert "tool_resources" in run_options
|
||||
assert "mcp" in run_options["tool_resources"]
|
||||
assert len(run_options["tool_resources"]["mcp"]) == 1
|
||||
|
||||
mcp_resource = run_options["tool_resources"]["mcp"][0]
|
||||
assert mcp_resource["server_label"] == "Test_MCP_Tool"
|
||||
assert mcp_resource["require_approval"] == "never"
|
||||
assert mcp_resource["headers"] == headers
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_web_search_bing_grounding(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prepare_tools_for_azure_ai with HostedWebSearchTool using Bing Grounding."""
|
||||
"""Test _prepare_tools_for_azure_ai with BingGroundingTool from get_web_search_tool()."""
|
||||
|
||||
client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
web_search_tool = HostedWebSearchTool(
|
||||
additional_properties={
|
||||
"connection_id": "test-connection-id",
|
||||
"count": 5,
|
||||
"freshness": "Day",
|
||||
"market": "en-US",
|
||||
"set_lang": "en",
|
||||
}
|
||||
)
|
||||
|
||||
# Mock BingGroundingTool
|
||||
# Mock BingGroundingTool to avoid SDK validation of connection ID
|
||||
with patch("agent_framework_azure_ai._chat_client.BingGroundingTool") as mock_bing_grounding:
|
||||
mock_bing_tool = MagicMock()
|
||||
mock_bing_tool.definitions = [{"type": "bing_grounding"}]
|
||||
mock_bing_grounding.return_value = mock_bing_tool
|
||||
|
||||
# get_web_search_tool now returns a BingGroundingTool directly
|
||||
web_search_tool = client.get_web_search_tool(bing_connection_id="test-connection-id")
|
||||
|
||||
# Verify the factory method created the tool with correct args
|
||||
mock_bing_grounding.assert_called_once_with(connection_id="test-connection-id")
|
||||
|
||||
result = await client._prepare_tools_for_azure_ai([web_search_tool]) # type: ignore
|
||||
|
||||
# BingGroundingTool.definitions should be extended into result
|
||||
assert len(result) == 1
|
||||
assert result[0] == {"type": "bing_grounding"}
|
||||
call_args = mock_bing_grounding.call_args[1]
|
||||
assert call_args["count"] == 5
|
||||
assert call_args["freshness"] == "Day"
|
||||
assert call_args["market"] == "en-US"
|
||||
assert call_args["set_lang"] == "en"
|
||||
assert "connection_id" in call_args
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_web_search_bing_grounding_with_connection_id(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prepare_tools_... with HostedWebSearchTool using Bing Grounding with connection_id (no HTTP call)."""
|
||||
"""Test _prepare_tools_for_azure_ai with BingGroundingTool using explicit connection_id."""
|
||||
|
||||
client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
web_search_tool = HostedWebSearchTool(
|
||||
additional_properties={
|
||||
"connection_id": "direct-connection-id",
|
||||
"count": 3,
|
||||
}
|
||||
)
|
||||
|
||||
# Mock BingGroundingTool
|
||||
# Mock BingGroundingTool to avoid SDK validation of connection ID
|
||||
with patch("agent_framework_azure_ai._chat_client.BingGroundingTool") as mock_bing_grounding:
|
||||
mock_bing_tool = MagicMock()
|
||||
mock_bing_tool.definitions = [{"type": "bing_grounding"}]
|
||||
mock_bing_grounding.return_value = mock_bing_tool
|
||||
|
||||
web_search_tool = client.get_web_search_tool(bing_connection_id="direct-connection-id")
|
||||
|
||||
mock_bing_grounding.assert_called_once_with(connection_id="direct-connection-id")
|
||||
|
||||
result = await client._prepare_tools_for_azure_ai([web_search_tool]) # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == {"type": "bing_grounding"}
|
||||
mock_bing_grounding.assert_called_once_with(connection_id="direct-connection-id", count=3)
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_web_search_custom_bing(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prepare_tools_for_azure_ai with HostedWebSearchTool using Custom Bing Search."""
|
||||
"""Test _prepare_tools_for_azure_ai with BingCustomSearchTool from get_web_search_tool()."""
|
||||
|
||||
client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
web_search_tool = HostedWebSearchTool(
|
||||
additional_properties={
|
||||
"custom_connection_id": "custom-connection-id",
|
||||
"custom_instance_name": "custom-instance",
|
||||
"count": 10,
|
||||
}
|
||||
)
|
||||
|
||||
# Mock BingCustomSearchTool
|
||||
# Mock BingCustomSearchTool to avoid SDK validation
|
||||
with patch("agent_framework_azure_ai._chat_client.BingCustomSearchTool") as mock_custom_bing:
|
||||
mock_custom_tool = MagicMock()
|
||||
mock_custom_tool.definitions = [{"type": "bing_custom_search"}]
|
||||
mock_custom_bing.return_value = mock_custom_tool
|
||||
|
||||
web_search_tool = client.get_web_search_tool(
|
||||
bing_custom_connection_id="custom-connection-id",
|
||||
bing_custom_instance_id="custom-instance",
|
||||
)
|
||||
|
||||
mock_custom_bing.assert_called_once_with(
|
||||
connection_id="custom-connection-id",
|
||||
instance_name="custom-instance",
|
||||
)
|
||||
|
||||
result = await client._prepare_tools_for_azure_ai([web_search_tool]) # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
@@ -871,27 +850,19 @@ async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_web_search_custom
|
||||
async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_file_search_with_vector_stores(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prepare_tools_for_azure_ai with HostedFileSearchTool using vector stores."""
|
||||
"""Test _prepare_tools_for_azure_ai with FileSearchTool from get_file_search_tool()."""
|
||||
|
||||
client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
vector_store_input = Content.from_hosted_vector_store(vector_store_id="vs-123")
|
||||
file_search_tool = HostedFileSearchTool(inputs=[vector_store_input])
|
||||
# get_file_search_tool() now returns a FileSearchTool instance directly
|
||||
file_search_tool = client.get_file_search_tool(vector_store_ids=["vs-123"])
|
||||
|
||||
# Mock FileSearchTool
|
||||
with patch("agent_framework_azure_ai._chat_client.FileSearchTool") as mock_file_search:
|
||||
mock_file_tool = MagicMock()
|
||||
mock_file_tool.definitions = [{"type": "file_search"}]
|
||||
mock_file_tool.resources = {"vector_store_ids": ["vs-123"]}
|
||||
mock_file_search.return_value = mock_file_tool
|
||||
run_options: dict[str, Any] = {}
|
||||
result = await client._prepare_tools_for_azure_ai([file_search_tool], run_options) # type: ignore
|
||||
|
||||
run_options = {}
|
||||
result = await client._prepare_tools_for_azure_ai([file_search_tool], run_options) # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == {"type": "file_search"}
|
||||
assert run_options["tool_resources"] == {"vector_store_ids": ["vs-123"]}
|
||||
mock_file_search.assert_called_once_with(vector_store_ids=["vs-123"])
|
||||
assert len(result) == 1
|
||||
assert result[0] == {"type": "file_search"}
|
||||
assert run_options["tool_resources"] == {"file_search": {"vector_store_ids": ["vs-123"]}}
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_create_agent_stream_submit_tool_approvals(
|
||||
@@ -1615,7 +1586,7 @@ async def test_azure_ai_chat_client_agent_code_interpreter():
|
||||
async with Agent(
|
||||
client=AzureAIAgentClient(credential=AzureCliCredential()),
|
||||
instructions="You are a helpful assistant that can write and execute Python code.",
|
||||
tools=[HostedCodeInterpreterTool()],
|
||||
tools=[AzureAIAgentClient.get_code_interpreter_tool()],
|
||||
) as agent:
|
||||
# Request code execution
|
||||
response = await agent.run("Write Python code to calculate the factorial of 5 and show the result.")
|
||||
@@ -1645,9 +1616,7 @@ async def test_azure_ai_chat_client_agent_file_search():
|
||||
)
|
||||
|
||||
# 2. Create file search tool with uploaded resources
|
||||
file_search_tool = HostedFileSearchTool(
|
||||
inputs=[Content.from_hosted_vector_store(vector_store_id=vector_store.id)]
|
||||
)
|
||||
file_search_tool = AzureAIAgentClient.get_file_search_tool(vector_store_ids=[vector_store.id])
|
||||
|
||||
async with Agent(
|
||||
client=client,
|
||||
@@ -1679,9 +1648,9 @@ async def test_azure_ai_chat_client_agent_file_search():
|
||||
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_azure_ai_chat_client_agent_hosted_mcp_tool() -> None:
|
||||
"""Integration test for HostedMCPTool with Azure AI Agent using Microsoft Learn MCP."""
|
||||
"""Integration test for MCP tool with Azure AI Agent using Microsoft Learn MCP."""
|
||||
|
||||
mcp_tool = HostedMCPTool(
|
||||
mcp_tool = AzureAIAgentClient.get_mcp_tool(
|
||||
name="Microsoft Learn MCP",
|
||||
url="https://learn.microsoft.com/api/mcp",
|
||||
description="A Microsoft Learn MCP server for documentation questions",
|
||||
@@ -2066,11 +2035,11 @@ def test_azure_ai_chat_client_prepare_mcp_resources_with_dict_approval_mode(
|
||||
"""Test _prepare_mcp_resources with dict-based approval mode (always_require_approval)."""
|
||||
client = create_test_azure_ai_chat_client(mock_agents_client)
|
||||
|
||||
# MCP tool with dict-based approval mode
|
||||
mcp_tool = HostedMCPTool(
|
||||
# MCP tool with dict-based approval mode - use approval_mode parameter
|
||||
mcp_tool = AzureAIAgentClient.get_mcp_tool(
|
||||
name="Test MCP",
|
||||
url="https://example.com/mcp",
|
||||
approval_mode={"always_require_approval": {"tool1", "tool2"}},
|
||||
approval_mode={"always_require_approval": ["tool1", "tool2"]},
|
||||
)
|
||||
|
||||
result = client._prepare_mcp_resources([mcp_tool]) # type: ignore
|
||||
@@ -2078,7 +2047,6 @@ def test_azure_ai_chat_client_prepare_mcp_resources_with_dict_approval_mode(
|
||||
assert len(result) == 1
|
||||
assert result[0]["server_label"] == "Test_MCP"
|
||||
assert "require_approval" in result[0]
|
||||
assert result[0]["require_approval"] == {"always": {"tool1", "tool2"}}
|
||||
|
||||
|
||||
def test_azure_ai_chat_client_prepare_mcp_resources_with_never_require_dict(
|
||||
@@ -2087,17 +2055,17 @@ def test_azure_ai_chat_client_prepare_mcp_resources_with_never_require_dict(
|
||||
"""Test _prepare_mcp_resources with dict-based approval mode (never_require_approval)."""
|
||||
client = create_test_azure_ai_chat_client(mock_agents_client)
|
||||
|
||||
# MCP tool with never_require_approval dict
|
||||
mcp_tool = HostedMCPTool(
|
||||
# MCP tool with never require approval - use approval_mode parameter
|
||||
mcp_tool = AzureAIAgentClient.get_mcp_tool(
|
||||
name="Test MCP",
|
||||
url="https://example.com/mcp",
|
||||
approval_mode={"never_require_approval": {"safe_tool"}},
|
||||
approval_mode={"never_require_approval": ["safe_tool"]},
|
||||
)
|
||||
|
||||
result = client._prepare_mcp_resources([mcp_tool]) # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0]["require_approval"] == {"never": {"safe_tool"}}
|
||||
assert "require_approval" in result[0]
|
||||
|
||||
|
||||
def test_azure_ai_chat_client_prepare_messages_with_function_result(
|
||||
@@ -2140,13 +2108,12 @@ def test_azure_ai_chat_client_prepare_messages_with_raw_content_block(
|
||||
async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_mcp_tool(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prepare_tools_for_azure_ai with HostedMCPTool."""
|
||||
"""Test _prepare_tools_for_azure_ai with MCP dict tool."""
|
||||
client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
mcp_tool = HostedMCPTool(
|
||||
mcp_tool = AzureAIAgentClient.get_mcp_tool(
|
||||
name="Test MCP Server",
|
||||
url="https://example.com/mcp",
|
||||
allowed_tools=["tool1", "tool2"],
|
||||
)
|
||||
|
||||
tool_definitions = await client._prepare_tools_for_azure_ai([mcp_tool]) # type: ignore
|
||||
@@ -2191,14 +2158,16 @@ async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_dict_passthrough(
|
||||
async def test_azure_ai_chat_client_prepare_tools_for_azure_ai_unsupported_type(
|
||||
mock_agents_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prepare_tools_for_azure_ai raises error for unsupported tool type."""
|
||||
"""Test _prepare_tools_for_azure_ai passes through unsupported tool types."""
|
||||
client = create_test_azure_ai_chat_client(mock_agents_client, agent_id="test-agent")
|
||||
|
||||
# Pass an unsupported tool type
|
||||
# Pass an unsupported tool type - it should be passed through unchanged
|
||||
class UnsupportedTool:
|
||||
pass
|
||||
|
||||
unsupported_tool = UnsupportedTool()
|
||||
|
||||
with pytest.raises(ServiceInitializationError, match="Unsupported tool type"):
|
||||
await client._prepare_tools_for_azure_ai([unsupported_tool]) # type: ignore
|
||||
# Unsupported tools are now passed through unchanged (server will reject if invalid)
|
||||
tool_definitions = await client._prepare_tools_for_azure_ai([unsupported_tool]) # type: ignore
|
||||
assert len(tool_definitions) == 1
|
||||
assert tool_definitions[0] is unsupported_tool
|
||||
|
||||
@@ -16,10 +16,6 @@ from agent_framework import (
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
Content,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
Message,
|
||||
SupportsChatGetResponse,
|
||||
tool,
|
||||
@@ -31,6 +27,7 @@ from azure.ai.projects.models import (
|
||||
CodeInterpreterTool,
|
||||
CodeInterpreterToolAuto,
|
||||
FileSearchTool,
|
||||
ImageGenTool,
|
||||
MCPTool,
|
||||
ResponseTextFormatConfigurationJsonSchema,
|
||||
WebSearchPreviewTool,
|
||||
@@ -1100,178 +1097,50 @@ def test_get_conversation_id_with_parsed_response_no_conversation() -> None:
|
||||
assert result == "resp_parsed_12345"
|
||||
|
||||
|
||||
def test_prepare_mcp_tool_basic() -> None:
|
||||
"""Test _prepare_mcp_tool with basic HostedMCPTool."""
|
||||
mcp_tool = HostedMCPTool(
|
||||
name="Test MCP Server",
|
||||
url="https://example.com/mcp",
|
||||
)
|
||||
|
||||
result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore
|
||||
|
||||
assert result["server_label"] == "Test_MCP_Server"
|
||||
assert result["server_url"] == "https://example.com/mcp"
|
||||
# region MCP Tool Dict Tests
|
||||
# These tests verify that dict-based MCP tools are processed correctly by from_azure_ai_tools
|
||||
|
||||
|
||||
def test_prepare_mcp_tool_with_description() -> None:
|
||||
"""Test _prepare_mcp_tool with description."""
|
||||
mcp_tool = HostedMCPTool(
|
||||
name="Test MCP",
|
||||
url="https://example.com/mcp",
|
||||
description="A test MCP server",
|
||||
)
|
||||
|
||||
result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore
|
||||
|
||||
assert result["server_description"] == "A test MCP server"
|
||||
|
||||
|
||||
def test_prepare_mcp_tool_with_project_connection_id() -> None:
|
||||
"""Test _prepare_mcp_tool with project_connection_id in additional_properties."""
|
||||
mcp_tool = HostedMCPTool(
|
||||
name="Test MCP",
|
||||
url="https://example.com/mcp",
|
||||
additional_properties={"project_connection_id": "conn-123"},
|
||||
)
|
||||
|
||||
result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore
|
||||
|
||||
assert result["project_connection_id"] == "conn-123"
|
||||
assert "headers" not in result # headers should not be set when project_connection_id is present
|
||||
|
||||
|
||||
def test_prepare_mcp_tool_with_headers() -> None:
|
||||
"""Test _prepare_mcp_tool with headers (no project_connection_id)."""
|
||||
mcp_tool = HostedMCPTool(
|
||||
name="Test MCP",
|
||||
url="https://example.com/mcp",
|
||||
headers={"Authorization": "Bearer token123"},
|
||||
)
|
||||
|
||||
result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore
|
||||
|
||||
assert result["headers"] == {"Authorization": "Bearer token123"}
|
||||
|
||||
|
||||
def test_prepare_mcp_tool_with_allowed_tools() -> None:
|
||||
"""Test _prepare_mcp_tool with allowed_tools."""
|
||||
mcp_tool = HostedMCPTool(
|
||||
name="Test MCP",
|
||||
url="https://example.com/mcp",
|
||||
allowed_tools=["tool1", "tool2"],
|
||||
)
|
||||
|
||||
result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore
|
||||
|
||||
assert set(result["allowed_tools"]) == {"tool1", "tool2"}
|
||||
|
||||
|
||||
def test_prepare_mcp_tool_with_approval_mode_always_require() -> None:
|
||||
"""Test _prepare_mcp_tool with string approval_mode 'always_require'."""
|
||||
mcp_tool = HostedMCPTool(
|
||||
name="Test MCP",
|
||||
url="https://example.com/mcp",
|
||||
approval_mode="always_require",
|
||||
)
|
||||
|
||||
result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore
|
||||
|
||||
assert result["require_approval"] == "always"
|
||||
|
||||
|
||||
def test_prepare_mcp_tool_with_approval_mode_never_require() -> None:
|
||||
"""Test _prepare_mcp_tool with string approval_mode 'never_require'."""
|
||||
mcp_tool = HostedMCPTool(
|
||||
name="Test MCP",
|
||||
url="https://example.com/mcp",
|
||||
approval_mode="never_require",
|
||||
)
|
||||
|
||||
result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore
|
||||
|
||||
assert result["require_approval"] == "never"
|
||||
|
||||
|
||||
def test_prepare_mcp_tool_with_dict_approval_mode_always() -> None:
|
||||
"""Test _prepare_mcp_tool with dict approval_mode containing always_require_approval."""
|
||||
mcp_tool = HostedMCPTool(
|
||||
name="Test MCP",
|
||||
url="https://example.com/mcp",
|
||||
approval_mode={"always_require_approval": {"dangerous_tool", "risky_tool"}},
|
||||
)
|
||||
|
||||
result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore
|
||||
|
||||
assert "require_approval" in result
|
||||
assert "always" in result["require_approval"]
|
||||
assert set(result["require_approval"]["always"]["tool_names"]) == {"dangerous_tool", "risky_tool"}
|
||||
|
||||
|
||||
def test_prepare_mcp_tool_with_dict_approval_mode_never() -> None:
|
||||
"""Test _prepare_mcp_tool with dict approval_mode containing never_require_approval."""
|
||||
mcp_tool = HostedMCPTool(
|
||||
name="Test MCP",
|
||||
url="https://example.com/mcp",
|
||||
approval_mode={"never_require_approval": {"safe_tool"}},
|
||||
)
|
||||
|
||||
result = AzureAIClient._prepare_mcp_tool(mcp_tool) # type: ignore
|
||||
|
||||
assert "require_approval" in result
|
||||
assert "never" in result["require_approval"]
|
||||
assert set(result["require_approval"]["never"]["tool_names"]) == {"safe_tool"}
|
||||
|
||||
|
||||
def test_from_azure_ai_tools() -> None:
|
||||
"""Test from_azure_ai_tools."""
|
||||
# Test MCP tool
|
||||
def test_from_azure_ai_tools_mcp() -> None:
|
||||
"""Test from_azure_ai_tools with MCP tool."""
|
||||
mcp_tool = MCPTool(server_label="test_server", server_url="http://localhost:8080")
|
||||
parsed_tools = from_azure_ai_tools([mcp_tool])
|
||||
assert len(parsed_tools) == 1
|
||||
assert isinstance(parsed_tools[0], HostedMCPTool)
|
||||
assert parsed_tools[0].name == "test server"
|
||||
assert str(parsed_tools[0].url).rstrip("/") == "http://localhost:8080"
|
||||
assert parsed_tools[0]["type"] == "mcp"
|
||||
assert parsed_tools[0]["server_label"] == "test_server"
|
||||
assert parsed_tools[0]["server_url"] == "http://localhost:8080"
|
||||
|
||||
# Test Code Interpreter tool
|
||||
|
||||
def test_from_azure_ai_tools_code_interpreter() -> None:
|
||||
"""Test from_azure_ai_tools with Code Interpreter tool."""
|
||||
ci_tool = CodeInterpreterTool(container=CodeInterpreterToolAuto(file_ids=["file-1"]))
|
||||
parsed_tools = from_azure_ai_tools([ci_tool])
|
||||
assert len(parsed_tools) == 1
|
||||
assert isinstance(parsed_tools[0], HostedCodeInterpreterTool)
|
||||
assert parsed_tools[0].inputs is not None
|
||||
assert len(parsed_tools[0].inputs) == 1
|
||||
assert parsed_tools[0]["type"] == "code_interpreter"
|
||||
|
||||
tool_input = parsed_tools[0].inputs[0]
|
||||
|
||||
assert tool_input and tool_input.type == "hosted_file" and tool_input.file_id == "file-1"
|
||||
|
||||
# Test File Search tool
|
||||
def test_from_azure_ai_tools_file_search() -> None:
|
||||
"""Test from_azure_ai_tools with File Search tool."""
|
||||
fs_tool = FileSearchTool(vector_store_ids=["vs-1"], max_num_results=5)
|
||||
parsed_tools = from_azure_ai_tools([fs_tool])
|
||||
assert len(parsed_tools) == 1
|
||||
assert isinstance(parsed_tools[0], HostedFileSearchTool)
|
||||
assert parsed_tools[0].inputs is not None
|
||||
assert len(parsed_tools[0].inputs) == 1
|
||||
assert parsed_tools[0]["type"] == "file_search"
|
||||
assert parsed_tools[0]["vector_store_ids"] == ["vs-1"]
|
||||
assert parsed_tools[0]["max_num_results"] == 5
|
||||
|
||||
tool_input = parsed_tools[0].inputs[0]
|
||||
|
||||
assert tool_input and tool_input.type == "hosted_vector_store" and tool_input.vector_store_id == "vs-1"
|
||||
assert parsed_tools[0].max_results == 5
|
||||
|
||||
# Test Web Search tool
|
||||
def test_from_azure_ai_tools_web_search() -> None:
|
||||
"""Test from_azure_ai_tools with Web Search tool."""
|
||||
ws_tool = WebSearchPreviewTool(
|
||||
user_location=ApproximateLocation(city="Seattle", country="US", region="WA", timezone="PST")
|
||||
)
|
||||
parsed_tools = from_azure_ai_tools([ws_tool])
|
||||
assert len(parsed_tools) == 1
|
||||
assert isinstance(parsed_tools[0], HostedWebSearchTool)
|
||||
assert parsed_tools[0].additional_properties
|
||||
assert parsed_tools[0]["type"] == "web_search_preview"
|
||||
assert parsed_tools[0]["user_location"]["city"] == "Seattle"
|
||||
|
||||
user_location = parsed_tools[0].additional_properties["user_location"]
|
||||
|
||||
assert user_location["city"] == "Seattle"
|
||||
assert user_location["country"] == "US"
|
||||
assert user_location["region"] == "WA"
|
||||
assert user_location["timezone"] == "PST"
|
||||
# endregion
|
||||
|
||||
|
||||
# region Integration Tests
|
||||
@@ -1535,7 +1404,7 @@ async def test_integration_web_search() -> None:
|
||||
"messages": "Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.",
|
||||
"options": {
|
||||
"tool_choice": "auto",
|
||||
"tools": [HostedWebSearchTool()],
|
||||
"tools": [client.get_web_search_tool()],
|
||||
},
|
||||
}
|
||||
if streaming:
|
||||
@@ -1550,17 +1419,11 @@ async def test_integration_web_search() -> None:
|
||||
assert "Zoey" in response.text
|
||||
|
||||
# Test that the client will use the web search tool with location
|
||||
additional_properties = {
|
||||
"user_location": {
|
||||
"country": "US",
|
||||
"city": "Seattle",
|
||||
}
|
||||
}
|
||||
content = {
|
||||
"messages": "What is the current weather? Do not ask for my current location.",
|
||||
"options": {
|
||||
"tool_choice": "auto",
|
||||
"tools": [HostedWebSearchTool(additional_properties=additional_properties)],
|
||||
"tools": [client.get_web_search_tool(user_location={"country": "US", "city": "Seattle"})],
|
||||
},
|
||||
}
|
||||
if streaming:
|
||||
@@ -1573,14 +1436,14 @@ async def test_integration_web_search() -> None:
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_integration_agent_hosted_mcp_tool() -> None:
|
||||
"""Integration test for HostedMCPTool with Azure Response Agent using Microsoft Learn MCP."""
|
||||
"""Integration test for MCP tool with Azure Response Agent using Microsoft Learn MCP."""
|
||||
async with temporary_chat_client(agent_name="af-int-test-mcp") as client:
|
||||
response = await client.get_response(
|
||||
"How to create an Azure storage account using az cli?",
|
||||
options={
|
||||
# this needs to be high enough to handle the full MCP tool response.
|
||||
"max_tokens": 5000,
|
||||
"tools": HostedMCPTool(
|
||||
"tools": client.get_mcp_tool(
|
||||
name="Microsoft Learn MCP",
|
||||
url="https://learn.microsoft.com/api/mcp",
|
||||
description="A Microsoft Learn MCP server for documentation questions",
|
||||
@@ -1597,12 +1460,12 @@ async def test_integration_agent_hosted_mcp_tool() -> None:
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_integration_agent_hosted_code_interpreter_tool():
|
||||
"""Test Azure Responses Client agent with HostedCodeInterpreterTool through AzureAIClient."""
|
||||
"""Test Azure Responses Client agent with code interpreter tool through AzureAIClient."""
|
||||
async with temporary_chat_client(agent_name="af-int-test-code-interpreter") as client:
|
||||
response = await client.get_response(
|
||||
"Calculate the sum of numbers from 1 to 10 using Python code.",
|
||||
options={
|
||||
"tools": [HostedCodeInterpreterTool()],
|
||||
"tools": [client.get_code_interpreter_tool()],
|
||||
},
|
||||
)
|
||||
# Should contain calculation result (sum of 1-10 = 55) or code execution content
|
||||
@@ -1651,3 +1514,115 @@ async def test_integration_agent_existing_thread():
|
||||
assert isinstance(second_response, AgentResponse)
|
||||
assert second_response.text is not None
|
||||
assert "photography" in second_response.text.lower()
|
||||
|
||||
|
||||
# region Factory Method Tests
|
||||
|
||||
|
||||
def test_get_code_interpreter_tool_basic() -> None:
|
||||
"""Test get_code_interpreter_tool returns CodeInterpreterTool."""
|
||||
tool = AzureAIClient.get_code_interpreter_tool()
|
||||
assert isinstance(tool, CodeInterpreterTool)
|
||||
|
||||
|
||||
def test_get_code_interpreter_tool_with_file_ids() -> None:
|
||||
"""Test get_code_interpreter_tool with file_ids."""
|
||||
tool = AzureAIClient.get_code_interpreter_tool(file_ids=["file-123", "file-456"])
|
||||
assert isinstance(tool, CodeInterpreterTool)
|
||||
assert tool["container"]["file_ids"] == ["file-123", "file-456"]
|
||||
|
||||
|
||||
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"])
|
||||
assert isinstance(tool, FileSearchTool)
|
||||
assert tool["vector_store_ids"] == ["vs-123"]
|
||||
|
||||
|
||||
def test_get_file_search_tool_with_options() -> None:
|
||||
"""Test get_file_search_tool with max_num_results."""
|
||||
tool = AzureAIClient.get_file_search_tool(
|
||||
vector_store_ids=["vs-123"],
|
||||
max_num_results=10,
|
||||
)
|
||||
assert isinstance(tool, FileSearchTool)
|
||||
assert tool["max_num_results"] == 10
|
||||
|
||||
|
||||
def test_get_file_search_tool_requires_vector_store_ids() -> None:
|
||||
"""Test get_file_search_tool raises ValueError when vector_store_ids is empty."""
|
||||
with pytest.raises(ValueError, match="vector_store_ids"):
|
||||
AzureAIClient.get_file_search_tool(vector_store_ids=[])
|
||||
|
||||
|
||||
def test_get_web_search_tool_basic() -> None:
|
||||
"""Test get_web_search_tool returns WebSearchPreviewTool."""
|
||||
tool = AzureAIClient.get_web_search_tool()
|
||||
assert isinstance(tool, WebSearchPreviewTool)
|
||||
|
||||
|
||||
def test_get_web_search_tool_with_location() -> None:
|
||||
"""Test get_web_search_tool with user_location."""
|
||||
tool = AzureAIClient.get_web_search_tool(
|
||||
user_location={"city": "Seattle", "country": "US"},
|
||||
)
|
||||
assert isinstance(tool, WebSearchPreviewTool)
|
||||
assert tool.user_location is not None
|
||||
assert tool.user_location.city == "Seattle"
|
||||
assert tool.user_location.country == "US"
|
||||
|
||||
|
||||
def test_get_web_search_tool_with_search_context_size() -> None:
|
||||
"""Test get_web_search_tool with search_context_size."""
|
||||
tool = AzureAIClient.get_web_search_tool(search_context_size="high")
|
||||
assert isinstance(tool, WebSearchPreviewTool)
|
||||
assert tool.search_context_size == "high"
|
||||
|
||||
|
||||
def test_get_mcp_tool_basic() -> None:
|
||||
"""Test get_mcp_tool returns MCPTool."""
|
||||
tool = AzureAIClient.get_mcp_tool(name="test_mcp", url="https://example.com")
|
||||
assert isinstance(tool, MCPTool)
|
||||
assert tool["server_label"] == "test_mcp"
|
||||
assert tool["server_url"] == "https://example.com"
|
||||
|
||||
|
||||
def test_get_mcp_tool_with_description() -> None:
|
||||
"""Test get_mcp_tool with description."""
|
||||
tool = AzureAIClient.get_mcp_tool(
|
||||
name="test_mcp",
|
||||
url="https://example.com",
|
||||
description="Test MCP server",
|
||||
)
|
||||
assert tool["server_description"] == "Test MCP server"
|
||||
|
||||
|
||||
def test_get_mcp_tool_with_project_connection_id() -> None:
|
||||
"""Test get_mcp_tool with project_connection_id."""
|
||||
tool = AzureAIClient.get_mcp_tool(
|
||||
name="test_mcp",
|
||||
project_connection_id="conn-123",
|
||||
)
|
||||
assert tool["project_connection_id"] == "conn-123"
|
||||
|
||||
|
||||
def test_get_image_generation_tool_basic() -> None:
|
||||
"""Test get_image_generation_tool returns ImageGenTool."""
|
||||
tool = AzureAIClient.get_image_generation_tool()
|
||||
assert isinstance(tool, ImageGenTool)
|
||||
|
||||
|
||||
def test_get_image_generation_tool_with_options() -> None:
|
||||
"""Test get_image_generation_tool with various options."""
|
||||
tool = AzureAIClient.get_image_generation_tool(
|
||||
size="1024x1024",
|
||||
quality="high",
|
||||
output_format="png",
|
||||
)
|
||||
assert isinstance(tool, ImageGenTool)
|
||||
assert tool["size"] == "1024x1024"
|
||||
assert tool["quality"] == "high"
|
||||
assert tool["output_format"] == "png"
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -440,19 +440,17 @@ def test_provider_merge_tools_skips_function_tool_dicts(mock_project_client: Mag
|
||||
# Call _merge_tools with user-provided function implementation
|
||||
merged = provider._merge_tools(definition_tools, [mock_ai_function]) # type: ignore
|
||||
|
||||
# Should have 2 items: the converted HostedMCPTool and the user-provided FunctionTool
|
||||
# Should have 2 items: the converted MCP dict and the user-provided FunctionTool
|
||||
assert len(merged) == 2
|
||||
|
||||
# Check that the function tool dict was NOT included (it was skipped)
|
||||
function_dicts = [t for t in merged if isinstance(t, dict) and t.get("type") == "function"]
|
||||
assert len(function_dicts) == 0
|
||||
|
||||
# Check that the MCP tool was converted to HostedMCPTool
|
||||
from agent_framework import HostedMCPTool
|
||||
|
||||
mcp_tools = [t for t in merged if isinstance(t, HostedMCPTool)]
|
||||
# Check that the MCP tool was converted to dict
|
||||
mcp_tools = [t for t in merged if isinstance(t, dict) and t.get("type") == "mcp"]
|
||||
assert len(mcp_tools) == 1
|
||||
assert mcp_tools[0].name == "my mcp" # server_label with _ replaced by space
|
||||
assert mcp_tools[0]["server_label"] == "my_mcp"
|
||||
|
||||
# Check that the user-provided FunctionTool was included
|
||||
ai_functions = [t for t in merged if isinstance(t, FunctionTool)]
|
||||
|
||||
@@ -5,29 +5,26 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from agent_framework import (
|
||||
Content,
|
||||
FunctionTool,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedImageGenerationTool,
|
||||
HostedMCPTool,
|
||||
HostedWebSearchTool,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError
|
||||
from agent_framework.exceptions import ServiceInvalidRequestError
|
||||
from azure.ai.agents.models import CodeInterpreterToolDefinition
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agent_framework_azure_ai import AzureAIAgentClient
|
||||
from agent_framework_azure_ai._shared import (
|
||||
_convert_response_format, # type: ignore
|
||||
_convert_sdk_tool, # type: ignore
|
||||
_extract_project_connection_id, # type: ignore
|
||||
_prepare_mcp_tool_for_azure_ai, # type: ignore
|
||||
create_text_format_config,
|
||||
from_azure_ai_agent_tools,
|
||||
from_azure_ai_tools,
|
||||
to_azure_ai_agent_tools,
|
||||
to_azure_ai_tools,
|
||||
)
|
||||
from agent_framework_azure_ai._shared import (
|
||||
_prepare_mcp_tool_dict_for_azure_ai as _prepare_mcp_tool_for_azure_ai, # type: ignore
|
||||
)
|
||||
|
||||
|
||||
def test_extract_project_connection_id_direct() -> None:
|
||||
@@ -69,16 +66,15 @@ def test_to_azure_ai_agent_tools_function_tool() -> None:
|
||||
|
||||
|
||||
def test_to_azure_ai_agent_tools_code_interpreter() -> None:
|
||||
"""Test converting HostedCodeInterpreterTool."""
|
||||
tool = HostedCodeInterpreterTool()
|
||||
"""Test converting code_interpreter dict tool."""
|
||||
tool = AzureAIAgentClient.get_code_interpreter_tool()
|
||||
result = to_azure_ai_agent_tools([tool])
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], CodeInterpreterToolDefinition)
|
||||
|
||||
|
||||
def test_to_azure_ai_agent_tools_web_search_missing_connection() -> None:
|
||||
"""Test HostedWebSearchTool raises without connection info."""
|
||||
tool = HostedWebSearchTool()
|
||||
"""Test web search tool raises without connection info."""
|
||||
# Clear any environment variables that could provide connection info
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
@@ -90,8 +86,9 @@ def test_to_azure_ai_agent_tools_web_search_missing_connection() -> None:
|
||||
for key in ["BING_CONNECTION_ID", "BING_CUSTOM_CONNECTION_ID", "BING_CUSTOM_INSTANCE_NAME"]:
|
||||
env_backup[key] = os.environ.pop(key, None)
|
||||
try:
|
||||
with pytest.raises(ServiceInitializationError, match="Bing search tool requires"):
|
||||
to_azure_ai_agent_tools([tool])
|
||||
# get_web_search_tool now raises ValueError when no connection info is available
|
||||
with pytest.raises(ValueError, match="Azure AI Agents requires a Bing connection"):
|
||||
AzureAIAgentClient.get_web_search_tool()
|
||||
finally:
|
||||
# Restore environment
|
||||
for key, value in env_backup.items():
|
||||
@@ -107,13 +104,15 @@ def test_to_azure_ai_agent_tools_dict_passthrough() -> None:
|
||||
|
||||
|
||||
def test_to_azure_ai_agent_tools_unsupported_type() -> None:
|
||||
"""Test unsupported tool type raises error."""
|
||||
"""Test unsupported tool type passes through unchanged."""
|
||||
|
||||
class UnsupportedTool:
|
||||
pass
|
||||
|
||||
with pytest.raises(ServiceInitializationError, match="Unsupported tool type"):
|
||||
to_azure_ai_agent_tools([UnsupportedTool()]) # type: ignore
|
||||
unsupported = UnsupportedTool()
|
||||
result = to_azure_ai_agent_tools([unsupported]) # type: ignore
|
||||
assert len(result) == 1
|
||||
assert result[0] is unsupported # Passed through unchanged
|
||||
|
||||
|
||||
def test_from_azure_ai_agent_tools_empty() -> None:
|
||||
@@ -127,7 +126,7 @@ def test_from_azure_ai_agent_tools_code_interpreter() -> None:
|
||||
tool = CodeInterpreterToolDefinition()
|
||||
result = from_azure_ai_agent_tools([tool])
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], HostedCodeInterpreterTool)
|
||||
assert result[0] == {"type": "code_interpreter"}
|
||||
|
||||
|
||||
def test_convert_sdk_tool_code_interpreter() -> None:
|
||||
@@ -135,7 +134,7 @@ def test_convert_sdk_tool_code_interpreter() -> None:
|
||||
tool = MagicMock()
|
||||
tool.type = "code_interpreter"
|
||||
result = _convert_sdk_tool(tool)
|
||||
assert isinstance(result, HostedCodeInterpreterTool)
|
||||
assert result == {"type": "code_interpreter"}
|
||||
|
||||
|
||||
def test_convert_sdk_tool_function_returns_none() -> None:
|
||||
@@ -161,8 +160,8 @@ def test_convert_sdk_tool_file_search() -> None:
|
||||
tool.file_search = MagicMock()
|
||||
tool.file_search.vector_store_ids = ["vs-1", "vs-2"]
|
||||
result = _convert_sdk_tool(tool)
|
||||
assert isinstance(result, HostedFileSearchTool)
|
||||
assert len(result.inputs) == 2 # type: ignore
|
||||
assert result["type"] == "file_search"
|
||||
assert result["vector_store_ids"] == ["vs-1", "vs-2"]
|
||||
|
||||
|
||||
def test_convert_sdk_tool_bing_grounding() -> None:
|
||||
@@ -172,8 +171,8 @@ def test_convert_sdk_tool_bing_grounding() -> None:
|
||||
tool.bing_grounding = MagicMock()
|
||||
tool.bing_grounding.connection_id = "conn-123"
|
||||
result = _convert_sdk_tool(tool)
|
||||
assert isinstance(result, HostedWebSearchTool)
|
||||
assert result.additional_properties["connection_id"] == "conn-123" # type: ignore
|
||||
assert result["type"] == "bing_grounding"
|
||||
assert result["connection_id"] == "conn-123"
|
||||
|
||||
|
||||
def test_convert_sdk_tool_bing_custom_search() -> None:
|
||||
@@ -184,9 +183,9 @@ def test_convert_sdk_tool_bing_custom_search() -> None:
|
||||
tool.bing_custom_search.connection_id = "conn-123"
|
||||
tool.bing_custom_search.instance_name = "my-instance"
|
||||
result = _convert_sdk_tool(tool)
|
||||
assert isinstance(result, HostedWebSearchTool)
|
||||
assert result.additional_properties["custom_connection_id"] == "conn-123" # type: ignore
|
||||
assert result.additional_properties["custom_instance_name"] == "my-instance" # type: ignore
|
||||
assert result["type"] == "bing_custom_search"
|
||||
assert result["connection_id"] == "conn-123"
|
||||
assert result["instance_name"] == "my-instance"
|
||||
|
||||
|
||||
def test_to_azure_ai_tools_empty() -> None:
|
||||
@@ -196,14 +195,14 @@ def test_to_azure_ai_tools_empty() -> None:
|
||||
|
||||
|
||||
def test_to_azure_ai_tools_code_interpreter_with_file_ids() -> None:
|
||||
"""Test converting HostedCodeInterpreterTool with file inputs."""
|
||||
tool = HostedCodeInterpreterTool(
|
||||
inputs=[Content.from_hosted_file(file_id="file-123")] # type: ignore
|
||||
)
|
||||
"""Test converting code_interpreter dict tool with file inputs."""
|
||||
tool = {
|
||||
"type": "code_interpreter",
|
||||
"file_ids": ["file-123"],
|
||||
}
|
||||
result = to_azure_ai_tools([tool])
|
||||
assert len(result) == 1
|
||||
assert result[0]["type"] == "code_interpreter"
|
||||
assert result[0]["container"]["file_ids"] == ["file-123"]
|
||||
|
||||
|
||||
def test_to_azure_ai_tools_function_tool() -> None:
|
||||
@@ -221,11 +220,12 @@ def test_to_azure_ai_tools_function_tool() -> None:
|
||||
|
||||
|
||||
def test_to_azure_ai_tools_file_search() -> None:
|
||||
"""Test converting HostedFileSearchTool."""
|
||||
tool = HostedFileSearchTool(
|
||||
inputs=[Content.from_hosted_vector_store(vector_store_id="vs-123")], # type: ignore
|
||||
max_results=10,
|
||||
)
|
||||
"""Test converting file_search dict tool."""
|
||||
tool = {
|
||||
"type": "file_search",
|
||||
"vector_store_ids": ["vs-123"],
|
||||
"max_num_results": 10,
|
||||
}
|
||||
result = to_azure_ai_tools([tool])
|
||||
assert len(result) == 1
|
||||
assert result[0]["type"] == "file_search"
|
||||
@@ -234,28 +234,29 @@ def test_to_azure_ai_tools_file_search() -> None:
|
||||
|
||||
|
||||
def test_to_azure_ai_tools_web_search_with_location() -> None:
|
||||
"""Test converting HostedWebSearchTool with user location."""
|
||||
tool = HostedWebSearchTool(
|
||||
additional_properties={
|
||||
"user_location": {
|
||||
"city": "Seattle",
|
||||
"country": "US",
|
||||
"region": "WA",
|
||||
"timezone": "PST",
|
||||
}
|
||||
}
|
||||
)
|
||||
"""Test converting web_search dict tool with user location."""
|
||||
tool = {
|
||||
"type": "web_search_preview",
|
||||
"user_location": {
|
||||
"city": "Seattle",
|
||||
"country": "US",
|
||||
"region": "WA",
|
||||
"timezone": "PST",
|
||||
},
|
||||
}
|
||||
result = to_azure_ai_tools([tool])
|
||||
assert len(result) == 1
|
||||
assert result[0]["type"] == "web_search_preview"
|
||||
|
||||
|
||||
def test_to_azure_ai_tools_image_generation() -> None:
|
||||
"""Test converting HostedImageGenerationTool."""
|
||||
tool = HostedImageGenerationTool(
|
||||
options={"model_id": "gpt-image-1", "image_size": "1024x1024"},
|
||||
additional_properties={"quality": "high"},
|
||||
)
|
||||
"""Test converting image_generation dict tool."""
|
||||
tool = {
|
||||
"type": "image_generation",
|
||||
"model": "gpt-image-1",
|
||||
"size": "1024x1024",
|
||||
"quality": "high",
|
||||
}
|
||||
result = to_azure_ai_tools([tool])
|
||||
assert len(result) == 1
|
||||
assert result[0]["type"] == "image_generation"
|
||||
@@ -264,7 +265,7 @@ def test_to_azure_ai_tools_image_generation() -> None:
|
||||
|
||||
def test_prepare_mcp_tool_basic() -> None:
|
||||
"""Test basic MCP tool conversion."""
|
||||
tool = HostedMCPTool(name="my tool", url="http://localhost:8080")
|
||||
tool = {"type": "mcp", "server_label": "my_tool", "server_url": "http://localhost:8080"}
|
||||
result = _prepare_mcp_tool_for_azure_ai(tool)
|
||||
assert result["server_label"] == "my_tool"
|
||||
assert "http://localhost:8080" in result["server_url"]
|
||||
@@ -272,26 +273,37 @@ def test_prepare_mcp_tool_basic() -> None:
|
||||
|
||||
def test_prepare_mcp_tool_with_description() -> None:
|
||||
"""Test MCP tool with description."""
|
||||
tool = HostedMCPTool(name="my tool", url="http://localhost:8080", description="My MCP server")
|
||||
tool = {
|
||||
"type": "mcp",
|
||||
"server_label": "my_tool",
|
||||
"server_url": "http://localhost:8080",
|
||||
"server_description": "My MCP server",
|
||||
}
|
||||
result = _prepare_mcp_tool_for_azure_ai(tool)
|
||||
assert result["server_description"] == "My MCP server"
|
||||
|
||||
|
||||
def test_prepare_mcp_tool_with_headers() -> None:
|
||||
"""Test MCP tool with headers (no project_connection_id)."""
|
||||
tool = HostedMCPTool(name="my tool", url="http://localhost:8080", headers={"X-Api-Key": "secret"})
|
||||
tool = {
|
||||
"type": "mcp",
|
||||
"server_label": "my_tool",
|
||||
"server_url": "http://localhost:8080",
|
||||
"headers": {"X-Api-Key": "secret"},
|
||||
}
|
||||
result = _prepare_mcp_tool_for_azure_ai(tool)
|
||||
assert result["headers"] == {"X-Api-Key": "secret"}
|
||||
|
||||
|
||||
def test_prepare_mcp_tool_project_connection_takes_precedence() -> None:
|
||||
"""Test project_connection_id takes precedence over headers."""
|
||||
tool = HostedMCPTool(
|
||||
name="my tool",
|
||||
url="http://localhost:8080",
|
||||
headers={"X-Api-Key": "secret"},
|
||||
additional_properties={"project_connection_id": "my-conn"},
|
||||
)
|
||||
tool = {
|
||||
"type": "mcp",
|
||||
"server_label": "my_tool",
|
||||
"server_url": "http://localhost:8080",
|
||||
"headers": {"X-Api-Key": "secret"},
|
||||
"project_connection_id": "my-conn",
|
||||
}
|
||||
result = _prepare_mcp_tool_for_azure_ai(tool)
|
||||
assert result["project_connection_id"] == "my-conn"
|
||||
assert "headers" not in result
|
||||
@@ -299,30 +311,38 @@ def test_prepare_mcp_tool_project_connection_takes_precedence() -> None:
|
||||
|
||||
def test_prepare_mcp_tool_approval_mode_always() -> None:
|
||||
"""Test MCP tool with always_require approval mode."""
|
||||
tool = HostedMCPTool(name="my tool", url="http://localhost:8080", approval_mode="always_require")
|
||||
tool = {
|
||||
"type": "mcp",
|
||||
"server_label": "my_tool",
|
||||
"server_url": "http://localhost:8080",
|
||||
"require_approval": "always",
|
||||
}
|
||||
result = _prepare_mcp_tool_for_azure_ai(tool)
|
||||
assert result["require_approval"] == "always"
|
||||
|
||||
|
||||
def test_prepare_mcp_tool_approval_mode_never() -> None:
|
||||
"""Test MCP tool with never_require approval mode."""
|
||||
tool = HostedMCPTool(name="my tool", url="http://localhost:8080", approval_mode="never_require")
|
||||
tool = {
|
||||
"type": "mcp",
|
||||
"server_label": "my_tool",
|
||||
"server_url": "http://localhost:8080",
|
||||
"require_approval": "never",
|
||||
}
|
||||
result = _prepare_mcp_tool_for_azure_ai(tool)
|
||||
assert result["require_approval"] == "never"
|
||||
|
||||
|
||||
def test_prepare_mcp_tool_approval_mode_dict() -> None:
|
||||
"""Test MCP tool with dict approval mode."""
|
||||
tool = HostedMCPTool(
|
||||
name="my tool",
|
||||
url="http://localhost:8080",
|
||||
approval_mode={
|
||||
"always_require_approval": {"sensitive_tool"},
|
||||
"never_require_approval": {"safe_tool"},
|
||||
},
|
||||
)
|
||||
tool = {
|
||||
"type": "mcp",
|
||||
"server_label": "my_tool",
|
||||
"server_url": "http://localhost:8080",
|
||||
"require_approval": {"always": {"tool_names": ["sensitive_tool", "dangerous_tool"]}},
|
||||
}
|
||||
result = _prepare_mcp_tool_for_azure_ai(tool)
|
||||
# The last assignment wins in the current implementation
|
||||
# The approval mode is passed through
|
||||
assert "require_approval" in result
|
||||
|
||||
|
||||
@@ -385,7 +405,7 @@ def test_convert_response_format_json_schema_missing_schema_raises() -> None:
|
||||
|
||||
|
||||
def test_from_azure_ai_tools_mcp_approval_mode_always() -> None:
|
||||
"""Test from_azure_ai_tools converts MCP require_approval='always' to approval_mode."""
|
||||
"""Test from_azure_ai_tools converts MCP require_approval='always' to dict."""
|
||||
tools = [
|
||||
{
|
||||
"type": "mcp",
|
||||
@@ -396,12 +416,12 @@ def test_from_azure_ai_tools_mcp_approval_mode_always() -> None:
|
||||
]
|
||||
result = from_azure_ai_tools(tools)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], HostedMCPTool)
|
||||
assert result[0].approval_mode == "always_require"
|
||||
assert result[0]["type"] == "mcp"
|
||||
assert result[0]["require_approval"] == "always"
|
||||
|
||||
|
||||
def test_from_azure_ai_tools_mcp_approval_mode_never() -> None:
|
||||
"""Test from_azure_ai_tools converts MCP require_approval='never' to approval_mode."""
|
||||
"""Test from_azure_ai_tools converts MCP require_approval='never' to dict."""
|
||||
tools = [
|
||||
{
|
||||
"type": "mcp",
|
||||
@@ -412,8 +432,8 @@ def test_from_azure_ai_tools_mcp_approval_mode_never() -> None:
|
||||
]
|
||||
result = from_azure_ai_tools(tools)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], HostedMCPTool)
|
||||
assert result[0].approval_mode == "never_require"
|
||||
assert result[0]["type"] == "mcp"
|
||||
assert result[0]["require_approval"] == "never"
|
||||
|
||||
|
||||
def test_from_azure_ai_tools_mcp_approval_mode_dict_always() -> None:
|
||||
@@ -428,8 +448,8 @@ def test_from_azure_ai_tools_mcp_approval_mode_dict_always() -> None:
|
||||
]
|
||||
result = from_azure_ai_tools(tools)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], HostedMCPTool)
|
||||
assert result[0].approval_mode == {"always_require_approval": {"sensitive_tool", "dangerous_tool"}}
|
||||
assert result[0]["type"] == "mcp"
|
||||
assert result[0]["require_approval"] == {"always": {"tool_names": ["sensitive_tool", "dangerous_tool"]}}
|
||||
|
||||
|
||||
def test_from_azure_ai_tools_mcp_approval_mode_dict_never() -> None:
|
||||
@@ -444,5 +464,5 @@ def test_from_azure_ai_tools_mcp_approval_mode_dict_never() -> None:
|
||||
]
|
||||
result = from_azure_ai_tools(tools)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], HostedMCPTool)
|
||||
assert result[0].approval_mode == {"never_require_approval": {"safe_tool"}}
|
||||
assert result[0]["type"] == "mcp"
|
||||
assert result[0]["require_approval"] == {"never": {"tool_names": ["safe_tool"]}}
|
||||
|
||||
Reference in New Issue
Block a user