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
@@ -40,17 +40,20 @@ def create_test_azure_assistants_client(
thread_id: str | None = None,
should_delete_assistant: bool = False,
) -> AzureOpenAIAssistantsClient:
"""Helper function to create AzureOpenAIAssistantsClient instances for testing, bypassing Pydantic validation."""
return AzureOpenAIAssistantsClient.model_construct(
ai_model_id=deployment_name or "test_chat_deployment",
"""Helper function to create AzureOpenAIAssistantsClient instances for testing."""
client = AzureOpenAIAssistantsClient(
deployment_name=deployment_name or "test_chat_deployment",
assistant_id=assistant_id,
assistant_name=assistant_name,
thread_id=thread_id,
api_key="test-api-key",
endpoint="https://test-endpoint.com",
client=mock_async_azure_openai,
_should_delete_assistant=should_delete_assistant,
async_client=mock_async_azure_openai,
)
# Set the _should_delete_assistant flag directly if needed
if should_delete_assistant:
object.__setattr__(client, "_should_delete_assistant", True)
return client
@pytest.fixture
@@ -88,7 +91,7 @@ def test_azure_assistants_client_init_with_client(mock_async_azure_openai: Magic
)
assert chat_client.client is mock_async_azure_openai
assert chat_client.ai_model_id == "test_chat_deployment"
assert chat_client.model_id == "test_chat_deployment"
assert chat_client.assistant_id == "existing-assistant-id"
assert chat_client.thread_id == "test-thread-id"
assert not chat_client._should_delete_assistant # type: ignore
@@ -100,19 +103,16 @@ def test_azure_assistants_client_init_auto_create_client(
mock_async_azure_openai: MagicMock,
) -> None:
"""Test AzureOpenAIAssistantsClient initialization with auto-created client."""
chat_client = AzureOpenAIAssistantsClient.model_construct(
ai_model_id=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
assistant_id=None,
chat_client = AzureOpenAIAssistantsClient(
deployment_name=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
assistant_name="TestAssistant",
thread_id=None,
api_key=azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"],
endpoint=azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"],
client=mock_async_azure_openai,
_should_delete_assistant=False,
async_client=mock_async_azure_openai,
)
assert chat_client.client is mock_async_azure_openai
assert chat_client.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
assert chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
assert chat_client.assistant_id is None
assert chat_client.assistant_name == "TestAssistant"
assert not chat_client._should_delete_assistant # type: ignore
@@ -145,7 +145,7 @@ def test_azure_assistants_client_init_with_default_headers(azure_openai_unit_tes
default_headers=default_headers,
)
assert chat_client.ai_model_id == "test_chat_deployment"
assert chat_client.model_id == "test_chat_deployment"
assert isinstance(chat_client, ChatClientProtocol)
# Assert that the default header we added is present in the client's default headers
@@ -241,11 +241,10 @@ def test_azure_assistants_client_serialize(azure_openai_unit_test_env: dict[str,
dumped_settings = chat_client.to_dict()
assert dumped_settings["ai_model_id"] == "test_chat_deployment"
assert dumped_settings["model_id"] == "test_chat_deployment"
assert dumped_settings["assistant_id"] == "test-assistant-id"
assert dumped_settings["assistant_name"] == "TestAssistant"
assert dumped_settings["thread_id"] == "test-thread-id"
assert dumped_settings["api_key"] == azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"]
# Assert that the default header we added is present in the dumped_settings default headers
for key, value in default_headers.items():
@@ -262,6 +261,7 @@ def get_weather(
return f"The weather in {location} is sunny with a high of 25°C."
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_assistants_client_get_response() -> None:
"""Test Azure Assistants Client response."""
@@ -286,6 +286,7 @@ async def test_azure_assistants_client_get_response() -> None:
assert any(word in response.text.lower() for word in ["sunny", "25", "weather", "seattle"])
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_assistants_client_get_response_tools() -> None:
"""Test Azure Assistants Client response with tools."""
@@ -307,6 +308,7 @@ async def test_azure_assistants_client_get_response_tools() -> None:
assert any(word in response.text.lower() for word in ["sunny", "25", "weather"])
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_assistants_client_streaming() -> None:
"""Test Azure Assistants Client streaming response."""
@@ -337,6 +339,7 @@ async def test_azure_assistants_client_streaming() -> None:
assert any(word in full_message.lower() for word in ["sunny", "25", "weather", "seattle"])
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_assistants_client_streaming_tools() -> None:
"""Test Azure Assistants Client streaming response with tools."""
@@ -363,6 +366,7 @@ async def test_azure_assistants_client_streaming_tools() -> None:
assert any(word in full_message.lower() for word in ["sunny", "25", "weather"])
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_assistants_client_with_existing_assistant() -> None:
"""Test Azure Assistants Client with existing assistant ID."""
@@ -390,6 +394,7 @@ async def test_azure_assistants_client_with_existing_assistant() -> None:
assert len(response.text) > 0
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_assistants_agent_basic_run():
"""Test ChatAgent basic run functionality with AzureOpenAIAssistantsClient."""
@@ -406,6 +411,7 @@ async def test_azure_assistants_agent_basic_run():
assert "Hello World" in response.text
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_assistants_agent_basic_run_streaming():
"""Test ChatAgent basic streaming functionality with AzureOpenAIAssistantsClient."""
@@ -425,6 +431,7 @@ async def test_azure_assistants_agent_basic_run_streaming():
assert "streaming response test" in full_message.lower()
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_assistants_agent_thread_persistence():
"""Test ChatAgent thread persistence across runs with AzureOpenAIAssistantsClient."""
@@ -453,6 +460,7 @@ async def test_azure_assistants_agent_thread_persistence():
assert thread.service_thread_id is not None
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_assistants_agent_existing_thread_id():
"""Test ChatAgent with existing thread ID to continue conversations across agent instances."""
@@ -497,6 +505,7 @@ async def test_azure_assistants_agent_existing_thread_id():
assert "paris" in response2.text.lower()
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_assistants_agent_code_interpreter():
"""Test ChatAgent with code interpreter through AzureOpenAIAssistantsClient."""
@@ -516,6 +525,7 @@ async def test_azure_assistants_agent_code_interpreter():
assert "120" in response.text or "factorial" in response.text.lower()
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_assistants_client_agent_level_tool_persistence():
"""Test that agent-level tools persist across multiple runs with Azure Assistants Client."""
@@ -53,7 +53,7 @@ def test_init(azure_openai_unit_test_env: dict[str, str]) -> None:
assert azure_chat_client.client is not None
assert isinstance(azure_chat_client.client, AsyncAzureOpenAI)
assert azure_chat_client.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
assert azure_chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
assert isinstance(azure_chat_client, BaseChatClient)
@@ -76,7 +76,7 @@ def test_init_base_url(azure_openai_unit_test_env: dict[str, str]) -> None:
assert azure_chat_client.client is not None
assert isinstance(azure_chat_client.client, AsyncAzureOpenAI)
assert azure_chat_client.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
assert azure_chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
assert isinstance(azure_chat_client, BaseChatClient)
for key, value in default_headers.items():
assert key in azure_chat_client.client.default_headers
@@ -89,7 +89,7 @@ def test_init_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None:
assert azure_chat_client.client is not None
assert isinstance(azure_chat_client.client, AsyncAzureOpenAI)
assert azure_chat_client.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
assert azure_chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]
assert isinstance(azure_chat_client, BaseChatClient)
@@ -130,10 +130,11 @@ def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None:
azure_chat_client = AzureOpenAIChatClient.from_dict(settings)
dumped_settings = azure_chat_client.to_dict()
assert dumped_settings["ai_model_id"] == settings["deployment_name"]
assert str(settings["deployment_name"]) in str(dumped_settings["base_url"])
assert settings["api_key"] == dumped_settings["api_key"]
assert dumped_settings["model_id"] == settings["deployment_name"]
assert str(settings["endpoint"]) in str(dumped_settings["endpoint"])
assert str(settings["deployment_name"]) == str(dumped_settings["deployment_name"])
assert settings["api_version"] == dumped_settings["api_version"]
assert "api_key" not in dumped_settings
# Assert that the default header we added is present in the dumped_settings default headers
for key, value in default_headers.items():
@@ -608,6 +609,7 @@ def get_weather(location: str) -> str:
return f"The weather in {location} is sunny and 72°F."
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_openai_chat_client_response() -> None:
"""Test Azure OpenAI chat completion responses."""
@@ -637,6 +639,7 @@ async def test_azure_openai_chat_client_response() -> None:
)
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_openai_chat_client_response_tools() -> None:
"""Test AzureOpenAI chat completion responses."""
@@ -658,6 +661,7 @@ async def test_azure_openai_chat_client_response_tools() -> None:
assert "scientists" in response.text
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_openai_chat_client_streaming() -> None:
"""Test Azure OpenAI chat completion responses."""
@@ -692,6 +696,7 @@ async def test_azure_openai_chat_client_streaming() -> None:
assert "scientists" in full_message
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_openai_chat_client_streaming_tools() -> None:
"""Test AzureOpenAI chat completion responses."""
@@ -718,6 +723,7 @@ async def test_azure_openai_chat_client_streaming_tools() -> None:
assert "scientists" in full_message
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_openai_chat_client_agent_basic_run():
"""Test Azure OpenAI chat client agent basic run functionality with AzureOpenAIChatClient."""
@@ -733,6 +739,7 @@ async def test_azure_openai_chat_client_agent_basic_run():
assert "hello world" in response.text.lower()
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_openai_chat_client_agent_basic_run_streaming():
"""Test Azure OpenAI chat client agent basic streaming functionality with AzureOpenAIChatClient."""
@@ -750,6 +757,7 @@ async def test_azure_openai_chat_client_agent_basic_run_streaming():
assert "streaming response test" in full_text.lower()
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_openai_chat_client_agent_thread_persistence():
"""Test Azure OpenAI chat client agent thread persistence across runs with AzureOpenAIChatClient."""
@@ -774,6 +782,7 @@ async def test_azure_openai_chat_client_agent_thread_persistence():
assert "alice" in response2.text.lower()
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_openai_chat_client_agent_existing_thread():
"""Test Azure OpenAI chat client agent with existing thread to continue conversations across agent instances."""
@@ -808,6 +817,7 @@ async def test_azure_openai_chat_client_agent_existing_thread():
assert "alice" in second_response.text.lower()
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_chat_client_agent_level_tool_persistence():
"""Test that agent-level tools persist across multiple runs with Azure Chat Client."""
@@ -76,7 +76,7 @@ def test_init(azure_openai_unit_test_env: dict[str, str]) -> None:
# Test successful initialization
azure_responses_client = AzureOpenAIResponsesClient()
assert azure_responses_client.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]
assert azure_responses_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]
assert isinstance(azure_responses_client, ChatClientProtocol)
@@ -86,12 +86,12 @@ def test_init_validation_fail() -> None:
AzureOpenAIResponsesClient(api_key="34523", deployment_name={"test": "dict"}) # type: ignore
def test_init_ai_model_id_constructor(azure_openai_unit_test_env: dict[str, str]) -> None:
def test_init_model_id_constructor(azure_openai_unit_test_env: dict[str, str]) -> None:
# Test successful initialization
ai_model_id = "test_model_id"
azure_responses_client = AzureOpenAIResponsesClient(deployment_name=ai_model_id)
model_id = "test_model_id"
azure_responses_client = AzureOpenAIResponsesClient(deployment_name=model_id)
assert azure_responses_client.ai_model_id == ai_model_id
assert azure_responses_client.model_id == model_id
assert isinstance(azure_responses_client, ChatClientProtocol)
@@ -103,7 +103,7 @@ def test_init_with_default_header(azure_openai_unit_test_env: dict[str, str]) ->
default_headers=default_headers,
)
assert azure_responses_client.ai_model_id == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]
assert azure_responses_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]
assert isinstance(azure_responses_client, ChatClientProtocol)
# Assert that the default header we added is present in the client's default headers
@@ -124,15 +124,15 @@ def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None:
default_headers = {"X-Unit-Test": "test-guid"}
settings = {
"ai_model_id": azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"],
"deployment_name": azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"],
"api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"],
"default_headers": default_headers,
}
azure_responses_client = AzureOpenAIResponsesClient.from_dict(settings)
dumped_settings = azure_responses_client.to_dict()
assert dumped_settings["ai_model_id"] == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]
assert dumped_settings["api_key"] == azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"]
assert dumped_settings["deployment_name"] == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]
assert "api_key" not in dumped_settings
# Assert that the default header we added is present in the dumped_settings default headers
for key, value in default_headers.items():
assert key in dumped_settings["default_headers"]
@@ -141,6 +141,7 @@ def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None:
assert "User-Agent" not in dumped_settings["default_headers"]
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_response() -> None:
"""Test azure responses client responses."""
@@ -184,6 +185,7 @@ async def test_azure_responses_client_response() -> None:
assert "sunny" in structured_response.value.weather.lower()
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_response_tools() -> None:
"""Test azure responses client tools."""
@@ -223,6 +225,7 @@ async def test_azure_responses_client_response_tools() -> None:
assert "sunny" in structured_response.value.weather.lower()
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_streaming() -> None:
"""Test Azure azure responses client streaming responses."""
@@ -273,6 +276,7 @@ async def test_azure_responses_client_streaming() -> None:
assert "sunny" in structured_response.value.weather.lower()
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_streaming_tools() -> None:
"""Test azure responses client streaming tools."""
@@ -320,6 +324,7 @@ async def test_azure_responses_client_streaming_tools() -> None:
assert "sunny" in output.weather.lower()
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_agent_basic_run():
"""Test Azure Responses Client agent basic run functionality with AzureOpenAIResponsesClient."""
@@ -336,6 +341,7 @@ async def test_azure_responses_client_agent_basic_run():
assert "hello world" in response.text.lower()
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_agent_basic_run_streaming():
"""Test Azure Responses Client agent basic streaming functionality with AzureOpenAIResponsesClient."""
@@ -353,6 +359,7 @@ async def test_azure_responses_client_agent_basic_run_streaming():
assert "streaming response test" in full_text.lower()
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_agent_thread_persistence():
"""Test Azure Responses Client agent thread persistence across runs with AzureOpenAIResponsesClient."""
@@ -376,6 +383,7 @@ async def test_azure_responses_client_agent_thread_persistence():
assert second_response.text is not None
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_agent_thread_storage_with_store_true():
"""Test Azure Responses Client agent with store=True to verify service_thread_id is returned."""
@@ -407,6 +415,7 @@ async def test_azure_responses_client_agent_thread_storage_with_store_true():
assert len(thread.service_thread_id) > 0
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_agent_existing_thread():
"""Test Azure Responses Client agent with existing thread to continue conversations across agent instances."""
@@ -441,6 +450,7 @@ async def test_azure_responses_client_agent_existing_thread():
assert "photography" in second_response.text.lower()
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_agent_hosted_code_interpreter_tool():
"""Test Azure Responses Client agent with HostedCodeInterpreterTool through AzureOpenAIResponsesClient."""
@@ -462,6 +472,7 @@ async def test_azure_responses_client_agent_hosted_code_interpreter_tool():
assert contains_relevant_content or len(response.text.strip()) > 10
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_agent_level_tool_persistence():
"""Test that agent-level tools persist across multiple runs with Azure Responses Client."""
@@ -488,6 +499,7 @@ async def test_azure_responses_client_agent_level_tool_persistence():
assert any(term in second_response.text.lower() for term in ["miami", "sunny", "72"])
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_agent_chat_options_run_level() -> None:
"""Integration test for comprehensive ChatOptions parameter coverage with Azure Response Agent."""
@@ -511,6 +523,7 @@ async def test_azure_responses_client_agent_chat_options_run_level() -> None:
assert len(response.text) > 0
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_agent_chat_options_agent_level() -> None:
"""Integration test for comprehensive ChatOptions parameter coverage with Azure Response Agent."""
@@ -534,6 +547,7 @@ async def test_azure_responses_client_agent_chat_options_agent_level() -> None:
assert len(response.text) > 0
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
async def test_azure_responses_client_agent_hosted_mcp_tool() -> None:
"""Integration test for HostedMCPTool with Azure Response Agent using Microsoft Learn MCP."""
@@ -562,6 +576,7 @@ async def test_azure_responses_client_agent_hosted_mcp_tool() -> None:
assert any(term in response.text.lower() for term in ["azure", "storage", "account", "cli"])
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
@pytest.mark.skip(reason="File search requires API key auth, subscription only allows token auth")
async def test_azure_responses_client_file_search() -> None:
@@ -588,6 +603,7 @@ async def test_azure_responses_client_file_search() -> None:
assert "75" in response.text
@pytest.mark.flaky
@skip_if_azure_integration_tests_disabled
@pytest.mark.skip(reason="File search requires API key auth, subscription only allows token auth")
async def test_azure_responses_client_file_search_streaming() -> None: