mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [BREAKING] Replace Hosted*Tool classes with tool methods (#3634)
* Replace Hosted*Tool classes with client static factory methods * fixed failing test * mypy fix * mypy fix 2 * declarative mypy fix * addressed comments * ToolProtocol removal * fixed test * agents mypy fix * fix failing tests * mypy fix * addressed comments * fixed tests * addressed comments + added factory method overrides for azureai v2 client * mypy fix * added kwargs to azureai tool methods * fixed in test * _sessions fix * test fix
This commit is contained in:
@@ -12,7 +12,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)]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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?"
|
||||
|
||||
+15
-8
@@ -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")
|
||||
|
||||
+12
-5
@@ -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")
|
||||
|
||||
+3
-1
@@ -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. |
|
||||
|
||||
+10
-11
@@ -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
|
||||
|
||||
+9
-11
@@ -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
|
||||
|
||||
+10
-11
@@ -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
|
||||
|
||||
+9
-5
@@ -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}")
|
||||
|
||||
+7
-6
@@ -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
|
||||
|
||||
+3
-1
@@ -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.")],
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
+17
-10
@@ -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. |
|
||||
|
||||
|
||||
+9
-6
@@ -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}")
|
||||
|
||||
+3
-1
@@ -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.")],
|
||||
|
||||
+3
-1
@@ -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.")],
|
||||
|
||||
+3
-1
@@ -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.")],
|
||||
|
||||
+3
-1
@@ -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.")],
|
||||
|
||||
+3
-1
@@ -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.")],
|
||||
|
||||
+9
-4
@@ -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
|
||||
|
||||
+2
-2
@@ -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",
|
||||
),
|
||||
],
|
||||
|
||||
+10
-5
@@ -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?"
|
||||
|
||||
+3
-1
@@ -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.")],
|
||||
|
||||
+8
-5
@@ -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__":
|
||||
|
||||
+3
-1
@@ -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.")],
|
||||
|
||||
+58
-42
@@ -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()
|
||||
|
||||
+6
-6
@@ -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__":
|
||||
|
||||
+3
-1
@@ -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
Reference in New Issue
Block a user