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:
Giles Odigwe
2026-02-11 00:04:27 +00:00
committed by GitHub
parent d249473a6d
commit 7a88af0aef
133 changed files with 3018 additions and 2650 deletions
@@ -12,7 +12,7 @@ from dataclasses import asdict, is_dataclass
from datetime import date, datetime
from typing import Any
from agent_framework import AgentResponseUpdate, ChatResponseUpdate, FunctionTool, ToolProtocol
from agent_framework import AgentResponseUpdate, ChatResponseUpdate, FunctionTool
# Role mapping constants
AGUI_TO_FRAMEWORK_ROLE: dict[str, str] = {
@@ -200,10 +200,10 @@ def convert_agui_tools_to_agent_framework(
def convert_tools_to_agui_format(
tools: (
ToolProtocol
FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None
),
) -> list[dict[str, Any]] | None:
@@ -225,7 +225,7 @@ def convert_tools_to_agui_format(
# Normalize to list
if not isinstance(tools, list):
tool_list: list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] = [tools] # type: ignore[list-item]
tool_list: list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] = [tools] # type: ignore[list-item]
else:
tool_list = tools # type: ignore[assignment]
@@ -256,12 +256,8 @@ def convert_tools_to_agui_format(
"parameters": ai_func.parameters(),
}
)
elif isinstance(tool_item, ToolProtocol):
# Handle other ToolProtocol implementations
# For now, we'll skip non-FunctionTool instances as they may not have
# the parameters() method. This matches .NET behavior which only
# converts FunctionToolDeclaration instances.
continue
# Note: dict-based hosted tools (CodeInterpreter, WebSearch, etc.) are passed through
# as-is in the first branch. Non-FunctionTool, non-dict items are skipped.
return results if results else None
@@ -20,9 +20,6 @@ from agent_framework import (
FunctionInvocationConfiguration,
FunctionInvocationLayer,
FunctionTool,
HostedCodeInterpreterTool,
HostedMCPTool,
HostedWebSearchTool,
Message,
ResponseStream,
TextSpanRegion,
@@ -350,6 +347,109 @@ class AnthropicClient(
# streaming requires tracking the last function call ID and name
self._last_call_id_name: tuple[str, str] | None = None
# region Static factory methods for hosted tools
@staticmethod
def get_code_interpreter_tool(
*,
type_name: str | None = None,
) -> dict[str, Any]:
"""Create a code interpreter tool configuration for Anthropic.
Keyword Args:
type_name: Override the tool type name. Defaults to "code_execution_20250825".
Returns:
A dict-based tool configuration ready to pass to ChatAgent.
Examples:
.. code-block:: python
from agent_framework.anthropic import AnthropicClient
tool = AnthropicClient.get_code_interpreter_tool()
agent = AnthropicClient().as_agent(tools=[tool])
"""
return {"type": type_name or "code_execution_20250825"}
@staticmethod
def get_web_search_tool(
*,
type_name: str | None = None,
) -> dict[str, Any]:
"""Create a web search tool configuration for Anthropic.
Keyword Args:
type_name: Override the tool type name. Defaults to "web_search_20250305".
Returns:
A dict-based tool configuration ready to pass to ChatAgent.
Examples:
.. code-block:: python
from agent_framework.anthropic import AnthropicClient
tool = AnthropicClient.get_web_search_tool()
agent = AnthropicClient().as_agent(tools=[tool])
"""
return {"type": type_name or "web_search_20250305"}
@staticmethod
def get_mcp_tool(
*,
name: str,
url: str,
allowed_tools: list[str] | None = None,
authorization_token: str | None = None,
) -> dict[str, Any]:
"""Create a hosted MCP tool configuration for Anthropic.
This configures an MCP (Model Context Protocol) server that will be called
by Anthropic's service. The tools from this MCP server are executed remotely
by Anthropic, 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.
allowed_tools: List of tool names that are allowed to be used from this MCP server.
authorization_token: Authorization token for the MCP server (e.g., Bearer token).
Returns:
A dict-based tool configuration ready to pass to ChatAgent.
Examples:
.. code-block:: python
from agent_framework.anthropic import AnthropicClient
tool = AnthropicClient.get_mcp_tool(
name="GitHub",
url="https://api.githubcopilot.com/mcp/",
authorization_token="Bearer ghp_xxx",
)
agent = AnthropicClient().as_agent(tools=[tool])
"""
result: dict[str, Any] = {
"type": "mcp",
"server_label": name.replace(" ", "_"),
"server_url": url,
}
if allowed_tools:
result["allowed_tools"] = allowed_tools
if authorization_token:
result["headers"] = {"authorization": authorization_token}
return result
# endregion
# region Get response methods
@override
@@ -590,6 +690,9 @@ class AnthropicClient(
def _prepare_tools_for_anthropic(self, options: Mapping[str, Any]) -> dict[str, Any] | None:
"""Prepare tools and tool choice configuration for the Anthropic API request.
Converts FunctionTool to Anthropic format. MCP tools are routed to separate
mcp_servers parameter. All other tools pass through unchanged.
Args:
options: The options dict containing tools and tool choice settings.
@@ -603,46 +706,32 @@ class AnthropicClient(
# Process tools
if tools:
tool_list: list[MutableMapping[str, Any]] = []
mcp_server_list: list[MutableMapping[str, Any]] = []
tool_list: list[Any] = []
mcp_server_list: list[Any] = []
for tool in tools:
match tool:
case MutableMapping():
tool_list.append(tool)
case FunctionTool():
tool_list.append({
"type": "custom",
"name": tool.name,
"description": tool.description,
"input_schema": tool.parameters(),
})
case HostedWebSearchTool():
search_tool: dict[str, Any] = {
"type": "web_search_20250305",
"name": "web_search",
}
if tool.additional_properties:
search_tool.update(tool.additional_properties)
tool_list.append(search_tool)
case HostedCodeInterpreterTool():
code_tool: dict[str, Any] = {
"type": "code_execution_20250825",
"name": "code_execution",
}
tool_list.append(code_tool)
case HostedMCPTool():
server_def: dict[str, Any] = {
"type": "url",
"name": tool.name,
"url": str(tool.url),
}
if tool.allowed_tools:
server_def["tool_configuration"] = {"allowed_tools": list(tool.allowed_tools)}
if tool.headers and (auth := tool.headers.get("authorization")):
server_def["authorization_token"] = auth
mcp_server_list.append(server_def)
case _:
logger.debug(f"Ignoring unsupported tool type: {type(tool)} for now")
if isinstance(tool, FunctionTool):
tool_list.append({
"type": "custom",
"name": tool.name,
"description": tool.description,
"input_schema": tool.parameters(),
})
elif isinstance(tool, MutableMapping) and tool.get("type") == "mcp":
# MCP servers must be routed to separate mcp_servers parameter
server_def: dict[str, Any] = {
"type": "url",
"name": tool.get("server_label", ""),
"url": tool.get("server_url", ""),
}
if allowed_tools := tool.get("allowed_tools"):
server_def["tool_configuration"] = {"allowed_tools": list(allowed_tools)}
headers = tool.get("headers")
if isinstance(headers, dict) and (auth := headers.get("authorization")):
server_def["authorization_token"] = auth
mcp_server_list.append(server_def)
else:
# Pass through all other tools (dicts, SDK types) unchanged
tool_list.append(tool)
if tool_list:
result["tools"] = tool_list
@@ -9,9 +9,6 @@ from agent_framework import (
ChatOptions,
ChatResponseUpdate,
Content,
HostedCodeInterpreterTool,
HostedMCPTool,
HostedWebSearchTool,
Message,
SupportsChatGetResponse,
tool,
@@ -278,9 +275,9 @@ def test_prepare_tools_for_anthropic_tool(mock_anthropic_client: MagicMock) -> N
def test_prepare_tools_for_anthropic_web_search(mock_anthropic_client: MagicMock) -> None:
"""Test converting HostedWebSearchTool to Anthropic format."""
"""Test converting web_search dict tool to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
chat_options = ChatOptions(tools=[HostedWebSearchTool()])
chat_options = ChatOptions(tools=[client.get_web_search_tool()])
result = client._prepare_tools_for_anthropic(chat_options)
@@ -288,13 +285,12 @@ def test_prepare_tools_for_anthropic_web_search(mock_anthropic_client: MagicMock
assert "tools" in result
assert len(result["tools"]) == 1
assert result["tools"][0]["type"] == "web_search_20250305"
assert result["tools"][0]["name"] == "web_search"
def test_prepare_tools_for_anthropic_code_interpreter(mock_anthropic_client: MagicMock) -> None:
"""Test converting HostedCodeInterpreterTool to Anthropic format."""
"""Test converting code_interpreter dict tool to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
chat_options = ChatOptions(tools=[HostedCodeInterpreterTool()])
chat_options = ChatOptions(tools=[client.get_code_interpreter_tool()])
result = client._prepare_tools_for_anthropic(chat_options)
@@ -302,13 +298,12 @@ def test_prepare_tools_for_anthropic_code_interpreter(mock_anthropic_client: Mag
assert "tools" in result
assert len(result["tools"]) == 1
assert result["tools"][0]["type"] == "code_execution_20250825"
assert result["tools"][0]["name"] == "code_execution"
def test_prepare_tools_for_anthropic_mcp_tool(mock_anthropic_client: MagicMock) -> None:
"""Test converting HostedMCPTool to Anthropic format."""
"""Test converting MCP dict tool to Anthropic format."""
client = create_test_anthropic_client(mock_anthropic_client)
chat_options = ChatOptions(tools=[HostedMCPTool(name="test-mcp", url="https://example.com/mcp")])
chat_options = ChatOptions(tools=[client.get_mcp_tool(name="test-mcp", url="https://example.com/mcp")])
result = client._prepare_tools_for_anthropic(chat_options)
@@ -321,23 +316,21 @@ def test_prepare_tools_for_anthropic_mcp_tool(mock_anthropic_client: MagicMock)
def test_prepare_tools_for_anthropic_mcp_with_auth(mock_anthropic_client: MagicMock) -> None:
"""Test converting HostedMCPTool with authorization headers."""
"""Test converting MCP dict tool with authorization token."""
client = create_test_anthropic_client(mock_anthropic_client)
chat_options = ChatOptions(
tools=[
HostedMCPTool(
name="test-mcp",
url="https://example.com/mcp",
headers={"authorization": "Bearer token123"},
)
]
# Use the static method with authorization_token
mcp_tool = client.get_mcp_tool(
name="test-mcp",
url="https://example.com/mcp",
authorization_token="Bearer token123",
)
chat_options = ChatOptions(tools=[mcp_tool])
result = client._prepare_tools_for_anthropic(chat_options)
assert result is not None
assert "mcp_servers" in result
# The authorization header is converted to authorization_token
# The authorization_token should be passed through
assert "authorization_token" in result["mcp_servers"][0]
assert result["mcp_servers"][0]["authorization_token"] == "Bearer token123"
@@ -806,12 +799,11 @@ async def test_anthropic_client_integration_hosted_tools() -> None:
messages = [Message(role="user", text="What tools do you have available?")]
tools = [
HostedWebSearchTool(),
HostedCodeInterpreterTool(),
HostedMCPTool(
AnthropicClient.get_web_search_tool(),
AnthropicClient.get_code_interpreter_tool(),
AnthropicClient.get_mcp_tool(
name="example-mcp",
url="https://learn.microsoft.com/api/mcp",
approval_mode="never_require",
),
]
@@ -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)]
+102 -82
View File
@@ -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"]}}
@@ -25,7 +25,6 @@ from agent_framework import (
FunctionTool,
Message,
ResponseStream,
ToolProtocol,
UsageDetails,
get_logger,
prepare_function_call_results,
@@ -564,7 +563,7 @@ class BedrockChatClient(
return {"text": str(value)}
return {"text": str(value)}
def _prepare_tools(self, tools: list[ToolProtocol | MutableMapping[str, Any]] | None) -> dict[str, Any] | None:
def _prepare_tools(self, tools: list[FunctionTool | MutableMapping[str, Any]] | None) -> dict[str, Any] | None:
converted: list[dict[str, Any]] = []
if not tools:
return None
@@ -18,7 +18,6 @@ from agent_framework import (
ContextProvider,
FunctionTool,
Message,
ToolProtocol,
get_logger,
normalize_messages,
)
@@ -217,11 +216,11 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
description: str | None = None,
context_provider: ContextProvider | None = None,
middleware: Sequence[AgentMiddlewareTypes] | None = None,
tools: ToolProtocol
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| str
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] | str]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | str]
| None = None,
default_options: OptionsT | MutableMapping[str, Any] | None = None,
env_file_path: str | None = None,
@@ -242,7 +241,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
middleware: List of middleware.
tools: Tools for the agent. Can be:
- Strings for built-in tools (e.g., "Read", "Write", "Bash", "Glob")
- Functions or ToolProtocol instances for custom tools
- Functions for custom tools
default_options: Default ClaudeAgentOptions including system_prompt, model, etc.
env_file_path: Path to .env file.
env_file_encoding: Encoding of .env file.
@@ -288,9 +287,9 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
except ValidationError as ex:
raise ServiceInitializationError("Failed to create Claude Agent settings.", ex) from ex
# Separate built-in tools (strings) from custom tools (callables/ToolProtocol)
# Separate built-in tools (strings) from custom tools (callables/FunctionTool)
self._builtin_tools: list[str] = []
self._custom_tools: list[ToolProtocol | MutableMapping[str, Any]] = []
self._custom_tools: list[FunctionTool | MutableMapping[str, Any]] = []
self._normalize_tools(tools)
self._default_options = opts
@@ -299,11 +298,11 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
def _normalize_tools(
self,
tools: ToolProtocol
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| str
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] | str]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | str]
| None,
) -> None:
"""Separate built-in tools (strings) from custom tools.
@@ -317,7 +316,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
# Normalize to sequence
if isinstance(tools, str):
tools_list: Sequence[Any] = [tools]
elif isinstance(tools, (ToolProtocol, MutableMapping)) or callable(tools):
elif isinstance(tools, (FunctionTool, MutableMapping)) or callable(tools):
tools_list = [tools]
else:
tools_list = list(tools)
@@ -458,7 +457,7 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
def _prepare_tools(
self,
tools: list[ToolProtocol | MutableMapping[str, Any]],
tools: list[FunctionTool | MutableMapping[str, Any]],
) -> tuple[Any, list[str]]:
"""Convert Agent Framework tools to SDK MCP server.
@@ -476,7 +475,8 @@ class ClaudeAgent(BaseAgent, Generic[OptionsT]):
sdk_tools.append(self._function_tool_to_sdk_mcp_tool(tool))
# Claude Agent SDK convention: MCP tools use format "mcp__{server}__{tool}"
tool_names.append(f"mcp__{TOOLS_MCP_SERVER_NAME}__{tool.name}")
elif isinstance(tool, ToolProtocol):
else:
# Non-FunctionTool items (e.g., dict-based hosted tools) cannot be converted to SDK MCP tools
logger.debug(f"Unsupported tool type: {type(tool)}")
if not sdk_tools:
+27 -21
View File
@@ -38,7 +38,6 @@ from ._threads import AgentThread, ChatMessageStoreProtocol
from ._tools import (
FunctionInvocationLayer,
FunctionTool,
ToolProtocol,
)
from ._types import (
AgentResponse,
@@ -615,10 +614,11 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
id: str | None = None,
name: str | None = None,
description: str | None = None,
tools: ToolProtocol
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| Any
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
| None = None,
default_options: OptionsCoT | None = None,
chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None,
@@ -681,10 +681,10 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
# Get tools from options or named parameter (named param takes precedence)
tools_ = tools if tools is not None else opts.pop("tools", None)
tools_ = cast(
ToolProtocol
FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None,
tools_,
)
@@ -694,10 +694,10 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
# We ignore the MCP Servers here and store them separately,
# we add their functions to the tools list at runtime
normalized_tools: list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] = ( # type:ignore[reportUnknownVariableType]
normalized_tools: list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] = ( # type:ignore[reportUnknownVariableType]
[] if tools_ is None else tools_ if isinstance(tools_, list) else [tools_] # type: ignore[list-item]
)
self.mcp_tools: list[MCPTool] = [tool for tool in normalized_tools if isinstance(tool, MCPTool)]
self.mcp_tools: list[MCPTool] = [tool for tool in normalized_tools if isinstance(tool, MCPTool)] # type: ignore[misc]
agent_tools = [tool for tool in normalized_tools if not isinstance(tool, MCPTool)]
# Build chat options dict
@@ -780,10 +780,11 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
*,
stream: Literal[False] = ...,
thread: AgentThread | None = None,
tools: ToolProtocol
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| Any
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
| None = None,
options: ChatOptions[ResponseModelBoundT],
**kwargs: Any,
@@ -796,10 +797,11 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
*,
stream: Literal[False] = ...,
thread: AgentThread | None = None,
tools: ToolProtocol
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| Any
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
| None = None,
options: OptionsCoT | ChatOptions[None] | None = None,
**kwargs: Any,
@@ -812,10 +814,11 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
*,
stream: Literal[True],
thread: AgentThread | None = None,
tools: ToolProtocol
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| Any
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
| None = None,
options: OptionsCoT | ChatOptions[Any] | None = None,
**kwargs: Any,
@@ -827,10 +830,11 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
*,
stream: bool = False,
thread: AgentThread | None = None,
tools: ToolProtocol
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| Any
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
| None = None,
options: OptionsCoT | ChatOptions[Any] | None = None,
**kwargs: Any,
@@ -981,10 +985,11 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
*,
messages: str | Message | Sequence[str | Message] | None,
thread: AgentThread | None,
tools: ToolProtocol
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| Any
| list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
| None,
options: Mapping[str, Any] | None,
kwargs: dict[str, Any],
@@ -1000,13 +1005,13 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc]
)
# Normalize tools
normalized_tools: list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] = (
normalized_tools: list[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any] = (
[] if tools_ is None else tools_ if isinstance(tools_, list) else [tools_]
)
agent_name = self._get_agent_name()
# Resolve final tool list (runtime provided tools + local MCP server tools)
final_tools: list[ToolProtocol | Callable[..., Any] | dict[str, Any]] = []
final_tools: list[FunctionTool | Callable[..., Any] | dict[str, Any] | Any] = []
for tool in normalized_tools:
if isinstance(tool, MCPTool):
if not tool.is_connected:
@@ -1392,10 +1397,11 @@ class Agent(
id: str | None = None,
name: str | None = None,
description: str | None = None,
tools: ToolProtocol
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| Any
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any] | Any]
| None = None,
default_options: OptionsCoT | None = None,
chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None,
@@ -33,7 +33,7 @@ from ._serialization import SerializationMixin
from ._threads import ChatMessageStoreProtocol
from ._tools import (
FunctionInvocationConfiguration,
ToolProtocol,
FunctionTool,
)
from ._types import (
ChatResponse,
@@ -68,6 +68,11 @@ logger = get_logger()
__all__ = [
"BaseChatClient",
"SupportsChatGetResponse",
"SupportsCodeInterpreterTool",
"SupportsFileSearchTool",
"SupportsImageGenerationTool",
"SupportsMCPTool",
"SupportsWebSearchTool",
]
@@ -437,10 +442,10 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
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: OptionsCoT | Mapping[str, Any] | None = None,
chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None,
@@ -510,3 +515,163 @@ class BaseChatClient(SerializationMixin, ABC, Generic[OptionsCoT]):
function_invocation_configuration=function_invocation_configuration,
**kwargs,
)
# endregion
# region Tool Support Protocols
@runtime_checkable
class SupportsCodeInterpreterTool(Protocol):
"""Protocol for clients that support code interpreter tools.
This protocol enables runtime checking to determine if a client
supports code interpreter functionality.
Examples:
.. code-block:: python
from agent_framework import SupportsCodeInterpreterTool
if isinstance(client, SupportsCodeInterpreterTool):
tool = client.get_code_interpreter_tool()
agent = ChatAgent(client, tools=[tool])
"""
@staticmethod
def get_code_interpreter_tool(**kwargs: Any) -> Any:
"""Create a code interpreter tool configuration.
Keyword Args:
**kwargs: Provider-specific configuration options.
Returns:
A tool configuration ready to pass to ChatAgent.
"""
...
@runtime_checkable
class SupportsWebSearchTool(Protocol):
"""Protocol for clients that support web search tools.
This protocol enables runtime checking to determine if a client
supports web search functionality.
Examples:
.. code-block:: python
from agent_framework import SupportsWebSearchTool
if isinstance(client, SupportsWebSearchTool):
tool = client.get_web_search_tool()
agent = ChatAgent(client, tools=[tool])
"""
@staticmethod
def get_web_search_tool(**kwargs: Any) -> Any:
"""Create a web search tool configuration.
Keyword Args:
**kwargs: Provider-specific configuration options.
Returns:
A tool configuration ready to pass to ChatAgent.
"""
...
@runtime_checkable
class SupportsImageGenerationTool(Protocol):
"""Protocol for clients that support image generation tools.
This protocol enables runtime checking to determine if a client
supports image generation functionality.
Examples:
.. code-block:: python
from agent_framework import SupportsImageGenerationTool
if isinstance(client, SupportsImageGenerationTool):
tool = client.get_image_generation_tool()
agent = ChatAgent(client, tools=[tool])
"""
@staticmethod
def get_image_generation_tool(**kwargs: Any) -> Any:
"""Create an image generation tool configuration.
Keyword Args:
**kwargs: Provider-specific configuration options.
Returns:
A tool configuration ready to pass to ChatAgent.
"""
...
@runtime_checkable
class SupportsMCPTool(Protocol):
"""Protocol for clients that support MCP (Model Context Protocol) tools.
This protocol enables runtime checking to determine if a client
supports MCP server connections.
Examples:
.. code-block:: python
from agent_framework import SupportsMCPTool
if isinstance(client, SupportsMCPTool):
tool = client.get_mcp_tool(name="my_mcp", url="https://...")
agent = ChatAgent(client, tools=[tool])
"""
@staticmethod
def get_mcp_tool(**kwargs: Any) -> Any:
"""Create an MCP tool configuration.
Keyword Args:
**kwargs: Provider-specific configuration options including
name and url for the MCP server.
Returns:
A tool configuration ready to pass to ChatAgent.
"""
...
@runtime_checkable
class SupportsFileSearchTool(Protocol):
"""Protocol for clients that support file search tools.
This protocol enables runtime checking to determine if a client
supports file search functionality with vector stores.
Examples:
.. code-block:: python
from agent_framework import SupportsFileSearchTool
if isinstance(client, SupportsFileSearchTool):
tool = client.get_file_search_tool(vector_store_ids=["vs_123"])
agent = ChatAgent(client, tools=[tool])
"""
@staticmethod
def get_file_search_tool(**kwargs: Any) -> Any:
"""Create a file search tool configuration.
Keyword Args:
**kwargs: Provider-specific configuration options.
Returns:
A tool configuration ready to pass to ChatAgent.
"""
...
# endregion
+20 -6
View File
@@ -12,7 +12,7 @@ from collections.abc import Callable, Collection, Sequence
from contextlib import AsyncExitStack, _AsyncGeneratorContextManager # type: ignore
from datetime import timedelta
from functools import partial
from typing import TYPE_CHECKING, Any, Literal
from typing import TYPE_CHECKING, Any, Literal, TypedDict
import httpx
from anyio import ClosedResourceError
@@ -28,7 +28,6 @@ from pydantic import BaseModel, create_model
from ._tools import (
FunctionTool,
HostedMCPSpecificApproval,
_build_pydantic_model_from_json_schema,
)
from ._types import (
@@ -45,6 +44,21 @@ else:
if TYPE_CHECKING:
from ._clients import SupportsChatGetResponse
class MCPSpecificApproval(TypedDict, total=False):
"""Represents the specific approval mode for an MCP tool.
When using this mode, the user must specify which tools always or never require approval.
Attributes:
always_require_approval: A sequence of tool names that always require approval.
never_require_approval: A sequence of tool names that never require approval.
"""
always_require_approval: Collection[str] | None
never_require_approval: Collection[str] | None
logger = logging.getLogger(__name__)
# region: Helpers
@@ -327,7 +341,7 @@ class MCPTool:
self,
name: str,
description: str | None = None,
approval_mode: (Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None) = None,
approval_mode: (Literal["always_require", "never_require"] | MCPSpecificApproval | None) = None,
allowed_tools: Collection[str] | None = None,
load_tools: bool = True,
parse_tool_results: Literal[True] | Callable[[types.CallToolResult], Any] | None = True,
@@ -937,7 +951,7 @@ class MCPStdioTool(MCPTool):
request_timeout: int | None = None,
session: ClientSession | None = None,
description: str | None = None,
approval_mode: (Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None) = None,
approval_mode: (Literal["always_require", "never_require"] | MCPSpecificApproval | None) = None,
allowed_tools: Collection[str] | None = None,
args: list[str] | None = None,
env: dict[str, str] | None = None,
@@ -1058,7 +1072,7 @@ class MCPStreamableHTTPTool(MCPTool):
request_timeout: int | None = None,
session: ClientSession | None = None,
description: str | None = None,
approval_mode: (Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None) = None,
approval_mode: (Literal["always_require", "never_require"] | MCPSpecificApproval | None) = None,
allowed_tools: Collection[str] | None = None,
terminate_on_close: bool | None = None,
client: SupportsChatGetResponse | None = None,
@@ -1173,7 +1187,7 @@ class MCPWebsocketTool(MCPTool):
request_timeout: int | None = None,
session: ClientSession | None = None,
description: str | None = None,
approval_mode: (Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None) = None,
approval_mode: (Literal["always_require", "never_require"] | MCPSpecificApproval | None) = None,
allowed_tools: Collection[str] | None = None,
client: SupportsChatGetResponse | None = None,
additional_properties: dict[str, Any] | None = None,
@@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, Any, Final
from ._types import Message
if TYPE_CHECKING:
from ._tools import ToolProtocol
from ._tools import FunctionTool
if sys.version_info >= (3, 11):
from typing import Self # pragma: no cover
@@ -52,7 +52,7 @@ class Context:
self,
instructions: str | None = None,
messages: Sequence[Message] | None = None,
tools: Sequence[ToolProtocol] | None = None,
tools: Sequence[FunctionTool] | None = None,
):
"""Create a new Context object.
@@ -63,7 +63,7 @@ class Context:
"""
self.instructions = instructions
self.messages: Sequence[Message] = messages or []
self.tools: Sequence[ToolProtocol] = tools or []
self.tools: Sequence[FunctionTool] = tools or []
# region ContextProvider
@@ -18,7 +18,6 @@ from abc import abstractmethod
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any
from ._tools import ToolProtocol
from ._types import AgentResponse, Message
if TYPE_CHECKING:
@@ -110,7 +109,7 @@ class SessionContext:
input_messages: list[Message],
context_messages: dict[str, list[Message]] | None = None,
instructions: list[str] | None = None,
tools: list[ToolProtocol] | None = None,
tools: list[Any] | None = None,
options: dict[str, Any] | None = None,
metadata: dict[str, Any] | None = None,
):
@@ -131,7 +130,7 @@ class SessionContext:
self.input_messages = input_messages
self.context_messages: dict[str, list[Message]] = context_messages or {}
self.instructions: list[str] = instructions or []
self.tools: list[ToolProtocol] = tools or []
self.tools: list[Any] = tools or []
self._response: AgentResponse | None = None
self.options: dict[str, Any] = options or {}
self.metadata: dict[str, Any] = metadata or {}
@@ -185,7 +184,7 @@ class SessionContext:
instructions = [instructions]
self.instructions.extend(instructions)
def extend_tools(self, source_id: str, tools: Sequence[ToolProtocol]) -> None:
def extend_tools(self, source_id: str, tools: Sequence[Any]) -> None:
"""Add tools to be available for this invocation.
Tools are added with source attribution in their metadata.
+36 -406
View File
@@ -10,7 +10,6 @@ from collections.abc import (
AsyncIterable,
Awaitable,
Callable,
Collection,
Mapping,
MutableMapping,
Sequence,
@@ -25,18 +24,16 @@ from typing import (
Final,
Generic,
Literal,
Protocol,
TypedDict,
Union,
cast,
get_args,
get_origin,
overload,
runtime_checkable,
)
from opentelemetry.metrics import Histogram, NoOpHistogram
from pydantic import AnyUrl, BaseModel, Field, ValidationError, create_model
from pydantic import BaseModel, Field, ValidationError, create_model
from ._logging import get_logger
from ._serialization import SerializationMixin
@@ -58,10 +55,6 @@ if sys.version_info >= (3, 12):
from typing import override # type: ignore # pragma: no cover
else:
from typing_extensions import override # type: ignore[import] # pragma: no cover
if sys.version_info >= (3, 11):
from typing import TypedDict # type: ignore # pragma: no cover
else:
from typing_extensions import TypedDict # type: ignore # pragma: no cover
if TYPE_CHECKING:
@@ -85,13 +78,6 @@ __all__ = [
"FunctionInvocationConfiguration",
"FunctionInvocationLayer",
"FunctionTool",
"HostedCodeInterpreterTool",
"HostedFileSearchTool",
"HostedImageGenerationTool",
"HostedMCPSpecificApproval",
"HostedMCPTool",
"HostedWebSearchTool",
"ToolProtocol",
"normalize_function_invocation_configuration",
"tool",
]
@@ -163,380 +149,6 @@ def _parse_inputs(
# region Tools
@runtime_checkable
class ToolProtocol(Protocol):
"""Represents a generic tool.
This protocol defines the interface that all tools must implement to be compatible
with the agent framework. It is implemented by various tool classes such as HostedMCPTool,
HostedWebSearchTool, and FunctionTool's. A FunctionTool is usually created by the `tool` decorator.
Since each connector needs to parse tools differently, users can pass a dict to
specify a service-specific tool when no abstraction is available.
Attributes:
name: The name of the tool.
description: A description of the tool, suitable for use in describing the purpose to a model.
additional_properties: Additional properties associated with the tool.
"""
name: str
"""The name of the tool."""
description: str
"""A description of the tool, suitable for use in describing the purpose to a model."""
additional_properties: dict[str, Any] | None
"""Additional properties associated with the tool."""
def __str__(self) -> str:
"""Return a string representation of the tool."""
...
class BaseTool(SerializationMixin):
"""Base class for AI tools, providing common attributes and methods.
Used as the base class for the various tools in the agent framework, such as HostedMCPTool,
HostedWebSearchTool, and FunctionTool.
Since each connector needs to parse tools differently, this class is not exposed directly to end users.
In most cases, users can pass a dict to specify a service-specific tool when no abstraction is available.
"""
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"additional_properties"}
def __init__(
self,
*,
name: str,
description: str = "",
additional_properties: dict[str, Any] | None = None,
**kwargs: Any,
) -> None:
"""Initialize the BaseTool.
Keyword Args:
name: The name of the tool.
description: A description of the tool.
additional_properties: Additional properties associated with the tool.
**kwargs: Additional keyword arguments.
"""
self.name = name
self.description = description
self.additional_properties = additional_properties
for key, value in kwargs.items():
setattr(self, key, value)
def __str__(self) -> str:
"""Return a string representation of the tool."""
if self.description:
return f"{self.__class__.__name__}(name={self.name}, description={self.description})"
return f"{self.__class__.__name__}(name={self.name})"
class HostedCodeInterpreterTool(BaseTool):
"""Represents a hosted tool that can be specified to an AI service to enable it to execute generated code.
This tool does not implement code interpretation itself. It serves as a marker to inform a service
that it is allowed to execute generated code if the service is capable of doing so.
Examples:
.. code-block:: python
from agent_framework import HostedCodeInterpreterTool
# Create a code interpreter tool
code_tool = HostedCodeInterpreterTool()
# With file inputs
code_tool_with_files = HostedCodeInterpreterTool(inputs=[{"file_id": "file-123"}, {"file_id": "file-456"}])
"""
def __init__(
self,
*,
inputs: Content | dict[str, Any] | str | list[Content | dict[str, Any] | str] | None = None,
description: str | None = None,
additional_properties: dict[str, Any] | None = None,
**kwargs: Any,
) -> None:
"""Initialize the HostedCodeInterpreterTool.
Keyword Args:
inputs: A list of contents that the tool can accept as input. Defaults to None.
This should mostly be HostedFileContent or HostedVectorStoreContent.
Can also be DataContent, depending on the service used.
When supplying a list, it can contain:
- Content instances
- dicts with properties for Content (e.g., {"uri": "http://example.com", "media_type": "text/html"})
- strings (which will be converted to UriContent with media_type "text/plain").
If None, defaults to an empty list.
description: A description of the tool.
additional_properties: Additional properties associated with the tool.
**kwargs: Additional keyword arguments to pass to the base class.
"""
if "name" in kwargs:
raise ValueError("The 'name' argument is reserved for the HostedCodeInterpreterTool and cannot be set.")
self.inputs = _parse_inputs(inputs) if inputs else []
super().__init__(
name="code_interpreter",
description=description or "",
additional_properties=additional_properties,
**kwargs,
)
class HostedWebSearchTool(BaseTool):
"""Represents a web search tool that can be specified to an AI service to enable it to perform web searches.
Examples:
.. code-block:: python
from agent_framework import HostedWebSearchTool
# Create a basic web search tool
search_tool = HostedWebSearchTool()
# With location context
search_tool_with_location = HostedWebSearchTool(
description="Search the web for information",
additional_properties={"user_location": {"city": "Seattle", "country": "US"}},
)
"""
def __init__(
self,
description: str | None = None,
additional_properties: dict[str, Any] | None = None,
**kwargs: Any,
):
"""Initialize a HostedWebSearchTool.
Keyword Args:
description: A description of the tool.
additional_properties: Additional properties associated with the tool
(e.g., {"user_location": {"city": "Seattle", "country": "US"}}).
**kwargs: Additional keyword arguments to pass to the base class.
if additional_properties is not provided, any kwargs will be added to additional_properties.
"""
args: dict[str, Any] = {
"name": "web_search",
}
if additional_properties is not None:
args["additional_properties"] = additional_properties
elif kwargs:
args["additional_properties"] = kwargs
if description is not None:
args["description"] = description
super().__init__(**args)
class HostedImageGenerationToolOptions(TypedDict, total=False):
"""Options for HostedImageGenerationTool."""
count: int
image_size: str
media_type: str
model_id: str
response_format: Literal["uri", "data", "hosted"]
streaming_count: int
class HostedImageGenerationTool(BaseTool):
"""Represents a hosted tool that can be specified to an AI service to enable it to perform image generation."""
def __init__(
self,
*,
options: HostedImageGenerationToolOptions | None = None,
description: str | None = None,
additional_properties: dict[str, Any] | None = None,
**kwargs: Any,
):
"""Initialize a HostedImageGenerationTool."""
if "name" in kwargs:
raise ValueError("The 'name' argument is reserved for the HostedImageGenerationTool and cannot be set.")
self.options = options
super().__init__(
name="image_generation",
description=description or "",
additional_properties=additional_properties,
**kwargs,
)
class HostedMCPSpecificApproval(TypedDict, total=False):
"""Represents the specific mode for a hosted tool.
When using this mode, the user must specify which tools always or never require approval.
This is represented as a dictionary with two optional keys:
Attributes:
always_require_approval: A sequence of tool names that always require approval.
never_require_approval: A sequence of tool names that never require approval.
"""
always_require_approval: Collection[str] | None
never_require_approval: Collection[str] | None
class HostedMCPTool(BaseTool):
"""Represents a MCP tool that is managed and executed by the service.
Examples:
.. code-block:: python
from agent_framework import HostedMCPTool
# Create a basic MCP tool
mcp_tool = HostedMCPTool(
name="my_mcp_tool",
url="https://example.com/mcp",
)
# With approval mode and allowed tools
mcp_tool_with_approval = HostedMCPTool(
name="my_mcp_tool",
description="My MCP tool",
url="https://example.com/mcp",
approval_mode="always_require",
allowed_tools=["tool1", "tool2"],
headers={"Authorization": "Bearer token"},
)
# With specific approval mode
mcp_tool_specific = HostedMCPTool(
name="my_mcp_tool",
url="https://example.com/mcp",
approval_mode={
"always_require_approval": ["dangerous_tool"],
"never_require_approval": ["safe_tool"],
},
)
"""
def __init__(
self,
*,
name: str,
description: str | None = None,
url: AnyUrl | str,
approval_mode: Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None = None,
allowed_tools: Collection[str] | None = None,
headers: dict[str, str] | None = None,
additional_properties: dict[str, Any] | None = None,
**kwargs: Any,
) -> None:
"""Create a hosted MCP tool.
Keyword Args:
name: The name of the tool.
description: A description of the tool.
url: The URL of the tool.
approval_mode: The approval mode for the tool. This can be:
- "always_require": The tool always requires approval before use.
- "never_require": The tool never requires approval before use.
- A dict with keys `always_require_approval` or `never_require_approval`,
followed by a sequence of strings with the names of the relevant tools.
allowed_tools: A list of tools that are allowed to use this tool.
headers: Headers to include in requests to the tool.
additional_properties: Additional properties to include in the tool definition.
**kwargs: Additional keyword arguments to pass to the base class.
"""
try:
# Validate approval_mode
if approval_mode is not None:
if isinstance(approval_mode, str):
if approval_mode not in ("always_require", "never_require"):
raise ValueError(
f"Invalid approval_mode: {approval_mode}. "
"Must be 'always_require', 'never_require', or a dict with 'always_require_approval' "
"or 'never_require_approval' keys."
)
elif isinstance(approval_mode, dict):
# Validate that the dict has sets
for key, value in approval_mode.items():
if not isinstance(value, set):
approval_mode[key] = set(value) # type: ignore
# Validate allowed_tools
if allowed_tools is not None and isinstance(allowed_tools, dict):
raise TypeError(
f"allowed_tools must be a sequence of strings, not a dict. Got: {type(allowed_tools).__name__}"
)
super().__init__(
name=name,
description=description or "",
additional_properties=additional_properties,
**kwargs,
)
self.url = url if isinstance(url, AnyUrl) else AnyUrl(url)
self.approval_mode = approval_mode
self.allowed_tools = set(allowed_tools) if allowed_tools else None
self.headers = headers
except (ValidationError, ValueError, TypeError) as err:
raise ToolException(f"Error initializing HostedMCPTool: {err}", inner_exception=err) from err
class HostedFileSearchTool(BaseTool):
"""Represents a file search tool that can be specified to an AI service to enable it to perform file searches.
Examples:
.. code-block:: python
from agent_framework import HostedFileSearchTool
# Create a basic file search tool
file_search = HostedFileSearchTool()
# With vector store inputs and max results
file_search_with_inputs = HostedFileSearchTool(
inputs=[{"vector_store_id": "vs_123"}],
max_results=10,
description="Search files in vector store",
)
"""
def __init__(
self,
*,
inputs: Content | dict[str, Any] | str | list[Content | dict[str, Any] | str] | None = None,
max_results: int | None = None,
description: str | None = None,
additional_properties: dict[str, Any] | None = None,
**kwargs: Any,
):
"""Initialize a FileSearchTool.
Keyword Args:
inputs: A list of contents that the tool can accept as input. Defaults to None.
This should be one or more HostedVectorStoreContents.
When supplying a list, it can contain:
- Content instances
- dicts with properties for Content (e.g., {"uri": "http://example.com", "media_type": "text/html"})
- strings (which will be converted to UriContent with media_type "text/plain").
If None, defaults to an empty list.
max_results: The maximum number of results to return from the file search.
If None, max limit is applied.
description: A description of the tool.
additional_properties: Additional properties associated with the tool.
**kwargs: Additional keyword arguments to pass to the base class.
"""
if "name" in kwargs:
raise ValueError("The 'name' argument is reserved for the HostedFileSearchTool and cannot be set.")
self.inputs = _parse_inputs(inputs) if inputs else None
self.max_results = max_results
super().__init__(
name="file_search",
description=description or "",
additional_properties=additional_properties,
**kwargs,
)
def _default_histogram() -> Histogram:
@@ -576,12 +188,17 @@ class EmptyInputModel(BaseModel):
"""An empty input model for functions with no parameters."""
class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]):
class FunctionTool(SerializationMixin, Generic[ArgsT, ReturnT]):
"""A tool that wraps a Python function to make it callable by AI models.
This class wraps a Python function to make it callable by AI models with automatic
parameter validation and JSON schema generation.
Attributes:
name: The name of the tool.
description: A description of the tool, suitable for use in describing the purpose to a model.
additional_properties: Additional properties associated with the tool.
Examples:
.. code-block:: python
@@ -619,7 +236,12 @@ class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]):
"""
INJECTABLE: ClassVar[set[str]] = {"func"}
DEFAULT_EXCLUDE: ClassVar[set[str]] = {"input_model", "_invocation_duration_histogram", "_cached_parameters"}
DEFAULT_EXCLUDE: ClassVar[set[str]] = {
"additional_properties",
"input_model",
"_invocation_duration_histogram",
"_cached_parameters",
}
def __init__(
self,
@@ -661,12 +283,14 @@ class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]):
the expected arguments.
**kwargs: Additional keyword arguments.
"""
super().__init__(
name=name,
description=description,
additional_properties=additional_properties,
**kwargs,
)
# Core attributes (formerly from BaseTool)
self.name = name
self.description = description
self.additional_properties = additional_properties
for key, value in kwargs.items():
setattr(self, key, value)
# FunctionTool-specific attributes
self.func = func
self._instance = None # Store the instance for bound methods
self.input_model = self._resolve_input_model(input_model)
@@ -690,6 +314,12 @@ class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]):
self._forward_runtime_kwargs = True
break
def __str__(self) -> str:
"""Return a string representation of the tool."""
if self.description:
return f"{self.__class__.__name__}(name={self.name}, description={self.description})"
return f"{self.__class__.__name__}(name={self.name})"
@property
def declaration_only(self) -> bool:
"""Indicate whether the function is declaration only (i.e., has no implementation)."""
@@ -907,10 +537,10 @@ class FunctionTool(BaseTool, Generic[ArgsT, ReturnT]):
def _tools_to_dict(
tools: (
ToolProtocol
FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None
),
) -> list[str | dict[str, Any]] | None:
@@ -1464,7 +1094,7 @@ class FunctionInvocationConfiguration(TypedDict, total=False):
max_iterations: int
max_consecutive_errors_per_request: int
terminate_on_unknown_calls: bool
additional_tools: Sequence[ToolProtocol]
additional_tools: Sequence[FunctionTool]
include_detailed_errors: bool
@@ -1638,10 +1268,10 @@ async def _auto_invoke_function(
def _get_tool_map(
tools: ToolProtocol
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]],
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]],
) -> dict[str, FunctionTool[Any, Any]]:
tool_list: dict[str, FunctionTool[Any, Any]] = {}
for tool_item in tools if isinstance(tools, list) else [tools]:
@@ -1659,10 +1289,10 @@ async def _try_execute_function_calls(
custom_args: dict[str, Any],
attempt_idx: int,
function_calls: Sequence[Content],
tools: ToolProtocol
tools: FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]],
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]],
config: FunctionInvocationConfiguration,
middleware_pipeline: Any = None, # Optional MiddlewarePipeline to avoid circular imports
) -> tuple[Sequence[Content], bool]:
@@ -1848,8 +1478,8 @@ def _extract_tools(options: dict[str, Any] | None) -> Any:
options: The options dict containing chat options.
Returns:
ToolProtocol | Callable[..., Any] | MutableMapping[str, Any] |
Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] | None
FunctionTool | Callable[..., Any] | MutableMapping[str, Any] |
Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]] | None
"""
if options and isinstance(options, dict):
return options.get("tools")
+15 -15
View File
@@ -15,7 +15,7 @@ from pydantic import BaseModel
from ._logging import get_logger
from ._serialization import SerializationMixin
from ._tools import ToolProtocol, tool
from ._tools import FunctionTool, tool
from .exceptions import AdditionItemMismatch, ContentError
if sys.version_info >= (3, 13):
@@ -2972,10 +2972,10 @@ class _ChatOptionsBase(TypedDict, total=False):
# Tool configuration (forward reference to avoid circular import)
tools: (
ToolProtocol
FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None
)
tool_choice: ToolMode | Literal["auto", "required", "none"]
@@ -3065,17 +3065,17 @@ async def validate_chat_options(options: dict[str, Any]) -> dict[str, Any]:
def normalize_tools(
tools: (
ToolProtocol
FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None
),
) -> list[ToolProtocol | MutableMapping[str, Any]]:
) -> list[FunctionTool | MutableMapping[str, Any]]:
"""Normalize tools into a list.
Converts callables to FunctionTool objects and ensures all tools are either
ToolProtocol instances or MutableMappings.
FunctionTool instances or MutableMappings.
Args:
tools: Tools to normalize - can be a single tool, callable, or sequence.
@@ -3100,16 +3100,16 @@ def normalize_tools(
# List of tools
tools = normalize_tools([my_tool, another_tool])
"""
final_tools: list[ToolProtocol | MutableMapping[str, Any]] = []
final_tools: list[FunctionTool | MutableMapping[str, Any]] = []
if not tools:
return final_tools
if not isinstance(tools, Sequence) or isinstance(tools, (str, MutableMapping)):
# Single tool (not a sequence, or is a mapping which shouldn't be treated as sequence)
if not isinstance(tools, (ToolProtocol, MutableMapping)):
if not isinstance(tools, (FunctionTool, MutableMapping)):
return [tool(tools)]
return [tools]
for tool_item in tools:
if isinstance(tool_item, (ToolProtocol, MutableMapping)):
if isinstance(tool_item, (FunctionTool, MutableMapping)):
final_tools.append(tool_item)
else:
# Convert callable to FunctionTool
@@ -3119,17 +3119,17 @@ def normalize_tools(
async def validate_tools(
tools: (
ToolProtocol
FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
| None
),
) -> list[ToolProtocol | MutableMapping[str, Any]]:
) -> list[FunctionTool | MutableMapping[str, Any]]:
"""Validate and normalize tools into a list.
Converts callables to FunctionTool objects, expands MCP tools to their constituent
functions (connecting them if needed), and ensures all tools are either ToolProtocol
functions (connecting them if needed), and ensures all tools are either FunctionTool
instances or MutableMappings.
Args:
@@ -3159,7 +3159,7 @@ async def validate_tools(
normalized = normalize_tools(tools)
# Handle MCP tool expansion (async-only)
final_tools: list[ToolProtocol | MutableMapping[str, Any]] = []
final_tools: list[FunctionTool | MutableMapping[str, Any]] = []
for tool_ in normalized:
# Import MCPTool here to avoid circular imports
from ._mcp import MCPTool
@@ -13,7 +13,7 @@ from pydantic import BaseModel, SecretStr, ValidationError
from .._agents import Agent
from .._memory import ContextProvider
from .._middleware import MiddlewareTypes
from .._tools import FunctionTool, ToolProtocol
from .._tools import FunctionTool
from .._types import normalize_tools
from ..exceptions import ServiceInitializationError
from ._assistants_client import OpenAIAssistantsClient
@@ -43,10 +43,10 @@ OptionsCoT = TypeVar(
)
_ToolsType = (
ToolProtocol
FunctionTool
| Callable[..., Any]
| MutableMapping[str, Any]
| Sequence[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
| Sequence[FunctionTool | Callable[..., Any] | MutableMapping[str, Any]]
)
@@ -221,8 +221,8 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
description: A description of the assistant.
tools: Tools available to the assistant. Can include:
- FunctionTool instances or callables decorated with @tool
- HostedCodeInterpreterTool for code execution
- HostedFileSearchTool for vector store search
- Dict-based tools from OpenAIAssistantsClient.get_code_interpreter_tool()
- Dict-based tools from OpenAIAssistantsClient.get_file_search_tool()
- Raw tool dictionaries
metadata: Metadata to attach to the assistant (max 16 key-value pairs).
default_options: A TypedDict containing default chat options for the agent.
@@ -494,7 +494,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
self,
assistant_tools: list[Any],
user_tools: _ToolsType | None,
) -> list[ToolProtocol | MutableMapping[str, Any]]:
) -> list[FunctionTool | MutableMapping[str, Any]]:
"""Merge hosted tools from assistant with user-provided function tools.
Args:
@@ -504,7 +504,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
Returns:
A list of all tools (hosted tools + user function implementations).
"""
merged: list[ToolProtocol | MutableMapping[str, Any]] = []
merged: list[FunctionTool | MutableMapping[str, Any]] = []
# Add hosted tools from assistant using shared conversion
hosted_tools = from_assistant_tools(assistant_tools)
@@ -520,7 +520,7 @@ class OpenAIAssistantProvider(Generic[OptionsCoT]):
def _create_chat_agent_from_assistant(
self,
assistant: Assistant,
tools: list[ToolProtocol | MutableMapping[str, Any]] | None,
tools: list[FunctionTool | MutableMapping[str, Any]] | None,
instructions: str | None,
middleware: Sequence[MiddlewareTypes] | None,
context_provider: ContextProvider | None,
@@ -35,8 +35,6 @@ from .._tools import (
FunctionInvocationConfiguration,
FunctionInvocationLayer,
FunctionTool,
HostedCodeInterpreterTool,
HostedFileSearchTool,
)
from .._types import (
ChatOptions,
@@ -214,6 +212,62 @@ class OpenAIAssistantsClient( # type: ignore[misc]
):
"""OpenAI Assistants client with middleware, telemetry, and function invocation support."""
# region Hosted Tool Factory Methods
@staticmethod
def get_code_interpreter_tool() -> dict[str, Any]:
"""Create a code interpreter tool configuration for the Assistants API.
Returns:
A dict tool configuration ready to pass to ChatAgent.
Examples:
.. code-block:: python
from agent_framework.openai import OpenAIAssistantsClient
# Enable code interpreter
tool = OpenAIAssistantsClient.get_code_interpreter_tool()
agent = ChatAgent(client, tools=[tool])
"""
return {"type": "code_interpreter"}
@staticmethod
def get_file_search_tool(
*,
max_num_results: int | None = None,
) -> dict[str, Any]:
"""Create a file search tool configuration for the Assistants API.
Keyword Args:
max_num_results: Maximum number of results to return from file search.
Returns:
A dict tool configuration ready to pass to ChatAgent.
Examples:
.. code-block:: python
from agent_framework.openai import OpenAIAssistantsClient
# Basic file search
tool = OpenAIAssistantsClient.get_file_search_tool()
# With result limit
tool = OpenAIAssistantsClient.get_file_search_tool(max_num_results=10)
agent = ChatAgent(client, tools=[tool])
"""
tool: dict[str, Any] = {"type": "file_search"}
if max_num_results is not None:
tool["file_search"] = {"max_num_results": max_num_results}
return tool
# endregion
def __init__(
self,
*,
@@ -643,16 +697,8 @@ class OpenAIAssistantsClient( # type: ignore[misc]
for tool in tools:
if isinstance(tool, FunctionTool):
tool_definitions.append(tool.to_json_schema_spec()) # type: ignore[reportUnknownArgumentType]
elif isinstance(tool, HostedCodeInterpreterTool):
tool_definitions.append({"type": "code_interpreter"})
elif isinstance(tool, HostedFileSearchTool):
params: dict[str, Any] = {
"type": "file_search",
}
if tool.max_results is not None:
params["max_num_results"] = tool.max_results
tool_definitions.append(params)
elif isinstance(tool, MutableMapping):
# Pass through dict-based tools directly (from static factory methods)
tool_definitions.append(tool)
if len(tool_definitions) > 0:
@@ -16,6 +16,7 @@ from openai.types.chat.chat_completion import ChatCompletion, Choice
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice
from openai.types.chat.chat_completion_message_custom_tool_call import ChatCompletionMessageCustomToolCall
from openai.types.chat.completion_create_params import WebSearchOptions
from pydantic import BaseModel, ValidationError
from .._clients import BaseChatClient
@@ -25,8 +26,6 @@ from .._tools import (
FunctionInvocationConfiguration,
FunctionInvocationLayer,
FunctionTool,
HostedWebSearchTool,
ToolProtocol,
)
from .._types import (
ChatOptions,
@@ -154,6 +153,58 @@ class RawOpenAIChatClient( # type: ignore[misc]
Use ``OpenAIChatClient`` instead for a fully-featured client with all layers applied.
"""
# region Hosted Tool Factory Methods
@staticmethod
def get_web_search_tool(
*,
web_search_options: WebSearchOptions | None = None,
) -> dict[str, Any]:
"""Create a web search tool configuration for the Chat Completions API.
Note: For the Chat Completions API, web search is passed via the `web_search_options`
parameter rather than in the `tools` array. This method returns a dict that can be
passed as a tool to ChatAgent, which will handle it appropriately.
Keyword Args:
web_search_options: The full WebSearchOptions configuration. This TypedDict includes:
- user_location: Location context with "type" and "approximate" containing
"city", "country", "region", "timezone".
- search_context_size: One of "low", "medium", "high".
Returns:
A dict configuration that enables web search when passed to ChatAgent.
Examples:
.. code-block:: python
from agent_framework.openai import OpenAIChatClient
# Basic web search
tool = OpenAIChatClient.get_web_search_tool()
# With location context
tool = OpenAIChatClient.get_web_search_tool(
web_search_options={
"user_location": {
"type": "approximate",
"approximate": {"city": "Seattle", "country": "US"},
},
"search_context_size": "medium",
}
)
agent = ChatAgent(client, tools=[tool])
"""
tool: dict[str, Any] = {"type": "web_search"}
if web_search_options:
tool.update(web_search_options)
return tool
# endregion
@override
def _inner_get_response(
self,
@@ -222,35 +273,35 @@ class RawOpenAIChatClient( # type: ignore[misc]
# region content creation
def _prepare_tools_for_openai(self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]]) -> dict[str, Any]:
chat_tools: list[dict[str, Any]] = []
def _prepare_tools_for_openai(self, tools: Sequence[Any]) -> dict[str, Any]:
"""Prepare tools for the OpenAI Chat Completions API.
Converts FunctionTool to JSON schema format. Web search tools are routed
to web_search_options parameter. All other tools pass through unchanged.
Args:
tools: Sequence of tools to prepare.
Returns:
Dict containing tools and optionally web_search_options.
"""
chat_tools: list[Any] = []
web_search_options: dict[str, Any] | None = None
for tool in tools:
if isinstance(tool, ToolProtocol):
match tool:
case FunctionTool():
chat_tools.append(tool.to_json_schema_spec())
case HostedWebSearchTool():
web_search_options = (
{
"user_location": {
"approximate": tool.additional_properties.get("user_location", None),
"type": "approximate",
}
}
if tool.additional_properties and "user_location" in tool.additional_properties
else {}
)
case _:
logger.debug("Unsupported tool passed (type: %s), ignoring", type(tool))
if isinstance(tool, FunctionTool):
chat_tools.append(tool.to_json_schema_spec())
elif isinstance(tool, MutableMapping) and tool.get("type") == "web_search":
# Web search is handled via web_search_options, not tools array
web_search_options = {k: v for k, v in tool.items() if k != "type"}
else:
chat_tools.append(tool) # type: ignore[arg-type]
ret_dict: dict[str, Any] = {}
# Pass through all other tools (dicts, SDK types) unchanged
chat_tools.append(tool)
result: dict[str, Any] = {}
if chat_tools:
ret_dict["tools"] = chat_tools
result["tools"] = chat_tools
if web_search_options is not None:
ret_dict["web_search_options"] = web_search_options
return ret_dict
result["web_search_options"] = web_search_options
return result
def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, Any]) -> dict[str, Any]:
# Prepend instructions from options if they exist
@@ -29,8 +29,8 @@ from openai.types.responses.response_usage import ResponseUsage
from openai.types.responses.tool_param import (
CodeInterpreter,
CodeInterpreterContainerCodeInterpreterToolAuto,
ImageGeneration,
Mcp,
ToolParam,
)
from openai.types.responses.web_search_tool_param import WebSearchToolParam
from pydantic import BaseModel, ValidationError
@@ -42,12 +42,6 @@ from .._tools import (
FunctionInvocationConfiguration,
FunctionInvocationLayer,
FunctionTool,
HostedCodeInterpreterTool,
HostedFileSearchTool,
HostedImageGenerationTool,
HostedMCPTool,
HostedWebSearchTool,
ToolProtocol,
)
from .._types import (
Annotation,
@@ -433,138 +427,334 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
# region Prep methods
def _prepare_tools_for_openai(
self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None
) -> list[ToolParam | dict[str, Any]]:
response_tools: list[ToolParam | dict[str, Any]] = []
if not tools:
return response_tools
for tool in tools:
if isinstance(tool, ToolProtocol):
match tool:
case HostedMCPTool():
response_tools.append(self._prepare_mcp_tool(tool))
case HostedCodeInterpreterTool():
tool_args: CodeInterpreterContainerCodeInterpreterToolAuto = {"type": "auto"}
if tool.inputs:
tool_args["file_ids"] = []
for tool_input in tool.inputs:
if tool_input.type == "hosted_file":
tool_args["file_ids"].append(tool_input.file_id) # type: ignore[attr-defined]
if not tool_args["file_ids"]:
tool_args.pop("file_ids")
response_tools.append(
CodeInterpreter(
type="code_interpreter",
container=tool_args,
)
)
case FunctionTool():
params = tool.parameters()
params["additionalProperties"] = False
response_tools.append(
FunctionToolParam(
name=tool.name,
parameters=params,
strict=False,
type="function",
description=tool.description,
)
)
case HostedFileSearchTool():
if not tool.inputs:
raise ValueError("HostedFileSearchTool requires inputs to be specified.")
inputs: list[str] = [
inp.vector_store_id # type: ignore[misc]
for inp in tool.inputs
if inp.type == "hosted_vector_store" # type: ignore[attr-defined]
]
if not inputs:
raise ValueError(
"HostedFileSearchTool requires inputs to be of type `HostedVectorStoreContent`."
)
def _prepare_tools_for_openai(self, tools: Sequence[Any] | None) -> list[Any]:
"""Prepare tools for the OpenAI Responses API.
response_tools.append(
FileSearchToolParam(
type="file_search",
vector_store_ids=inputs,
max_num_results=tool.max_results
or self.FILE_SEARCH_MAX_RESULTS, # default to max results if not specified
)
)
case HostedWebSearchTool():
web_search_tool = WebSearchToolParam(type="web_search")
if location := (
tool.additional_properties.get("user_location", None)
if tool.additional_properties
else None
):
web_search_tool["user_location"] = {
"type": "approximate",
"city": location.get("city", None),
"country": location.get("country", None),
"region": location.get("region", None),
"timezone": location.get("timezone", None),
}
if filters := (
tool.additional_properties.get("filters", None) if tool.additional_properties else None
):
web_search_tool["filters"] = filters
if search_context_size := (
tool.additional_properties.get("search_context_size", None)
if tool.additional_properties
else None
):
web_search_tool["search_context_size"] = search_context_size
response_tools.append(web_search_tool)
case HostedImageGenerationTool():
mapped_tool: dict[str, Any] = {"type": "image_generation"}
if tool.options:
option_mapping = {
"image_size": "size",
"media_type": "output_format",
"model_id": "model",
"streaming_count": "partial_images",
}
# count and response_format are not supported by Responses API
for key, value in tool.options.items():
mapped_key = option_mapping.get(key, key)
mapped_tool[mapped_key] = value
if tool.additional_properties:
mapped_tool.update(tool.additional_properties)
response_tools.append(mapped_tool)
case _:
logger.debug("Unsupported tool passed (type: %s)", type(tool))
Converts FunctionTool to Responses API format. All other tools pass through unchanged.
Args:
tools: Sequence of tools to prepare.
Returns:
List of tool parameters ready for the OpenAI API.
"""
if not tools:
return []
response_tools: list[Any] = []
for tool in tools:
if isinstance(tool, FunctionTool):
params = tool.parameters()
params["additionalProperties"] = False
response_tools.append(
FunctionToolParam(
name=tool.name,
parameters=params,
strict=False,
type="function",
description=tool.description,
)
)
else:
# Handle raw dictionary tools
tool_dict = tool if isinstance(tool, dict) else dict(tool)
response_tools.append(tool_dict)
# Pass through all other tools (dicts, SDK types) unchanged
response_tools.append(tool)
return response_tools
# region Hosted Tool Factory Methods
@staticmethod
def _prepare_mcp_tool(tool: HostedMCPTool) -> Mcp:
"""Get MCP tool from HostedMCPTool."""
def get_code_interpreter_tool(
*,
file_ids: list[str] | None = None,
container: Literal["auto"] | CodeInterpreterContainerCodeInterpreterToolAuto = "auto",
) -> Any:
"""Create a code interpreter tool configuration for the Responses API.
Keyword Args:
file_ids: List of file IDs to make available to the code interpreter.
container: Container configuration. Use "auto" for automatic container management,
or provide a TypedDict with custom container settings.
Returns:
A CodeInterpreter tool parameter ready to pass to ChatAgent.
Examples:
.. code-block:: python
from agent_framework.openai import OpenAIResponsesClient
# Basic code interpreter
tool = OpenAIResponsesClient.get_code_interpreter_tool()
# With file access
tool = OpenAIResponsesClient.get_code_interpreter_tool(file_ids=["file-abc123"])
# Use with agent
agent = ChatAgent(client, tools=[tool])
"""
container_config: CodeInterpreterContainerCodeInterpreterToolAuto = (
container if isinstance(container, dict) else {"type": "auto"}
)
if file_ids:
container_config["file_ids"] = file_ids
return CodeInterpreter(type="code_interpreter", container=container_config)
@staticmethod
def get_web_search_tool(
*,
user_location: dict[str, str] | None = None,
search_context_size: Literal["low", "medium", "high"] | None = None,
filters: dict[str, Any] | None = None,
) -> Any:
"""Create a web search tool configuration for the Responses API.
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".
filters: Additional search filters.
Returns:
A WebSearchToolParam dict ready to pass to ChatAgent.
Examples:
.. code-block:: python
from agent_framework.openai import OpenAIResponsesClient
# Basic web search
tool = OpenAIResponsesClient.get_web_search_tool()
# With location context
tool = OpenAIResponsesClient.get_web_search_tool(
user_location={"city": "Seattle", "country": "US"},
search_context_size="medium",
)
agent = ChatAgent(client, tools=[tool])
"""
web_search_tool = WebSearchToolParam(type="web_search")
if user_location:
web_search_tool["user_location"] = {
"type": "approximate",
"city": user_location.get("city"),
"country": user_location.get("country"),
"region": user_location.get("region"),
"timezone": user_location.get("timezone"),
}
if search_context_size:
web_search_tool["search_context_size"] = search_context_size
if filters:
web_search_tool["filters"] = filters # type: ignore[typeddict-item]
return web_search_tool
@staticmethod
def get_image_generation_tool(
*,
size: Literal["1024x1024", "1024x1536", "1536x1024", "auto"] | None = None,
output_format: Literal["png", "jpeg", "webp"] | None = None,
model: Literal["gpt-image-1", "gpt-image-1-mini"] | str | None = None,
quality: Literal["low", "medium", "high", "auto"] | None = None,
partial_images: int | None = None,
background: Literal["transparent", "opaque", "auto"] | None = None,
moderation: Literal["auto", "low"] | None = None,
output_compression: int | None = None,
) -> Any:
"""Create an image generation tool configuration for the Responses API.
Keyword Args:
size: Image dimensions. One of "1024x1024", "1024x1536", "1536x1024", or "auto".
output_format: Output image format. One of "png", "jpeg", or "webp".
model: Model to use for image generation. One of "gpt-image-1" or "gpt-image-1-mini".
quality: Image quality level. One of "low", "medium", "high", or "auto".
partial_images: Number of partial images to stream during generation.
background: Background type. One of "transparent", "opaque", or "auto".
moderation: Moderation level. One of "auto" or "low".
output_compression: Compression level for output (0-100).
Returns:
An ImageGeneration tool parameter dict ready to pass to ChatAgent.
Examples:
.. code-block:: python
from agent_framework.openai import OpenAIResponsesClient
# Basic image generation
tool = OpenAIResponsesClient.get_image_generation_tool()
# High quality large image
tool = OpenAIResponsesClient.get_image_generation_tool(
size="1536x1024",
quality="high",
output_format="png",
)
agent = ChatAgent(client, tools=[tool])
"""
tool: ImageGeneration = {"type": "image_generation"}
if size:
tool["size"] = size
if output_format:
tool["output_format"] = output_format
if model:
tool["model"] = model
if quality:
tool["quality"] = quality
if partial_images is not None:
tool["partial_images"] = partial_images
if background:
tool["background"] = background
if moderation:
tool["moderation"] = moderation
if output_compression is not None:
tool["output_compression"] = output_compression
return tool
@staticmethod
def get_mcp_tool(
*,
name: str,
url: str,
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,
) -> Any:
"""Create a hosted MCP (Model Context Protocol) tool configuration for the Responses API.
This configures an MCP server that will be called by OpenAI's service.
The tools from this MCP server are executed remotely by OpenAI,
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 Mcp tool parameter dict ready to pass to ChatAgent.
Examples:
.. code-block:: python
from agent_framework.openai import OpenAIResponsesClient
# Basic MCP tool
tool = OpenAIResponsesClient.get_mcp_tool(
name="my_mcp",
url="https://mcp.example.com",
)
# With approval settings
tool = OpenAIResponsesClient.get_mcp_tool(
name="github_mcp",
url="https://mcp.github.com",
description="GitHub MCP server",
approval_mode="always_require",
headers={"Authorization": "Bearer token"},
)
# With specific tool approvals
tool = OpenAIResponsesClient.get_mcp_tool(
name="tools_mcp",
url="https://tools.example.com",
approval_mode={
"always_require_approval": ["dangerous_tool"],
"never_require_approval": ["safe_tool"],
},
)
agent = ChatAgent(client, tools=[tool])
"""
mcp: Mcp = {
"type": "mcp",
"server_label": tool.name.replace(" ", "_"),
"server_url": str(tool.url),
"server_description": tool.description,
"headers": tool.headers,
"server_label": name.replace(" ", "_"),
"server_url": url,
}
if tool.allowed_tools:
mcp["allowed_tools"] = list(tool.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 description:
mcp["server_description"] = description
if headers:
mcp["headers"] = headers
if allowed_tools:
mcp["allowed_tools"] = allowed_tools
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
@staticmethod
def get_file_search_tool(
*,
vector_store_ids: list[str],
max_num_results: int | None = None,
) -> Any:
"""Create a file search tool configuration for the Responses API.
Keyword Args:
vector_store_ids: List of vector store IDs to search within.
max_num_results: Maximum number of results to return. Defaults to 50 if not specified.
Returns:
A FileSearchToolParam dict ready to pass to ChatAgent.
Examples:
.. code-block:: python
from agent_framework.openai import OpenAIResponsesClient
# Basic file search
tool = OpenAIResponsesClient.get_file_search_tool(
vector_store_ids=["vs_abc123"],
)
# With result limit
tool = OpenAIResponsesClient.get_file_search_tool(
vector_store_ids=["vs_abc123", "vs_def456"],
max_num_results=10,
)
agent = ChatAgent(client, tools=[tool])
"""
tool = FileSearchToolParam(
type="file_search",
vector_store_ids=vector_store_ids,
)
if max_num_results is not None:
tool["max_num_results"] = max_num_results
return tool
# endregion
async def _prepare_options(
self,
messages: Sequence[Message],
@@ -904,7 +1094,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
for annotation in message_content.annotations:
match annotation.type:
case "file_path":
text_content.annotations.append(
text_content.annotations.append( # pyright: ignore[reportUnknownMemberType]
Annotation(
type="citation",
file_id=annotation.file_id,
@@ -915,7 +1105,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
)
)
case "file_citation":
text_content.annotations.append(
text_content.annotations.append( # pyright: ignore[reportUnknownMemberType]
Annotation(
type="citation",
url=annotation.filename,
@@ -927,7 +1117,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
)
)
case "url_citation":
text_content.annotations.append(
text_content.annotations.append( # pyright: ignore[reportUnknownMemberType]
Annotation(
type="citation",
title=annotation.title,
@@ -943,7 +1133,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
)
)
case "container_file_citation":
text_content.annotations.append(
text_content.annotations.append( # pyright: ignore[reportUnknownMemberType]
Annotation(
type="citation",
file_id=annotation.file_id,
@@ -1107,7 +1297,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
"raw_representation": response,
}
if conversation_id := self._get_conversation_id(response, options.get("store")):
if conversation_id := self._get_conversation_id(response, options.get("store")): # pyright: ignore[reportUnknownArgumentType]
args["conversation_id"] = conversation_id
if response.usage and (usage_details := self._parse_usage_from_openai(response.usage)):
args["usage_details"] = usage_details
@@ -1329,13 +1519,13 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
)
parsed_output: list[Content] | None = None
if result_output:
normalized = (
normalized = ( # pyright: ignore[reportUnknownVariableType]
result_output
if isinstance(result_output, Sequence)
and not isinstance(result_output, (str, bytes, MutableMapping))
else [result_output]
)
parsed_output = [Content.from_dict(output_item) for output_item in normalized]
parsed_output = [Content.from_dict(output_item) for output_item in normalized] # pyright: ignore[reportArgumentType,reportUnknownVariableType]
contents.append(
Content.from_mcp_server_tool_result(
call_id=call_id,
@@ -26,7 +26,7 @@ from .._logging import get_logger
from .._pydantic import AFBaseSettings
from .._serialization import SerializationMixin
from .._telemetry import APP_INFO, USER_AGENT_KEY, prepend_agent_framework_to_user_agent
from .._tools import FunctionTool, HostedCodeInterpreterTool, HostedFileSearchTool, ToolProtocol
from .._tools import FunctionTool
from ..exceptions import ServiceInitializationError
logger: logging.Logger = get_logger("agent_framework.openai")
@@ -284,12 +284,14 @@ class OpenAIConfigMixin(OpenAIBase):
def to_assistant_tools(
tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None,
tools: Sequence[FunctionTool | MutableMapping[str, Any]] | None,
) -> list[dict[str, Any]]:
"""Convert Agent Framework tools to OpenAI Assistants API format.
Handles FunctionTool instances and dict-based tools from static factory methods.
Args:
tools: Normalized tools (from ChatOptions.tools).
tools: Sequence of Agent Framework tools.
Returns:
List of tool definitions for OpenAI Assistants API.
@@ -302,15 +304,8 @@ def to_assistant_tools(
for tool in tools:
if isinstance(tool, FunctionTool):
tool_definitions.append(tool.to_json_schema_spec())
elif isinstance(tool, HostedCodeInterpreterTool):
tool_definitions.append({"type": "code_interpreter"})
elif isinstance(tool, HostedFileSearchTool):
params: dict[str, Any] = {"type": "file_search"}
if tool.max_results is not None:
params["file_search"] = {"max_num_results": tool.max_results}
tool_definitions.append(params)
elif isinstance(tool, MutableMapping):
# Pass through raw dict definitions
# Pass through dict-based tools directly (from static factory methods)
tool_definitions.append(dict(tool))
return tool_definitions
@@ -318,11 +313,11 @@ def to_assistant_tools(
def from_assistant_tools(
assistant_tools: list[Any] | None,
) -> list[ToolProtocol]:
"""Convert OpenAI Assistant tools to Agent Framework format.
) -> list[dict[str, Any]]:
"""Convert OpenAI Assistant tools to dict-based format.
This converts hosted tools (code_interpreter, file_search) from an OpenAI
Assistant definition back to Agent Framework tool instances.
Assistant definition back to dict-based tool definitions.
Note: Function tools are skipped - user must provide implementations separately.
@@ -330,12 +325,12 @@ def from_assistant_tools(
assistant_tools: Tools from OpenAI Assistant object (assistant.tools).
Returns:
List of Agent Framework tool instances for hosted tools.
List of dict-based tool definitions for hosted tools.
"""
if not assistant_tools:
return []
tools: list[ToolProtocol] = []
tools: list[dict[str, Any]] = []
for tool in assistant_tools:
if hasattr(tool, "type"):
@@ -346,9 +341,9 @@ def from_assistant_tools(
tool_type = None
if tool_type == "code_interpreter":
tools.append(HostedCodeInterpreterTool())
tools.append({"type": "code_interpreter"})
elif tool_type == "file_search":
tools.append(HostedFileSearchTool())
tools.append({"type": "file_search"})
# Skip function tools - user must provide implementations
return tools
@@ -15,7 +15,6 @@ from agent_framework import (
AgentThread,
ChatResponse,
ChatResponseUpdate,
HostedCodeInterpreterTool,
Message,
SupportsChatGetResponse,
tool,
@@ -513,7 +512,7 @@ async def test_azure_assistants_agent_code_interpreter():
async with Agent(
client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()),
instructions="You are a helpful assistant that can write and execute Python code.",
tools=[HostedCodeInterpreterTool()],
tools=[AzureOpenAIAssistantsClient.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.")
@@ -14,10 +14,6 @@ from agent_framework import (
AgentResponse,
ChatResponse,
Content,
HostedCodeInterpreterTool,
HostedFileSearchTool,
HostedMCPTool,
HostedWebSearchTool,
Message,
SupportsChatGetResponse,
tool,
@@ -289,7 +285,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": [AzureOpenAIResponsesClient.get_web_search_tool()],
},
"stream": streaming,
}
@@ -305,17 +301,13 @@ 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": [
AzureOpenAIResponsesClient.get_web_search_tool(user_location={"country": "US", "city": "Seattle"})
],
},
"stream": streaming,
}
@@ -341,7 +333,12 @@ async def test_integration_client_file_search() -> None:
text="What is the weather today? Do a file search to find the answer.",
)
],
options={"tools": [HostedFileSearchTool(inputs=vector_store)], "tool_choice": "auto"},
options={
"tools": [
AzureOpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id])
],
"tool_choice": "auto",
},
)
assert "sunny" in response.text.lower()
@@ -366,7 +363,12 @@ async def test_integration_client_file_search_streaming() -> None:
)
],
stream=True,
options={"tools": [HostedFileSearchTool(inputs=vector_store)], "tool_choice": "auto"},
options={
"tools": [
AzureOpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id])
],
"tool_choice": "auto",
},
)
full_response = await response_stream.get_final_response()
@@ -379,23 +381,23 @@ async def test_integration_client_file_search_streaming() -> None:
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_integration_client_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."""
client = AzureOpenAIResponsesClient(credential=AzureCliCredential())
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": AzureOpenAIResponsesClient.get_mcp_tool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
description="A Microsoft Learn MCP server for documentation questions",
approval_mode="never_require",
),
},
)
assert isinstance(response, ChatResponse)
assert response.text
# MCP server may return empty response intermittently - skip test rather than fail
if not response.text:
pytest.skip("MCP server returned empty response - service-side issue")
# Should contain Azure-related content since it's asking about Azure CLI
assert any(term in response.text.lower() for term in ["azure", "storage", "account", "cli"])
@@ -403,13 +405,13 @@ async def test_integration_client_agent_hosted_mcp_tool() -> None:
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_integration_client_agent_hosted_code_interpreter_tool():
"""Test Azure Responses Client agent with HostedCodeInterpreterTool through AzureOpenAIResponsesClient."""
"""Test Azure Responses Client agent with code interpreter tool."""
client = AzureOpenAIResponsesClient(credential=AzureCliCredential())
response = await client.get_response(
"Calculate the sum of numbers from 1 to 10 using Python code.",
options={
"tools": [HostedCodeInterpreterTool()],
"tools": [AzureOpenAIResponsesClient.get_code_interpreter_tool()],
},
)
# Should contain calculation result (sum of 1-10 = 55) or code execution content
+10 -17
View File
@@ -8,7 +8,6 @@ from typing import Any, Generic
from unittest.mock import patch
from uuid import uuid4
from pydantic import BaseModel
from pytest import fixture
from agent_framework import (
@@ -21,10 +20,10 @@ from agent_framework import (
ChatResponseUpdate,
Content,
FunctionInvocationLayer,
FunctionTool,
Message,
ResponseStream,
SupportsAgentRun,
ToolProtocol,
tool,
)
from agent_framework._clients import OptionsCoT
@@ -48,26 +47,20 @@ def chat_history() -> list[Message]:
@fixture
def ai_tool() -> ToolProtocol:
"""Returns a generic ToolProtocol."""
def ai_tool() -> FunctionTool:
"""Returns a generic FunctionTool."""
class GenericTool(BaseModel):
name: str
description: str
additional_properties: dict[str, Any] | None = None
@tool
def generic_tool(name: str) -> str:
"""A generic tool that echoes the name."""
return f"Hello, {name}"
def parameters(self) -> dict[str, Any]:
"""Return the parameters of the tool as a JSON schema."""
return {
"name": {"type": "string"},
}
return GenericTool(name="generic_tool", description="A generic tool")
return generic_tool
@fixture
def tool_tool() -> ToolProtocol:
"""Returns a executable ToolProtocol."""
def tool_tool() -> FunctionTool:
"""Returns a executable FunctionTool."""
@tool(approval_mode="never_require")
def simple_function(x: int, y: int) -> int:
+9 -10
View File
@@ -20,11 +20,10 @@ from agent_framework import (
Content,
Context,
ContextProvider,
HostedCodeInterpreterTool,
FunctionTool,
Message,
SupportsAgentRun,
SupportsChatGetResponse,
ToolProtocol,
tool,
)
from agent_framework._agents import _merge_options, _sanitize_agent_name
@@ -117,7 +116,7 @@ async def test_chat_client_agent_prepare_thread_and_messages(client: SupportsCha
async def test_prepare_thread_does_not_mutate_agent_chat_options(client: SupportsChatGetResponse) -> None:
tool = HostedCodeInterpreterTool()
tool = {"type": "code_interpreter"}
agent = Agent(client=client, tools=[tool])
assert agent.default_options.get("tools") is not None
@@ -132,7 +131,7 @@ async def test_prepare_thread_does_not_mutate_agent_chat_options(client: Support
assert prepared_chat_options.get("tools") is not None
assert base_tools is not prepared_chat_options["tools"]
prepared_chat_options["tools"].append(HostedCodeInterpreterTool()) # type: ignore[arg-type]
prepared_chat_options["tools"].append({"type": "code_interpreter"}) # type: ignore[arg-type]
assert len(agent.default_options["tools"]) == 1
@@ -144,7 +143,7 @@ async def test_chat_client_agent_update_thread_id(chat_client_base: SupportsChat
chat_client_base.run_responses = [mock_response]
agent = Agent(
client=chat_client_base,
tools=HostedCodeInterpreterTool(),
tools={"type": "code_interpreter"},
)
thread = agent.get_new_thread()
@@ -207,7 +206,7 @@ async def test_chat_client_agent_author_name_is_used_from_response(chat_client_b
)
]
agent = Agent(client=chat_client_base, tools=HostedCodeInterpreterTool())
agent = Agent(client=chat_client_base, tools={"type": "code_interpreter"})
result = await agent.run("Hello")
assert result.text == "test response"
@@ -806,7 +805,7 @@ def test_sanitize_agent_name_replaces_invalid_chars():
@pytest.mark.asyncio
async def test_agent_get_new_thread(chat_client_base: SupportsChatGetResponse, tool_tool: ToolProtocol):
async def test_agent_get_new_thread(chat_client_base: SupportsChatGetResponse, tool_tool: FunctionTool):
"""Test that get_new_thread returns a new AgentThread."""
agent = Agent(client=chat_client_base, tools=[tool_tool])
@@ -818,7 +817,7 @@ async def test_agent_get_new_thread(chat_client_base: SupportsChatGetResponse, t
@pytest.mark.asyncio
async def test_agent_get_new_thread_with_context_provider(
chat_client_base: SupportsChatGetResponse, tool_tool: ToolProtocol
chat_client_base: SupportsChatGetResponse, tool_tool: FunctionTool
):
"""Test that get_new_thread passes context_provider to the thread."""
@@ -837,7 +836,7 @@ async def test_agent_get_new_thread_with_context_provider(
@pytest.mark.asyncio
async def test_agent_get_new_thread_with_service_thread_id(
chat_client_base: SupportsChatGetResponse, tool_tool: ToolProtocol
chat_client_base: SupportsChatGetResponse, tool_tool: FunctionTool
):
"""Test that get_new_thread passes kwargs like service_thread_id to the thread."""
agent = Agent(client=chat_client_base, tools=[tool_tool])
@@ -849,7 +848,7 @@ async def test_agent_get_new_thread_with_service_thread_id(
@pytest.mark.asyncio
async def test_agent_deserialize_thread(chat_client_base: SupportsChatGetResponse, tool_tool: ToolProtocol):
async def test_agent_deserialize_thread(chat_client_base: SupportsChatGetResponse, tool_tool: FunctionTool):
"""Test deserialize_thread restores a thread from serialized state."""
agent = Agent(client=chat_client_base, tools=[tool_tool])
@@ -8,6 +8,11 @@ from agent_framework import (
ChatResponse,
Message,
SupportsChatGetResponse,
SupportsCodeInterpreterTool,
SupportsFileSearchTool,
SupportsImageGenerationTool,
SupportsMCPTool,
SupportsWebSearchTool,
)
@@ -73,3 +78,66 @@ async def test_chat_client_instructions_handling(chat_client_base: SupportsChatG
assert appended_messages[0].text == "You are a helpful assistant."
assert appended_messages[1].role == "user"
assert appended_messages[1].text == "hello"
# region Tool Support Protocol Tests
def test_openai_responses_client_supports_all_tool_protocols():
"""Test that OpenAIResponsesClient supports all hosted tool protocols."""
from agent_framework.openai import OpenAIResponsesClient
assert isinstance(OpenAIResponsesClient, SupportsCodeInterpreterTool)
assert isinstance(OpenAIResponsesClient, SupportsWebSearchTool)
assert isinstance(OpenAIResponsesClient, SupportsImageGenerationTool)
assert isinstance(OpenAIResponsesClient, SupportsMCPTool)
assert isinstance(OpenAIResponsesClient, SupportsFileSearchTool)
def test_openai_chat_client_supports_web_search_only():
"""Test that OpenAIChatClient only supports web search tool."""
from agent_framework.openai import OpenAIChatClient
assert not isinstance(OpenAIChatClient, SupportsCodeInterpreterTool)
assert isinstance(OpenAIChatClient, SupportsWebSearchTool)
assert not isinstance(OpenAIChatClient, SupportsImageGenerationTool)
assert not isinstance(OpenAIChatClient, SupportsMCPTool)
assert not isinstance(OpenAIChatClient, SupportsFileSearchTool)
def test_openai_assistants_client_supports_code_interpreter_and_file_search():
"""Test that OpenAIAssistantsClient supports code interpreter and file search."""
from agent_framework.openai import OpenAIAssistantsClient
assert isinstance(OpenAIAssistantsClient, SupportsCodeInterpreterTool)
assert not isinstance(OpenAIAssistantsClient, SupportsWebSearchTool)
assert not isinstance(OpenAIAssistantsClient, SupportsImageGenerationTool)
assert not isinstance(OpenAIAssistantsClient, SupportsMCPTool)
assert isinstance(OpenAIAssistantsClient, SupportsFileSearchTool)
def test_protocol_isinstance_with_client_instance():
"""Test that protocol isinstance works with client instances."""
from agent_framework.openai import OpenAIResponsesClient
# Create mock client instance (won't connect to API)
client = OpenAIResponsesClient.__new__(OpenAIResponsesClient)
assert isinstance(client, SupportsCodeInterpreterTool)
assert isinstance(client, SupportsWebSearchTool)
def test_protocol_tool_methods_return_dict():
"""Test that static tool methods return dict[str, Any]."""
from agent_framework.openai import OpenAIResponsesClient
code_tool = OpenAIResponsesClient.get_code_interpreter_tool()
assert isinstance(code_tool, dict)
assert code_tool.get("type") == "code_interpreter"
web_tool = OpenAIResponsesClient.get_web_search_tool()
assert isinstance(web_tool, dict)
assert web_tool.get("type") == "web_search"
# endregion
+7 -3
View File
@@ -18,7 +18,6 @@ from agent_framework import (
MCPStreamableHTTPTool,
MCPWebsocketTool,
Message,
ToolProtocol,
)
from agent_framework._mcp import (
MCPTool,
@@ -744,7 +743,10 @@ def test_get_input_model_from_mcp_prompt():
async def test_local_mcp_server_initialization():
"""Test MCPTool initialization."""
server = MCPTool(name="test_server")
assert isinstance(server, ToolProtocol)
# MCPTool has the same core attributes as FunctionTool
assert hasattr(server, "name")
assert hasattr(server, "description")
assert hasattr(server, "additional_properties")
assert server.name == "test_server"
assert server.session is None
assert server.functions == []
@@ -795,7 +797,9 @@ async def test_local_mcp_server_load_functions():
return None
server = TestServer(name="test_server")
assert isinstance(server, ToolProtocol)
# MCPTool has the same core attributes as FunctionTool
assert hasattr(server, "name")
assert hasattr(server, "description")
async with server:
await server.load_tools()
assert len(server.functions) == 1
+3 -213
View File
@@ -10,10 +10,6 @@ from pydantic import BaseModel, ValidationError
from agent_framework import (
Content,
FunctionTool,
HostedCodeInterpreterTool,
HostedImageGenerationTool,
HostedMCPTool,
ToolProtocol,
tool,
)
from agent_framework._tools import (
@@ -21,7 +17,6 @@ from agent_framework._tools import (
_parse_annotation,
_parse_inputs,
)
from agent_framework.exceptions import ToolException
from agent_framework.observability import OtelAttr
# region FunctionTool and tool decorator tests
@@ -35,7 +30,6 @@ def test_tool_decorator():
"""A simple function that adds two numbers."""
return x + y
assert isinstance(test_tool, ToolProtocol)
assert isinstance(test_tool, FunctionTool)
assert test_tool.name == "test_tool"
assert test_tool.description == "A test tool"
@@ -56,7 +50,6 @@ def test_tool_decorator_without_args():
"""A simple function that adds two numbers."""
return x + y
assert isinstance(test_tool, ToolProtocol)
assert isinstance(test_tool, FunctionTool)
assert test_tool.name == "test_tool"
assert test_tool.description == "A simple function that adds two numbers."
@@ -174,7 +167,7 @@ def test_tool_without_args():
"""A simple function that adds two numbers."""
return 1 + 2
assert isinstance(test_tool, ToolProtocol)
assert isinstance(test_tool, FunctionTool)
assert isinstance(test_tool, FunctionTool)
assert test_tool.name == "test_tool"
assert test_tool.description == "A simple function that adds two numbers."
@@ -194,7 +187,6 @@ async def test_tool_decorator_with_async():
"""An async function that adds two numbers."""
return x + y
assert isinstance(async_test_tool, ToolProtocol)
assert isinstance(async_test_tool, FunctionTool)
assert async_test_tool.name == "async_test_tool"
assert async_test_tool.description == "An async test tool"
@@ -218,7 +210,6 @@ def test_tool_decorator_in_class():
test_tool = my_tools().test_tool
assert isinstance(test_tool, ToolProtocol)
assert isinstance(test_tool, FunctionTool)
assert test_tool.name == "test_tool"
assert test_tool.description == "A test tool"
@@ -701,30 +692,7 @@ def test_tool_serialization():
assert restored_tool_2(10, 4) == 6
# region HostedCodeInterpreterTool and _parse_inputs
def test_hosted_code_interpreter_tool_default():
"""Test HostedCodeInterpreterTool with default parameters."""
tool = HostedCodeInterpreterTool()
assert tool.name == "code_interpreter"
assert tool.inputs == []
assert tool.description == ""
assert tool.additional_properties is None
assert str(tool) == "HostedCodeInterpreterTool(name=code_interpreter)"
def test_hosted_code_interpreter_tool_with_description():
"""Test HostedCodeInterpreterTool with description and additional properties."""
tool = HostedCodeInterpreterTool(
description="A test code interpreter",
additional_properties={"version": "1.0", "language": "python"},
)
assert tool.name == "code_interpreter"
assert tool.description == "A test code interpreter"
assert tool.additional_properties == {"version": "1.0", "language": "python"}
# region _parse_inputs tests
def test_parse_inputs_none():
@@ -853,185 +821,7 @@ def test_parse_inputs_unsupported_type():
_parse_inputs(123)
def test_hosted_code_interpreter_tool_with_string_input():
"""Test HostedCodeInterpreterTool with string input."""
tool = HostedCodeInterpreterTool(inputs="http://example.com")
assert len(tool.inputs) == 1
assert tool.inputs[0].type == "uri"
assert tool.inputs[0].uri == "http://example.com"
def test_hosted_code_interpreter_tool_with_dict_inputs():
"""Test HostedCodeInterpreterTool with dictionary inputs."""
inputs = [{"uri": "http://example.com", "media_type": "text/html"}, {"file_id": "file-123"}]
tool = HostedCodeInterpreterTool(inputs=inputs)
assert len(tool.inputs) == 2
assert tool.inputs[0].type == "uri"
assert tool.inputs[0].uri == "http://example.com"
assert tool.inputs[0].media_type == "text/html"
assert tool.inputs[1].type == "hosted_file"
assert tool.inputs[1].file_id == "file-123"
def test_hosted_code_interpreter_tool_with_ai_contents():
"""Test HostedCodeInterpreterTool with Content instances."""
inputs = [Content.from_text(text="Hello, world!"), Content.from_data(data=b"test", media_type="text/plain")]
tool = HostedCodeInterpreterTool(inputs=inputs)
assert len(tool.inputs) == 2
assert tool.inputs[0].type == "text"
assert tool.inputs[0].text == "Hello, world!"
assert tool.inputs[1].type == "data"
assert tool.inputs[1].media_type == "text/plain"
def test_hosted_code_interpreter_tool_with_single_input():
"""Test HostedCodeInterpreterTool with single input (not in list)."""
input_dict = {"file_id": "file-single"}
tool = HostedCodeInterpreterTool(inputs=input_dict)
assert len(tool.inputs) == 1
assert tool.inputs[0].type == "hosted_file"
assert tool.inputs[0].file_id == "file-single"
def test_hosted_code_interpreter_tool_with_unknown_input():
"""Test HostedCodeInterpreterTool with single unknown input."""
with pytest.raises(ValueError, match="Unsupported input type"):
HostedCodeInterpreterTool(inputs={"hosted_file": "file-single"})
def test_hosted_image_generation_tool_defaults():
"""HostedImageGenerationTool should default name and empty description."""
tool = HostedImageGenerationTool()
assert tool.name == "image_generation"
assert tool.description == ""
assert tool.options is None
assert str(tool) == "HostedImageGenerationTool(name=image_generation)"
def test_hosted_image_generation_tool_with_options():
"""HostedImageGenerationTool should store options."""
tool = HostedImageGenerationTool(
description="Generate images",
options={"format": "png", "size": "1024x1024"},
additional_properties={"quality": "high"},
)
assert tool.name == "image_generation"
assert tool.description == "Generate images"
assert tool.options == {"format": "png", "size": "1024x1024"}
assert tool.additional_properties == {"quality": "high"}
# region HostedMCPTool tests
def test_hosted_mcp_tool_with_other_fields():
"""Test creating a HostedMCPTool with a specific approval dict, headers and additional properties."""
tool = HostedMCPTool(
name="mcp-tool",
url="https://mcp.example",
description="A test MCP tool",
headers={"x": "y"},
additional_properties={"p": 1},
)
assert tool.name == "mcp-tool"
# pydantic AnyUrl preserves as string-like
assert str(tool.url).startswith("https://")
assert tool.headers == {"x": "y"}
assert tool.additional_properties == {"p": 1}
assert tool.description == "A test MCP tool"
@pytest.mark.parametrize(
"approval_mode",
[
"always_require",
"never_require",
{
"always_require_approval": {"toolA"},
"never_require_approval": {"toolB"},
},
{
"always_require_approval": ["toolA"],
"never_require_approval": ("toolB",),
},
],
ids=["always_require", "never_require", "specific", "specific_with_parsing"],
)
def test_hosted_mcp_tool_with_approval_mode(approval_mode: str | dict[str, Any]):
"""Test creating a HostedMCPTool with a specific approval dict, headers and additional properties."""
tool = HostedMCPTool(name="mcp-tool", url="https://mcp.example", approval_mode=approval_mode)
assert tool.name == "mcp-tool"
# pydantic AnyUrl preserves as string-like
assert str(tool.url).startswith("https://")
if not isinstance(approval_mode, dict):
assert tool.approval_mode == approval_mode
else:
# approval_mode parsed to sets
assert isinstance(tool.approval_mode["always_require_approval"], set)
assert isinstance(tool.approval_mode["never_require_approval"], set)
assert "toolA" in tool.approval_mode["always_require_approval"]
assert "toolB" in tool.approval_mode["never_require_approval"]
def test_hosted_mcp_tool_invalid_approval_mode_raises():
"""Invalid approval_mode string should raise ServiceInitializationError."""
with pytest.raises(ToolException):
HostedMCPTool(name="bad", url="https://x", approval_mode="invalid_mode")
@pytest.mark.parametrize(
"tools",
[
{"toolA", "toolB"},
("toolA", "toolB"),
["toolA", "toolB"],
["toolA", "toolB", "toolA"],
],
ids=[
"set",
"tuple",
"list",
"list_with_duplicates",
],
)
def test_hosted_mcp_tool_with_allowed_tools(tools: list[str] | tuple[str, ...] | set[str]):
"""Test creating a HostedMCPTool with a list of allowed tools."""
tool = HostedMCPTool(
name="mcp-tool",
url="https://mcp.example",
allowed_tools=tools,
)
assert tool.name == "mcp-tool"
# pydantic AnyUrl preserves as string-like
assert str(tool.url).startswith("https://")
# approval_mode parsed to set
assert isinstance(tool.allowed_tools, set)
assert tool.allowed_tools == {"toolA", "toolB"}
def test_hosted_mcp_tool_with_dict_of_allowed_tools():
"""Test creating a HostedMCPTool with a dict of allowed tools."""
with pytest.raises(ToolException):
HostedMCPTool(
name="mcp-tool",
url="https://mcp.example",
allowed_tools={"toolA": "Tool A", "toolC": "Tool C"},
)
# endregion
async def test_ai_function_with_kwargs_injection():
+10 -16
View File
@@ -18,11 +18,11 @@ from agent_framework import (
ChatResponse,
ChatResponseUpdate,
Content,
FunctionTool,
Message,
ResponseStream,
TextSpanRegion,
ToolMode,
ToolProtocol,
UsageDetails,
detect_media_type_from_base64,
merge_chat_options,
@@ -41,26 +41,20 @@ from agent_framework.exceptions import ContentError
@fixture
def ai_tool() -> ToolProtocol:
"""Returns a generic ToolProtocol."""
def ai_tool() -> FunctionTool:
"""Returns a generic FunctionTool."""
class GenericTool(BaseModel):
name: str
description: str | None = None
additional_properties: dict[str, Any] | None = None
@tool
def generic_tool(name: str) -> str:
"""A generic tool that echoes the name."""
return f"Hello, {name}"
def parameters(self) -> dict[str, Any]:
"""Return the parameters of the tool as a JSON schema."""
return {
"name": {"type": "string"},
}
return GenericTool(name="generic_tool", description="A generic tool")
return generic_tool
@fixture
def tool_tool() -> ToolProtocol:
"""Returns a executable ToolProtocol."""
def tool_tool() -> FunctionTool:
"""Returns a executable FunctionTool."""
@tool
def simple_function(x: int, y: int) -> int:
@@ -8,9 +8,9 @@ import pytest
from openai.types.beta.assistant import Assistant
from pydantic import BaseModel, Field
from agent_framework import Agent, HostedCodeInterpreterTool, HostedFileSearchTool, normalize_tools, tool
from agent_framework import Agent, normalize_tools, tool
from agent_framework.exceptions import ServiceInitializationError
from agent_framework.openai import OpenAIAssistantProvider
from agent_framework.openai import OpenAIAssistantProvider, OpenAIAssistantsClient
from agent_framework.openai._shared import from_assistant_tools, to_assistant_tools
# region Test Helpers
@@ -269,7 +269,7 @@ class TestOpenAIAssistantProviderCreateAgent:
await provider.create_agent(
name="CodeAgent",
model="gpt-4",
tools=[HostedCodeInterpreterTool()],
tools=[OpenAIAssistantsClient.get_code_interpreter_tool()],
)
call_kwargs = mock_async_openai.beta.assistants.create.call_args.kwargs
@@ -282,7 +282,7 @@ class TestOpenAIAssistantProviderCreateAgent:
await provider.create_agent(
name="SearchAgent",
model="gpt-4",
tools=[HostedFileSearchTool()],
tools=[OpenAIAssistantsClient.get_file_search_tool()],
)
call_kwargs = mock_async_openai.beta.assistants.create.call_args.kwargs
@@ -295,7 +295,7 @@ class TestOpenAIAssistantProviderCreateAgent:
await provider.create_agent(
name="SearchAgent",
model="gpt-4",
tools=[HostedFileSearchTool(max_results=10)],
tools=[OpenAIAssistantsClient.get_file_search_tool(max_num_results=10)],
)
call_kwargs = mock_async_openai.beta.assistants.create.call_args.kwargs
@@ -309,7 +309,11 @@ class TestOpenAIAssistantProviderCreateAgent:
await provider.create_agent(
name="MultiToolAgent",
model="gpt-4",
tools=[get_weather, HostedCodeInterpreterTool(), HostedFileSearchTool()],
tools=[
get_weather,
OpenAIAssistantsClient.get_code_interpreter_tool(),
OpenAIAssistantsClient.get_file_search_tool(),
],
)
call_kwargs = mock_async_openai.beta.assistants.create.call_args.kwargs
@@ -564,22 +568,22 @@ class TestToolConversion:
assert api_tools[0]["function"]["name"] == "get_weather"
def test_to_assistant_tools_code_interpreter(self) -> None:
"""Test HostedCodeInterpreterTool conversion."""
api_tools = to_assistant_tools([HostedCodeInterpreterTool()])
"""Test code_interpreter tool dict conversion."""
api_tools = to_assistant_tools([OpenAIAssistantsClient.get_code_interpreter_tool()])
assert len(api_tools) == 1
assert api_tools[0] == {"type": "code_interpreter"}
def test_to_assistant_tools_file_search(self) -> None:
"""Test HostedFileSearchTool conversion."""
api_tools = to_assistant_tools([HostedFileSearchTool()])
"""Test file_search tool dict conversion."""
api_tools = to_assistant_tools([OpenAIAssistantsClient.get_file_search_tool()])
assert len(api_tools) == 1
assert api_tools[0]["type"] == "file_search"
def test_to_assistant_tools_file_search_with_max_results(self) -> None:
"""Test HostedFileSearchTool with max_results conversion."""
api_tools = to_assistant_tools([HostedFileSearchTool(max_results=5)])
"""Test file_search tool with max_results conversion."""
api_tools = to_assistant_tools([OpenAIAssistantsClient.get_file_search_tool(max_num_results=5)])
assert api_tools[0]["file_search"]["max_num_results"] == 5
@@ -605,7 +609,7 @@ class TestToolConversion:
tools = from_assistant_tools(assistant_tools)
assert len(tools) == 1
assert isinstance(tools[0], HostedCodeInterpreterTool)
assert tools[0] == {"type": "code_interpreter"}
def test_from_assistant_tools_file_search(self) -> None:
"""Test converting file_search tool from OpenAI format."""
@@ -614,7 +618,7 @@ class TestToolConversion:
tools = from_assistant_tools(assistant_tools)
assert len(tools) == 1
assert isinstance(tools[0], HostedFileSearchTool)
assert tools[0] == {"type": "file_search"}
def test_from_assistant_tools_function_skipped(self) -> None:
"""Test that function tools are skipped (no implementations)."""
@@ -707,7 +711,7 @@ class TestToolMerging:
merged = provider._merge_tools(assistant_tools, None) # type: ignore[reportPrivateUsage]
assert len(merged) == 1
assert isinstance(merged[0], HostedCodeInterpreterTool)
assert merged[0] == {"type": "code_interpreter"}
def test_merge_file_search(self, mock_async_openai: MagicMock) -> None:
"""Test merging file search tool."""
@@ -717,7 +721,7 @@ class TestToolMerging:
merged = provider._merge_tools(assistant_tools, None) # type: ignore[reportPrivateUsage]
assert len(merged) == 1
assert isinstance(merged[0], HostedFileSearchTool)
assert merged[0] == {"type": "file_search"}
def test_merge_with_user_tools(self, mock_async_openai: MagicMock) -> None:
"""Test merging hosted and user tools."""
@@ -727,7 +731,7 @@ class TestToolMerging:
merged = provider._merge_tools(assistant_tools, [get_weather]) # type: ignore[reportPrivateUsage]
assert len(merged) == 2
assert isinstance(merged[0], HostedCodeInterpreterTool)
assert merged[0] == {"type": "code_interpreter"}
def test_merge_multiple_hosted_tools(self, mock_async_openai: MagicMock) -> None:
"""Test merging multiple hosted tools."""
@@ -18,8 +18,6 @@ from agent_framework import (
ChatResponse,
ChatResponseUpdate,
Content,
HostedCodeInterpreterTool,
HostedFileSearchTool,
Message,
SupportsChatGetResponse,
tool,
@@ -736,11 +734,11 @@ def test_prepare_options_with_tool_tool(mock_async_openai: MagicMock) -> None:
def test_prepare_options_with_code_interpreter(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with HostedCodeInterpreterTool."""
"""Test _prepare_options with code interpreter tool."""
client = create_test_openai_assistants_client(mock_async_openai)
# Create a real HostedCodeInterpreterTool
code_tool = HostedCodeInterpreterTool()
# Create a code interpreter tool dict
code_tool = OpenAIAssistantsClient.get_code_interpreter_tool()
options = {
"tools": [code_tool],
@@ -831,12 +829,12 @@ def test_prepare_options_required_function(mock_async_openai: MagicMock) -> None
def test_prepare_options_with_file_search_tool(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with HostedFileSearchTool."""
"""Test _prepare_options with file_search tool."""
client = create_test_openai_assistants_client(mock_async_openai)
# Create a HostedFileSearchTool with max_results
file_search_tool = HostedFileSearchTool(max_results=10)
# Create a file_search tool with max_results
file_search_tool = OpenAIAssistantsClient.get_file_search_tool(max_num_results=10)
options = {
"tools": [file_search_tool],
@@ -851,7 +849,7 @@ def test_prepare_options_with_file_search_tool(mock_async_openai: MagicMock) ->
# Check file search tool was set correctly
assert "tools" in run_options
assert len(run_options["tools"]) == 1
expected_tool = {"type": "file_search", "max_num_results": 10}
expected_tool = {"type": "file_search", "file_search": {"max_num_results": 10}}
assert run_options["tools"][0] == expected_tool
assert run_options["tool_choice"] == "auto"
@@ -1182,7 +1180,7 @@ async def test_file_search() -> None:
response = await openai_assistants_client.get_response(
messages=messages,
options={
"tools": [HostedFileSearchTool()],
"tools": [OpenAIAssistantsClient.get_file_search_tool()],
"tool_resources": {"file_search": {"vector_store_ids": [vector_store.vector_store_id]}},
},
)
@@ -1209,7 +1207,7 @@ async def test_file_search_streaming() -> None:
stream=True,
messages=messages,
options={
"tools": [HostedFileSearchTool()],
"tools": [OpenAIAssistantsClient.get_file_search_tool()],
"tool_resources": {"file_search": {"vector_store_ids": [vector_store.vector_store_id]}},
},
)
@@ -1346,7 +1344,7 @@ async def test_openai_assistants_agent_code_interpreter():
async with Agent(
client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL),
instructions="You are a helpful assistant that can write and execute Python code.",
tools=[HostedCodeInterpreterTool()],
tools=[OpenAIAssistantsClient.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.")
@@ -15,10 +15,8 @@ from pytest import param
from agent_framework import (
ChatResponse,
Content,
HostedWebSearchTool,
Message,
SupportsChatGetResponse,
ToolProtocol,
prepare_function_call_results,
tool,
)
@@ -172,18 +170,22 @@ async def test_content_filter_exception_handling(openai_unit_test_env: dict[str,
def test_unsupported_tool_handling(openai_unit_test_env: dict[str, str]) -> None:
"""Test that unsupported tool types are handled correctly."""
"""Test that unsupported tool types are passed through unchanged."""
client = OpenAIChatClient()
# Create a mock ToolProtocol that's not a FunctionTool
unsupported_tool = MagicMock(spec=ToolProtocol)
unsupported_tool.__class__.__name__ = "UnsupportedAITool"
# Create a random object that's not a FunctionTool, dict, or callable
# This simulates an unsupported tool type that gets passed through
class UnsupportedTool:
pass
# This should ignore the unsupported ToolProtocol and return empty list
unsupported_tool = UnsupportedTool()
# Unsupported tools are passed through for the API to handle/reject
result = client._prepare_tools_for_openai([unsupported_tool]) # type: ignore
assert result == {}
assert "tools" in result
assert len(result["tools"]) == 1
# Also test with a non-ToolProtocol that should be converted to dict
# Also test with a dict-based tool that should be passed through
dict_tool = {"type": "function", "name": "test"}
result = client._prepare_tools_for_openai([dict_tool]) # type: ignore
assert result["tools"] == [dict_tool]
@@ -770,8 +772,8 @@ def test_prepare_tools_with_web_search_no_location(openai_unit_test_env: dict[st
"""Test preparing web search tool without user location."""
client = OpenAIChatClient()
# Web search tool without additional_properties
web_search_tool = HostedWebSearchTool()
# Web search tool using static method
web_search_tool = OpenAIChatClient.get_web_search_tool()
result = client._prepare_tools_for_openai([web_search_tool])
@@ -1071,11 +1073,13 @@ async def test_integration_web_search() -> None:
client = OpenAIChatClient(model_id="gpt-4o-search-preview")
for streaming in [False, True]:
# Use static method for web search tool
web_search_tool = OpenAIChatClient.get_web_search_tool()
content = {
"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": [web_search_tool],
},
}
if streaming:
@@ -1090,17 +1094,19 @@ 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",
web_search_tool_with_location = OpenAIChatClient.get_web_search_tool(
web_search_options={
"user_location": {
"type": "approximate",
"approximate": {"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": [web_search_tool_with_location],
},
}
if streaming:
@@ -31,11 +31,6 @@ from agent_framework import (
ChatResponse,
ChatResponseUpdate,
Content,
HostedCodeInterpreterTool,
HostedFileSearchTool,
HostedImageGenerationTool,
HostedMCPTool,
HostedWebSearchTool,
Message,
SupportsChatGetResponse,
tool,
@@ -236,19 +231,18 @@ async def test_get_response_with_all_parameters() -> None:
)
@pytest.mark.asyncio
async def test_web_search_tool_with_location() -> None:
"""Test HostedWebSearchTool with location parameters."""
"""Test web search tool with location parameters."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test web search tool with location
web_search_tool = HostedWebSearchTool(
additional_properties={
"user_location": {
"country": "US",
"city": "Seattle",
"region": "WA",
"timezone": "America/Los_Angeles",
}
# Test web search tool with location using static method
web_search_tool = OpenAIResponsesClient.get_web_search_tool(
user_location={
"city": "Seattle",
"country": "US",
"region": "WA",
"timezone": "America/Los_Angeles",
}
)
@@ -260,38 +254,21 @@ async def test_web_search_tool_with_location() -> None:
)
async def test_file_search_tool_with_invalid_inputs() -> None:
"""Test HostedFileSearchTool with invalid vector store inputs."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test with invalid inputs type (should trigger ValueError)
file_search_tool = HostedFileSearchTool(inputs=[Content.from_hosted_file(file_id="invalid")])
# Should raise an error due to invalid inputs
with pytest.raises(ValueError, match="HostedFileSearchTool requires inputs to be of type"):
await client.get_response(
messages=[Message(role="user", text="Search files")],
options={"tools": [file_search_tool]},
)
async def test_code_interpreter_tool_variations() -> None:
"""Test HostedCodeInterpreterTool with and without file inputs."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test code interpreter without files
code_tool_empty = HostedCodeInterpreterTool()
# Test code interpreter using static method
code_tool = OpenAIResponsesClient.get_code_interpreter_tool()
with pytest.raises(ServiceResponseException):
await client.get_response(
messages=[Message(role="user", text="Run some code")],
options={"tools": [code_tool_empty]},
messages=[Message("user", ["Run some code"])],
options={"tools": [code_tool]},
)
# Test code interpreter with files
code_tool_with_files = HostedCodeInterpreterTool(
inputs=[Content.from_hosted_file(file_id="file1"), Content.from_hosted_file(file_id="file2")]
)
# Test code interpreter with files using static method
code_tool_with_files = OpenAIResponsesClient.get_code_interpreter_tool(file_ids=["file1", "file2"])
with pytest.raises(ServiceResponseException):
await client.get_response(
@@ -319,18 +296,20 @@ async def test_content_filter_exception() -> None:
assert "content error" in str(exc_info.value)
@pytest.mark.asyncio
async def test_hosted_file_search_tool_validation() -> None:
"""Test get_response HostedFileSearchTool validation."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test HostedFileSearchTool without inputs (should raise ValueError)
empty_file_search_tool = HostedFileSearchTool()
# Test file search tool with vector store IDs
file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=["vs_123"])
with pytest.raises((ValueError, ServiceInvalidRequestError)):
# Test using file search tool - may raise various exceptions depending on API response
with pytest.raises((ValueError, ServiceInvalidRequestError, ServiceResponseException)):
await client.get_response(
messages=[Message(role="user", text="Test")],
options={"tools": [empty_file_search_tool]},
messages=[Message("user", ["Test"])],
options={"tools": [file_search_tool]},
)
@@ -1074,18 +1053,17 @@ def test_streaming_chunk_with_usage_only() -> None:
assert update.contents[0].usage_details["total_token_count"] == 75
def test_prepare_tools_for_openai_with_hosted_mcp() -> None:
"""Test that HostedMCPTool is converted to the correct response tool dict."""
def test_prepare_tools_for_openai_with_mcp() -> None:
"""Test that MCP tool dict is converted to the correct response tool dict."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
tool = HostedMCPTool(
name="My MCP",
# Use static method to create MCP tool
tool = OpenAIResponsesClient.get_mcp_tool(
name="My_MCP",
url="https://mcp.example",
description="An MCP server",
approval_mode={"always_require_approval": ["tool_a", "tool_b"]},
allowed_tools={"tool_a", "tool_b"},
allowed_tools=["tool_a", "tool_b"],
headers={"X-Test": "yes"},
additional_properties={"custom": "value"},
approval_mode={"always_require_approval": ["tool_a", "tool_b"]},
)
resp_tools = client._prepare_tools_for_openai([tool])
@@ -1097,7 +1075,6 @@ def test_prepare_tools_for_openai_with_hosted_mcp() -> None:
assert mcp["server_label"] == "My_MCP"
# server_url may be normalized to include a trailing slash by the client
assert str(mcp["server_url"]).rstrip("/") == "https://mcp.example"
assert mcp["server_description"] == "An MCP server"
assert mcp["headers"]["X-Test"] == "yes"
assert set(mcp["allowed_tools"]) == {"tool_a", "tool_b"}
# approval mapping created from approval_mode dict
@@ -1258,13 +1235,15 @@ def test_prepare_tools_for_openai_with_raw_image_generation_minimal() -> None:
assert len(image_tool) == 1
def test_prepare_tools_for_openai_with_hosted_image_generation() -> None:
"""Test HostedImageGenerationTool conversion."""
def test_prepare_tools_for_openai_with_image_generation_options() -> None:
"""Test image generation tool conversion with options."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
tool = HostedImageGenerationTool(
description="Generate images",
options={"output_format": "png", "size": "512x512"},
additional_properties={"quality": "high"},
# Use static method to create image generation tool
tool = OpenAIResponsesClient.get_image_generation_tool(
output_format="png",
size="512x512",
quality="high",
)
resp_tools = client._prepare_tools_for_openai([tool])
@@ -2324,11 +2303,13 @@ async def test_integration_web_search() -> None:
client = OpenAIResponsesClient(model_id="gpt-5")
for streaming in [False, True]:
# Use static method for web search tool
web_search_tool = OpenAIResponsesClient.get_web_search_tool()
content = {
"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": [web_search_tool],
},
}
if streaming:
@@ -2343,17 +2324,14 @@ 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",
}
}
web_search_tool_with_location = OpenAIResponsesClient.get_web_search_tool(
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": [web_search_tool_with_location],
},
}
if streaming:
@@ -2375,7 +2353,9 @@ async def test_integration_file_search() -> None:
assert isinstance(openai_responses_client, SupportsChatGetResponse)
file_id, vector_store = await create_vector_store(openai_responses_client)
# Test that the client will use the web search tool
# Use static method for file search tool
file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id])
# Test that the client will use the file search tool
response = await openai_responses_client.get_response(
messages=[
Message(
@@ -2385,7 +2365,7 @@ async def test_integration_file_search() -> None:
],
options={
"tool_choice": "auto",
"tools": [HostedFileSearchTool(inputs=vector_store)],
"tools": [file_search_tool],
},
)
@@ -2406,9 +2386,10 @@ async def test_integration_streaming_file_search() -> None:
assert isinstance(openai_responses_client, SupportsChatGetResponse)
file_id, vector_store = await create_vector_store(openai_responses_client)
# Use static method for file search tool
file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id])
# Test that the client will use the web search tool
response = openai_responses_client.get_response(
stream=True,
response = openai_responses_client.get_streaming_response(
messages=[
Message(
role="user",
@@ -2417,7 +2398,7 @@ async def test_integration_streaming_file_search() -> None:
],
options={
"tool_choice": "auto",
"tools": [HostedFileSearchTool(inputs=vector_store)],
"tools": [file_search_tool],
},
)
@@ -5,19 +5,12 @@ from __future__ import annotations
import sys
from collections.abc import Callable, Mapping
from pathlib import Path
from typing import Any, Literal, cast
from typing import Any, cast
import yaml
from agent_framework import (
Agent,
Content,
HostedCodeInterpreterTool,
HostedFileSearchTool,
HostedMCPSpecificApproval,
HostedMCPTool,
HostedWebSearchTool,
SupportsChatGetResponse,
ToolProtocol,
)
from agent_framework import (
FunctionTool as AFFunctionTool,
@@ -714,14 +707,14 @@ class AgentFactory:
chat_options["additional_chat_options"] = options.additionalProperties
return chat_options
def _parse_tools(self, tools: list[Tool] | None) -> list[ToolProtocol] | None:
"""Parse tool resources into ToolProtocol instances."""
def _parse_tools(self, tools: list[Tool] | None) -> list[AFFunctionTool | dict[str, Any]] | None:
"""Parse tool resources into AFFunctionTool instances or dict-based tools."""
if not tools:
return None
return [self._parse_tool(tool_resource) for tool_resource in tools]
def _parse_tool(self, tool_resource: Tool) -> ToolProtocol:
"""Parse a single tool resource into a ToolProtocol instance."""
def _parse_tool(self, tool_resource: Tool) -> AFFunctionTool | dict[str, Any]:
"""Parse a single tool resource into an AFFunctionTool instance."""
match tool_resource:
case FunctionTool():
func: Callable[..., Any] | None = None
@@ -736,88 +729,81 @@ class AgentFactory:
func=func,
)
case WebSearchTool():
return HostedWebSearchTool(
description=tool_resource.description, additional_properties=tool_resource.options
)
result: dict[str, Any] = {"type": "web_search_preview"}
if tool_resource.description:
result["description"] = tool_resource.description
if tool_resource.options:
result.update(tool_resource.options)
return result
case FileSearchTool():
add_props: dict[str, Any] = {}
result = {
"type": "file_search",
"vector_store_ids": tool_resource.vectorStoreIds or [],
}
if tool_resource.maximumResultCount is not None:
result["max_num_results"] = tool_resource.maximumResultCount
if tool_resource.description:
result["description"] = tool_resource.description
if tool_resource.ranker is not None:
add_props["ranker"] = tool_resource.ranker
result["ranker"] = tool_resource.ranker
if tool_resource.scoreThreshold is not None:
add_props["score_threshold"] = tool_resource.scoreThreshold
result["score_threshold"] = tool_resource.scoreThreshold
if tool_resource.filters:
add_props["filters"] = tool_resource.filters
return HostedFileSearchTool(
inputs=[Content.from_hosted_vector_store(id) for id in tool_resource.vectorStoreIds or []],
description=tool_resource.description,
max_results=tool_resource.maximumResultCount,
additional_properties=add_props,
)
result["filters"] = tool_resource.filters
return result
case CodeInterpreterTool():
return HostedCodeInterpreterTool(
inputs=[Content.from_hosted_file(file_id=file) for file in tool_resource.fileIds or []],
description=tool_resource.description,
)
result = {"type": "code_interpreter"}
if tool_resource.fileIds:
result["file_ids"] = tool_resource.fileIds
if tool_resource.description:
result["description"] = tool_resource.description
return result
case McpTool():
approval_mode: HostedMCPSpecificApproval | Literal["always_require", "never_require"] | None = None
result = {
"type": "mcp",
"server_label": tool_resource.name.replace(" ", "_") if tool_resource.name else "",
"server_url": str(tool_resource.url) if tool_resource.url else "",
}
if tool_resource.description:
result["server_description"] = tool_resource.description
if tool_resource.allowedTools:
result["allowed_tools"] = list(tool_resource.allowedTools)
# Handle approval mode
if tool_resource.approvalMode is not None:
if tool_resource.approvalMode.kind == "always":
approval_mode = "always_require"
result["require_approval"] = "always"
elif tool_resource.approvalMode.kind == "never":
approval_mode = "never_require"
result["require_approval"] = "never"
elif isinstance(tool_resource.approvalMode, McpServerToolSpecifyApprovalMode):
approval_mode = {}
approval_config: dict[str, Any] = {}
if tool_resource.approvalMode.alwaysRequireApprovalTools:
approval_mode["always_require_approval"] = (
tool_resource.approvalMode.alwaysRequireApprovalTools
)
approval_config["always"] = {
"tool_names": list(tool_resource.approvalMode.alwaysRequireApprovalTools)
}
if tool_resource.approvalMode.neverRequireApprovalTools:
approval_mode["never_require_approval"] = (
tool_resource.approvalMode.neverRequireApprovalTools
)
if not approval_mode:
approval_mode = None
approval_config["never"] = {
"tool_names": list(tool_resource.approvalMode.neverRequireApprovalTools)
}
if approval_config:
result["require_approval"] = approval_config
# Handle connection settings
headers: dict[str, str] | None = None
additional_properties: dict[str, Any] | None = None
if tool_resource.connection is not None:
match tool_resource.connection:
case ApiKeyConnection():
if tool_resource.connection.apiKey:
headers = {"Authorization": f"Bearer {tool_resource.connection.apiKey}"}
result["headers"] = {"Authorization": f"Bearer {tool_resource.connection.apiKey}"}
case RemoteConnection():
additional_properties = {
"connection": {
"kind": tool_resource.connection.kind,
"name": tool_resource.connection.name,
"authenticationMode": tool_resource.connection.authenticationMode,
"endpoint": tool_resource.connection.endpoint,
}
}
result["project_connection_id"] = tool_resource.connection.name
case ReferenceConnection():
additional_properties = {
"connection": {
"kind": tool_resource.connection.kind,
"name": tool_resource.connection.name,
"authenticationMode": tool_resource.connection.authenticationMode,
}
}
result["project_connection_id"] = tool_resource.connection.name
case AnonymousConnection():
pass
case _:
raise ValueError(f"Unsupported connection kind: {tool_resource.connection.kind}")
return HostedMCPTool(
name=tool_resource.name, # type: ignore
description=tool_resource.description,
url=tool_resource.url, # type: ignore
allowed_tools=tool_resource.allowedTools,
approval_mode=approval_mode,
headers=headers,
additional_properties=additional_properties,
)
return result
case _:
raise ValueError(f"Unsupported tool kind: {tool_resource.kind}")
@@ -698,11 +698,9 @@ class TestAgentFactoryMcpToolConnection:
"""Tests for MCP tool connection handling in AgentFactory._parse_tool."""
def _get_mcp_tools(self, agent):
"""Helper to get MCP tools from agent's default_options."""
from agent_framework import HostedMCPTool
"""Helper to get MCP dict tools from agent's default_options."""
tools = agent.default_options.get("tools", [])
return [t for t in tools if isinstance(t, HostedMCPTool)]
return [t for t in tools if isinstance(t, dict) and t.get("type") == "mcp"]
def test_mcp_tool_with_api_key_connection_sets_headers(self):
"""Test that MCP tool with ApiKeyConnection sets headers correctly."""
@@ -735,11 +733,11 @@ tools:
mcp_tool = mcp_tools[0]
# Verify headers are set with the API key
assert mcp_tool.headers is not None
assert mcp_tool.headers == {"Authorization": "Bearer my-secret-api-key"}
assert mcp_tool.get("headers") is not None
assert mcp_tool.get("headers") == {"Authorization": "Bearer my-secret-api-key"}
def test_mcp_tool_with_remote_connection_sets_additional_properties(self):
"""Test that MCP tool with RemoteConnection sets additional_properties correctly."""
"""Test that MCP tool with RemoteConnection sets project_connection_id correctly."""
from unittest.mock import MagicMock
from agent_framework_declarative import AgentFactory
@@ -769,16 +767,11 @@ tools:
assert len(mcp_tools) == 1
mcp_tool = mcp_tools[0]
# Verify additional_properties are set with connection info
assert mcp_tool.additional_properties is not None
assert "connection" in mcp_tool.additional_properties
conn = mcp_tool.additional_properties["connection"]
assert conn["kind"] == "remote"
assert conn["authenticationMode"] == "oauth"
assert conn["name"] == "github-mcp-oauth-connection"
# Verify project_connection_id is set from connection name
assert mcp_tool.get("project_connection_id") == "github-mcp-oauth-connection"
def test_mcp_tool_with_reference_connection_sets_additional_properties(self):
"""Test that MCP tool with ReferenceConnection sets additional_properties correctly."""
"""Test that MCP tool with ReferenceConnection sets project_connection_id correctly."""
from unittest.mock import MagicMock
from agent_framework_declarative import AgentFactory
@@ -808,15 +801,11 @@ tools:
assert len(mcp_tools) == 1
mcp_tool = mcp_tools[0]
# Verify additional_properties are set with connection info
assert mcp_tool.additional_properties is not None
assert "connection" in mcp_tool.additional_properties
conn = mcp_tool.additional_properties["connection"]
assert conn["kind"] == "reference"
assert conn["name"] == "my-connection-ref"
# Verify project_connection_id is set from connection name
assert mcp_tool.get("project_connection_id") == "my-connection-ref"
def test_mcp_tool_with_anonymous_connection_no_headers_or_properties(self):
"""Test that MCP tool with AnonymousConnection doesn't set headers or additional_properties."""
"""Test that MCP tool with AnonymousConnection doesn't set headers or project_connection_id."""
from unittest.mock import MagicMock
from agent_framework_declarative import AgentFactory
@@ -844,9 +833,9 @@ tools:
assert len(mcp_tools) == 1
mcp_tool = mcp_tools[0]
# Verify no headers or additional_properties are set
assert mcp_tool.headers is None
assert mcp_tool.additional_properties is None
# Verify no headers or project_connection_id are set
assert mcp_tool.get("headers") is None
assert mcp_tool.get("project_connection_id") is None
def test_mcp_tool_without_connection_preserves_existing_behavior(self):
"""Test that MCP tool without connection works as before (no headers or additional_properties)."""
@@ -877,14 +866,13 @@ tools:
mcp_tool = mcp_tools[0]
# Verify tool is created correctly without connection
assert mcp_tool.name == "simple-mcp-tool"
assert str(mcp_tool.url) == "https://api.example.com/mcp"
assert mcp_tool.approval_mode == "never_require"
assert mcp_tool.headers is None
assert mcp_tool.additional_properties is None
assert mcp_tool["server_label"] == "simple-mcp-tool"
assert mcp_tool["server_url"] == "https://api.example.com/mcp"
assert mcp_tool.get("require_approval") == "never"
assert mcp_tool.get("headers") is None
def test_mcp_tool_with_remote_connection_with_endpoint(self):
"""Test that MCP tool with RemoteConnection including endpoint sets it in additional_properties."""
"""Test that MCP tool with RemoteConnection including endpoint sets project_connection_id."""
from unittest.mock import MagicMock
from agent_framework_declarative import AgentFactory
@@ -915,7 +903,5 @@ tools:
assert len(mcp_tools) == 1
mcp_tool = mcp_tools[0]
# Verify additional_properties include endpoint
assert mcp_tool.additional_properties is not None
conn = mcp_tool.additional_properties["connection"]
assert conn["endpoint"] == "https://auth.example.com"
# Verify project_connection_id is set from connection name
assert mcp_tool.get("project_connection_id") == "my-oauth-connection"
@@ -21,7 +21,7 @@ from agent_framework import (
ResponseStream,
normalize_messages,
)
from agent_framework._tools import FunctionTool, ToolProtocol
from agent_framework._tools import FunctionTool
from agent_framework._types import normalize_tools
from agent_framework.exceptions import ServiceException, ServiceInitializationError
from copilot import CopilotClient, CopilotSession
@@ -151,10 +151,10 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
description: str | None = None,
context_provider: ContextProvider | None = None,
middleware: Sequence[AgentMiddlewareTypes] | 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: OptionsT | None = None,
env_file_path: str | None = None,
@@ -173,7 +173,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
description: Description of the GitHubCopilotAgent.
context_provider: Context Provider, to be used by the agent.
middleware: Agent middleware used by the agent.
tools: Tools to use for the agent. Can be functions, ToolProtocol instances,
tools: Tools to use for the agent. Can be functions
or tool definition dicts. These are converted to Copilot SDK tools internally.
default_options: Default options for the agent. Can include cli_path, model,
timeout, log_level, etc.
@@ -479,7 +479,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
def _prepare_tools(
self,
tools: list[ToolProtocol | MutableMapping[str, Any]],
tools: list[FunctionTool | MutableMapping[str, Any]],
) -> list[CopilotTool]:
"""Convert Agent Framework tools to Copilot SDK tools.
@@ -492,14 +492,11 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
copilot_tools: list[CopilotTool] = []
for tool in tools:
if isinstance(tool, ToolProtocol):
match tool:
case FunctionTool():
copilot_tools.append(self._tool_to_copilot_tool(tool)) # type: ignore
case _:
logger.debug(f"Unsupported tool type: {type(tool)}")
if isinstance(tool, FunctionTool):
copilot_tools.append(self._tool_to_copilot_tool(tool)) # type: ignore
elif isinstance(tool, CopilotTool):
copilot_tools.append(tool)
# Note: Other tool types (e.g., dict-based hosted tools) are skipped
return copilot_tools
@@ -26,7 +26,7 @@ Example:
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from agent_framework import Agent, HostedCodeInterpreterTool, HostedWebSearchTool
from agent_framework import Agent
from agent_framework.azure import AzureAIAgentClient
from azure.identity.aio import AzureCliCredential
@@ -54,11 +54,8 @@ async def create_gaia_agent() -> AsyncIterator[Agent]:
instructions="Solve tasks to your best ability. Use Bing Search to find "
"information and Code Interpreter to perform calculations and data analysis.",
tools=[
HostedWebSearchTool(
name="Bing Grounding Search",
description="Search the web for current information using Bing",
),
HostedCodeInterpreterTool(),
AzureAIAgentClient.get_web_search_tool(),
AzureAIAgentClient.get_code_interpreter_tool(),
],
) as agent,
):
@@ -25,7 +25,7 @@ Example:
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from agent_framework import Agent, HostedCodeInterpreterTool, HostedWebSearchTool
from agent_framework import Agent
from agent_framework.openai import OpenAIResponsesClient
@@ -54,11 +54,8 @@ async def create_gaia_agent() -> AsyncIterator[Agent]:
instructions="Solve tasks to your best ability. Use Web Search to find "
"information and Code Interpreter to perform calculations and data analysis.",
tools=[
HostedWebSearchTool(
name="Web Search",
description="Search the web for current information",
),
HostedCodeInterpreterTool(),
OpenAIResponsesClient.get_web_search_tool(),
OpenAIResponsesClient.get_code_interpreter_tool(),
],
) as agent:
yield agent
@@ -9,7 +9,6 @@ from collections.abc import (
Awaitable,
Callable,
Mapping,
MutableMapping,
Sequence,
)
from itertools import chain
@@ -26,10 +25,8 @@ from agent_framework import (
FunctionInvocationConfiguration,
FunctionInvocationLayer,
FunctionTool,
HostedWebSearchTool,
Message,
ResponseStream,
ToolProtocol,
UsageDetails,
get_logger,
)
@@ -343,7 +340,7 @@ class OllamaChatClient(
self.model_id = ollama_settings.model_id
self.client = client or AsyncClient(host=ollama_settings.host)
# Save Host URL for serialization with to_dict()
self.host = str(self.client._client.base_url)
self.host = str(self.client._client.base_url) # pyright: ignore[reportUnknownMemberType,reportPrivateUsage,reportUnknownArgumentType]
super().__init__(
middleware=middleware,
@@ -559,21 +556,22 @@ class OllamaChatClient(
resp.append(fcc)
return resp
def _prepare_tools_for_ollama(self, tools: list[ToolProtocol | MutableMapping[str, Any]]) -> list[dict[str, Any]]:
chat_tools: list[dict[str, Any]] = []
def _prepare_tools_for_ollama(self, tools: list[Any]) -> list[Any]:
"""Prepare tools for the Ollama API.
Converts FunctionTool to JSON schema format. All other tools pass through unchanged.
Args:
tools: List of tools to prepare.
Returns:
List of tool definitions ready for the Ollama API.
"""
chat_tools: list[Any] = []
for tool in tools:
if isinstance(tool, ToolProtocol):
match tool:
case FunctionTool():
chat_tools.append(tool.to_json_schema_spec())
case HostedWebSearchTool():
raise ServiceInvalidRequestError("HostedWebSearchTool is not supported by the Ollama client.")
case _:
raise ServiceInvalidRequestError(
"Unsupported tool type '"
f"{type(tool).__name__}"
"' for Ollama client. Supported tool types: FunctionTool."
)
if isinstance(tool, FunctionTool):
chat_tools.append(tool.to_json_schema_spec())
else:
chat_tools.append(tool if isinstance(tool, dict) else dict(tool))
# Pass through all other tools unchanged
chat_tools.append(tool)
return chat_tools
@@ -10,7 +10,6 @@ from agent_framework import (
BaseChatClient,
ChatResponseUpdate,
Content,
HostedWebSearchTool,
Message,
chat_middleware,
tool,
@@ -384,27 +383,30 @@ async def test_cmc_streaming_with_tool_call(
assert text_result.text == "test"
async def test_cmc_with_hosted_tool_call(
@patch.object(AsyncClient, "chat", new_callable=AsyncMock)
async def test_cmc_with_dict_tool_passthrough(
mock_chat: AsyncMock,
ollama_unit_test_env: dict[str, str],
chat_history: list[Message],
mock_chat_completion_response: OllamaChatResponse,
) -> None:
with pytest.raises(ServiceInvalidRequestError):
additional_properties = {
"user_location": {
"country": "US",
"city": "Seattle",
}
}
"""Test that dict-based tools are passed through to Ollama."""
mock_chat.return_value = mock_chat_completion_response
chat_history.append(Message(text="hello world", role="user"))
chat_history.append(Message(text="hello world", role="user"))
ollama_client = OllamaChatClient()
await ollama_client.get_response(
messages=chat_history,
options={
"tools": [{"type": "function", "function": {"name": "custom_tool", "parameters": {}}}],
},
)
ollama_client = OllamaChatClient()
await ollama_client.get_response(
messages=chat_history,
options={
"tools": HostedWebSearchTool(additional_properties=additional_properties),
},
)
# Verify the tool was passed through to the Ollama client
mock_chat.assert_called_once()
call_kwargs = mock_chat.call_args.kwargs
assert "tools" in call_kwargs
assert call_kwargs["tools"] == [{"type": "function", "function": {"name": "custom_tool", "parameters": {}}}]
@patch.object(AsyncClient, "chat", new_callable=AsyncMock)
@@ -1,20 +1,23 @@
# Copyright (c) Microsoft. All rights reserved.
from agent_framework import HostedMCPTool
from agent_framework.azure import AzureOpenAIChatClient
from azure.ai.agentserver.agentframework import from_agent_framework # pyright: ignore[reportUnknownVariableType]
from azure.identity import DefaultAzureCredential
def main():
# Create MCP tool configuration as dict
mcp_tool = {
"type": "mcp",
"server_label": "Microsoft_Learn_MCP",
"server_url": "https://learn.microsoft.com/api/mcp",
}
# Create an Agent using the Azure OpenAI Chat Client with a MCP Tool that connects to Microsoft Learn MCP
agent = AzureOpenAIChatClient(credential=DefaultAzureCredential()).as_agent(
name="DocsAgent",
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
tools=HostedMCPTool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
),
tools=mcp_tool,
)
# Run the agent as a hosted agent
@@ -2,7 +2,6 @@
import asyncio
from agent_framework import HostedMCPTool, HostedWebSearchTool
from agent_framework.anthropic import AnthropicChatOptions, AnthropicClient
"""
@@ -17,16 +16,21 @@ This sample demonstrates using Anthropic with:
async def main() -> None:
"""Example of streaming response (get results as they are generated)."""
agent = AnthropicClient[AnthropicChatOptions]().as_agent(
client = AnthropicClient[AnthropicChatOptions]()
# Create MCP tool configuration using instance method
mcp_tool = client.get_mcp_tool(
name="Microsoft_Learn_MCP",
url="https://learn.microsoft.com/api/mcp",
)
# Create web search tool configuration using instance method
web_search_tool = client.get_web_search_tool()
agent = client.as_agent(
name="DocsAgent",
instructions="You are a helpful agent for both Microsoft docs questions and general questions.",
tools=[
HostedMCPTool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
),
HostedWebSearchTool(),
],
tools=[mcp_tool, web_search_tool],
default_options={
# anthropic needs a value for the max_tokens parameter
# we set it to 1024, but you can override like this:
@@ -2,7 +2,6 @@
import asyncio
from agent_framework import HostedMCPTool, HostedWebSearchTool
from agent_framework.anthropic import AnthropicClient
from anthropic import AsyncAnthropicFoundry
@@ -28,16 +27,21 @@ To use the Foundry integration ensure you have the following environment variabl
async def main() -> None:
"""Example of streaming response (get results as they are generated)."""
agent = AnthropicClient(anthropic_client=AsyncAnthropicFoundry()).as_agent(
client = AnthropicClient(anthropic_client=AsyncAnthropicFoundry())
# Create MCP tool configuration using instance method
mcp_tool = client.get_mcp_tool(
name="Microsoft_Learn_MCP",
url="https://learn.microsoft.com/api/mcp",
)
# Create web search tool configuration using instance method
web_search_tool = client.get_web_search_tool()
agent = client.as_agent(
name="DocsAgent",
instructions="You are a helpful agent for both Microsoft docs questions and general questions.",
tools=[
HostedMCPTool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
),
HostedWebSearchTool(),
],
tools=[mcp_tool, web_search_tool],
default_options={
# anthropic needs a value for the max_tokens parameter
# we set it to 1024, but you can override like this:
@@ -4,7 +4,7 @@ import asyncio
import logging
from pathlib import Path
from agent_framework import Content, HostedCodeInterpreterTool
from agent_framework import Content
from agent_framework.anthropic import AnthropicChatOptions, AnthropicClient
logger = logging.getLogger(__name__)
@@ -34,7 +34,7 @@ async def main() -> None:
agent = client.as_agent(
name="DocsAgent",
instructions="You are a helpful agent for creating powerpoint presentations.",
tools=HostedCodeInterpreterTool(),
tools=client.get_code_interpreter_tool(),
default_options={
"max_tokens": 20000,
"thinking": {"type": "enabled", "budget_tokens": 10000},
@@ -15,7 +15,7 @@ This folder contains examples demonstrating different ways to create and use age
| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to search the web for current information and provide grounded responses with citations. Requires a Bing connection configured in your Azure AI project. |
| [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to search custom search instances and provide responses with relevant results. Requires a Bing Custom Search connection and instance configured in your Azure AI project. |
| [`azure_ai_with_browser_automation.py`](azure_ai_with_browser_automation.py) | Shows how to use Browser Automation with Azure AI agents to perform automated web browsing tasks and provide responses based on web interactions. Requires a Browser Automation connection configured in your Azure AI project. |
| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use the `HostedCodeInterpreterTool` with Azure AI agents to write and execute Python code for mathematical problem solving and data analysis. |
| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use `AzureAIClient.get_code_interpreter_tool()` with Azure AI agents to write and execute Python code for mathematical problem solving and data analysis. |
| [`azure_ai_with_code_interpreter_file_generation.py`](azure_ai_with_code_interpreter_file_generation.py) | Shows how to retrieve file IDs from code interpreter generated files using both streaming and non-streaming approaches. |
| [`azure_ai_with_code_interpreter_file_download.py`](azure_ai_with_code_interpreter_file_download.py) | Shows how to download files generated by code interpreter using the OpenAI containers API. |
| [`azure_ai_with_content_filtering.py`](azure_ai_with_content_filtering.py) | Shows how to enable content filtering (RAI policy) on Azure AI agents using `RaiConfig`. Requires creating an RAI policy in Azure AI Foundry portal first. |
@@ -23,8 +23,8 @@ This folder contains examples demonstrating different ways to create and use age
| [`azure_ai_with_existing_conversation.py`](azure_ai_with_existing_conversation.py) | Demonstrates how to use an existing conversation created on the service side with Azure AI agents. Shows two approaches: specifying conversation ID at the client level and using AgentThread with an existing conversation ID. |
| [`azure_ai_with_application_endpoint.py`](azure_ai_with_application_endpoint.py) | Demonstrates calling the Azure AI application-scoped endpoint. |
| [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured `AzureAIClient` settings, including project endpoint, model deployment, and credentials rather than relying on environment variable defaults. |
| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Shows how to use the `HostedFileSearchTool` with Azure AI agents to upload files, create vector stores, and enable agents to search through uploaded documents to answer user questions. |
| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate hosted Model Context Protocol (MCP) tools with Azure AI Agent. |
| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Shows how to use `AzureAIClient.get_file_search_tool()` with Azure AI agents to upload files, create vector stores, and enable agents to search through uploaded documents to answer user questions. |
| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate hosted Model Context Protocol (MCP) tools with Azure AI Agent using `AzureAIClient.get_mcp_tool()`. |
| [`azure_ai_with_local_mcp.py`](azure_ai_with_local_mcp.py) | Shows how to integrate local Model Context Protocol (MCP) tools with Azure AI agents. |
| [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Shows how to use structured outputs (response format) with Azure AI agents using Pydantic models to enforce specific response schemas. |
| [`azure_ai_with_runtime_json_schema.py`](azure_ai_with_runtime_json_schema.py) | Shows how to use structured outputs (response format) with Azure AI agents using a JSON schema to enforce specific response schemas. |
@@ -32,12 +32,12 @@ This folder contains examples demonstrating different ways to create and use age
| [`azure_ai_with_search_context_semantic.py`](../../context_providers/azure_ai_search/azure_ai_with_search_context_semantic.py) | Shows how to use AzureAISearchContextProvider with semantic mode. Fast hybrid search with vector + keyword search and semantic ranking for RAG. Best for simple queries where speed is critical. |
| [`azure_ai_with_sharepoint.py`](azure_ai_with_sharepoint.py) | Shows how to use SharePoint grounding with Azure AI agents to search through SharePoint content and answer user questions with proper citations. Requires a SharePoint connection configured in your Azure AI project. |
| [`azure_ai_with_thread.py`](azure_ai_with_thread.py) | Demonstrates thread management with Azure AI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. |
| [`azure_ai_with_image_generation.py`](azure_ai_with_image_generation.py) | Shows how to use the `ImageGenTool` with Azure AI agents to generate images based on text prompts. |
| [`azure_ai_with_image_generation.py`](azure_ai_with_image_generation.py) | Shows how to use `AzureAIClient.get_image_generation_tool()` with Azure AI agents to generate images based on text prompts. |
| [`azure_ai_with_memory_search.py`](azure_ai_with_memory_search.py) | Shows how to use memory search functionality with Azure AI agents for conversation persistence. Demonstrates creating memory stores and enabling agents to search through conversation history. |
| [`azure_ai_with_microsoft_fabric.py`](azure_ai_with_microsoft_fabric.py) | Shows how to use Microsoft Fabric with Azure AI agents to query Fabric data sources and provide responses based on data analysis. Requires a Microsoft Fabric connection configured in your Azure AI project. |
| [`azure_ai_with_openapi.py`](azure_ai_with_openapi.py) | Shows how to integrate OpenAPI specifications with Azure AI agents using dictionary-based tool configuration. Demonstrates using external REST APIs for dynamic data lookup. |
| [`azure_ai_with_reasoning.py`](azure_ai_with_reasoning.py) | Shows how to enable reasoning for a model that supports it. |
| [`azure_ai_with_web_search.py`](azure_ai_with_web_search.py) | Shows how to use the `HostedWebSearchTool` with Azure AI agents to perform web searches and retrieve up-to-date information from the internet. |
| [`azure_ai_with_web_search.py`](azure_ai_with_web_search.py) | Shows how to use `AzureAIClient.get_web_search_tool()` with Azure AI agents to perform web searches and retrieve up-to-date information from the internet. |
## Environment Variables
@@ -17,7 +17,9 @@ Shows both streaming and non-streaming responses with function tools.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -27,7 +27,9 @@ Each method returns a Agent that can be used for conversations.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -18,7 +18,9 @@ while subsequent calls with `get_agent()` reuse the latest agent version.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -2,8 +2,8 @@
import asyncio
from agent_framework import ChatResponse, HostedCodeInterpreterTool
from agent_framework.azure import AzureAIProjectAgentProvider
from agent_framework import ChatResponse
from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider
from azure.identity.aio import AzureCliCredential
from openai.types.responses.response import Response as OpenAIResponse
from openai.types.responses.response_code_interpreter_tool_call import ResponseCodeInterpreterToolCall
@@ -11,22 +11,26 @@ from openai.types.responses.response_code_interpreter_tool_call import ResponseC
"""
Azure AI Agent Code Interpreter Example
This sample demonstrates using HostedCodeInterpreterTool with AzureAIProjectAgentProvider
This sample demonstrates using get_code_interpreter_tool() with AzureAIProjectAgentProvider
for Python code execution and mathematical problem solving.
"""
async def main() -> None:
"""Example showing how to use the HostedCodeInterpreterTool with AzureAIProjectAgentProvider."""
"""Example showing how to use the code interpreter tool with AzureAIProjectAgentProvider."""
async with (
AzureCliCredential() as credential,
AzureAIProjectAgentProvider(credential=credential) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIClient(credential=credential)
code_interpreter_tool = client.get_code_interpreter_tool()
agent = await provider.create_agent(
name="MyCodeInterpreterAgent",
instructions="You are a helpful assistant that can write and execute Python code to solve problems.",
tools=HostedCodeInterpreterTool(),
tools=[code_interpreter_tool],
)
query = "Use code to get the factorial of 100?"
@@ -9,9 +9,8 @@ from agent_framework import (
AgentResponseUpdate,
Annotation,
Content,
HostedCodeInterpreterTool,
)
from agent_framework.azure import AzureAIProjectAgentProvider
from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider
from azure.identity.aio import AzureCliCredential
"""
@@ -119,17 +118,21 @@ async def download_container_files(file_contents: list[Annotation | Content], ag
async def non_streaming_example() -> None:
"""Example of downloading files from non-streaming response using CitationAnnotation."""
"""Example of downloading files from non-streaming response using Annotation."""
print("=== Non-Streaming Response Example ===")
async with (
AzureCliCredential() as credential,
AzureAIProjectAgentProvider(credential=credential) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIClient(credential=credential)
code_interpreter_tool = client.get_code_interpreter_tool()
agent = await provider.create_agent(
name="V2CodeInterpreterFileAgent",
instructions="You are a helpful assistant that can write and execute Python code to create files.",
tools=HostedCodeInterpreterTool(),
tools=[code_interpreter_tool],
)
print(f"User: {QUERY}\n")
@@ -154,8 +157,8 @@ async def non_streaming_example() -> None:
if annotations_found:
print(f"SUCCESS: Found {len(annotations_found)} file annotation(s)")
# Download the container files
downloaded_paths = await download_container_files(annotations_found, agent)
# Download the container files (cast to Sequence for type compatibility)
downloaded_paths = await download_container_files(list(annotations_found), agent)
if downloaded_paths:
print("\nDownloaded files available at:")
@@ -166,17 +169,21 @@ async def non_streaming_example() -> None:
async def streaming_example() -> None:
"""Example of downloading files from streaming response using HostedFileContent."""
"""Example of downloading files from streaming response using Content with type='hosted_file'."""
print("\n=== Streaming Response Example ===")
async with (
AzureCliCredential() as credential,
AzureAIProjectAgentProvider(credential=credential) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIClient(credential=credential)
code_interpreter_tool = client.get_code_interpreter_tool()
agent = await provider.create_agent(
name="V2CodeInterpreterFileAgentStreaming",
instructions="You are a helpful assistant that can write and execute Python code to create files.",
tools=HostedCodeInterpreterTool(),
tools=[code_interpreter_tool],
)
print(f"User: {QUERY}\n")
@@ -4,9 +4,8 @@ import asyncio
from agent_framework import (
AgentResponseUpdate,
HostedCodeInterpreterTool,
)
from agent_framework.azure import AzureAIProjectAgentProvider
from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider
from azure.identity.aio import AzureCliCredential
"""
@@ -31,10 +30,14 @@ async def non_streaming_example() -> None:
AzureCliCredential() as credential,
AzureAIProjectAgentProvider(credential=credential) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIClient(credential=credential)
code_interpreter_tool = client.get_code_interpreter_tool()
agent = await provider.create_agent(
name="V2CodeInterpreterFileAgent",
name="CodeInterpreterFileAgent",
instructions="You are a helpful assistant that can write and execute Python code to create files.",
tools=HostedCodeInterpreterTool(),
tools=[code_interpreter_tool],
)
print(f"User: {QUERY}\n")
@@ -67,10 +70,14 @@ async def streaming_example() -> None:
AzureCliCredential() as credential,
AzureAIProjectAgentProvider(credential=credential) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIClient(credential=credential)
code_interpreter_tool = client.get_code_interpreter_tool()
agent = await provider.create_agent(
name="V2CodeInterpreterFileAgentStreaming",
instructions="You are a helpful assistant that can write and execute Python code to create files.",
tools=HostedCodeInterpreterTool(),
tools=[code_interpreter_tool],
)
print(f"User: {QUERY}\n")
@@ -17,7 +17,9 @@ This sample demonstrates usage of AzureAIProjectAgentProvider with existing conv
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -18,7 +18,9 @@ settings rather than relying on environment variable defaults.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -4,8 +4,7 @@ import asyncio
import os
from pathlib import Path
from agent_framework import Content, HostedFileSearchTool
from agent_framework.azure import AzureAIProjectAgentProvider
from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider
from azure.ai.agents.aio import AgentsClient
from azure.ai.agents.models import FileInfo, VectorStore
from azure.identity.aio import AzureCliCredential
@@ -45,8 +44,9 @@ async def main() -> None:
vector_store = await agents_client.vector_stores.create_and_poll(file_ids=[file.id], name="my_vectorstore")
print(f"Created vector store, vector store ID: {vector_store.id}")
# 2. Create file search tool with uploaded resources
file_search_tool = HostedFileSearchTool(inputs=[Content.from_hosted_vector_store(vector_store_id=vector_store.id)])
# 2. Create a client to access hosted tool factory methods
client = AzureAIClient(credential=credential)
file_search_tool = client.get_file_search_tool(vector_store_ids=[vector_store.id])
# 3. Create an agent with file search capabilities using the provider
agent = await provider.create_agent(
@@ -55,7 +55,7 @@ async def main() -> None:
"You are a helpful assistant that can search through uploaded employee files "
"to answer questions about employees."
),
tools=file_search_tool,
tools=[file_search_tool],
)
# 4. Simulate conversation with the agent
@@ -3,8 +3,8 @@
import asyncio
from typing import Any
from agent_framework import AgentResponse, AgentThread, HostedMCPTool, Message, SupportsAgentRun
from agent_framework.azure import AzureAIProjectAgentProvider
from agent_framework import AgentResponse, AgentThread, Message, SupportsAgentRun
from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider
from azure.identity.aio import AzureCliCredential
"""
@@ -65,14 +65,19 @@ async def run_hosted_mcp_without_approval() -> None:
AzureCliCredential() as credential,
AzureAIProjectAgentProvider(credential=credential) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIClient(credential=credential)
# Create MCP tool using instance method
mcp_tool = client.get_mcp_tool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
approval_mode="never_require",
)
agent = await provider.create_agent(
name="MyLearnDocsAgent",
instructions="You are a helpful assistant that can help with Microsoft documentation questions.",
tools=HostedMCPTool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
approval_mode="never_require",
),
tools=[mcp_tool],
)
query = "How to create an Azure storage account using az cli?"
@@ -91,14 +96,19 @@ async def run_hosted_mcp_with_approval_and_thread() -> None:
AzureCliCredential() as credential,
AzureAIProjectAgentProvider(credential=credential) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIClient(credential=credential)
# Create MCP tool using instance method
mcp_tool = client.get_mcp_tool(
name="api-specs",
url="https://gitmcp.io/Azure/azure-rest-api-specs",
approval_mode="always_require",
)
agent = await provider.create_agent(
name="MyApiSpecsAgent",
instructions="You are a helpful agent that can use MCP tools to assist users.",
tools=HostedMCPTool(
name="api-specs",
url="https://gitmcp.io/Azure/azure-rest-api-specs",
approval_mode="always_require",
),
tools=[mcp_tool],
)
thread = agent.get_new_thread()
@@ -5,8 +5,7 @@ import tempfile
from pathlib import Path
from urllib import request as urllib_request
from agent_framework import HostedImageGenerationTool
from agent_framework.azure import AzureAIProjectAgentProvider
from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider
from azure.identity.aio import AzureCliCredential
"""
@@ -28,22 +27,21 @@ async def main() -> None:
AzureCliCredential() as credential,
AzureAIProjectAgentProvider(credential=credential) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIClient(credential=credential)
# Create image generation tool using instance method
image_gen_tool = client.get_image_generation_tool(
model="gpt-image-1",
size="1024x1024",
output_format="png",
quality="low",
background="opaque",
)
agent = await provider.create_agent(
name="ImageGenAgent",
instructions="Generate images based on user requirements.",
tools=[
HostedImageGenerationTool(
options={
"model_id": "gpt-image-1",
"image_size": "1024x1024",
"media_type": "png",
},
additional_properties={
"quality": "low",
"background": "opaque",
},
)
],
tools=[image_gen_tool],
)
query = "Generate an image of Microsoft logo."
@@ -79,22 +79,22 @@ async def example_with_thread_persistence_in_memory() -> None:
thread = agent.get_new_thread()
# First conversation
query1 = "What's the weather like in Tokyo?"
print(f"User: {query1}")
result1 = await agent.run(query1, thread=thread, options={"store": False})
print(f"Agent: {result1.text}")
first_query = "What's the weather like in Tokyo?"
print(f"User: {first_query}")
first_result = await agent.run(first_query, thread=thread, options={"store": False})
print(f"Agent: {first_result.text}")
# Second conversation using the same thread - maintains context
query2 = "How about London?"
print(f"\nUser: {query2}")
result2 = await agent.run(query2, thread=thread, options={"store": False})
print(f"Agent: {result2.text}")
second_query = "How about London?"
print(f"\nUser: {second_query}")
second_result = await agent.run(second_query, thread=thread, options={"store": False})
print(f"Agent: {second_result.text}")
# Third conversation - agent should remember both previous cities
query3 = "Which of the cities I asked about has better weather?"
print(f"\nUser: {query3}")
result3 = await agent.run(query3, thread=thread, options={"store": False})
print(f"Agent: {result3.text}")
third_query = "Which of the cities I asked about has better weather?"
print(f"\nUser: {third_query}")
third_result = await agent.run(third_query, thread=thread, options={"store": False})
print(f"Agent: {third_result.text}")
print("Note: The agent remembers context from previous messages in the same thread.\n")
@@ -121,10 +121,10 @@ async def example_with_existing_thread_id() -> None:
# Start a conversation and get the thread ID
thread = agent.get_new_thread()
query1 = "What's the weather in Paris?"
print(f"User: {query1}")
result1 = await agent.run(query1, thread=thread)
print(f"Agent: {result1.text}")
first_query = "What's the weather in Paris?"
print(f"User: {first_query}")
first_result = await agent.run(first_query, thread=thread)
print(f"Agent: {first_result.text}")
# The thread ID is set after the first response
existing_thread_id = thread.service_thread_id
@@ -134,19 +134,19 @@ async def example_with_existing_thread_id() -> None:
print("\n--- Continuing with the same thread ID in a new agent instance ---")
# Create a new agent instance from the same provider
agent2 = await provider.create_agent(
second_agent = await provider.create_agent(
name="BasicWeatherAgent",
instructions="You are a helpful weather agent.",
tools=get_weather,
)
# Create a thread with the existing ID
thread = agent2.get_new_thread(service_thread_id=existing_thread_id)
thread = second_agent.get_new_thread(service_thread_id=existing_thread_id)
query2 = "What was the last city I asked about?"
print(f"User: {query2}")
result2 = await agent2.run(query2, thread=thread)
print(f"Agent: {result2.text}")
second_query = "What was the last city I asked about?"
print(f"User: {second_query}")
second_result = await second_agent.run(second_query, thread=thread)
print(f"Agent: {second_result.text}")
print("Note: The agent continues the conversation from the previous thread by using thread ID.\n")
@@ -2,15 +2,14 @@
import asyncio
from agent_framework import HostedWebSearchTool
from agent_framework.azure import AzureAIProjectAgentProvider
from agent_framework.azure import AzureAIClient, AzureAIProjectAgentProvider
from azure.identity.aio import AzureCliCredential
"""
Azure AI Agent With Web Search
This sample demonstrates basic usage of AzureAIProjectAgentProvider to create an agent
that can perform web searches using the HostedWebSearchTool.
that can perform web searches using get_web_search_tool().
Pre-requisites:
- Make sure to set up the AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME
@@ -25,10 +24,15 @@ async def main() -> None:
AzureCliCredential() as credential,
AzureAIProjectAgentProvider(credential=credential) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIClient(credential=credential)
# Create web search tool using instance method
web_search_tool = client.get_web_search_tool()
agent = await provider.create_agent(
name="WebsearchAgent",
instructions="You are a helpful assistant that can search the web",
tools=[HostedWebSearchTool()],
tools=[web_search_tool],
)
query = "What's the weather today in Seattle?"
@@ -32,20 +32,20 @@ async with (
|------|-------------|
| [`azure_ai_provider_methods.py`](azure_ai_provider_methods.py) | Comprehensive example demonstrating all `AzureAIAgentsProvider` methods: `create_agent()`, `get_agent()`, `as_agent()`, and managing multiple agents from a single provider. |
| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIAgentsProvider`. It automatically handles all configuration using environment variables. Shows both streaming and non-streaming responses. |
| [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to find real-time information from the web using custom search configurations. Demonstrates how to set up and use HostedWebSearchTool with custom search instances. |
| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to find real-time information from the web. Demonstrates web search capabilities with proper source citations and comprehensive error handling. |
| [`azure_ai_with_bing_custom_search.py`](azure_ai_with_bing_custom_search.py) | Shows how to use Bing Custom Search with Azure AI agents to find real-time information from the web using custom search configurations. Demonstrates how to use `AzureAIAgentClient.get_web_search_tool()` with custom search instances. |
| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to find real-time information from the web. Demonstrates `AzureAIAgentClient.get_web_search_tool()` with proper source citations and comprehensive error handling. |
| [`azure_ai_with_bing_grounding_citations.py`](azure_ai_with_bing_grounding_citations.py) | Demonstrates how to extract and display citations from Bing Grounding search responses. Shows how to collect citation annotations (title, URL, snippet) during streaming responses, enabling users to verify sources and access referenced content. |
| [`azure_ai_with_code_interpreter_file_generation.py`](azure_ai_with_code_interpreter_file_generation.py) | Shows how to retrieve file IDs from code interpreter generated files using both streaming and non-streaming approaches. |
| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use the HostedCodeInterpreterTool with Azure AI agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. |
| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use `AzureAIAgentClient.get_code_interpreter_tool()` with Azure AI agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. |
| [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with an existing SDK Agent object using `provider.as_agent()`. This wraps the agent without making HTTP calls. |
| [`azure_ai_with_existing_thread.py`](azure_ai_with_existing_thread.py) | Shows how to work with a pre-existing thread by providing the thread ID. Demonstrates proper cleanup of manually created threads. |
| [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured provider settings, including project endpoint and model deployment name. |
| [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Demonstrates how to use Azure AI Search with Azure AI agents. Shows how to create an agent with search tools using the SDK directly and wrap it with `provider.get_agent()`. |
| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Demonstrates how to use the HostedFileSearchTool with Azure AI agents to search through uploaded documents. Shows file upload, vector store creation, and querying document content. |
| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Demonstrates how to use `AzureAIAgentClient.get_file_search_tool()` with Azure AI agents to search through uploaded documents. Shows file upload, vector store creation, and querying document content. |
| [`azure_ai_with_function_tools.py`](azure_ai_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). |
| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate Azure AI agents with hosted Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates remote MCP server connections and tool discovery. |
| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to use `AzureAIAgentClient.get_mcp_tool()` with hosted Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates remote MCP server connections and tool discovery. |
| [`azure_ai_with_local_mcp.py`](azure_ai_with_local_mcp.py) | Shows how to integrate Azure AI agents with local Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates both agent-level and run-level tool configuration. |
| [`azure_ai_with_multiple_tools.py`](azure_ai_with_multiple_tools.py) | Demonstrates how to use multiple tools together with Azure AI agents, including web search, MCP servers, and function tools. Shows coordinated multi-tool interactions and approval workflows. |
| [`azure_ai_with_multiple_tools.py`](azure_ai_with_multiple_tools.py) | Demonstrates how to use multiple tools together with Azure AI agents, including web search, MCP servers, and function tools using client static methods. Shows coordinated multi-tool interactions and approval workflows. |
| [`azure_ai_with_openapi_tools.py`](azure_ai_with_openapi_tools.py) | Demonstrates how to use OpenAPI tools with Azure AI agents to integrate external REST APIs. Shows OpenAPI specification loading, anonymous authentication, thread context management, and coordinated multi-API conversations. |
| [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Demonstrates how to use structured outputs with Azure AI agents using Pydantic models. |
| [`azure_ai_with_thread.py`](azure_ai_with_thread.py) | Demonstrates thread management with Azure AI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. |
@@ -2,8 +2,7 @@
import asyncio
from agent_framework import HostedWebSearchTool
from agent_framework.azure import AzureAIAgentsProvider
from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider
from azure.identity.aio import AzureCliCredential
"""
@@ -30,25 +29,25 @@ To set up Bing Custom Search:
async def main() -> None:
"""Main function demonstrating Azure AI agent with Bing Custom Search."""
# 1. Create Bing Custom Search tool using HostedWebSearchTool
# The connection ID and instance name will be automatically picked up from environment variables
bing_search_tool = HostedWebSearchTool(
name="Bing Custom Search",
description="Search the web for current information using Bing Custom Search",
)
# 2. Use AzureAIAgentsProvider for agent creation and management
# Use AzureAIAgentsProvider for agent creation and management
async with (
AzureCliCredential() as credential,
AzureAIAgentsProvider(credential=credential) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIAgentClient(credential=credential)
# Create Bing Custom Search tool using instance method
# The connection ID and instance name will be automatically picked up from environment variables
# (BING_CUSTOM_CONNECTION_ID and BING_CUSTOM_INSTANCE_NAME)
bing_search_tool = client.get_web_search_tool()
agent = await provider.create_agent(
name="BingSearchAgent",
instructions=(
"You are a helpful agent that can use Bing Custom Search tools to assist users. "
"Use the available Bing Custom Search tools to answer questions and perform tasks."
),
tools=bing_search_tool,
tools=[bing_search_tool],
)
# 3. Demonstrate agent capabilities with bing custom search
@@ -2,8 +2,7 @@
import asyncio
from agent_framework import HostedWebSearchTool
from agent_framework.azure import AzureAIAgentsProvider
from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider
from azure.identity.aio import AzureCliCredential
"""
@@ -25,18 +24,17 @@ To set up Bing Grounding:
async def main() -> None:
"""Main function demonstrating Azure AI agent with Bing Grounding search."""
# 1. Create Bing Grounding search tool using HostedWebSearchTool
# The connection ID will be automatically picked up from environment variable
bing_search_tool = HostedWebSearchTool(
name="Bing Grounding Search",
description="Search the web for current information using Bing",
)
# 2. Use AzureAIAgentsProvider for agent creation and management
# Use AzureAIAgentsProvider for agent creation and management
async with (
AzureCliCredential() as credential,
AzureAIAgentsProvider(credential=credential) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIAgentClient(credential=credential)
# Create Bing Grounding search tool using instance method
# The connection ID will be automatically picked up from environment variable
bing_search_tool = client.get_web_search_tool()
agent = await provider.create_agent(
name="BingSearchAgent",
instructions=(
@@ -44,7 +42,7 @@ async def main() -> None:
"Use the Bing search tool to find up-to-date information and provide accurate, "
"well-sourced answers. Always cite your sources when possible."
),
tools=bing_search_tool,
tools=[bing_search_tool],
)
# 3. Demonstrate agent capabilities with web search
@@ -2,8 +2,8 @@
import asyncio
from agent_framework import Annotation, HostedWebSearchTool
from agent_framework.azure import AzureAIAgentsProvider
from agent_framework import Annotation
from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider
from azure.identity.aio import AzureCliCredential
"""
@@ -27,18 +27,17 @@ To set up Bing Grounding:
async def main() -> None:
"""Main function demonstrating Azure AI agent with Bing Grounding search."""
# 1. Create Bing Grounding search tool using HostedWebSearchTool
# The connection ID will be automatically picked up from environment variable
bing_search_tool = HostedWebSearchTool(
name="Bing Grounding Search",
description="Search the web for current information using Bing",
)
# 2. Use AzureAIAgentsProvider for agent creation and management
# Use AzureAIAgentsProvider for agent creation and management
async with (
AzureCliCredential() as credential,
AzureAIAgentsProvider(credential=credential) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIAgentClient(credential=credential)
# Create Bing Grounding search tool using instance method
# The connection ID will be automatically picked up from environment variable
bing_search_tool = client.get_web_search_tool()
agent = await provider.create_agent(
name="BingSearchAgent",
instructions=(
@@ -46,7 +45,7 @@ async def main() -> None:
"Use the Bing search tool to find up-to-date information and provide accurate, "
"well-sourced answers. Always cite your sources when possible."
),
tools=bing_search_tool,
tools=[bing_search_tool],
)
# 3. Demonstrate agent capabilities with web search
@@ -2,8 +2,8 @@
import asyncio
from agent_framework import AgentResponse, ChatResponseUpdate, HostedCodeInterpreterTool
from agent_framework.azure import AzureAIAgentsProvider
from agent_framework import AgentResponse, ChatResponseUpdate
from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider
from azure.ai.agents.models import (
RunStepDeltaCodeInterpreterDetailItemObject,
)
@@ -12,7 +12,7 @@ from azure.identity.aio import AzureCliCredential
"""
Azure AI Agent with Code Interpreter Example
This sample demonstrates using HostedCodeInterpreterTool with Azure AI Agents
This sample demonstrates using get_code_interpreter_tool() with Azure AI Agents
for Python code execution and mathematical problem solving.
"""
@@ -32,7 +32,7 @@ def print_code_interpreter_inputs(response: AgentResponse) -> None:
async def main() -> None:
"""Example showing how to use the HostedCodeInterpreterTool with Azure AI."""
"""Example showing how to use the code interpreter tool with Azure AI."""
print("=== Azure AI Agent with Code Interpreter Example ===")
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
@@ -41,10 +41,14 @@ async def main() -> None:
AzureCliCredential() as credential,
AzureAIAgentsProvider(credential=credential) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIAgentClient(credential=credential)
code_interpreter_tool = client.get_code_interpreter_tool()
agent = await provider.create_agent(
name="CodingAgent",
instructions=("You are a helpful assistant that can write and execute Python code to solve problems."),
tools=HostedCodeInterpreterTool(),
tools=[code_interpreter_tool],
)
query = "Generate the factorial of 100 using python code, show the code and execute it."
print(f"User: {query}")
@@ -3,17 +3,14 @@
import asyncio
import os
from agent_framework import (
HostedCodeInterpreterTool,
)
from agent_framework.azure import AzureAIAgentsProvider
from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider
from azure.ai.agents.aio import AgentsClient
from azure.identity.aio import AzureCliCredential
"""
Azure AI Agent Code Interpreter File Generation Example
This sample demonstrates using HostedCodeInterpreterTool with AzureAIAgentsProvider
This sample demonstrates using get_code_interpreter_tool() with AzureAIAgentsProvider
to generate a text file and then retrieve it.
The test flow:
@@ -32,6 +29,10 @@ async def main() -> None:
AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client,
AzureAIAgentsProvider(agents_client=agents_client) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIAgentClient(credential=credential)
code_interpreter_tool = client.get_code_interpreter_tool()
agent = await provider.create_agent(
name="CodeInterpreterAgent",
instructions=(
@@ -39,7 +40,7 @@ async def main() -> None:
"ALWAYS use the code interpreter tool to execute Python code when asked to create files. "
"Write actual Python code to create files, do not just describe what you would do."
),
tools=[HostedCodeInterpreterTool()],
tools=[code_interpreter_tool],
)
# Be very explicit about wanting code execution and a download link
@@ -19,7 +19,9 @@ by providing thread IDs for thread reuse patterns.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -18,7 +18,9 @@ settings rather than relying on environment variable defaults.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -4,8 +4,7 @@ import asyncio
import os
from pathlib import Path
from agent_framework import Content, HostedFileSearchTool
from agent_framework.azure import AzureAIAgentsProvider
from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider
from azure.ai.agents.aio import AgentsClient
from azure.ai.agents.models import FileInfo, VectorStore
from azure.identity.aio import AzureCliCredential
@@ -45,8 +44,9 @@ async def main() -> None:
vector_store = await agents_client.vector_stores.create_and_poll(file_ids=[file.id], name="my_vectorstore")
print(f"Created vector store, vector store ID: {vector_store.id}")
# 2. Create file search tool with uploaded resources
file_search_tool = HostedFileSearchTool(inputs=[Content.from_hosted_vector_store(vector_store_id=vector_store.id)])
# 2. Create a client to access hosted tool factory methods
client = AzureAIAgentClient(credential=credential)
file_search_tool = client.get_file_search_tool(vector_store_ids=[vector_store.id])
# 3. Create an agent with file search capabilities
agent = await provider.create_agent(
@@ -55,7 +55,7 @@ async def main() -> None:
"You are a helpful assistant that can search through uploaded employee files "
"to answer questions about employees."
),
tools=file_search_tool,
tools=[file_search_tool],
)
# 4. Simulate conversation with the agent
@@ -18,7 +18,9 @@ showing both agent-level and query-level tool configuration patterns.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -3,8 +3,8 @@
import asyncio
from typing import Any
from agent_framework import AgentResponse, AgentThread, HostedMCPTool, SupportsAgentRun
from agent_framework.azure import AzureAIAgentsProvider
from agent_framework import AgentResponse, AgentThread, SupportsAgentRun
from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider
from azure.identity.aio import AzureCliCredential
"""
@@ -40,17 +40,23 @@ async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", th
async def main() -> None:
"""Example showing Hosted MCP tools for a Azure AI Agent."""
async with (
AzureCliCredential() as credential,
AzureAIAgentsProvider(credential=credential) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIAgentClient(credential=credential)
# Create MCP tool using instance method
mcp_tool = client.get_mcp_tool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
)
agent = await provider.create_agent(
name="DocsAgent",
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
tools=HostedMCPTool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
),
tools=[mcp_tool],
)
thread = agent.get_new_thread()
# First query
@@ -6,12 +6,10 @@ from typing import Any
from agent_framework import (
AgentThread,
HostedMCPTool,
HostedWebSearchTool,
SupportsAgentRun,
tool,
)
from agent_framework.azure import AzureAIAgentsProvider
from agent_framework.azure import AzureAIAgentClient, AzureAIAgentsProvider
from azure.identity.aio import AzureCliCredential
"""
@@ -35,7 +33,9 @@ To set up Bing Grounding:
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_time() -> str:
"""Get the current UTC time."""
@@ -67,20 +67,27 @@ async def handle_approvals_with_thread(query: str, agent: "SupportsAgentRun", th
async def main() -> None:
"""Example showing Hosted MCP tools for a Azure AI Agent."""
"""Example showing multiple tools for an Azure AI Agent."""
async with (
AzureCliCredential() as credential,
AzureAIAgentsProvider(credential=credential) as provider,
):
# Create a client to access hosted tool factory methods
client = AzureAIAgentClient(credential=credential)
# Create tools using instance methods
mcp_tool = client.get_mcp_tool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
)
web_search_tool = client.get_web_search_tool()
agent = await provider.create_agent(
name="DocsAgent",
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
tools=[
HostedMCPTool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
),
HostedWebSearchTool(count=5),
mcp_tool,
web_search_tool,
get_time,
],
)
@@ -17,7 +17,9 @@ automatic thread creation with explicit thread management for persistent context
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -7,7 +7,7 @@ This folder contains examples demonstrating different ways to create and use age
| File | Description |
|------|-------------|
| [`azure_assistants_basic.py`](azure_assistants_basic.py) | The simplest way to create an agent using `Agent` with `AzureOpenAIAssistantsClient`. Shows both streaming and non-streaming responses with automatic assistant creation and cleanup. |
| [`azure_assistants_with_code_interpreter.py`](azure_assistants_with_code_interpreter.py) | Shows how to use the HostedCodeInterpreterTool with Azure agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. |
| [`azure_assistants_with_code_interpreter.py`](azure_assistants_with_code_interpreter.py) | Shows how to use `AzureOpenAIAssistantsClient.get_code_interpreter_tool()` with Azure agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. |
| [`azure_assistants_with_existing_assistant.py`](azure_assistants_with_existing_assistant.py) | Shows how to work with a pre-existing assistant by providing the assistant ID to the Azure Assistants client. Demonstrates proper cleanup of manually created assistants. |
| [`azure_assistants_with_explicit_settings.py`](azure_assistants_with_explicit_settings.py) | Shows how to initialize an agent with a specific assistants client, configuring settings explicitly including endpoint and deployment name. |
| [`azure_assistants_with_function_tools.py`](azure_assistants_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). |
@@ -17,12 +17,13 @@ This folder contains examples demonstrating different ways to create and use age
| [`azure_chat_client_with_function_tools.py`](azure_chat_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). |
| [`azure_chat_client_with_thread.py`](azure_chat_client_with_thread.py) | Demonstrates thread management with Azure agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. |
| [`azure_responses_client_basic.py`](azure_responses_client_basic.py) | The simplest way to create an agent using `Agent` with `AzureOpenAIResponsesClient`. Shows both streaming and non-streaming responses for structured response generation with Azure OpenAI models. |
| [`azure_responses_client_code_interpreter_files.py`](azure_responses_client_code_interpreter_files.py) | Demonstrates using HostedCodeInterpreterTool with file uploads for data analysis. Shows how to create, upload, and analyze CSV files using Python code execution with Azure OpenAI Responses. |
| [`azure_responses_client_code_interpreter_files.py`](azure_responses_client_code_interpreter_files.py) | Demonstrates using `AzureOpenAIResponsesClient.get_code_interpreter_tool()` with file uploads for data analysis. Shows how to create, upload, and analyze CSV files using Python code execution with Azure OpenAI Responses. |
| [`azure_responses_client_image_analysis.py`](azure_responses_client_image_analysis.py) | Shows how to use Azure OpenAI Responses for image analysis and vision tasks. Demonstrates multi-modal messages combining text and image content using remote URLs. |
| [`azure_responses_client_with_code_interpreter.py`](azure_responses_client_with_code_interpreter.py) | Shows how to use the HostedCodeInterpreterTool with Azure agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. |
| [`azure_responses_client_with_code_interpreter.py`](azure_responses_client_with_code_interpreter.py) | Shows how to use `AzureOpenAIResponsesClient.get_code_interpreter_tool()` with Azure agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. |
| [`azure_responses_client_with_explicit_settings.py`](azure_responses_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific responses client, configuring settings explicitly including endpoint and deployment name. |
| [`azure_responses_client_with_file_search.py`](azure_responses_client_with_file_search.py) | Demonstrates using HostedFileSearchTool with Azure OpenAI Responses Client for direct document-based question answering and information retrieval from vector stores. |
| [`azure_responses_client_with_file_search.py`](azure_responses_client_with_file_search.py) | Demonstrates using `AzureOpenAIResponsesClient.get_file_search_tool()` with Azure OpenAI Responses Client for direct document-based question answering and information retrieval from vector stores. |
| [`azure_responses_client_with_function_tools.py`](azure_responses_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). |
| [`azure_responses_client_with_hosted_mcp.py`](azure_responses_client_with_hosted_mcp.py) | Shows how to integrate Azure OpenAI Responses Client with hosted Model Context Protocol (MCP) servers using `AzureOpenAIResponsesClient.get_mcp_tool()` for extended functionality. |
| [`azure_responses_client_with_local_mcp.py`](azure_responses_client_with_local_mcp.py) | Shows how to integrate Azure OpenAI Responses Client with local Model Context Protocol (MCP) servers using MCPStreamableHTTPTool for extended functionality. |
| [`azure_responses_client_with_thread.py`](azure_responses_client_with_thread.py) | Demonstrates thread management with Azure agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. |
@@ -2,9 +2,8 @@
import asyncio
from agent_framework import Agent, AgentResponseUpdate, ChatResponseUpdate, HostedCodeInterpreterTool
from agent_framework import Agent, AgentResponseUpdate, ChatResponseUpdate
from agent_framework.azure import AzureOpenAIAssistantsClient
from azure.identity import AzureCliCredential
from openai.types.beta.threads.runs import (
CodeInterpreterToolCallDelta,
RunStepDelta,
@@ -16,7 +15,7 @@ from openai.types.beta.threads.runs.code_interpreter_tool_call_delta import Code
"""
Azure OpenAI Assistants with Code Interpreter Example
This sample demonstrates using HostedCodeInterpreterTool with Azure OpenAI Assistants
This sample demonstrates using get_code_interpreter_tool() with Azure OpenAI Assistants
for Python code execution and mathematical problem solving.
"""
@@ -41,15 +40,19 @@ def get_code_interpreter_chunk(chunk: AgentResponseUpdate) -> str | None:
async def main() -> None:
"""Example showing how to use the HostedCodeInterpreterTool with Azure OpenAI Assistants."""
"""Example showing how to use the code interpreter tool with Azure OpenAI Assistants."""
print("=== Azure OpenAI Assistants Agent with Code Interpreter Example ===")
# Create code interpreter tool using static method
client = AzureOpenAIAssistantsClient()
code_interpreter_tool = client.get_code_interpreter_tool()
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
# authentication option.
async with Agent(
client=AzureOpenAIAssistantsClient(credential=AzureCliCredential()),
client=client,
instructions="You are a helpful assistant that can write and execute Python code to solve problems.",
tools=HostedCodeInterpreterTool(),
tools=[code_interpreter_tool],
) as agent:
query = "What is current datetime?"
print(f"User: {query}")
@@ -19,7 +19,9 @@ using existing assistant IDs rather than creating new ones.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -18,7 +18,9 @@ settings rather than relying on environment variable defaults.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -18,7 +18,9 @@ showing both agent-level and query-level tool configuration patterns.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -17,7 +17,9 @@ automatic thread creation with explicit thread management for persistent context
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -17,7 +17,9 @@ interactions, showing both streaming and non-streaming responses.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -18,7 +18,9 @@ settings rather than relying on environment variable defaults.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -18,7 +18,9 @@ showing both agent-level and query-level tool configuration patterns.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -17,7 +17,9 @@ automatic thread creation with explicit thread management for persistent context
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -17,7 +17,9 @@ response generation, showing both streaming and non-streaming responses.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -4,7 +4,7 @@ import asyncio
import os
import tempfile
from agent_framework import Agent, HostedCodeInterpreterTool
from agent_framework import Agent
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from openai import AsyncAzureOpenAI
@@ -12,7 +12,7 @@ from openai import AsyncAzureOpenAI
"""
Azure OpenAI Responses Client with Code Interpreter and Files Example
This sample demonstrates using HostedCodeInterpreterTool with Azure OpenAI Responses
This sample demonstrates using get_code_interpreter_tool() with Azure OpenAI Responses
for Python code execution and data analysis with uploaded files.
"""
@@ -76,10 +76,15 @@ async def main() -> None:
temp_file_path, file_id = await create_sample_file_and_upload(openai_client)
# Create agent using Azure OpenAI Responses client
client = AzureOpenAIResponsesClient(credential=credential)
# Create code interpreter tool with file access
code_interpreter_tool = client.get_code_interpreter_tool(file_ids=[file_id])
agent = Agent(
client=AzureOpenAIResponsesClient(credential=credential),
client=client,
instructions="You are a helpful assistant that can analyze data files using Python code.",
tools=HostedCodeInterpreterTool(inputs=[{"file_id": file_id}]),
tools=[code_interpreter_tool],
)
# Test the code interpreter with the uploaded file
@@ -27,9 +27,9 @@ async def main():
user_message = Message(
role="user",
contents=[
Content.from_text(text="What do you see in this image?"),
Content.from_text("What do you see in this image?"),
Content.from_uri(
uri="https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg",
uri="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800",
media_type="image/jpeg",
),
],
@@ -2,7 +2,7 @@
import asyncio
from agent_framework import Agent, ChatResponse, HostedCodeInterpreterTool
from agent_framework import Agent, ChatResponse
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
from openai.types.responses.response import Response as OpenAIResponse
@@ -11,21 +11,26 @@ from openai.types.responses.response_code_interpreter_tool_call import ResponseC
"""
Azure OpenAI Responses Client with Code Interpreter Example
This sample demonstrates using HostedCodeInterpreterTool with Azure OpenAI Responses
This sample demonstrates using get_code_interpreter_tool() with Azure OpenAI Responses
for Python code execution and mathematical problem solving.
"""
async def main() -> None:
"""Example showing how to use the HostedCodeInterpreterTool with Azure OpenAI Responses."""
"""Example showing how to use the code interpreter tool with Azure OpenAI Responses."""
print("=== Azure OpenAI Responses Agent with Code Interpreter Example ===")
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
# authentication option.
client = AzureOpenAIResponsesClient(credential=AzureCliCredential())
# Create code interpreter tool using instance method
code_interpreter_tool = client.get_code_interpreter_tool()
agent = Agent(
client=AzureOpenAIResponsesClient(credential=AzureCliCredential()),
client=client,
instructions="You are a helpful assistant that can write and execute Python code to solve problems.",
tools=HostedCodeInterpreterTool(),
tools=[code_interpreter_tool],
)
query = "Use code to calculate the factorial of 100?"
@@ -18,7 +18,9 @@ settings rather than relying on environment variable defaults.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -2,14 +2,14 @@
import asyncio
from agent_framework import Agent, Content, HostedFileSearchTool
from agent_framework import Agent, Content
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
"""
Azure OpenAI Responses Client with File Search Example
This sample demonstrates using HostedFileSearchTool with Azure OpenAI Responses Client
This sample demonstrates using get_file_search_tool() with Azure OpenAI Responses Client
for direct document-based question answering and information retrieval.
Prerequisites:
@@ -51,12 +51,15 @@ async def main() -> None:
# Make sure you're logged in via 'az login' before running this sample
client = AzureOpenAIResponsesClient(credential=AzureCliCredential())
file_id, vector_store = await create_vector_store(client)
file_id, vector_store_id = await create_vector_store(client)
# Create file search tool using instance method
file_search_tool = client.get_file_search_tool(vector_store_ids=[vector_store_id])
agent = Agent(
client=client,
instructions="You are a helpful assistant that can search through files to find information.",
tools=[HostedFileSearchTool(inputs=vector_store)],
tools=[file_search_tool],
)
query = "What is the weather today? Do a file search to find the answer."
@@ -64,7 +67,7 @@ async def main() -> None:
result = await agent.run(query)
print(f"Agent: {result}\n")
await delete_vector_store(client, file_id, vector_store.vector_store_id)
await delete_vector_store(client, file_id, vector_store_id)
if __name__ == "__main__":
@@ -18,7 +18,9 @@ showing both agent-level and query-level tool configuration patterns.
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -3,7 +3,7 @@
import asyncio
from typing import TYPE_CHECKING, Any
from agent_framework import Agent, HostedMCPTool
from agent_framework import Agent
from agent_framework.azure import AzureOpenAIResponsesClient
from azure.identity import AzureCliCredential
@@ -33,7 +33,10 @@ async def handle_approvals_without_thread(query: str, agent: "SupportsAgentRun")
new_inputs.append(Message(role="assistant", contents=[user_input_needed]))
user_approval = input("Approve function call? (y/n): ")
new_inputs.append(
Message(role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")])
Message(
role="user",
contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")],
)
)
result = await agent.run(new_inputs)
@@ -82,7 +85,8 @@ async def handle_approvals_with_thread_streaming(query: str, agent: "SupportsAge
user_approval = input("Approve function call? (y/n): ")
new_input.append(
Message(
role="user", contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")]
role="user",
contents=[user_input_needed.to_function_approval_response(user_approval.lower() == "y")],
)
)
new_input_added = True
@@ -94,21 +98,24 @@ async def run_hosted_mcp_without_thread_and_specific_approval() -> None:
"""Example showing Mcp Tools with approvals without using a thread."""
print("=== Mcp with approvals and without thread ===")
credential = AzureCliCredential()
client = AzureOpenAIResponsesClient(credential=credential)
# Create MCP tool with specific approval settings
mcp_tool = client.get_mcp_tool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
# we don't require approval for microsoft_docs_search tool calls
# but we do for any other tool
approval_mode={"never_require_approval": ["microsoft_docs_search"]},
)
# Tools are provided when creating the agent
# The agent can use these tools for any query during its lifetime
async with Agent(
client=AzureOpenAIResponsesClient(
credential=credential,
),
client=client,
name="DocsAgent",
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
tools=HostedMCPTool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
# we don't require approval for microsoft_docs_search tool calls
# but we do for any other tool
approval_mode={"never_require_approval": ["microsoft_docs_search"]},
),
tools=[mcp_tool],
) as agent:
# First query
query1 = "How to create an Azure storage account using az cli?"
@@ -127,22 +134,25 @@ async def run_hosted_mcp_without_approval() -> None:
"""Example showing Mcp Tools without approvals."""
print("=== Mcp without approvals ===")
credential = AzureCliCredential()
client = AzureOpenAIResponsesClient(credential=credential)
# Create MCP tool without approval requirements
mcp_tool = client.get_mcp_tool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
# we don't require approval for any function calls
# this means we will not see the approval messages,
# it is fully handled by the service and a final response is returned.
approval_mode="never_require",
)
# Tools are provided when creating the agent
# The agent can use these tools for any query during its lifetime
async with Agent(
client=AzureOpenAIResponsesClient(
credential=credential,
),
client=client,
name="DocsAgent",
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
tools=HostedMCPTool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
# we don't require approval for any function calls
# this means we will not see the approval messages,
# it is fully handled by the service and a final response is returned.
approval_mode="never_require",
),
tools=[mcp_tool],
) as agent:
# First query
query1 = "How to create an Azure storage account using az cli?"
@@ -161,20 +171,23 @@ async def run_hosted_mcp_with_thread() -> None:
"""Example showing Mcp Tools with approvals using a thread."""
print("=== Mcp with approvals and with thread ===")
credential = AzureCliCredential()
client = AzureOpenAIResponsesClient(credential=credential)
# Create MCP tool with always require approval
mcp_tool = client.get_mcp_tool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
# we require approval for all function calls
approval_mode="always_require",
)
# Tools are provided when creating the agent
# The agent can use these tools for any query during its lifetime
async with Agent(
client=AzureOpenAIResponsesClient(
credential=credential,
),
client=client,
name="DocsAgent",
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
tools=HostedMCPTool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
# we require approval for all function calls
approval_mode="always_require",
),
tools=[mcp_tool],
) as agent:
# First query
thread = agent.get_new_thread()
@@ -194,20 +207,23 @@ async def run_hosted_mcp_with_thread_streaming() -> None:
"""Example showing Mcp Tools with approvals using a thread."""
print("=== Mcp with approvals and with thread ===")
credential = AzureCliCredential()
client = AzureOpenAIResponsesClient(credential=credential)
# Create MCP tool with always require approval
mcp_tool = client.get_mcp_tool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
# we require approval for all function calls
approval_mode="always_require",
)
# Tools are provided when creating the agent
# The agent can use these tools for any query during its lifetime
async with Agent(
client=AzureOpenAIResponsesClient(
credential=credential,
),
client=client,
name="DocsAgent",
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
tools=HostedMCPTool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
# we require approval for all function calls
approval_mode="always_require",
),
tools=[mcp_tool],
) as agent:
# First query
thread = agent.get_new_thread()
@@ -48,14 +48,14 @@ async def main():
url=MCP_URL,
) as mcp_tool:
# First query — expect the agent to use the MCP tool if it helps
q1 = "How to create an Azure storage account using az cli?"
r1 = await agent.run(q1, tools=mcp_tool)
print("\n=== Answer 1 ===\n", r1.text)
first_query = "How to create an Azure storage account using az cli?"
first_response = await agent.run(first_query, tools=mcp_tool)
print("\n=== Answer 1 ===\n", first_response.text)
# Follow-up query (connection is reused)
q2 = "What is Microsoft Agent Framework?"
r2 = await agent.run(q2, tools=mcp_tool)
print("\n=== Answer 2 ===\n", r2.text)
second_query = "What is Microsoft Agent Framework?"
second_response = await agent.run(second_query, tools=mcp_tool)
print("\n=== Answer 2 ===\n", second_response.text)
if __name__ == "__main__":
@@ -17,7 +17,9 @@ automatic thread creation with explicit thread management for persistent context
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -1,6 +1,6 @@
# OpenAI Agent Framework Examples
This folder contains examples demonstrating different ways to create and use agents with the OpenAI Assistants client from the `agent_framework.openai` package.
This folder contains examples demonstrating different ways to create and use agents with the OpenAI clients from the `agent_framework.openai` package.
## Examples
@@ -8,10 +8,10 @@ This folder contains examples demonstrating different ways to create and use age
|------|-------------|
| [`openai_assistants_basic.py`](openai_assistants_basic.py) | Basic usage of `OpenAIAssistantProvider` with streaming and non-streaming responses. |
| [`openai_assistants_provider_methods.py`](openai_assistants_provider_methods.py) | Demonstrates all `OpenAIAssistantProvider` methods: `create_agent()`, `get_agent()`, and `as_agent()`. |
| [`openai_assistants_with_code_interpreter.py`](openai_assistants_with_code_interpreter.py) | Using `HostedCodeInterpreterTool` with `OpenAIAssistantProvider` to execute Python code. |
| [`openai_assistants_with_code_interpreter.py`](openai_assistants_with_code_interpreter.py) | Using `OpenAIAssistantsClient.get_code_interpreter_tool()` with `OpenAIAssistantProvider` to execute Python code. |
| [`openai_assistants_with_existing_assistant.py`](openai_assistants_with_existing_assistant.py) | Working with pre-existing assistants using `get_agent()` and `as_agent()` methods. |
| [`openai_assistants_with_explicit_settings.py`](openai_assistants_with_explicit_settings.py) | Configuring `OpenAIAssistantProvider` with explicit settings including API key and model ID. |
| [`openai_assistants_with_file_search.py`](openai_assistants_with_file_search.py) | Using `HostedFileSearchTool` with `OpenAIAssistantProvider` for file search capabilities. |
| [`openai_assistants_with_file_search.py`](openai_assistants_with_file_search.py) | Using `OpenAIAssistantsClient.get_file_search_tool()` with `OpenAIAssistantProvider` for file search capabilities. |
| [`openai_assistants_with_function_tools.py`](openai_assistants_with_function_tools.py) | Function tools with `OpenAIAssistantProvider` at both agent-level and query-level. |
| [`openai_assistants_with_response_format.py`](openai_assistants_with_response_format.py) | Structured outputs with `OpenAIAssistantProvider` using Pydantic models. |
| [`openai_assistants_with_thread.py`](openai_assistants_with_thread.py) | Thread management with `OpenAIAssistantProvider` for conversation context persistence. |
@@ -20,24 +20,25 @@ This folder contains examples demonstrating different ways to create and use age
| [`openai_chat_client_with_function_tools.py`](openai_chat_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). |
| [`openai_chat_client_with_local_mcp.py`](openai_chat_client_with_local_mcp.py) | Shows how to integrate OpenAI agents with local Model Context Protocol (MCP) servers for enhanced functionality and tool integration. |
| [`openai_chat_client_with_thread.py`](openai_chat_client_with_thread.py) | Demonstrates thread management with OpenAI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. |
| [`openai_chat_client_with_web_search.py`](openai_chat_client_with_web_search.py) | Shows how to use web search capabilities with OpenAI agents to retrieve and use information from the internet in responses. |
| [`openai_chat_client_with_web_search.py`](openai_chat_client_with_web_search.py) | Shows how to use `OpenAIChatClient.get_web_search_tool()` for web search capabilities with OpenAI agents. |
| [`openai_chat_client_with_runtime_json_schema.py`](openai_chat_client_with_runtime_json_schema.py) | Shows how to supply a runtime JSON Schema via `additional_chat_options` for structured output without defining a Pydantic model. |
| [`openai_responses_client_basic.py`](openai_responses_client_basic.py) | The simplest way to create an agent using `Agent` with `OpenAIResponsesClient`. Shows both streaming and non-streaming responses for structured response generation with OpenAI models. |
| [`openai_responses_client_image_analysis.py`](openai_responses_client_image_analysis.py) | Demonstrates how to use vision capabilities with agents to analyze images. |
| [`openai_responses_client_image_generation.py`](openai_responses_client_image_generation.py) | Demonstrates how to use image generation capabilities with OpenAI agents to create images based on text descriptions. Requires PIL (Pillow) for image display. |
| [`openai_responses_client_image_generation.py`](openai_responses_client_image_generation.py) | Demonstrates how to use `OpenAIResponsesClient.get_image_generation_tool()` to create images based on text descriptions. |
| [`openai_responses_client_reasoning.py`](openai_responses_client_reasoning.py) | Demonstrates how to use reasoning capabilities with OpenAI agents, showing how the agent can provide detailed reasoning for its responses. |
| [`openai_responses_client_streaming_image_generation.py`](openai_responses_client_streaming_image_generation.py) | Demonstrates streaming image generation with partial images for real-time image creation feedback and improved user experience. |
| [`openai_responses_client_with_agent_as_tool.py`](openai_responses_client_with_agent_as_tool.py) | Shows how to use the agent-as-tool pattern with OpenAI Responses Client, where one agent delegates work to specialized sub-agents wrapped as tools using `as_tool()`. Demonstrates hierarchical agent architectures. |
| [`openai_responses_client_with_code_interpreter.py`](openai_responses_client_with_code_interpreter.py) | Shows how to use the HostedCodeInterpreterTool with OpenAI agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. |
| [`openai_responses_client_with_code_interpreter.py`](openai_responses_client_with_code_interpreter.py) | Shows how to use `OpenAIResponsesClient.get_code_interpreter_tool()` to write and execute Python code. |
| [`openai_responses_client_with_code_interpreter_files.py`](openai_responses_client_with_code_interpreter_files.py) | Shows how to use code interpreter with uploaded files for data analysis. |
| [`openai_responses_client_with_explicit_settings.py`](openai_responses_client_with_explicit_settings.py) | Shows how to initialize an agent with a specific responses client, configuring settings explicitly including API key and model ID. |
| [`openai_responses_client_with_file_search.py`](openai_responses_client_with_file_search.py) | Demonstrates how to use file search capabilities with OpenAI agents, allowing the agent to search through uploaded files to answer questions. |
| [`openai_responses_client_with_file_search.py`](openai_responses_client_with_file_search.py) | Demonstrates how to use `OpenAIResponsesClient.get_file_search_tool()` for searching through uploaded files. |
| [`openai_responses_client_with_function_tools.py`](openai_responses_client_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and run-level tools (provided with specific queries). |
| [`openai_responses_client_with_hosted_mcp.py`](openai_responses_client_with_hosted_mcp.py) | Shows how to integrate OpenAI agents with hosted Model Context Protocol (MCP) servers, including approval workflows and tool management for remote MCP services. |
| [`openai_responses_client_with_hosted_mcp.py`](openai_responses_client_with_hosted_mcp.py) | Shows how to use `OpenAIResponsesClient.get_mcp_tool()` for hosted MCP servers, including approval workflows. |
| [`openai_responses_client_with_local_mcp.py`](openai_responses_client_with_local_mcp.py) | Shows how to integrate OpenAI agents with local Model Context Protocol (MCP) servers for enhanced functionality and tool integration. |
| [`openai_responses_client_with_runtime_json_schema.py`](openai_responses_client_with_runtime_json_schema.py) | Shows how to supply a runtime JSON Schema via `additional_chat_options` for structured output without defining a Pydantic model. |
| [`openai_responses_client_with_structured_output.py`](openai_responses_client_with_structured_output.py) | Demonstrates how to use structured outputs with OpenAI agents to get structured data responses in predefined formats. |
| [`openai_responses_client_with_thread.py`](openai_responses_client_with_thread.py) | Demonstrates thread management with OpenAI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. |
| [`openai_responses_client_with_web_search.py`](openai_responses_client_with_web_search.py) | Shows how to use web search capabilities with OpenAI agents to retrieve and use information from the internet in responses. |
| [`openai_responses_client_with_web_search.py`](openai_responses_client_with_web_search.py) | Shows how to use `OpenAIResponsesClient.get_web_search_tool()` for web search capabilities. |
## Environment Variables
@@ -18,7 +18,9 @@ assistant lifecycle management, showing both streaming and non-streaming respons
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -20,7 +20,9 @@ This sample demonstrates the methods available on the OpenAIAssistantProvider cl
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production;
# see samples/getting_started/tools/function_tool_with_approval.py
# and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
@tool(approval_mode="never_require")
def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],

Some files were not shown because too many files have changed in this diff Show More