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
|
||||
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user