mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
df9d85d1f0
* openai updates * rebuild of openai structure * updated responses structure * renamed sample * added file id support to code interpreter * added hosted file ids to code interpretor * mypy fixes * removed default az cred from codebase * updated agent name setup * added kwargs to entra methods * and further kwargs * extra comment * updated all samples * readded custom get methods for responses * updated int tests with ad credential * missed one
405 lines
16 KiB
Python
405 lines
16 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
import os
|
|
from typing import Annotated
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from agent_framework import (
|
|
ChatClient,
|
|
ChatMessage,
|
|
ChatOptions,
|
|
ChatResponse,
|
|
ChatResponseUpdate,
|
|
ChatRole,
|
|
TextContent,
|
|
)
|
|
from agent_framework.exceptions import ServiceInitializationError
|
|
from azure.identity.aio import DefaultAzureCredential
|
|
from pydantic import Field
|
|
|
|
from agent_framework_foundry import FoundryChatClient, FoundrySettings
|
|
|
|
skip_if_foundry_integration_tests_disabled = pytest.mark.skipif(
|
|
os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true"
|
|
or os.getenv("FOUNDRY_PROJECT_ENDPOINT", "") in ("", "https://test-project.cognitiveservices.azure.com/"),
|
|
reason="No real FOUNDRY_PROJECT_ENDPOINT provided; skipping integration tests."
|
|
if os.getenv("RUN_INTEGRATION_TESTS", "false").lower() == "true"
|
|
else "Integration tests are disabled.",
|
|
)
|
|
|
|
|
|
def create_test_foundry_chat_client(
|
|
mock_ai_project_client: MagicMock,
|
|
agent_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
foundry_settings: FoundrySettings | None = None,
|
|
should_delete_agent: bool = False,
|
|
) -> FoundryChatClient:
|
|
"""Helper function to create FoundryChatClient instances for testing, bypassing Pydantic validation."""
|
|
if foundry_settings is None:
|
|
foundry_settings = FoundrySettings(env_file_path="test.env")
|
|
|
|
return FoundryChatClient.model_construct(
|
|
client=mock_ai_project_client,
|
|
agent_id=agent_id,
|
|
thread_id=thread_id,
|
|
_should_delete_agent=should_delete_agent,
|
|
agent_name=foundry_settings.agent_name, # type: ignore[reportCallIssue]
|
|
ai_model_deployment_name=foundry_settings.model_deployment_name, # type:
|
|
credential=None,
|
|
)
|
|
|
|
|
|
def test_foundry_settings_init(foundry_unit_test_env: dict[str, str]) -> None:
|
|
"""Test FoundrySettings initialization."""
|
|
settings = FoundrySettings()
|
|
|
|
assert settings.project_endpoint == foundry_unit_test_env["FOUNDRY_PROJECT_ENDPOINT"]
|
|
assert settings.model_deployment_name == foundry_unit_test_env["FOUNDRY_MODEL_DEPLOYMENT_NAME"]
|
|
assert settings.agent_name == foundry_unit_test_env["FOUNDRY_AGENT_NAME"]
|
|
|
|
|
|
def test_foundry_settings_init_with_explicit_values() -> None:
|
|
"""Test FoundrySettings initialization with explicit values."""
|
|
settings = FoundrySettings(
|
|
project_endpoint="https://custom-endpoint.com/",
|
|
model_deployment_name="custom-model",
|
|
agent_name="CustomAgent",
|
|
)
|
|
|
|
assert settings.project_endpoint == "https://custom-endpoint.com/"
|
|
assert settings.model_deployment_name == "custom-model"
|
|
assert settings.agent_name == "CustomAgent"
|
|
|
|
|
|
def test_foundry_chat_client_init_with_client(mock_ai_project_client: MagicMock) -> None:
|
|
"""Test FoundryChatClient initialization with existing client."""
|
|
chat_client = create_test_foundry_chat_client(
|
|
mock_ai_project_client, agent_id="existing-agent-id", thread_id="test-thread-id"
|
|
)
|
|
|
|
assert chat_client.client is mock_ai_project_client
|
|
assert chat_client.agent_id == "existing-agent-id"
|
|
assert chat_client.thread_id == "test-thread-id"
|
|
assert not chat_client._should_delete_agent # type: ignore
|
|
assert isinstance(chat_client, ChatClient)
|
|
|
|
|
|
def test_foundry_chat_client_init_auto_create_client(
|
|
foundry_unit_test_env: dict[str, str],
|
|
mock_ai_project_client: MagicMock,
|
|
) -> None:
|
|
"""Test FoundryChatClient initialization with auto-created client."""
|
|
foundry_settings = FoundrySettings(**foundry_unit_test_env) # type: ignore
|
|
chat_client = FoundryChatClient.model_construct(
|
|
client=mock_ai_project_client,
|
|
agent_id=None,
|
|
thread_id=None,
|
|
_should_delete_agent=False,
|
|
_foundry_settings=foundry_settings,
|
|
credential=None,
|
|
)
|
|
|
|
assert chat_client.client is mock_ai_project_client
|
|
assert chat_client.agent_id is None
|
|
assert not chat_client._should_delete_agent # type: ignore
|
|
|
|
|
|
def test_foundry_chat_client_from_dict(mock_ai_project_client: MagicMock) -> None:
|
|
"""Test FoundryChatClient.from_dict method."""
|
|
settings = {
|
|
"client": mock_ai_project_client,
|
|
"agent_id": "test-agent-id",
|
|
"thread_id": "test-thread-id",
|
|
"project_endpoint": "https://test-endpoint.com/",
|
|
"model_deployment_name": "test-model",
|
|
"agent_name": "TestAgent",
|
|
}
|
|
|
|
foundry_settings = FoundrySettings(
|
|
project_endpoint=settings["project_endpoint"],
|
|
model_deployment_name=settings["model_deployment_name"],
|
|
agent_name=settings["agent_name"],
|
|
)
|
|
|
|
chat_client: FoundryChatClient = create_test_foundry_chat_client(
|
|
mock_ai_project_client,
|
|
agent_id=settings["agent_id"], # type: ignore
|
|
thread_id=settings["thread_id"], # type: ignore
|
|
foundry_settings=foundry_settings,
|
|
)
|
|
|
|
assert chat_client.client is mock_ai_project_client
|
|
assert chat_client.agent_id == "test-agent-id"
|
|
assert chat_client.thread_id == "test-thread-id"
|
|
|
|
|
|
async def test_foundry_chat_client_get_agent_id_or_create_existing_agent(
|
|
mock_ai_project_client: MagicMock,
|
|
) -> None:
|
|
"""Test _get_agent_id_or_create when agent_id is already provided."""
|
|
chat_client = create_test_foundry_chat_client(mock_ai_project_client, agent_id="existing-agent-id")
|
|
|
|
agent_id = await chat_client._get_agent_id_or_create() # type: ignore
|
|
|
|
assert agent_id == "existing-agent-id"
|
|
assert not chat_client._should_delete_agent # type: ignore
|
|
|
|
|
|
async def test_foundry_chat_client_get_agent_id_or_create_create_new(
|
|
mock_ai_project_client: MagicMock,
|
|
foundry_unit_test_env: dict[str, str],
|
|
) -> None:
|
|
"""Test _get_agent_id_or_create when creating a new agent."""
|
|
foundry_settings = FoundrySettings(
|
|
model_deployment_name=foundry_unit_test_env["FOUNDRY_MODEL_DEPLOYMENT_NAME"], agent_name="TestAgent"
|
|
)
|
|
chat_client = create_test_foundry_chat_client(mock_ai_project_client, foundry_settings=foundry_settings)
|
|
|
|
agent_id = await chat_client._get_agent_id_or_create() # type: ignore
|
|
|
|
assert agent_id == "test-agent-id"
|
|
assert chat_client._should_delete_agent # type: ignore
|
|
|
|
|
|
@pytest.mark.parametrize("exclude_list", [["FOUNDRY_MODEL_DEPLOYMENT_NAME"]], indirect=True)
|
|
async def test_foundry_chat_client_get_agent_id_or_create_missing_model(
|
|
mock_ai_project_client: MagicMock, foundry_unit_test_env: dict[str, str]
|
|
) -> None:
|
|
"""Test _get_agent_id_or_create when model_deployment_name is missing."""
|
|
chat_client = create_test_foundry_chat_client(mock_ai_project_client)
|
|
|
|
with pytest.raises(ServiceInitializationError, match="Model deployment name is required"):
|
|
await chat_client._get_agent_id_or_create() # type: ignore
|
|
|
|
|
|
async def test_foundry_chat_client_cleanup_agent_if_needed_should_delete(
|
|
mock_ai_project_client: MagicMock,
|
|
) -> None:
|
|
"""Test _cleanup_agent_if_needed when agent should be deleted."""
|
|
chat_client = create_test_foundry_chat_client(
|
|
mock_ai_project_client, agent_id="agent-to-delete", should_delete_agent=True
|
|
)
|
|
|
|
await chat_client._cleanup_agent_if_needed() # type: ignore
|
|
|
|
# Verify agent deletion was called
|
|
mock_ai_project_client.agents.delete_agent.assert_called_once_with("agent-to-delete")
|
|
assert not chat_client._should_delete_agent # type: ignore
|
|
|
|
|
|
async def test_foundry_chat_client_cleanup_agent_if_needed_should_not_delete(
|
|
mock_ai_project_client: MagicMock,
|
|
) -> None:
|
|
"""Test _cleanup_agent_if_needed when agent should not be deleted."""
|
|
chat_client = create_test_foundry_chat_client(
|
|
mock_ai_project_client, agent_id="agent-to-keep", should_delete_agent=False
|
|
)
|
|
|
|
await chat_client._cleanup_agent_if_needed() # type: ignore
|
|
|
|
# Verify agent deletion was not called
|
|
mock_ai_project_client.agents.delete_agent.assert_not_called()
|
|
assert not chat_client._should_delete_agent # type: ignore
|
|
|
|
|
|
async def test_foundry_chat_client_cleanup_agent_if_needed_exception_handling(
|
|
mock_ai_project_client: MagicMock,
|
|
) -> None:
|
|
"""Test _cleanup_agent_if_needed propagates exceptions (it doesn't handle them)."""
|
|
chat_client = create_test_foundry_chat_client(
|
|
mock_ai_project_client, agent_id="agent-to-delete", should_delete_agent=True
|
|
)
|
|
mock_ai_project_client.agents.delete_agent.side_effect = Exception("Deletion failed")
|
|
|
|
with pytest.raises(Exception, match="Deletion failed"):
|
|
await chat_client._cleanup_agent_if_needed() # type: ignore
|
|
|
|
|
|
async def test_foundry_chat_client_aclose(mock_ai_project_client: MagicMock) -> None:
|
|
"""Test aclose method calls cleanup."""
|
|
chat_client = create_test_foundry_chat_client(
|
|
mock_ai_project_client, agent_id="agent-to-delete", should_delete_agent=True
|
|
)
|
|
|
|
await chat_client.close()
|
|
|
|
# Verify agent deletion was called
|
|
mock_ai_project_client.agents.delete_agent.assert_called_once_with("agent-to-delete")
|
|
|
|
|
|
async def test_foundry_chat_client_async_context_manager(mock_ai_project_client: MagicMock) -> None:
|
|
"""Test async context manager functionality."""
|
|
chat_client = create_test_foundry_chat_client(
|
|
mock_ai_project_client, agent_id="agent-to-delete", should_delete_agent=True
|
|
)
|
|
|
|
# Test context manager
|
|
async with chat_client:
|
|
pass # Just test that we can enter and exit
|
|
|
|
# Verify cleanup was called on exit
|
|
mock_ai_project_client.agents.delete_agent.assert_called_once_with("agent-to-delete")
|
|
|
|
|
|
def test_foundry_chat_client_create_run_options_basic(mock_ai_project_client: MagicMock) -> None:
|
|
"""Test _create_run_options with basic ChatOptions."""
|
|
chat_client = create_test_foundry_chat_client(mock_ai_project_client)
|
|
|
|
messages = [ChatMessage(role=ChatRole.USER, text="Hello")]
|
|
chat_options = ChatOptions(max_tokens=100, temperature=0.7)
|
|
|
|
run_options, tool_results = chat_client._create_run_options(messages, chat_options) # type: ignore
|
|
|
|
assert run_options is not None
|
|
assert tool_results is None
|
|
|
|
|
|
def test_foundry_chat_client_create_run_options_no_chat_options(mock_ai_project_client: MagicMock) -> None:
|
|
"""Test _create_run_options with no ChatOptions."""
|
|
chat_client = create_test_foundry_chat_client(mock_ai_project_client)
|
|
|
|
messages = [ChatMessage(role=ChatRole.USER, text="Hello")]
|
|
|
|
run_options, tool_results = chat_client._create_run_options(messages, None) # type: ignore
|
|
|
|
assert run_options is not None
|
|
assert tool_results is None
|
|
|
|
|
|
def test_foundry_chat_client_convert_function_results_to_tool_output(mock_ai_project_client: MagicMock) -> None:
|
|
"""Test _convert_function_results_to_tool_output method."""
|
|
from agent_framework import FunctionResultContent
|
|
|
|
chat_client = create_test_foundry_chat_client(mock_ai_project_client)
|
|
|
|
function_results = [
|
|
FunctionResultContent(call_id='["run_123", "call_456"]', result="Result 1"),
|
|
FunctionResultContent(call_id='["run_123", "call_789"]', result="Result 2"),
|
|
]
|
|
|
|
run_id, tool_outputs = chat_client._convert_function_results_to_tool_output(function_results) # type: ignore
|
|
|
|
assert run_id == "run_123"
|
|
assert tool_outputs is not None
|
|
assert len(tool_outputs) == 2
|
|
|
|
|
|
def test_foundry_chat_client_convert_function_results_to_tool_output_none(mock_ai_project_client: MagicMock) -> None:
|
|
"""Test _convert_function_results_to_tool_output with None input."""
|
|
chat_client = create_test_foundry_chat_client(mock_ai_project_client)
|
|
|
|
run_id, tool_outputs = chat_client._convert_function_results_to_tool_output(None) # type: ignore
|
|
|
|
assert run_id is None
|
|
assert tool_outputs is None
|
|
|
|
|
|
def get_weather(
|
|
location: Annotated[str, Field(description="The location to get the weather for.")],
|
|
) -> str:
|
|
"""Get the weather for a given location."""
|
|
return f"The weather in {location} is sunny with a high of 25°C."
|
|
|
|
|
|
@skip_if_foundry_integration_tests_disabled
|
|
async def test_foundry_chat_client_get_response() -> None:
|
|
"""Test Foundry Chat Client response."""
|
|
async with FoundryChatClient(async_ad_credential=DefaultAzureCredential()) as foundry_chat_client:
|
|
assert isinstance(foundry_chat_client, ChatClient)
|
|
|
|
messages: list[ChatMessage] = []
|
|
messages.append(
|
|
ChatMessage(
|
|
role="user",
|
|
text="The weather in Seattle is currently sunny with a high of 25°C. "
|
|
"It's a beautiful day for outdoor activities.",
|
|
)
|
|
)
|
|
messages.append(ChatMessage(role="user", text="What's the weather like today?"))
|
|
|
|
# Test that the client can be used to get a response
|
|
response = await foundry_chat_client.get_response(messages=messages)
|
|
|
|
assert response is not None
|
|
assert isinstance(response, ChatResponse)
|
|
assert any(word in response.text.lower() for word in ["sunny", "25"])
|
|
|
|
|
|
@skip_if_foundry_integration_tests_disabled
|
|
async def test_foundry_chat_client_get_response_tools() -> None:
|
|
"""Test Foundry Chat Client response with tools."""
|
|
async with FoundryChatClient(async_ad_credential=DefaultAzureCredential()) as foundry_chat_client:
|
|
assert isinstance(foundry_chat_client, ChatClient)
|
|
|
|
messages: list[ChatMessage] = []
|
|
messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?"))
|
|
|
|
# Test that the client can be used to get a response
|
|
response = await foundry_chat_client.get_response(
|
|
messages=messages,
|
|
tools=[get_weather],
|
|
tool_choice="auto",
|
|
)
|
|
|
|
assert response is not None
|
|
assert isinstance(response, ChatResponse)
|
|
assert any(word in response.text.lower() for word in ["sunny", "25"])
|
|
|
|
|
|
@skip_if_foundry_integration_tests_disabled
|
|
async def test_foundry_chat_client_streaming() -> None:
|
|
"""Test Foundry Chat Client streaming response."""
|
|
async with FoundryChatClient(async_ad_credential=DefaultAzureCredential()) as foundry_chat_client:
|
|
assert isinstance(foundry_chat_client, ChatClient)
|
|
|
|
messages: list[ChatMessage] = []
|
|
messages.append(
|
|
ChatMessage(
|
|
role="user",
|
|
text="The weather in Seattle is currently sunny with a high of 25°C. "
|
|
"It's a beautiful day for outdoor activities.",
|
|
)
|
|
)
|
|
messages.append(ChatMessage(role="user", text="What's the weather like today?"))
|
|
|
|
# Test that the client can be used to get a response
|
|
response = foundry_chat_client.get_streaming_response(messages=messages)
|
|
|
|
full_message: str = ""
|
|
async for chunk in response:
|
|
assert chunk is not None
|
|
assert isinstance(chunk, ChatResponseUpdate)
|
|
for content in chunk.contents:
|
|
if isinstance(content, TextContent) and content.text:
|
|
full_message += content.text
|
|
|
|
assert any(word in full_message.lower() for word in ["sunny", "25"])
|
|
|
|
|
|
@skip_if_foundry_integration_tests_disabled
|
|
async def test_foundry_chat_client_streaming_tools() -> None:
|
|
"""Test Foundry Chat Client streaming response with tools."""
|
|
async with FoundryChatClient(async_ad_credential=DefaultAzureCredential()) as foundry_chat_client:
|
|
assert isinstance(foundry_chat_client, ChatClient)
|
|
|
|
messages: list[ChatMessage] = []
|
|
messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?"))
|
|
|
|
# Test that the client can be used to get a response
|
|
response = foundry_chat_client.get_streaming_response(
|
|
messages=messages,
|
|
tools=[get_weather],
|
|
tool_choice="auto",
|
|
)
|
|
full_message: str = ""
|
|
async for chunk in response:
|
|
assert chunk is not None
|
|
assert isinstance(chunk, ChatResponseUpdate)
|
|
for content in chunk.contents:
|
|
if isinstance(content, TextContent) and content.text:
|
|
full_message += content.text
|
|
|
|
assert any(word in full_message.lower() for word in ["sunny", "25"])
|