mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Foundry Agent Completeness (#954)
* foundry completeness * tests + openapi sample * bing grounding sample * options integration tests * merge conflict fix * fix failing test * add mcp approval handling
This commit is contained in:
committed by
GitHub
Unverified
parent
74e2e2e21d
commit
b0971fdec6
@@ -319,6 +319,8 @@ class AzureAIAgentClient(BaseChatClient):
|
||||
if run_options:
|
||||
if "tools" in run_options:
|
||||
args["tools"] = run_options["tools"]
|
||||
if "tool_resources" in run_options:
|
||||
args["tool_resources"] = run_options["tool_resources"]
|
||||
if "instructions" in run_options:
|
||||
args["instructions"] = run_options["instructions"]
|
||||
if "response_format" in run_options:
|
||||
@@ -636,10 +638,43 @@ class AzureAIAgentClient(BaseChatClient):
|
||||
|
||||
if chat_options.tool_choice is not None:
|
||||
if chat_options.tool_choice != "none" and chat_options.tools:
|
||||
tool_definitions = await self._prep_tools(chat_options.tools)
|
||||
tool_definitions = await self._prep_tools(chat_options.tools, run_options)
|
||||
if tool_definitions:
|
||||
run_options["tools"] = tool_definitions
|
||||
|
||||
# Handle MCP tool resources for approval mode
|
||||
mcp_tools = [tool for tool in chat_options.tools if isinstance(tool, HostedMCPTool)]
|
||||
if mcp_tools:
|
||||
mcp_resources = []
|
||||
for mcp_tool in mcp_tools:
|
||||
server_label = mcp_tool.name.replace(" ", "_")
|
||||
mcp_resource: dict[str, Any] = {"server_label": server_label}
|
||||
|
||||
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)
|
||||
|
||||
# Add MCP resources to tool_resources
|
||||
if "tool_resources" not in run_options:
|
||||
run_options["tool_resources"] = {}
|
||||
run_options["tool_resources"]["mcp"] = mcp_resources
|
||||
|
||||
if chat_options.tool_choice == "none":
|
||||
run_options["tool_choice"] = AgentsToolChoiceOptionMode.NONE
|
||||
elif chat_options.tool_choice == "auto":
|
||||
@@ -710,7 +745,7 @@ class AzureAIAgentClient(BaseChatClient):
|
||||
return run_options, required_action_results
|
||||
|
||||
async def _prep_tools(
|
||||
self, tools: Sequence["ToolProtocol | MutableMapping[str, Any]"]
|
||||
self, tools: Sequence["ToolProtocol | MutableMapping[str, Any]"], run_options: dict[str, Any] | None = None
|
||||
) -> list[ToolDefinition | dict[str, Any]]:
|
||||
"""Prepare tool definitions for the run options."""
|
||||
tool_definitions: list[ToolDefinition | dict[str, Any]] = []
|
||||
@@ -768,18 +803,20 @@ class AzureAIAgentClient(BaseChatClient):
|
||||
case HostedCodeInterpreterTool():
|
||||
tool_definitions.append(CodeInterpreterToolDefinition())
|
||||
case HostedMCPTool():
|
||||
tool_definitions.extend(
|
||||
McpTool(
|
||||
server_label=tool.name.replace(" ", "_"),
|
||||
server_url=str(tool.url),
|
||||
allowed_tools=list(tool.allowed_tools) if tool.allowed_tools else [],
|
||||
).definitions
|
||||
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 isinstance(inp, HostedVectorStoreContent)]
|
||||
if vector_stores:
|
||||
file_search = FileSearchTool(vector_store_ids=[vs.vector_store_id for vs in vector_stores])
|
||||
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
|
||||
else:
|
||||
additional_props = tool.additional_properties or {}
|
||||
index_name = additional_props.get("index_name") or os.getenv("AZURE_AI_SEARCH_INDEX_NAME")
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
%PDF-1.7
|
||||
%����
|
||||
1 0 obj
|
||||
<</Type/Catalog/Pages 2 0 R/Lang(en) /StructTreeRoot 22 0 R/MarkInfo<</Marked true>>/Metadata 132 0 R/ViewerPreferences 133 0 R>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<</Type/Pages/Count 1/Kids[ 4 0 R] >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<</Author(Test Author) /Creator(Test Creator) /Title(Employee Directory) >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]/Resources<</Font<</F1 5 0 R>>>>/Contents 6 0 R>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<</Type/Font/Subtype/Type1/BaseFont/Times-Roman>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<</Length 200>>
|
||||
stream
|
||||
BT
|
||||
/F1 12 Tf
|
||||
50 750 Td
|
||||
(Employee Directory) Tj
|
||||
0 -30 Td
|
||||
(Name: John Smith) Tj
|
||||
0 -15 Td
|
||||
(Department: Engineering) Tj
|
||||
0 -15 Td
|
||||
(Age: 28) Tj
|
||||
0 -30 Td
|
||||
(Name: Alice Johnson) Tj
|
||||
0 -15 Td
|
||||
(Department: Sales) Tj
|
||||
0 -15 Td
|
||||
(Age: 24) Tj
|
||||
0 -30 Td
|
||||
(Name: Bob Wilson) Tj
|
||||
0 -15 Td
|
||||
(Department: Marketing) Tj
|
||||
0 -15 Td
|
||||
(Age: 35) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
22 0 obj
|
||||
<</Type/StructTreeRoot>>
|
||||
endobj
|
||||
132 0 obj
|
||||
<</Type/Metadata/Subtype/XML>>
|
||||
endobj
|
||||
133 0 obj
|
||||
<</DisplayDocTitle true>>
|
||||
endobj
|
||||
xref
|
||||
0 10
|
||||
0000000000 65535 f
|
||||
0000000015 00000 n
|
||||
0000000152 00000 n
|
||||
0000000209 00000 n
|
||||
0000000300 00000 n
|
||||
0000000420 00000 n
|
||||
0000000490 00000 n
|
||||
0000000000 65535 f
|
||||
0000000000 65535 f
|
||||
0000000000 65535 f
|
||||
22 1
|
||||
0000000740 00000 n
|
||||
132 2
|
||||
0000000780 00000 n
|
||||
0000000820 00000 n
|
||||
trailer
|
||||
<</Size 134/Root 1 0 R/Info 3 0 R>>
|
||||
startxref
|
||||
860
|
||||
%%EOF
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -9,30 +11,45 @@ from agent_framework import (
|
||||
AgentRunResponse,
|
||||
AgentRunResponseUpdate,
|
||||
AgentThread,
|
||||
AIFunction,
|
||||
ChatAgent,
|
||||
ChatClientProtocol,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
FunctionApprovalRequestContent,
|
||||
FunctionApprovalResponseContent,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
HostedCodeInterpreterTool,
|
||||
MCPStreamableHTTPTool,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedVectorStoreContent,
|
||||
HostedWebSearchTool,
|
||||
Role,
|
||||
TextContent,
|
||||
UriContent,
|
||||
)
|
||||
from agent_framework.azure import AzureAIAgentClient, AzureAISettings
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
from azure.ai.agents.models import (
|
||||
CodeInterpreterToolDefinition,
|
||||
FileInfo,
|
||||
RequiredFunctionToolCall,
|
||||
RequiredMcpToolCall,
|
||||
RunStatus,
|
||||
SubmitToolApprovalAction,
|
||||
SubmitToolOutputsAction,
|
||||
ThreadRun,
|
||||
VectorStore,
|
||||
)
|
||||
from azure.ai.projects.models import ConnectionType
|
||||
from azure.core.credentials_async import AsyncTokenCredential
|
||||
from azure.core.exceptions import HttpResponseError
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from pydantic import Field, ValidationError
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
|
||||
from agent_framework_azure_ai import AzureAIAgentClient, AzureAISettings
|
||||
|
||||
skip_if_azure_ai_integration_tests_disabled = pytest.mark.skipif(
|
||||
os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true"
|
||||
@@ -49,6 +66,7 @@ def create_test_azure_ai_chat_client(
|
||||
thread_id: str | None = None,
|
||||
azure_ai_settings: AzureAISettings | None = None,
|
||||
should_delete_agent: bool = False,
|
||||
agent_name: str | None = None,
|
||||
) -> AzureAIAgentClient:
|
||||
"""Helper function to create AzureAIAgentClient instances for testing, bypassing normal validation."""
|
||||
if azure_ai_settings is None:
|
||||
@@ -225,6 +243,26 @@ def test_azure_ai_chat_client_init_validation_error(mock_azure_credential: Magic
|
||||
)
|
||||
|
||||
|
||||
def test_azure_ai_chat_client_from_settings() -> None:
|
||||
"""Test from_settings class method."""
|
||||
mock_project_client = MagicMock()
|
||||
settings = {
|
||||
"project_client": mock_project_client,
|
||||
"agent_id": "test-agent",
|
||||
"thread_id": "test-thread",
|
||||
"project_endpoint": "https://test.com",
|
||||
"model_deployment_name": "test-model",
|
||||
"agent_name": "TestAgent",
|
||||
}
|
||||
|
||||
client = AzureAIAgentClient.from_settings(settings)
|
||||
|
||||
assert client.project_client is mock_project_client
|
||||
assert client.agent_id == "test-agent"
|
||||
assert client.thread_id == "test-thread"
|
||||
assert client.agent_name == "TestAgent"
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_get_agent_id_or_create_existing_agent(
|
||||
mock_ai_project_client: MagicMock,
|
||||
) -> None:
|
||||
@@ -620,6 +658,711 @@ def test_azure_ai_chat_client_create_function_call_contents_non_function_tool_ca
|
||||
assert result == []
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_create_run_options_with_none_tool_choice(
|
||||
mock_ai_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _create_run_options with tool_choice set to 'none'."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client)
|
||||
|
||||
chat_options = ChatOptions()
|
||||
chat_options.tool_choice = "none"
|
||||
|
||||
run_options, _ = await chat_client._create_run_options([], chat_options)
|
||||
|
||||
from azure.ai.agents.models import AgentsToolChoiceOptionMode
|
||||
|
||||
assert run_options["tool_choice"] == AgentsToolChoiceOptionMode.NONE
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_create_run_options_with_auto_tool_choice(
|
||||
mock_ai_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _create_run_options with tool_choice set to 'auto'."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client)
|
||||
|
||||
chat_options = ChatOptions()
|
||||
chat_options.tool_choice = "auto"
|
||||
|
||||
run_options, _ = await chat_client._create_run_options([], chat_options)
|
||||
|
||||
from azure.ai.agents.models import AgentsToolChoiceOptionMode
|
||||
|
||||
assert run_options["tool_choice"] == AgentsToolChoiceOptionMode.AUTO
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_create_run_options_with_response_format(
|
||||
mock_ai_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _create_run_options with response_format configured."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client)
|
||||
|
||||
class TestResponseModel(BaseModel):
|
||||
name: str = Field(description="Test name")
|
||||
|
||||
chat_options = ChatOptions()
|
||||
chat_options.response_format = TestResponseModel
|
||||
|
||||
run_options, _ = await chat_client._create_run_options([], chat_options)
|
||||
|
||||
assert "response_format" in run_options
|
||||
response_format = run_options["response_format"]
|
||||
assert response_format.json_schema.name == "TestResponseModel"
|
||||
|
||||
|
||||
def test_azure_ai_chat_client_service_url_method(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test service_url method returns endpoint."""
|
||||
mock_ai_project_client._config.endpoint = "https://test-endpoint.com/"
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client)
|
||||
|
||||
url = chat_client.service_url()
|
||||
assert url == "https://test-endpoint.com/"
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prep_tools_ai_function(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _prep_tools with AIFunction tool."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
# Create a mock AIFunction
|
||||
mock_ai_function = MagicMock(spec=AIFunction)
|
||||
mock_ai_function.to_json_schema_spec.return_value = {"type": "function", "function": {"name": "test_function"}}
|
||||
|
||||
result = await chat_client._prep_tools([mock_ai_function]) # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == {"type": "function", "function": {"name": "test_function"}}
|
||||
mock_ai_function.to_json_schema_spec.assert_called_once()
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prep_tools_code_interpreter(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _prep_tools with HostedCodeInterpreterTool."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
code_interpreter_tool = HostedCodeInterpreterTool()
|
||||
|
||||
result = await chat_client._prep_tools([code_interpreter_tool]) # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], CodeInterpreterToolDefinition)
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prep_tools_mcp_tool(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _prep_tools with HostedMCPTool."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
mcp_tool = HostedMCPTool(name="Test MCP Tool", url="https://example.com/mcp", allowed_tools=["tool1", "tool2"])
|
||||
|
||||
# Mock McpTool to have a definitions attribute
|
||||
with patch("agent_framework_azure_ai._chat_client.McpTool") as mock_mcp_tool_class:
|
||||
mock_mcp_tool = MagicMock()
|
||||
mock_mcp_tool.definitions = [{"type": "mcp", "name": "test_mcp"}]
|
||||
mock_mcp_tool_class.return_value = mock_mcp_tool
|
||||
|
||||
result = await chat_client._prep_tools([mcp_tool]) # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == {"type": "mcp", "name": "test_mcp"}
|
||||
# Check that the call was made (order of allowed_tools may vary)
|
||||
mock_mcp_tool_class.assert_called_once()
|
||||
call_args = mock_mcp_tool_class.call_args[1]
|
||||
assert call_args["server_label"] == "Test_MCP_Tool"
|
||||
assert call_args["server_url"] == "https://example.com/mcp"
|
||||
assert set(call_args["allowed_tools"]) == {"tool1", "tool2"}
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_create_run_options_mcp_never_require(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _create_run_options with HostedMCPTool having never_require approval mode."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client)
|
||||
|
||||
mcp_tool = HostedMCPTool(name="Test MCP Tool", url="https://example.com/mcp", approval_mode="never_require")
|
||||
|
||||
messages = [ChatMessage(role=Role.USER, text="Hello")]
|
||||
chat_options = ChatOptions(tools=[mcp_tool], tool_choice="auto")
|
||||
|
||||
with patch("agent_framework_azure_ai._chat_client.McpTool") as mock_mcp_tool_class:
|
||||
# Mock _prep_tools to avoid actual tool preparation
|
||||
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 chat_client._create_run_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_prep_tools_web_search_bing_grounding(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _prep_tools with HostedWebSearchTool using Bing Grounding."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_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
|
||||
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
|
||||
|
||||
result = await chat_client._prep_tools([web_search_tool]) # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == {"type": "bing_grounding"}
|
||||
mock_bing_grounding.assert_called_once_with(
|
||||
connection_id="test-connection-id", count=5, freshness="Day", market="en-US", set_lang="en"
|
||||
)
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prep_tools_web_search_custom_bing(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _prep_tools with HostedWebSearchTool using Custom Bing Search."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
web_search_tool = HostedWebSearchTool(
|
||||
additional_properties={
|
||||
"custom_connection_name": "custom-bing-connection",
|
||||
"custom_instance_name": "custom-instance",
|
||||
"count": 10,
|
||||
}
|
||||
)
|
||||
|
||||
# Mock connection get
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.id = "custom-connection-id"
|
||||
mock_ai_project_client.connections.get = AsyncMock(return_value=mock_connection)
|
||||
|
||||
# Mock BingCustomSearchTool
|
||||
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
|
||||
|
||||
result = await chat_client._prep_tools([web_search_tool]) # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == {"type": "bing_custom_search"}
|
||||
mock_ai_project_client.connections.get.assert_called_once_with(name="custom-bing-connection")
|
||||
mock_custom_bing.assert_called_once_with(
|
||||
connection_id="custom-connection-id", instance_name="custom-instance", count=10
|
||||
)
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prep_tools_web_search_custom_bing_connection_error(
|
||||
mock_ai_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prep_tools with HostedWebSearchTool when custom connection is not found."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
web_search_tool = HostedWebSearchTool(
|
||||
additional_properties={
|
||||
"custom_connection_name": "nonexistent-connection",
|
||||
"custom_instance_name": "custom-instance",
|
||||
}
|
||||
)
|
||||
|
||||
# Mock connection get to raise HttpResponseError
|
||||
mock_ai_project_client.connections.get = AsyncMock(side_effect=HttpResponseError("Connection not found"))
|
||||
|
||||
with pytest.raises(ServiceInitializationError, match="Bing custom connection 'nonexistent-connection' not found"):
|
||||
await chat_client._prep_tools([web_search_tool]) # type: ignore
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prep_tools_web_search_missing_config(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _prep_tools with HostedWebSearchTool missing required configuration."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
# Web search tool with no connection configuration
|
||||
web_search_tool = HostedWebSearchTool()
|
||||
|
||||
with pytest.raises(ServiceInitializationError, match="Bing search tool requires either a 'connection_id'"):
|
||||
await chat_client._prep_tools([web_search_tool]) # type: ignore
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prep_tools_file_search_with_vector_stores(
|
||||
mock_ai_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prep_tools with HostedFileSearchTool using vector stores."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
vector_store_input = HostedVectorStoreContent(vector_store_id="vs-123")
|
||||
file_search_tool = HostedFileSearchTool(inputs=[vector_store_input])
|
||||
|
||||
# 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 = {}
|
||||
result = await chat_client._prep_tools([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"])
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prep_tools_file_search_with_ai_search(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _prep_tools with HostedFileSearchTool using Azure AI Search."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
file_search_tool = HostedFileSearchTool(
|
||||
additional_properties={
|
||||
"index_name": "test-index",
|
||||
"query_type": "simple",
|
||||
"top_k": 5,
|
||||
"filter": "category eq 'docs'",
|
||||
}
|
||||
)
|
||||
|
||||
# Mock connections.get_default
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.id = "search-connection-id"
|
||||
mock_ai_project_client.connections.get_default = AsyncMock(return_value=mock_connection)
|
||||
|
||||
# Mock AzureAISearchTool
|
||||
with patch("agent_framework_azure_ai._chat_client.AzureAISearchTool") as mock_ai_search:
|
||||
mock_search_tool = MagicMock()
|
||||
mock_search_tool.definitions = [{"type": "azure_ai_search"}]
|
||||
mock_ai_search.return_value = mock_search_tool
|
||||
|
||||
# Mock AzureAISearchQueryType
|
||||
with patch("agent_framework_azure_ai._chat_client.AzureAISearchQueryType") as mock_query_type:
|
||||
mock_query_type.SIMPLE = "simple"
|
||||
mock_query_type.return_value = "simple"
|
||||
|
||||
result = await chat_client._prep_tools([file_search_tool]) # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == {"type": "azure_ai_search"}
|
||||
mock_ai_project_client.connections.get_default.assert_called_once_with(ConnectionType.AZURE_AI_SEARCH)
|
||||
mock_ai_search.assert_called_once_with(
|
||||
index_connection_id="search-connection-id",
|
||||
index_name="test-index",
|
||||
query_type="simple",
|
||||
top_k=5,
|
||||
filter="category eq 'docs'",
|
||||
)
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prep_tools_file_search_invalid_query_type(
|
||||
mock_ai_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prep_tools with HostedFileSearchTool using invalid query_type."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
file_search_tool = HostedFileSearchTool(
|
||||
additional_properties={"index_name": "test-index", "query_type": "invalid_type"}
|
||||
)
|
||||
|
||||
# Mock connections.get_default
|
||||
mock_connection = MagicMock()
|
||||
mock_connection.id = "search-connection-id"
|
||||
mock_ai_project_client.connections.get_default = AsyncMock(return_value=mock_connection)
|
||||
|
||||
# Mock AzureAISearchQueryType to raise ValueError
|
||||
with patch("agent_framework_azure_ai._chat_client.AzureAISearchQueryType") as mock_query_type:
|
||||
mock_query_type.side_effect = ValueError("Invalid query type")
|
||||
|
||||
with pytest.raises(ServiceInitializationError, match="Invalid query_type 'invalid_type'"):
|
||||
await chat_client._prep_tools([file_search_tool]) # type: ignore
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prep_tools_file_search_no_connection(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _prep_tools with HostedFileSearchTool when no AI Search connection exists."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
file_search_tool = HostedFileSearchTool(additional_properties={"index_name": "test-index"})
|
||||
|
||||
# Mock connections.get_default to raise HttpResponseError
|
||||
mock_ai_project_client.connections.get_default = AsyncMock(side_effect=HttpResponseError("No connection found"))
|
||||
|
||||
with pytest.raises(ServiceInitializationError, match="No default Azure AI Search connection found"):
|
||||
await chat_client._prep_tools([file_search_tool]) # type: ignore
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prep_tools_file_search_no_index_name(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _prep_tools with HostedFileSearchTool missing index_name and vector stores."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
# File search tool with no vector stores and no index_name
|
||||
file_search_tool = HostedFileSearchTool()
|
||||
|
||||
with pytest.raises(ServiceInitializationError, match="File search tool requires at least one vector store input"):
|
||||
await chat_client._prep_tools([file_search_tool]) # type: ignore
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prep_tools_dict_tool(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _prep_tools with dictionary tool definition."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
dict_tool = {"type": "custom_tool", "config": {"param": "value"}}
|
||||
|
||||
result = await chat_client._prep_tools([dict_tool]) # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0] == dict_tool
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_prep_tools_unsupported_tool(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _prep_tools with unsupported tool type."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
unsupported_tool = "not_a_tool"
|
||||
|
||||
with pytest.raises(ServiceInitializationError, match="Unsupported tool type: <class 'str'>"):
|
||||
await chat_client._prep_tools([unsupported_tool]) # type: ignore
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_get_active_thread_run_with_active_run(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _get_active_thread_run when there's an active run."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
# Mock an active run
|
||||
mock_run = MagicMock()
|
||||
mock_run.status = RunStatus.IN_PROGRESS
|
||||
|
||||
async def mock_list_runs(*args, **kwargs):
|
||||
yield mock_run
|
||||
|
||||
mock_ai_project_client.agents.runs.list = mock_list_runs
|
||||
|
||||
result = await chat_client._get_active_thread_run("thread-123") # type: ignore
|
||||
|
||||
assert result == mock_run
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_get_active_thread_run_no_active_run(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _get_active_thread_run when there's no active run."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
# Mock a completed run (not active)
|
||||
mock_run = MagicMock()
|
||||
mock_run.status = RunStatus.COMPLETED
|
||||
|
||||
async def mock_list_runs(*args, **kwargs):
|
||||
yield mock_run
|
||||
|
||||
mock_ai_project_client.agents.runs.list = mock_list_runs
|
||||
|
||||
result = await chat_client._get_active_thread_run("thread-123") # type: ignore
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_get_active_thread_run_no_thread(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _get_active_thread_run with None thread_id."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
result = await chat_client._get_active_thread_run(None) # type: ignore
|
||||
|
||||
assert result is None
|
||||
# Should not call list since thread_id is None
|
||||
mock_ai_project_client.agents.runs.list.assert_not_called()
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_service_url(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test service_url method."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
# Mock the config endpoint
|
||||
mock_config = MagicMock()
|
||||
mock_config.endpoint = "https://test-endpoint.com/"
|
||||
mock_ai_project_client._config = mock_config
|
||||
|
||||
result = chat_client.service_url()
|
||||
|
||||
assert result == "https://test-endpoint.com/"
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_convert_required_action_to_tool_output_function_result(
|
||||
mock_ai_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _convert_required_action_to_tool_output with FunctionResultContent."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
# Test with simple result
|
||||
function_result = FunctionResultContent(call_id='["run_123", "call_456"]', result="Simple result")
|
||||
|
||||
run_id, tool_outputs, tool_approvals = chat_client._convert_required_action_to_tool_output([function_result]) # type: ignore
|
||||
|
||||
assert run_id == "run_123"
|
||||
assert tool_approvals is None
|
||||
assert tool_outputs is not None
|
||||
assert len(tool_outputs) == 1
|
||||
assert tool_outputs[0].tool_call_id == "call_456"
|
||||
assert tool_outputs[0].output == '"Simple result"'
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_convert_required_action_invalid_call_id(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test _convert_required_action_to_tool_output with invalid call_id format."""
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
# Invalid call_id format - should raise JSONDecodeError
|
||||
function_result = FunctionResultContent(call_id="invalid_json", result="result")
|
||||
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
chat_client._convert_required_action_to_tool_output([function_result]) # type: ignore
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_convert_required_action_invalid_structure(
|
||||
mock_ai_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _convert_required_action_to_tool_output with invalid call_id structure."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
# Valid JSON but invalid structure (missing second element)
|
||||
function_result = FunctionResultContent(call_id='["run_123"]', result="result")
|
||||
|
||||
run_id, tool_outputs, tool_approvals = chat_client._convert_required_action_to_tool_output([function_result]) # type: ignore
|
||||
|
||||
# Should return None values when structure is invalid
|
||||
assert run_id is None
|
||||
assert tool_outputs is None
|
||||
assert tool_approvals is None
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_convert_required_action_basemodel_results(
|
||||
mock_ai_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _convert_required_action_to_tool_output with BaseModel results."""
|
||||
|
||||
class MockResult(BaseModel):
|
||||
name: str
|
||||
value: int
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
# Test with BaseModel result
|
||||
mock_result = MockResult(name="test", value=42)
|
||||
function_result = FunctionResultContent(call_id='["run_123", "call_456"]', result=mock_result)
|
||||
|
||||
run_id, tool_outputs, tool_approvals = chat_client._convert_required_action_to_tool_output([function_result]) # type: ignore
|
||||
|
||||
assert run_id == "run_123"
|
||||
assert tool_approvals is None
|
||||
assert tool_outputs is not None
|
||||
assert len(tool_outputs) == 1
|
||||
assert tool_outputs[0].tool_call_id == "call_456"
|
||||
# Should use model_dump_json for BaseModel
|
||||
expected_json = mock_result.model_dump_json()
|
||||
assert tool_outputs[0].output == expected_json
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_convert_required_action_multiple_results(
|
||||
mock_ai_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _convert_required_action_to_tool_output with multiple results."""
|
||||
|
||||
class MockResult(BaseModel):
|
||||
data: str
|
||||
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
# Test with multiple results - mix of BaseModel and regular objects
|
||||
mock_basemodel = MockResult(data="model_data")
|
||||
results_list = [mock_basemodel, {"key": "value"}, "string_result"]
|
||||
function_result = FunctionResultContent(call_id='["run_123", "call_456"]', result=results_list)
|
||||
|
||||
run_id, tool_outputs, tool_approvals = chat_client._convert_required_action_to_tool_output([function_result]) # type: ignore
|
||||
|
||||
assert run_id == "run_123"
|
||||
assert tool_outputs is not None
|
||||
assert len(tool_outputs) == 1
|
||||
assert tool_outputs[0].tool_call_id == "call_456"
|
||||
|
||||
# Should JSON dump the entire results array since len > 1
|
||||
expected_results = [
|
||||
mock_basemodel.model_dump_json(), # BaseModel uses model_dump_json
|
||||
json.dumps({"key": "value"}), # Dict uses json.dumps
|
||||
json.dumps("string_result"), # String uses json.dumps
|
||||
]
|
||||
expected_output = json.dumps(expected_results)
|
||||
assert tool_outputs[0].output == expected_output
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_convert_required_action_approval_response(
|
||||
mock_ai_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _convert_required_action_to_tool_output with FunctionApprovalResponseContent."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
# Test with approval response - need to provide required fields
|
||||
approval_response = FunctionApprovalResponseContent(
|
||||
id='["run_123", "call_456"]',
|
||||
function_call=FunctionCallContent(call_id='["run_123", "call_456"]', name="test_function", arguments="{}"),
|
||||
approved=True,
|
||||
)
|
||||
|
||||
run_id, tool_outputs, tool_approvals = chat_client._convert_required_action_to_tool_output([approval_response]) # type: ignore
|
||||
|
||||
assert run_id == "run_123"
|
||||
assert tool_outputs is None
|
||||
assert tool_approvals is not None
|
||||
assert len(tool_approvals) == 1
|
||||
assert tool_approvals[0].tool_call_id == "call_456"
|
||||
assert tool_approvals[0].approve is True
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_create_function_call_contents_approval_request(
|
||||
mock_ai_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _create_function_call_contents with approval action."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
# Mock SubmitToolApprovalAction with RequiredMcpToolCall
|
||||
mock_tool_call = MagicMock(spec=RequiredMcpToolCall)
|
||||
mock_tool_call.id = "approval_call_123"
|
||||
mock_tool_call.name = "approve_action"
|
||||
mock_tool_call.arguments = '{"action": "approve"}'
|
||||
|
||||
mock_approval_action = MagicMock(spec=SubmitToolApprovalAction)
|
||||
mock_approval_action.submit_tool_approval.tool_calls = [mock_tool_call]
|
||||
|
||||
mock_event_data = MagicMock(spec=ThreadRun)
|
||||
mock_event_data.required_action = mock_approval_action
|
||||
|
||||
result = chat_client._create_function_call_contents(mock_event_data, "response_123") # type: ignore
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], FunctionApprovalRequestContent)
|
||||
assert result[0].id == '["response_123", "approval_call_123"]'
|
||||
assert result[0].function_call.name == "approve_action"
|
||||
assert result[0].function_call.call_id == '["response_123", "approval_call_123"]'
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_get_agent_id_or_create_with_agent_name(
|
||||
mock_ai_project_client: MagicMock, azure_ai_unit_test_env: dict[str, str]
|
||||
) -> None:
|
||||
"""Test _get_agent_id_or_create uses default name when no agent_name set."""
|
||||
azure_ai_settings = AzureAISettings(model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"])
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, azure_ai_settings=azure_ai_settings)
|
||||
|
||||
# Ensure agent_name is None to test the default
|
||||
chat_client.agent_name = None # type: ignore
|
||||
|
||||
agent_id = await chat_client._get_agent_id_or_create() # type: ignore
|
||||
|
||||
assert agent_id == "test-agent-id"
|
||||
# Verify create_agent was called with default "UnnamedAgent"
|
||||
mock_ai_project_client.agents.create_agent.assert_called_once()
|
||||
call_kwargs = mock_ai_project_client.agents.create_agent.call_args[1]
|
||||
assert call_kwargs["name"] == "UnnamedAgent"
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_get_agent_id_or_create_with_response_format(
|
||||
mock_ai_project_client: MagicMock, azure_ai_unit_test_env: dict[str, str]
|
||||
) -> None:
|
||||
"""Test _get_agent_id_or_create with response_format in run_options."""
|
||||
azure_ai_settings = AzureAISettings(model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"])
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, azure_ai_settings=azure_ai_settings)
|
||||
|
||||
# Test with response_format in run_options
|
||||
run_options = {"response_format": {"type": "json_object"}}
|
||||
|
||||
agent_id = await chat_client._get_agent_id_or_create(run_options) # type: ignore
|
||||
|
||||
assert agent_id == "test-agent-id"
|
||||
# Verify create_agent was called with response_format
|
||||
mock_ai_project_client.agents.create_agent.assert_called_once()
|
||||
call_kwargs = mock_ai_project_client.agents.create_agent.call_args[1]
|
||||
assert call_kwargs["response_format"] == {"type": "json_object"}
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_get_agent_id_or_create_with_tool_resources(
|
||||
mock_ai_project_client: MagicMock, azure_ai_unit_test_env: dict[str, str]
|
||||
) -> None:
|
||||
"""Test _get_agent_id_or_create with tool_resources in run_options."""
|
||||
azure_ai_settings = AzureAISettings(model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"])
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, azure_ai_settings=azure_ai_settings)
|
||||
|
||||
# Test with tool_resources in run_options
|
||||
run_options = {"tool_resources": {"vector_store_ids": ["vs-123"]}}
|
||||
|
||||
agent_id = await chat_client._get_agent_id_or_create(run_options) # type: ignore
|
||||
|
||||
assert agent_id == "test-agent-id"
|
||||
# Verify create_agent was called with tool_resources
|
||||
mock_ai_project_client.agents.create_agent.assert_called_once()
|
||||
call_kwargs = mock_ai_project_client.agents.create_agent.call_args[1]
|
||||
assert call_kwargs["tool_resources"] == {"vector_store_ids": ["vs-123"]}
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_close_method(mock_ai_project_client: MagicMock) -> None:
|
||||
"""Test close method."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, should_delete_agent=True)
|
||||
chat_client._should_close_client = True
|
||||
chat_client.agent_id = "test-agent"
|
||||
|
||||
# Mock cleanup methods
|
||||
mock_ai_project_client.agents.delete_agent = AsyncMock()
|
||||
mock_ai_project_client.close = AsyncMock()
|
||||
|
||||
await chat_client.close()
|
||||
|
||||
# Verify cleanup was called
|
||||
mock_ai_project_client.agents.delete_agent.assert_called_once_with("test-agent")
|
||||
mock_ai_project_client.close.assert_called_once()
|
||||
|
||||
|
||||
async def test_azure_ai_chat_client_create_agent_stream_submit_tool_outputs(
|
||||
mock_ai_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _create_agent_stream with tool outputs submission path."""
|
||||
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, agent_id="test-agent")
|
||||
|
||||
# Mock active thread run that matches the tool run ID
|
||||
mock_thread_run = MagicMock()
|
||||
mock_thread_run.thread_id = "test-thread"
|
||||
mock_thread_run.id = "test-run-id"
|
||||
chat_client._get_active_thread_run = AsyncMock(return_value=mock_thread_run)
|
||||
|
||||
# Mock required action results with matching run ID
|
||||
function_result = FunctionResultContent(call_id='["test-run-id", "test-call-id"]', result="test result")
|
||||
|
||||
# Mock submit_tool_outputs_stream
|
||||
mock_handler = MagicMock()
|
||||
mock_ai_project_client.agents.runs.submit_tool_outputs_stream = AsyncMock()
|
||||
|
||||
with patch("azure.ai.agents.models.AsyncAgentEventHandler", return_value=mock_handler):
|
||||
stream, final_thread_id = await chat_client._create_agent_stream(
|
||||
thread_id="test-thread", agent_id="test-agent", run_options={}, required_action_results=[function_result]
|
||||
)
|
||||
|
||||
# Should call submit_tool_outputs_stream since we have matching run ID
|
||||
mock_ai_project_client.agents.runs.submit_tool_outputs_stream.assert_called_once()
|
||||
assert final_thread_id == "test-thread"
|
||||
|
||||
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
@@ -855,24 +1598,86 @@ async def test_azure_ai_chat_client_agent_code_interpreter():
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_azure_ai_chat_client_agent_with_mcp_tools() -> None:
|
||||
"""Test MCP tools defined at agent creation with AzureAIAgentClient."""
|
||||
async def test_azure_ai_chat_client_agent_file_search():
|
||||
"""Test ChatAgent with file search through AzureAIAgentClient."""
|
||||
|
||||
client = AzureAIAgentClient(async_credential=AzureCliCredential())
|
||||
file: FileInfo | None = None
|
||||
vector_store: VectorStore | None = None
|
||||
|
||||
try:
|
||||
# 1. Read and upload the test file to the Azure AI agent service
|
||||
test_file_path = Path(__file__).parent / "resources" / "employees.pdf"
|
||||
file = await client.project_client.agents.files.upload_and_poll(
|
||||
file_path=str(test_file_path), purpose="assistants"
|
||||
)
|
||||
vector_store = await client.project_client.agents.vector_stores.create_and_poll(
|
||||
file_ids=[file.id], name="test_employees_vectorstore"
|
||||
)
|
||||
|
||||
# 2. Create file search tool with uploaded resources
|
||||
file_search_tool = HostedFileSearchTool(inputs=[HostedVectorStoreContent(vector_store_id=vector_store.id)])
|
||||
|
||||
async with ChatAgent(
|
||||
chat_client=client,
|
||||
instructions="You are a helpful assistant that can search through uploaded employee files.",
|
||||
tools=[file_search_tool],
|
||||
) as agent:
|
||||
# 3. Test file search functionality
|
||||
response = await agent.run("Who is the youngest employee in the files?")
|
||||
|
||||
# Validate response
|
||||
assert isinstance(response, AgentRunResponse)
|
||||
assert response.text is not None
|
||||
# Should find information about Alice Johnson (age 24) being the youngest
|
||||
assert any(term in response.text.lower() for term in ["alice", "johnson", "24"])
|
||||
|
||||
finally:
|
||||
# 4. Cleanup: Delete the vector store and file
|
||||
try:
|
||||
if vector_store:
|
||||
await client.project_client.agents.vector_stores.delete(vector_store.id)
|
||||
if file:
|
||||
await client.project_client.agents.files.delete(file.id)
|
||||
except Exception:
|
||||
# Ignore cleanup errors to avoid masking the actual test failure
|
||||
pass
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
||||
@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."""
|
||||
|
||||
mcp_tool = HostedMCPTool(
|
||||
name="Microsoft Learn MCP",
|
||||
url="https://learn.microsoft.com/api/mcp",
|
||||
description="A Microsoft Learn MCP server for documentation questions",
|
||||
approval_mode="never_require",
|
||||
)
|
||||
|
||||
async with ChatAgent(
|
||||
chat_client=AzureAIAgentClient(async_credential=AzureCliCredential()),
|
||||
name="DocsAgent",
|
||||
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
|
||||
tools=MCPStreamableHTTPTool(
|
||||
name="Microsoft Learn MCP",
|
||||
url="https://learn.microsoft.com/api/mcp",
|
||||
),
|
||||
tools=[mcp_tool],
|
||||
) as agent:
|
||||
# Test that the agent can use MCP tools to answer questions
|
||||
response = await agent.run("What is Azure App Service?")
|
||||
response = await agent.run(
|
||||
"How to create an Azure storage account using az cli?",
|
||||
max_tokens=200,
|
||||
)
|
||||
|
||||
assert isinstance(response, AgentRunResponse)
|
||||
assert response.text is not None
|
||||
# Verify the response contains relevant information about Azure App Service
|
||||
assert any(term in response.text.lower() for term in ["app service", "azure", "web", "application"])
|
||||
assert len(response.text) > 0
|
||||
|
||||
# With never_require approval mode, there should be no approval requests
|
||||
assert len(response.user_input_requests) == 0, (
|
||||
f"Expected no approval requests with never_require mode, but got {len(response.user_input_requests)}"
|
||||
)
|
||||
|
||||
# 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"])
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@@ -899,3 +1704,63 @@ async def test_azure_ai_chat_client_agent_level_tool_persistence():
|
||||
assert second_response.text is not None
|
||||
# Should use the agent-level weather tool again
|
||||
assert any(term in second_response.text.lower() for term in ["miami", "sunny", "25"])
|
||||
|
||||
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_azure_ai_chat_client_agent_chat_options_run_level() -> None:
|
||||
"""Test ChatOptions parameter coverage at run level."""
|
||||
async with ChatAgent(
|
||||
chat_client=AzureAIAgentClient(async_credential=AzureCliCredential()),
|
||||
instructions="You are a helpful assistant.",
|
||||
) as agent:
|
||||
response = await agent.run(
|
||||
"Provide a brief, helpful response.",
|
||||
max_tokens=100,
|
||||
temperature=0.7,
|
||||
top_p=0.9,
|
||||
seed=123,
|
||||
user="comprehensive-test-user",
|
||||
tools=[get_weather],
|
||||
tool_choice="auto",
|
||||
frequency_penalty=0.1,
|
||||
presence_penalty=0.1,
|
||||
stop=["END"],
|
||||
store=True,
|
||||
logit_bias={"test": 1},
|
||||
metadata={"test": "value"},
|
||||
additional_properties={"custom_param": "test_value"},
|
||||
)
|
||||
|
||||
assert isinstance(response, AgentRunResponse)
|
||||
assert response.text is not None
|
||||
assert len(response.text) > 0
|
||||
|
||||
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_azure_ai_chat_client_agent_chat_options_agent_level() -> None:
|
||||
"""Test ChatOptions parameter coverage agent level."""
|
||||
async with ChatAgent(
|
||||
chat_client=AzureAIAgentClient(async_credential=AzureCliCredential()),
|
||||
instructions="You are a helpful assistant.",
|
||||
max_tokens=100,
|
||||
temperature=0.7,
|
||||
top_p=0.9,
|
||||
seed=123,
|
||||
user="comprehensive-test-user",
|
||||
tools=[get_weather],
|
||||
tool_choice="auto",
|
||||
frequency_penalty=0.1,
|
||||
presence_penalty=0.1,
|
||||
stop=["END"],
|
||||
store=True,
|
||||
logit_bias={"test": 1},
|
||||
metadata={"test": "value"},
|
||||
request_kwargs={"custom_param": "test_value"},
|
||||
) as agent:
|
||||
response = await agent.run(
|
||||
"Provide a brief, helpful response.",
|
||||
)
|
||||
|
||||
assert isinstance(response, AgentRunResponse)
|
||||
assert response.text is not None
|
||||
assert len(response.text) > 0
|
||||
|
||||
Reference in New Issue
Block a user