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:
Giles Odigwe
2025-10-01 06:38:51 -07:00
committed by GitHub
Unverified
parent 74e2e2e21d
commit b0971fdec6
10 changed files with 1520 additions and 25 deletions
@@ -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
@@ -7,12 +7,17 @@ This folder contains examples demonstrating different ways to create and use age
| File | Description |
|------|-------------|
| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `ChatAgent` with `AzureAIAgentClient`. It automatically handles all configuration using environment variables. |
| [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured `AzureAIAgentClient` settings, including project endpoint, model deployment, credentials, and agent name. |
| [`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_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_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with a pre-existing agent by providing the agent ID to the Azure AI chat client. This example also demonstrates proper cleanup of manually created agents. |
| [`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 to the Azure AI chat client. This example also 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 `AzureAIAgentClient` settings, including project endpoint, model deployment, credentials, and agent name. |
| [`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. Includes both streaming and non-streaming examples. |
| [`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_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_local_mcp.py`](azure_ai_with_local_mcp.py) | Shows how to integrate Azure AI agents with Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates both agent-level and run-level tool configuration. |
| [`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_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_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 using weather and countries APIs. |
| [`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. |
## Environment Variables
@@ -0,0 +1,61 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import ChatAgent, HostedWebSearchTool
from agent_framework_azure_ai import AzureAIAgentClient
from azure.identity.aio import AzureCliCredential
"""
The following sample demonstrates how to create an Azure AI agent that
uses Bing Grounding search to find real-time information from the web.
Prerequisites:
1. A connected Grounding with Bing Search resource in your Azure AI project
2. Set the BING_CONNECTION_ID environment variable to your Bing connection ID
Example: BING_CONNECTION_ID="/subscriptions/{subscription-id}/resourceGroups/{resource-group}/
providers/Microsoft.CognitiveServices/accounts/{ai-service-name}/projects/{project-name}/
connections/{connection-name}"
To set up Bing Grounding:
1. Go to Azure AI Foundry portal (https://ai.azure.com)
2. Navigate to your project's "Connected resources" section
3. Add a new connection for "Grounding with Bing Search"
4. Copy the connection ID and set it as the BING_CONNECTION_ID environment variable
"""
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 BING_CONNECTION_ID environment variable
bing_search_tool = HostedWebSearchTool(
name="Bing Grounding Search",
description="Search the web for current information using Bing",
)
# 2. Use AzureAIAgentClient as async context manager for automatic cleanup
async with (
AzureAIAgentClient(async_credential=AzureCliCredential()) as client,
ChatAgent(
chat_client=client,
name="BingSearchAgent",
instructions=(
"You are a helpful assistant that can search the web for current information. "
"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,
) as agent,
):
# 4. Demonstrate agent capabilities with web search
print("=== Azure AI Agent with Bing Grounding Search ===\n")
user_input = "What is the most popular programming language?"
print(f"User: {user_input}")
response = await agent.run(user_input)
print(f"Agent: {response.text}\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,81 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from pathlib import Path
from agent_framework import ChatAgent, HostedFileSearchTool, HostedVectorStoreContent
from agent_framework_azure_ai import AzureAIAgentClient
from azure.ai.agents.models import FileInfo, VectorStore
from azure.identity.aio import AzureCliCredential
"""
The following sample demonstrates how to create a simple, Azure AI agent that
uses a file search tool to answer user questions.
"""
# Simulate a conversation with the agent
USER_INPUTS = [
"Who is the youngest employee?",
"Who works in sales?",
"I have a customer request, who can help me?",
]
async def main() -> None:
"""Main function demonstrating Azure AI agent with file search capabilities."""
client = AzureAIAgentClient(async_credential=AzureCliCredential())
file: FileInfo | None = None
vector_store: VectorStore | None = None
try:
# 1. Upload file and create vector store
pdf_file_path = Path(__file__).parent.parent / "resources" / "employees.pdf"
print(f"Uploading file from: {pdf_file_path}")
file = await client.project_client.agents.files.upload_and_poll(
file_path=str(pdf_file_path), purpose="assistants"
)
print(f"Uploaded file, file ID: {file.id}")
vector_store = await client.project_client.agents.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=[HostedVectorStoreContent(vector_store_id=vector_store.id)])
# 3. Create an agent with file search capabilities
# The tool_resources are automatically extracted from HostedFileSearchTool
async with ChatAgent(
chat_client=client,
name="EmployeeSearchAgent",
instructions=(
"You are a helpful assistant that can search through uploaded employee files "
"to answer questions about employees."
),
tools=file_search_tool,
) as agent:
# 4. Simulate conversation with the agent
for user_input in USER_INPUTS:
print(f"# User: '{user_input}'")
response = await agent.run(user_input)
print(f"# Agent: {response.text}")
finally:
# 5. Cleanup: Delete the vector store and file
try:
if vector_store is not None:
await client.project_client.agents.vector_stores.delete(vector_store.id)
if file is not None:
await client.project_client.agents.files.delete(file.id)
except Exception:
# Ignore cleanup errors to avoid masking issues
pass
finally:
await client.close()
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,91 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import json
from pathlib import Path
from typing import Any
from agent_framework import ChatAgent
from agent_framework_azure_ai import AzureAIAgentClient
from azure.ai.agents.models import OpenApiAnonymousAuthDetails, OpenApiTool
from azure.identity.aio import AzureCliCredential
"""
The following sample demonstrates how to create a simple, Azure AI agent that
uses OpenAPI tools to answer user questions.
"""
# Simulate a conversation with the agent
USER_INPUTS = [
"What is the name and population of the country that uses currency with abbreviation THB?",
"What is the current weather in the capital city of that country?",
]
def load_openapi_specs() -> tuple[dict[str, Any], dict[str, Any]]:
"""Load OpenAPI specification files."""
resources_path = Path(__file__).parent.parent / "resources"
with open(resources_path / "weather.json") as weather_file:
weather_spec = json.load(weather_file)
with open(resources_path / "countries.json") as countries_file:
countries_spec = json.load(countries_file)
return weather_spec, countries_spec
async def main() -> None:
"""Main function demonstrating Azure AI agent with OpenAPI tools."""
# 1. Load OpenAPI specifications (synchronous operation)
weather_openapi_spec, countries_openapi_spec = load_openapi_specs()
# 2. Use AzureAIAgentClient as async context manager for automatic cleanup
async with AzureAIAgentClient(async_credential=AzureCliCredential()) as client:
# 3. Create OpenAPI tools using Azure AI's OpenApiTool
auth = OpenApiAnonymousAuthDetails()
openapi_weather = OpenApiTool(
name="get_weather",
spec=weather_openapi_spec,
description="Retrieve weather information for a location using wttr.in service",
auth=auth,
)
openapi_countries = OpenApiTool(
name="get_country_info",
spec=countries_openapi_spec,
description="Retrieve country information including population and capital city",
auth=auth,
)
# 4. Create an agent with OpenAPI tools
# Note: We need to pass the Azure AI native OpenApiTool definitions directly
# since the agent framework doesn't have a HostedOpenApiTool wrapper yet
async with ChatAgent(
chat_client=client,
name="OpenAPIAgent",
instructions=(
"You are a helpful assistant that can search for country information "
"and weather data using APIs. When asked about countries, use the country "
"API to find information. When asked about weather, use the weather API. "
"Provide clear, informative answers based on the API results."
),
# Pass the raw tool definitions from Azure AI's OpenApiTool
tools=[*openapi_countries.definitions, *openapi_weather.definitions],
) as agent:
# 5. Simulate conversation with the agent maintaining thread context
print("=== Azure AI Agent with OpenAPI Tools ===\n")
# Create a thread to maintain conversation context across multiple runs
thread = agent.get_new_thread()
for user_input in USER_INPUTS:
print(f"User: {user_input}")
# Pass the thread to maintain context across multiple agent.run() calls
response = await agent.run(user_input, thread=thread)
print(f"Agent: {response.text}\n")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,141 @@
{
"openapi": "3.0.1",
"info": {
"title": "REST Countries API",
"description": "Get information about countries of the world",
"version": "3.1"
},
"servers": [
{
"url": "https://restcountries.com/v3.1"
}
],
"paths": {
"/currency/{currency}": {
"get": {
"operationId": "getCountriesByCurrency",
"summary": "Get countries by currency",
"description": "Search for countries by currency code",
"parameters": [
{
"name": "currency",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "Currency code (e.g., THB, USD, EUR)"
}
],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "object",
"properties": {
"common": {"type": "string"},
"official": {"type": "string"}
}
},
"population": {"type": "integer"},
"region": {"type": "string"},
"subregion": {"type": "string"},
"capital": {
"type": "array",
"items": {"type": "string"}
},
"currencies": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"name": {"type": "string"},
"symbol": {"type": "string"}
}
}
},
"languages": {
"type": "object",
"additionalProperties": {"type": "string"}
},
"latlng": {
"type": "array",
"items": {"type": "number"}
}
}
}
}
}
}
}
}
}
},
"/name/{name}": {
"get": {
"operationId": "getCountryByName",
"summary": "Get country by name",
"description": "Search for countries by name",
"parameters": [
{
"name": "name",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "Country name"
}
],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "object",
"properties": {
"common": {"type": "string"},
"official": {"type": "string"}
}
},
"population": {"type": "integer"},
"region": {"type": "string"},
"subregion": {"type": "string"},
"capital": {
"type": "array",
"items": {"type": "string"}
},
"currencies": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"name": {"type": "string"},
"symbol": {"type": "string"}
}
}
}
}
}
}
}
}
}
}
}
}
}
}
@@ -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
@@ -0,0 +1,62 @@
{
"openapi": "3.1.0",
"info": {
"title": "wttr.in Weather API",
"description": "Retrieves current weather data for a location using wttr.in service",
"version": "v1.0.0"
},
"servers": [
{
"url": "https://wttr.in"
}
],
"paths": {
"/{location}": {
"get": {
"operationId": "GetCurrentWeather",
"summary": "Get weather information for a specific location",
"description": "Get weather information for a specific location",
"parameters": [
{
"name": "location",
"in": "path",
"description": "City or location to retrieve the weather for",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "format",
"in": "query",
"description": "Format in which to return data. Always use 3.",
"required": true,
"schema": {
"type": "integer",
"default": 3
}
}
],
"responses": {
"200": {
"description": "Successful response",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
},
"404": {
"description": "Location not found"
}
},
"deprecated": false
}
}
},
"components": {
"schemas": {}
}
}