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:
Eduard van Valkenburg
2025-09-30 19:53:46 +00:00
committed by GitHub
parent 3eb26632ce
commit 54ad135914
84 changed files with 2302 additions and 1957 deletions
@@ -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."""