Skip to content

Commit ee8936d

Browse files
dmytrostrukCopilot
andauthored
Python: Integration tests for Azure AI client and fixes in samples (#2387)
* Added integration tests * Update python/packages/azure-ai/tests/test_azure_ai_client.py Co-authored-by: Copilot <[email protected]> * Small fixes in samples * Small fix * Small fix --------- Co-authored-by: Copilot <[email protected]>
1 parent b7a19b0 commit ee8936d

File tree

6 files changed

+116
-15
lines changed

6 files changed

+116
-15
lines changed

python/packages/azure-ai/tests/test_azure_ai_client.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,67 @@
11
# Copyright (c) Microsoft. All rights reserved.
22

3+
import os
4+
from collections.abc import AsyncIterator
5+
from contextlib import asynccontextmanager
6+
from typing import Annotated
37
from unittest.mock import AsyncMock, MagicMock, patch
48

59
import pytest
610
from agent_framework import (
11+
AgentRunResponse,
12+
AgentRunResponseUpdate,
13+
ChatAgent,
714
ChatClientProtocol,
815
ChatMessage,
916
ChatOptions,
1017
Role,
1118
TextContent,
1219
)
1320
from agent_framework.exceptions import ServiceInitializationError
21+
from azure.ai.projects.aio import AIProjectClient
1422
from azure.ai.projects.models import (
1523
ResponseTextFormatConfigurationJsonSchema,
1624
)
25+
from azure.identity.aio import AzureCliCredential
1726
from openai.types.responses.parsed_response import ParsedResponse
1827
from openai.types.responses.response import Response as OpenAIResponse
19-
from pydantic import BaseModel, ConfigDict, ValidationError
28+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
2029

2130
from agent_framework_azure_ai import AzureAIClient, AzureAISettings
2231

32+
skip_if_azure_ai_integration_tests_disabled = pytest.mark.skipif(
33+
os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true"
34+
or os.getenv("AZURE_AI_PROJECT_ENDPOINT", "") in ("", "https://test-project.cognitiveservices.azure.com/")
35+
or os.getenv("AZURE_AI_MODEL_DEPLOYMENT_NAME", "") == "",
36+
reason=(
37+
"No real AZURE_AI_PROJECT_ENDPOINT or AZURE_AI_MODEL_DEPLOYMENT_NAME provided; skipping integration tests."
38+
if os.getenv("RUN_INTEGRATION_TESTS", "false").lower() == "true"
39+
else "Integration tests are disabled."
40+
),
41+
)
42+
43+
44+
@asynccontextmanager
45+
async def temporary_chat_client(agent_name: str) -> AsyncIterator[AzureAIClient]:
46+
"""Async context manager that creates an Azure AI agent and yields an `AzureAIClient`.
47+
48+
The underlying agent version is cleaned up automatically after use.
49+
Tests can construct their own `ChatAgent` instances from the yielded client.
50+
"""
51+
endpoint = os.environ["AZURE_AI_PROJECT_ENDPOINT"]
52+
async with (
53+
AzureCliCredential() as credential,
54+
AIProjectClient(endpoint=endpoint, credential=credential) as project_client,
55+
):
56+
chat_client = AzureAIClient(
57+
project_client=project_client,
58+
agent_name=agent_name,
59+
)
60+
try:
61+
yield chat_client
62+
finally:
63+
await project_client.agents.delete(agent_name=agent_name)
64+
2365

2466
def create_test_azure_ai_client(
2567
mock_project_client: MagicMock,
@@ -751,3 +793,64 @@ def mock_project_client() -> MagicMock:
751793
mock_client.close = AsyncMock()
752794

753795
return mock_client
796+
797+
798+
def get_weather(
799+
location: Annotated[str, Field(description="The location to get the weather for.")],
800+
) -> str:
801+
"""Get the weather for a given location."""
802+
return f"The weather in {location} is sunny with a high of 25°C."
803+
804+
805+
@pytest.mark.flaky
806+
@skip_if_azure_ai_integration_tests_disabled
807+
async def test_azure_ai_chat_client_agent_basic_run() -> None:
808+
"""Test ChatAgent basic run functionality with AzureAIClient."""
809+
async with (
810+
temporary_chat_client(agent_name="BasicRunAgent") as chat_client,
811+
ChatAgent(chat_client=chat_client) as agent,
812+
):
813+
response = await agent.run("Hello! Please respond with 'Hello World' exactly.")
814+
815+
# Validate response
816+
assert isinstance(response, AgentRunResponse)
817+
assert response.text is not None
818+
assert len(response.text) > 0
819+
assert "Hello World" in response.text
820+
821+
822+
@pytest.mark.flaky
823+
@skip_if_azure_ai_integration_tests_disabled
824+
async def test_azure_ai_chat_client_agent_basic_run_streaming() -> None:
825+
"""Test ChatAgent basic streaming functionality with AzureAIClient."""
826+
async with (
827+
temporary_chat_client(agent_name="BasicRunStreamingAgent") as chat_client,
828+
ChatAgent(chat_client=chat_client) as agent,
829+
):
830+
full_message: str = ""
831+
async for chunk in agent.run_stream("Please respond with exactly: 'This is a streaming response test.'"):
832+
assert chunk is not None
833+
assert isinstance(chunk, AgentRunResponseUpdate)
834+
if chunk.text:
835+
full_message += chunk.text
836+
837+
# Validate streaming response
838+
assert len(full_message) > 0
839+
assert "streaming response test" in full_message.lower()
840+
841+
842+
@pytest.mark.flaky
843+
@skip_if_azure_ai_integration_tests_disabled
844+
async def test_azure_ai_chat_client_agent_with_tools() -> None:
845+
"""Test ChatAgent tools with AzureAIClient."""
846+
async with (
847+
temporary_chat_client(agent_name="RunToolsAgent") as chat_client,
848+
ChatAgent(chat_client=chat_client, tools=[get_weather]) as agent,
849+
):
850+
response = await agent.run("What's the weather like in Seattle?")
851+
852+
# Validate response
853+
assert isinstance(response, AgentRunResponse)
854+
assert response.text is not None
855+
assert len(response.text) > 0
856+
assert any(word in response.text.lower() for word in ["sunny", "25"])

python/samples/getting_started/agents/azure_ai/README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ This folder contains examples demonstrating different ways to create and use age
2020
| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Shows how to use the `HostedFileSearchTool` with Azure AI agents to upload files, create vector stores, and enable agents to search through uploaded documents to answer user questions. |
2121
| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate hosted Model Context Protocol (MCP) tools with Azure AI Agent. |
2222
| [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Shows how to use structured outputs (response format) with Azure AI agents using Pydantic models to enforce specific response schemas. |
23-
| [`azure_ai_with_search_context_agentic.py`](azure_ai_with_search_context_agentic.py) | Shows how to use AzureAISearchContextProvider with agentic mode. Uses Knowledge Bases for multi-hop reasoning across documents with query planning. Recommended for most scenarios - slightly slower with more token consumption for query planning, but more accurate results. |
24-
| [`azure_ai_with_search_context_semantic.py`](azure_ai_with_search_context_semantic.py) | Shows how to use AzureAISearchContextProvider with semantic mode. Fast hybrid search with vector + keyword search and semantic ranking for RAG. Best for simple queries where speed is critical. |
2523
| [`azure_ai_with_sharepoint.py`](azure_ai_with_sharepoint.py) | Shows how to use SharePoint grounding with Azure AI agents to search through SharePoint content and answer user questions with proper citations. Requires a SharePoint connection configured in your Azure AI project. |
2624
| [`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. |
2725
| [`azure_ai_with_image_generation.py`](azure_ai_with_image_generation.py) | Shows how to use the `ImageGenTool` with Azure AI agents to generate images based on text prompts. |

python/samples/getting_started/agents/azure_ai/azure_ai_with_existing_conversation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ async def example_with_client() -> None:
3232
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
3333
):
3434
# Create a conversation using OpenAI client
35-
openai_client = await project_client.get_openai_client()
35+
openai_client = project_client.get_openai_client()
3636
conversation = await openai_client.conversations.create()
3737
conversation_id = conversation.id
3838
print(f"Conversation ID: {conversation_id}")
@@ -70,7 +70,7 @@ async def example_with_thread() -> None:
7070
) as agent,
7171
):
7272
# Create a conversation using OpenAI client
73-
openai_client = await project_client.get_openai_client()
73+
openai_client = project_client.get_openai_client()
7474
conversation = await openai_client.conversations.create()
7575
conversation_id = conversation.id
7676
print(f"Conversation ID: {conversation_id}")

python/samples/getting_started/agents/azure_ai_agent/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ This folder contains examples demonstrating different ways to create and use age
2020
| [`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. |
2121
| [`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. |
2222
| [`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. |
23+
| [`azure_ai_with_search_context_agentic.py`](azure_ai_with_search_context_agentic.py) | Shows how to use AzureAISearchContextProvider with agentic mode. Uses Knowledge Bases for multi-hop reasoning across documents with query planning. Recommended for most scenarios - slightly slower with more token consumption for query planning, but more accurate results. |
24+
| [`azure_ai_with_search_context_semantic.py`](azure_ai_with_search_context_semantic.py) | Shows how to use AzureAISearchContextProvider with semantic mode. Fast hybrid search with vector + keyword search and semantic ranking for RAG. Best for simple queries where speed is critical. |
2325
| [`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. |
2426

2527
## Environment Variables

python/samples/getting_started/agents/azure_ai/azure_ai_with_search_context_agentic.py renamed to python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_search_context_agentic.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
import asyncio
44
import os
55

6-
from dotenv import load_dotenv
7-
86
from agent_framework import ChatAgent
97
from agent_framework_aisearch import AzureAISearchContextProvider
108
from agent_framework_azure_ai import AzureAIAgentClient
11-
from azure.identity.aio import DefaultAzureCredential
9+
from azure.identity.aio import AzureCliCredential
10+
from dotenv import load_dotenv
1211

1312
# Load environment variables from .env file
1413
load_dotenv()
@@ -68,7 +67,7 @@ async def main() -> None:
6867
endpoint=search_endpoint,
6968
index_name=index_name,
7069
api_key=search_key, # Use api_key for API key auth, or credential for managed identity
71-
credential=DefaultAzureCredential() if not search_key else None,
70+
credential=AzureCliCredential() if not search_key else None,
7271
mode="agentic", # Advanced mode for multi-hop reasoning
7372
# Agentic mode configuration
7473
azure_ai_project_endpoint=project_endpoint,
@@ -87,7 +86,7 @@ async def main() -> None:
8786
AzureAIAgentClient(
8887
project_endpoint=project_endpoint,
8988
model_deployment_name=model_deployment,
90-
async_credential=DefaultAzureCredential(),
89+
async_credential=AzureCliCredential(),
9190
) as client,
9291
ChatAgent(
9392
chat_client=client,

python/samples/getting_started/agents/azure_ai/azure_ai_with_search_context_semantic.py renamed to python/samples/getting_started/agents/azure_ai_agent/azure_ai_with_search_context_semantic.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
import asyncio
44
import os
55

6-
from dotenv import load_dotenv
7-
86
from agent_framework import ChatAgent
97
from agent_framework_aisearch import AzureAISearchContextProvider
108
from agent_framework_azure_ai import AzureAIAgentClient
11-
from azure.identity.aio import DefaultAzureCredential
9+
from azure.identity.aio import AzureCliCredential
10+
from dotenv import load_dotenv
1211

1312
# Load environment variables from .env file
1413
load_dotenv()
@@ -58,7 +57,7 @@ async def main() -> None:
5857
endpoint=search_endpoint,
5958
index_name=index_name,
6059
api_key=search_key, # Use api_key for API key auth, or credential for managed identity
61-
credential=DefaultAzureCredential() if not search_key else None,
60+
credential=AzureCliCredential() if not search_key else None,
6261
mode="semantic", # Default mode
6362
top_k=3, # Retrieve top 3 most relevant documents
6463
)
@@ -69,7 +68,7 @@ async def main() -> None:
6968
AzureAIAgentClient(
7069
project_endpoint=project_endpoint,
7170
model_deployment_name=model_deployment,
72-
async_credential=DefaultAzureCredential(),
71+
async_credential=AzureCliCredential(),
7372
) as client,
7473
ChatAgent(
7574
chat_client=client,

0 commit comments

Comments
 (0)