mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [BREAKING] added SerializationMixin and applied to contents, agents, chat client… (#1012)
* added SerializationMixin and applied to contents, agents, chat clients, removed AFBaseModel * fix annotations type * mypy fixes * fix tests * fix serializable subvalues and added large docstring * updated indents in code block * fixed exported urls
This commit is contained in:
@@ -84,7 +84,7 @@ from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import ConnectionType
|
||||
from azure.core.credentials_async import AsyncTokenCredential
|
||||
from azure.core.exceptions import HttpResponseError, ResourceNotFoundError
|
||||
from pydantic import BaseModel, Field, PrivateAttr, ValidationError
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import Self # pragma: no cover
|
||||
@@ -128,14 +128,6 @@ class AzureAIAgentClient(BaseChatClient):
|
||||
"""Azure AI Agent Chat client."""
|
||||
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai" # type: ignore[reportIncompatibleVariableOverride, misc]
|
||||
project_client: AIProjectClient = Field(...)
|
||||
credential: AsyncTokenCredential | None = Field(...)
|
||||
agent_id: str | None = Field(default=None)
|
||||
agent_name: str | None = Field(default=None)
|
||||
ai_model_id: str | None = Field(default=None)
|
||||
thread_id: str | None = Field(default=None)
|
||||
_should_delete_agent: bool = PrivateAttr(default=False) # Track whether we should delete the agent
|
||||
_should_close_client: bool = PrivateAttr(default=False) # Track whether we should close client connection
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -205,16 +197,18 @@ class AzureAIAgentClient(BaseChatClient):
|
||||
)
|
||||
should_close_client = True
|
||||
|
||||
super().__init__(
|
||||
project_client=project_client, # type: ignore[reportCallIssue]
|
||||
credential=async_credential, # type: ignore[reportCallIssue]
|
||||
agent_id=agent_id, # type: ignore[reportCallIssue]
|
||||
thread_id=thread_id, # type: ignore[reportCallIssue]
|
||||
agent_name=agent_name, # type: ignore[reportCallIssue]
|
||||
ai_model_id=azure_ai_settings.model_deployment_name, # type: ignore[reportCallIssue]
|
||||
**kwargs,
|
||||
)
|
||||
self._should_close_client = should_close_client
|
||||
# Initialize parent
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Initialize instance variables
|
||||
self.project_client = project_client
|
||||
self.credential = async_credential
|
||||
self.agent_id = agent_id
|
||||
self.agent_name = agent_name
|
||||
self.model_id = azure_ai_settings.model_deployment_name
|
||||
self.thread_id = thread_id
|
||||
self._should_delete_agent = False # Track whether we should delete the agent
|
||||
self._should_close_client = should_close_client # Track whether we should close client connection
|
||||
|
||||
async def setup_azure_ai_observability(self, enable_sensitive_data: bool | None = None) -> None:
|
||||
"""Use this method to setup tracing in your Azure AI Project.
|
||||
@@ -251,7 +245,7 @@ class AzureAIAgentClient(BaseChatClient):
|
||||
await self._close_client_if_needed()
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: type[TAzureAIAgentClient], settings: dict[str, Any]) -> TAzureAIAgentClient:
|
||||
def from_settings(cls: type[TAzureAIAgentClient], settings: dict[str, Any]) -> TAzureAIAgentClient:
|
||||
"""Initialize a AzureAIAgentClient from a dictionary of settings.
|
||||
|
||||
Args:
|
||||
@@ -317,11 +311,11 @@ class AzureAIAgentClient(BaseChatClient):
|
||||
"""
|
||||
# If no agent_id is provided, create a temporary agent
|
||||
if self.agent_id is None:
|
||||
if not self.ai_model_id:
|
||||
if not self.model_id:
|
||||
raise ServiceInitializationError("Model deployment name is required for agent creation.")
|
||||
|
||||
agent_name: str = self.agent_name or "UnnamedAgent"
|
||||
args: dict[str, Any] = {"model": self.ai_model_id, "name": agent_name}
|
||||
args: dict[str, Any] = {"model": self.model_id, "name": agent_name}
|
||||
if run_options:
|
||||
if "tools" in run_options:
|
||||
args["tools"] = run_options["tools"]
|
||||
@@ -866,8 +860,8 @@ class AzureAIAgentClient(BaseChatClient):
|
||||
if isinstance(content, FunctionResultContent):
|
||||
if tool_outputs is None:
|
||||
tool_outputs = []
|
||||
result_contents: list[Any] = ( # type: ignore
|
||||
content.result if isinstance(content.result, list) else [content.result] # type: ignore
|
||||
result_contents: list[Any] = (
|
||||
content.result if isinstance(content.result, list) else [content.result]
|
||||
)
|
||||
results: list[Any] = []
|
||||
for item in result_contents:
|
||||
|
||||
@@ -50,17 +50,26 @@ def create_test_azure_ai_chat_client(
|
||||
azure_ai_settings: AzureAISettings | None = None,
|
||||
should_delete_agent: bool = False,
|
||||
) -> AzureAIAgentClient:
|
||||
"""Helper function to create AzureAIAgentClient instances for testing, bypassing Pydantic validation."""
|
||||
"""Helper function to create AzureAIAgentClient instances for testing, bypassing normal validation."""
|
||||
if azure_ai_settings is None:
|
||||
azure_ai_settings = AzureAISettings(env_file_path="test.env")
|
||||
|
||||
return AzureAIAgentClient.model_construct(
|
||||
project_client=mock_ai_project_client,
|
||||
agent_id=agent_id,
|
||||
thread_id=thread_id,
|
||||
_should_delete_agent=should_delete_agent,
|
||||
ai_model_id=azure_ai_settings.model_deployment_name,
|
||||
)
|
||||
# Create client instance directly
|
||||
client = object.__new__(AzureAIAgentClient)
|
||||
|
||||
# Set attributes directly
|
||||
client.project_client = mock_ai_project_client
|
||||
client.credential = None
|
||||
client.agent_id = agent_id
|
||||
client.agent_name = None
|
||||
client.model_id = azure_ai_settings.model_deployment_name
|
||||
client.thread_id = thread_id
|
||||
client._should_delete_agent = should_delete_agent
|
||||
client._should_close_client = False
|
||||
client.additional_properties = {}
|
||||
client.middleware = None
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def test_azure_ai_settings_init(azure_ai_unit_test_env: dict[str, str]) -> None:
|
||||
@@ -101,14 +110,19 @@ def test_azure_ai_chat_client_init_auto_create_client(
|
||||
) -> None:
|
||||
"""Test AzureAIAgentClient initialization with auto-created project_client."""
|
||||
azure_ai_settings = AzureAISettings(**azure_ai_unit_test_env) # type: ignore
|
||||
chat_client = AzureAIAgentClient.model_construct(
|
||||
project_client=mock_ai_project_client,
|
||||
agent_id=None,
|
||||
thread_id=None,
|
||||
_should_delete_agent=False,
|
||||
_azure_ai_settings=azure_ai_settings,
|
||||
credential=None,
|
||||
)
|
||||
|
||||
# Create client instance directly
|
||||
chat_client = object.__new__(AzureAIAgentClient)
|
||||
chat_client.project_client = mock_ai_project_client
|
||||
chat_client.agent_id = None
|
||||
chat_client.thread_id = None
|
||||
chat_client._should_delete_agent = False
|
||||
chat_client._should_close_client = False
|
||||
chat_client.credential = None
|
||||
chat_client.model_id = azure_ai_settings.model_deployment_name
|
||||
chat_client.agent_name = None
|
||||
chat_client.additional_properties = {}
|
||||
chat_client.middleware = None
|
||||
|
||||
assert chat_client.project_client is mock_ai_project_client
|
||||
assert chat_client.agent_id is None
|
||||
@@ -169,7 +183,6 @@ def test_azure_ai_chat_client_from_dict(mock_ai_project_client: MagicMock) -> No
|
||||
azure_ai_settings = AzureAISettings(
|
||||
project_endpoint=settings["project_endpoint"],
|
||||
model_deployment_name=settings["model_deployment_name"],
|
||||
agent_name=settings["agent_name"],
|
||||
)
|
||||
|
||||
chat_client: AzureAIAgentClient = create_test_azure_ai_chat_client(
|
||||
@@ -229,9 +242,7 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_create_new(
|
||||
azure_ai_unit_test_env: dict[str, str],
|
||||
) -> None:
|
||||
"""Test _get_agent_id_or_create when creating a new agent."""
|
||||
azure_ai_settings = AzureAISettings(
|
||||
model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"], agent_name="TestAgent"
|
||||
)
|
||||
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)
|
||||
|
||||
agent_id = await chat_client._get_agent_id_or_create() # type: ignore
|
||||
@@ -519,9 +530,7 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_with_run_options(
|
||||
mock_ai_project_client: MagicMock, azure_ai_unit_test_env: dict[str, str]
|
||||
) -> None:
|
||||
"""Test _get_agent_id_or_create with run_options containing tools and instructions."""
|
||||
azure_ai_settings = AzureAISettings(
|
||||
model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"], agent_name="TestAgent"
|
||||
)
|
||||
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)
|
||||
|
||||
run_options = {
|
||||
@@ -618,6 +627,7 @@ def get_weather(
|
||||
return f"The weather in {location} is sunny with a high of 25°C."
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_azure_ai_chat_client_get_response() -> None:
|
||||
"""Test Azure AI Chat Client response."""
|
||||
@@ -642,6 +652,7 @@ async def test_azure_ai_chat_client_get_response() -> None:
|
||||
assert any(word in response.text.lower() for word in ["sunny", "25"])
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_azure_ai_chat_client_get_response_tools() -> None:
|
||||
"""Test Azure AI Chat Client response with tools."""
|
||||
@@ -663,6 +674,7 @@ async def test_azure_ai_chat_client_get_response_tools() -> None:
|
||||
assert any(word in response.text.lower() for word in ["sunny", "25"])
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_azure_ai_chat_client_streaming() -> None:
|
||||
"""Test Azure AI Chat Client streaming response."""
|
||||
@@ -693,6 +705,7 @@ async def test_azure_ai_chat_client_streaming() -> None:
|
||||
assert any(word in full_message.lower() for word in ["sunny", "25"])
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_azure_ai_chat_client_streaming_tools() -> None:
|
||||
"""Test Azure AI Chat Client streaming response with tools."""
|
||||
@@ -719,6 +732,7 @@ async def test_azure_ai_chat_client_streaming_tools() -> None:
|
||||
assert any(word in full_message.lower() for word in ["sunny", "25"])
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_azure_ai_chat_client_agent_basic_run() -> None:
|
||||
"""Test ChatAgent basic run functionality with AzureAIAgentClient."""
|
||||
@@ -735,6 +749,7 @@ async def test_azure_ai_chat_client_agent_basic_run() -> None:
|
||||
assert "Hello World" in response.text
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_azure_ai_chat_client_agent_basic_run_streaming() -> None:
|
||||
"""Test ChatAgent basic streaming functionality with AzureAIAgentClient."""
|
||||
@@ -754,6 +769,7 @@ async def test_azure_ai_chat_client_agent_basic_run_streaming() -> None:
|
||||
assert "streaming response test" in full_message.lower()
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_azure_ai_chat_client_agent_thread_persistence() -> None:
|
||||
"""Test ChatAgent thread persistence across runs with AzureAIAgentClient."""
|
||||
@@ -779,6 +795,7 @@ async def test_azure_ai_chat_client_agent_thread_persistence() -> None:
|
||||
assert "42" in second_response.text
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_azure_ai_chat_client_agent_existing_thread_id() -> None:
|
||||
"""Test ChatAgent existing thread ID functionality with AzureAIAgentClient."""
|
||||
@@ -816,6 +833,7 @@ async def test_azure_ai_chat_client_agent_existing_thread_id() -> None:
|
||||
assert "alice" in response2.text.lower()
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_azure_ai_chat_client_agent_code_interpreter():
|
||||
"""Test ChatAgent with code interpreter through AzureAIAgentClient."""
|
||||
@@ -835,6 +853,7 @@ async def test_azure_ai_chat_client_agent_code_interpreter():
|
||||
assert "120" in response.text or "factorial" in response.text.lower()
|
||||
|
||||
|
||||
@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."""
|
||||
@@ -856,6 +875,7 @@ async def test_azure_ai_chat_client_agent_with_mcp_tools() -> None:
|
||||
assert any(term in response.text.lower() for term in ["app service", "azure", "web", "application"])
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
@skip_if_azure_ai_integration_tests_disabled
|
||||
async def test_azure_ai_chat_client_agent_level_tool_persistence():
|
||||
"""Test that agent-level tools persist across multiple runs with AzureAIAgentClient."""
|
||||
|
||||
Reference in New Issue
Block a user