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 21:53:46 +02:00
committed by GitHub
Unverified
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:
@@ -8,7 +8,7 @@ from typing import Any
from unittest.mock import patch
from uuid import uuid4
from pydantic import BaseModel, Field
from pydantic import BaseModel
from pytest import fixture
from agent_framework import (
@@ -116,9 +116,11 @@ class MockChatClient:
class MockBaseChatClient(BaseChatClient):
"""Mock implementation of the BaseChatClient."""
run_responses: list[ChatResponse] = Field(default_factory=list)
streaming_responses: list[list[ChatResponseUpdate]] = Field(default_factory=list)
call_count: int = Field(default=0)
def __init__(self, **kwargs: Any):
super().__init__(**kwargs)
self.run_responses: list[ChatResponse] = []
self.streaming_responses: list[list[ChatResponseUpdate]] = []
self.call_count: int = 0
@override
async def _inner_get_response(
@@ -510,6 +510,7 @@ def test_local_mcp_streamable_http_tool_init():
# Integration test
@pytest.mark.flaky
@skip_if_mcp_integration_tests_disabled
async def test_streamable_http_integration():
"""Test MCP StreamableHTTP integration."""
@@ -0,0 +1,190 @@
# Copyright (c) Microsoft. All rights reserved.
"""Tests for SerializationMixin functionality."""
import logging
from typing import Any
from agent_framework._serialization import SerializationMixin
class TestSerializationMixin:
"""Test SerializationMixin serialization, deserialization, and dependency injection."""
def test_basic_serialization(self):
"""Test basic to_dict and from_dict functionality."""
class TestClass(SerializationMixin):
def __init__(self, value: str, number: int):
self.value = value
self.number = number
obj = TestClass(value="test", number=42)
data = obj.to_dict()
assert data["type"] == "test_class"
assert data["value"] == "test"
assert data["number"] == 42
restored = TestClass.from_dict(data)
assert restored.value == "test"
assert restored.number == 42
def test_injectable_dependency_no_warning(self, caplog):
"""Test that injectable dependencies don't trigger debug logging."""
class TestClass(SerializationMixin):
INJECTABLE = {"client"}
def __init__(self, value: str, client: Any = None):
self.value = value
self.client = client
mock_client = "mock_client_instance"
with caplog.at_level(logging.DEBUG):
obj = TestClass.from_dict(
{"type": "test_class", "value": "test"},
dependencies={"test_class.client": mock_client},
)
assert obj.value == "test"
assert obj.client == mock_client
# No debug message should be logged for injectable dependency
assert not any("is not in INJECTABLE set" in record.message for record in caplog.records)
def test_non_injectable_dependency_logs_debug(self, caplog):
"""Test that non-injectable dependencies trigger debug logging."""
class TestClass(SerializationMixin):
INJECTABLE = {"client"}
def __init__(self, value: str, other: Any = None):
self.value = value
self.other = other
mock_other = "mock_other_instance"
with caplog.at_level(logging.DEBUG):
obj = TestClass.from_dict(
{"type": "test_class", "value": "test"},
dependencies={"test_class.other": mock_other},
)
assert obj.value == "test"
assert obj.other == mock_other
# Debug message should be logged for non-injectable dependency
debug_messages = [record.message for record in caplog.records if record.levelname == "DEBUG"]
assert any("is not in INJECTABLE set" in msg for msg in debug_messages)
assert any("other" in msg for msg in debug_messages)
assert any("client" in msg for msg in debug_messages) # Should mention available injectable
def test_multiple_dependencies_mixed_injectable(self, caplog):
"""Test with both injectable and non-injectable dependencies."""
class TestClass(SerializationMixin):
INJECTABLE = {"client", "logger"}
def __init__(
self,
value: str,
client: Any = None,
logger: Any = None,
other: Any = None,
):
self.value = value
self.client = client
self.logger = logger
self.other = other
mock_client = "mock_client"
mock_logger = "mock_logger"
mock_other = "mock_other"
with caplog.at_level(logging.DEBUG):
obj = TestClass.from_dict(
{"type": "test_class", "value": "test"},
dependencies={
"test_class.client": mock_client,
"test_class.logger": mock_logger,
"test_class.other": mock_other,
},
)
assert obj.value == "test"
assert obj.client == mock_client
assert obj.logger == mock_logger
assert obj.other == mock_other
# Only 'other' should trigger debug logging
debug_messages = [record.message for record in caplog.records if record.levelname == "DEBUG"]
assert any("other" in msg and "is not in INJECTABLE set" in msg for msg in debug_messages)
# 'client' and 'logger' should not be mentioned as non-injectable dependencies
assert not any("Dependency 'client'" in msg and "is not in INJECTABLE set" in msg for msg in debug_messages)
assert not any("Dependency 'logger'" in msg and "is not in INJECTABLE set" in msg for msg in debug_messages)
def test_no_injectable_set_defined(self, caplog):
"""Test behavior when INJECTABLE is not defined (empty set default)."""
class TestClass(SerializationMixin):
def __init__(self, value: str, client: Any = None):
self.value = value
self.client = client
mock_client = "mock_client"
with caplog.at_level(logging.DEBUG):
obj = TestClass.from_dict(
{"type": "test_class", "value": "test"},
dependencies={"test_class.client": mock_client},
)
assert obj.value == "test"
assert obj.client == mock_client
# Should log debug message since INJECTABLE is empty by default
debug_messages = [record.message for record in caplog.records if record.levelname == "DEBUG"]
assert any("client" in msg and "is not in INJECTABLE set" in msg for msg in debug_messages)
def test_default_exclude_serialization(self):
"""Test that DEFAULT_EXCLUDE fields are not included in to_dict()."""
class TestClass(SerializationMixin):
DEFAULT_EXCLUDE = {"secret"}
def __init__(self, value: str, secret: str):
self.value = value
self.secret = secret
obj = TestClass(value="test", secret="hidden")
data = obj.to_dict()
assert "value" in data
assert "secret" not in data
assert data["value"] == "test"
def test_roundtrip_with_injectable_dependency(self):
"""Test full roundtrip serialization/deserialization with injectable dependency."""
class TestClass(SerializationMixin):
INJECTABLE = {"client"}
DEFAULT_EXCLUDE = {"client"}
def __init__(self, value: str, number: int, client: Any = None):
self.value = value
self.number = number
self.client = client
mock_client = "mock_client"
obj = TestClass(value="test", number=42, client=mock_client)
# Serialize
data = obj.to_dict()
assert data["value"] == "test"
assert data["number"] == 42
assert "client" not in data # Excluded from serialization
# Deserialize with dependency injection
restored = TestClass.from_dict(data, dependencies={"test_class.client": mock_client})
assert restored.value == "test"
assert restored.number == 42
assert restored.client == mock_client
@@ -377,10 +377,10 @@ class TestThreadState:
def test_init_with_chat_message_store_state(self) -> None:
"""Test AgentThreadState initialization with chat_message_store_state."""
store_data: dict[str, Any] = {"messages": []}
state = AgentThreadState(chat_message_store_state=store_data)
state = AgentThreadState.model_validate({"chat_message_store_state": store_data})
assert state.service_thread_id is None
assert state.chat_message_store_state == store_data
assert state.chat_message_store_state.messages == []
def test_init_with_both(self) -> None:
"""Test AgentThreadState initialization with both parameters."""
@@ -3,6 +3,7 @@
from collections.abc import AsyncIterable
from typing import Any
import pytest
from pydantic import BaseModel, ValidationError
from pytest import fixture, mark, raises
@@ -10,7 +11,6 @@ from agent_framework import (
AgentRunResponse,
AgentRunResponseUpdate,
AIFunction,
BaseAnnotation,
BaseContent,
ChatMessage,
ChatOptions,
@@ -209,7 +209,7 @@ def test_hosted_file_content_minimal():
# Check the type and content
assert content.type == "hosted_file"
assert content.file_id == "file-456"
assert content.additional_properties is None
assert content.additional_properties == {}
assert content.raw_representation is None
# Ensure the instance is of type BaseContent
@@ -240,7 +240,7 @@ def test_hosted_vector_store_content_minimal():
# Check the type and content
assert content.type == "hosted_vector_store"
assert content.vector_store_id == "vs-101112"
assert content.additional_properties is None
assert content.additional_properties == {}
assert content.raw_representation is None
# Ensure the instance is of type BaseContent
@@ -1204,33 +1204,6 @@ def test_text_content_add_comprehensive_coverage():
assert result.raw_representation == ["raw1", "raw2", "raw3"]
def test_text_content_add_annotations_coverage():
"""Test TextContent __add__ method with annotation combinations to improve coverage."""
ann1 = BaseAnnotation()
ann2 = BaseAnnotation()
# Test first has annotations, second has None
t1 = TextContent("Hello", annotations=[ann1])
t2 = TextContent(" World", annotations=None)
result = t1 + t2
assert result.annotations == [ann1]
# Test first has None, second has annotations
t1 = TextContent("Hello", annotations=None)
t2 = TextContent(" World", annotations=[ann2])
result = t1 + t2
assert result.annotations == [ann2]
# Test both have annotations
t1 = TextContent("Hello", annotations=[ann1])
t2 = TextContent(" World", annotations=[ann2])
result = t1 + t2
assert len(result.annotations) == 2
assert ann1 in result.annotations
assert ann2 in result.annotations
def test_text_content_iadd_coverage():
"""Test TextContent __iadd__ method for better coverage."""
@@ -1277,7 +1250,7 @@ def test_comprehensive_to_dict_exclude_options():
text_content = TextContent("Hello", raw_representation=None, additional_properties={"prop": "val"})
text_dict = text_content.to_dict(exclude_none=True)
assert "raw_representation" not in text_dict
assert text_dict["additional_properties"] == {"prop": "val"}
assert text_dict["prop"] == "val"
# Test with custom exclude set
text_dict_exclude = text_content.to_dict(exclude={"additional_properties"})
@@ -1328,8 +1301,6 @@ def test_chat_message_from_dict_with_mixed_content():
{"type": "text", "text": "Hello"},
{"type": "function_call", "call_id": "call1", "name": "func", "arguments": {"arg": "val"}},
{"type": "function_result", "call_id": "call1", "result": "success"},
# Test with unknown type that falls back to BaseContent
{"type": "unknown_type", "raw_representation": "something"},
],
}
@@ -1383,7 +1354,7 @@ def test_comprehensive_serialization_methods():
text_data = {
"text": "Hello world",
"raw_representation": {"key": "value"},
"additional_properties": {"prop": "val"},
"prop": "val",
"annotations": None,
}
text_content = TextContent.from_dict(text_data)
@@ -1394,7 +1365,7 @@ def test_comprehensive_serialization_methods():
# Test round-trip
text_dict = text_content.to_dict()
assert text_dict["text"] == "Hello world"
assert text_dict["additional_properties"] == {"prop": "val"}
assert text_dict["prop"] == "val"
# Note: raw_representation is always excluded from to_dict() output
# Test with exclude_none
@@ -1464,18 +1435,19 @@ def test_usage_content_serialization_with_details():
# Test from_dict with details as dict
usage_data = {
"details": {"input_token_count": 10, "output_token_count": 20, "total_token_count": 30},
"annotations": [
{"type": "citation", "start": 0, "end": 5, "citation": "source1"},
{"type": "unknown", "custom_field": "value"}, # Tests fallback to BaseAnnotation
],
"type": "usage",
"details": {
"type": "usage_details",
"input_token_count": 10,
"output_token_count": 20,
"total_token_count": 30,
"custom_count": 5,
},
}
usage_content = UsageContent.from_dict(usage_data)
assert isinstance(usage_content.details, UsageDetails)
assert usage_content.details.input_token_count == 10
assert len(usage_content.annotations) == 2
assert isinstance(usage_content.annotations[0], CitationAnnotation)
assert isinstance(usage_content.annotations[1], BaseAnnotation)
assert usage_content.details.additional_counts["custom_count"] == 5
# Test to_dict with UsageDetails object
usage_dict = usage_content.to_dict()
@@ -1488,9 +1460,15 @@ def test_function_approval_response_content_serialization():
# Test from_dict with function_call as dict
response_data = {
"type": "function_approval_response",
"id": "response123",
"approved": True,
"function_call": {"call_id": "call123", "name": "test_func", "arguments": {"param": "value"}},
"function_call": {
"type": "function_call",
"call_id": "call123",
"name": "test_func",
"arguments": {"param": "value"},
},
}
response_content = FunctionApprovalResponseContent.from_dict(response_data)
assert isinstance(response_content.function_call, FunctionCallContent)
@@ -1512,7 +1490,12 @@ def test_chat_response_complex_serialization():
{"role": "assistant", "contents": [{"type": "text", "text": "Hi there"}]},
],
"finish_reason": {"value": "stop"},
"usage_details": {"input_token_count": 5, "output_token_count": 8, "total_token_count": 13},
"usage_details": {
"type": "usage_details",
"input_token_count": 5,
"output_token_count": 8,
"total_token_count": 13,
},
"model_id": "gpt-4", # Test alias handling
}
@@ -1543,22 +1526,21 @@ def test_chat_response_update_all_content_types():
{"type": "error", "error": "An error occurred"},
{"type": "function_call", "call_id": "call1", "name": "func", "arguments": {}},
{"type": "function_result", "call_id": "call1", "result": "success"},
{"type": "usage", "details": {"input_token_count": 1}},
{"type": "usage", "details": {"type": "usage_details", "input_token_count": 1}},
{"type": "hosted_file", "file_id": "file123"},
{"type": "hosted_vector_store", "vector_store_id": "vs123"},
{
"type": "function_approval_request",
"id": "req1",
"function_call": {"call_id": "call1", "name": "func", "arguments": {}},
"function_call": {"type": "function_call", "call_id": "call1", "name": "func", "arguments": {}},
},
{
"type": "function_approval_response",
"id": "resp1",
"approved": True,
"function_call": {"call_id": "call1", "name": "func", "arguments": {}},
"function_call": {"type": "function_call", "call_id": "call1", "name": "func", "arguments": {}},
},
{"type": "text_reasoning", "text": "reasoning"},
{"type": "unknown_type", "custom_field": "value"}, # Tests fallback
]
}
@@ -1586,7 +1568,12 @@ def test_agent_run_response_complex_serialization():
{"role": "user", "contents": [{"type": "text", "text": "Hello"}]},
{"role": "assistant", "contents": [{"type": "text", "text": "Hi"}]},
],
"usage_details": {"input_token_count": 3, "output_token_count": 2, "total_token_count": 5},
"usage_details": {
"type": "usage_details",
"input_token_count": 3,
"output_token_count": 2,
"total_token_count": 5,
},
}
response = AgentRunResponse.from_dict(response_data)
@@ -1612,22 +1599,21 @@ def test_agent_run_response_update_all_content_types():
{"type": "error", "error": "An error occurred"},
{"type": "function_call", "call_id": "call1", "name": "func", "arguments": {}},
{"type": "function_result", "call_id": "call1", "result": "success"},
{"type": "usage", "details": {"input_token_count": 1}},
{"type": "usage", "details": {"type": "usage_details", "input_token_count": 1}},
{"type": "hosted_file", "file_id": "file123"},
{"type": "hosted_vector_store", "vector_store_id": "vs123"},
{
"type": "function_approval_request",
"id": "req1",
"function_call": {"call_id": "call1", "name": "func", "arguments": {}},
"function_call": {"type": "function_call", "call_id": "call1", "name": "func", "arguments": {}},
},
{
"type": "function_approval_response",
"id": "resp1",
"approved": True,
"function_call": {"call_id": "call1", "name": "func", "arguments": {}},
"function_call": {"type": "function_call", "call_id": "call1", "name": "func", "arguments": {}},
},
{"type": "text_reasoning", "text": "reasoning"},
{"type": "unknown_type", "custom_field": "value"}, # Tests fallback
],
"role": {"value": "assistant"}, # Test role as dict
}
@@ -1648,3 +1634,394 @@ def test_agent_run_response_update_all_content_types():
update_str = AgentRunResponseUpdate.from_dict(update_data_str_role)
assert isinstance(update_str.role, Role)
assert update_str.role.value == "user"
# region Serialization
@mark.parametrize(
"content_class,init_kwargs",
[
pytest.param(
TextContent,
{
"type": "text",
"text": "Hello world",
"raw_representation": "raw",
},
id="text_content",
),
pytest.param(
TextReasoningContent,
{
"type": "text_reasoning",
"text": "Reasoning text",
"raw_representation": "raw",
},
id="text_reasoning_content",
),
pytest.param(
DataContent,
{
"type": "data",
"uri": "data:text/plain;base64,dGVzdCBkYXRh",
},
id="data_content_with_uri",
),
pytest.param(
DataContent,
{
"type": "data",
"data": b"test data",
"media_type": "text/plain",
},
id="data_content_with_bytes",
),
pytest.param(
UriContent,
{
"type": "uri",
"uri": "http://example.com",
"media_type": "text/html",
},
id="uri_content",
),
pytest.param(
HostedFileContent,
{"type": "hosted_file", "file_id": "file-123"},
id="hosted_file_content",
),
pytest.param(
HostedVectorStoreContent,
{
"type": "hosted_vector_store",
"vector_store_id": "vs-789",
},
id="hosted_vector_store_content",
),
pytest.param(
FunctionCallContent,
{
"type": "function_call",
"call_id": "call-1",
"name": "test_func",
"arguments": {"arg": "val"},
},
id="function_call_content",
),
pytest.param(
FunctionResultContent,
{
"type": "function_result",
"call_id": "call-1",
"result": "success",
},
id="function_result_content",
),
pytest.param(
ErrorContent,
{
"type": "error",
"message": "Error occurred",
"error_code": "E001",
},
id="error_content",
),
pytest.param(
UsageContent,
{
"type": "usage",
"details": {
"type": "usage_details",
"input_token_count": 10,
"output_token_count": 20,
"reasoning_tokens": 5,
},
},
id="usage_content",
),
pytest.param(
FunctionApprovalRequestContent,
{
"type": "function_approval_request",
"id": "req-1",
"function_call": {"type": "function_call", "call_id": "call-1", "name": "test_func", "arguments": {}},
},
id="function_approval_request",
),
pytest.param(
FunctionApprovalResponseContent,
{
"type": "function_approval_response",
"id": "resp-1",
"approved": True,
"function_call": {"type": "function_call", "call_id": "call-1", "name": "test_func", "arguments": {}},
},
id="function_approval_response",
),
pytest.param(
ChatMessage,
{
"role": {"type": "role", "value": "user"},
"contents": [
{"type": "text", "text": "Hello"},
{"type": "function_call", "call_id": "call-1", "name": "test_func", "arguments": {}},
],
"message_id": "msg-123",
"author_name": "User",
},
id="chat_message",
),
pytest.param(
ChatResponse,
{
"type": "chat_response",
"messages": [
{
"type": "chat_message",
"role": {"type": "role", "value": "user"},
"contents": [{"type": "text", "text": "Hello"}],
},
{
"type": "chat_message",
"role": {"type": "role", "value": "assistant"},
"contents": [{"type": "text", "text": "Hi there"}],
},
],
"finish_reason": {"type": "finish_reason", "value": "stop"},
"usage_details": {
"type": "usage_details",
"input_token_count": 10,
"output_token_count": 20,
"total_token_count": 30,
},
"response_id": "resp-123",
"model_id": "gpt-4",
},
id="chat_response",
),
pytest.param(
ChatResponseUpdate,
{
"contents": [
{"type": "text", "text": "Hello"},
{"type": "function_call", "call_id": "call-1", "name": "test_func", "arguments": {}},
],
"role": {"type": "role", "value": "assistant"},
"finish_reason": {"type": "finish_reason", "value": "stop"},
"message_id": "msg-123",
"response_id": "resp-123",
},
id="chat_response_update",
),
pytest.param(
AgentRunResponse,
{
"messages": [
{
"role": {"type": "role", "value": "user"},
"contents": [{"type": "text", "text": "Question"}],
},
{
"role": {"type": "role", "value": "assistant"},
"contents": [{"type": "text", "text": "Answer"}],
},
],
"response_id": "run-123",
"usage_details": {
"type": "usage_details",
"input_token_count": 5,
"output_token_count": 3,
"total_token_count": 8,
},
},
id="agent_run_response",
),
pytest.param(
AgentRunResponseUpdate,
{
"contents": [
{"type": "text", "text": "Streaming"},
{"type": "function_call", "call_id": "call-1", "name": "test_func", "arguments": {}},
],
"role": {"type": "role", "value": "assistant"},
"message_id": "msg-123",
"response_id": "run-123",
"author_name": "Agent",
},
id="agent_run_response_update",
),
],
)
def test_content_roundtrip_serialization(content_class: type[BaseContent], init_kwargs: dict[str, Any]):
"""Test to_dict/from_dict roundtrip for all content types."""
# Create instance
content = content_class(**init_kwargs)
# Serialize to dict
content_dict = content.to_dict()
# Verify type key is in serialized dict
assert "type" in content_dict
if hasattr(content, "type"):
assert content_dict["type"] == content.type # type: ignore[attr-defined]
# Deserialize from dict
reconstructed = content_class.from_dict(content_dict)
# Verify type
assert isinstance(reconstructed, content_class)
# Check type attribute dynamically
if hasattr(content, "type"):
assert reconstructed.type == content.type # type: ignore[attr-defined]
# Verify key attributes (excluding raw_representation which is not serialized)
for key, value in init_kwargs.items():
if key == "type":
continue
if key == "raw_representation":
# raw_representation is intentionally excluded from serialization
continue
# Special handling for DataContent created with 'data' parameter
if content_class == DataContent and key == "data":
# DataContent converts 'data' to 'uri', so we skip checking 'data' attribute
# Instead we verify that uri and media_type are set correctly
assert hasattr(reconstructed, "uri")
assert hasattr(reconstructed, "media_type")
assert reconstructed.media_type == init_kwargs.get("media_type")
# Verify the uri contains the encoded data
assert reconstructed.uri.startswith(f"data:{init_kwargs.get('media_type')};base64,")
continue
reconstructed_value = getattr(reconstructed, key)
# Special handling for nested SerializationMixin objects
if hasattr(value, "to_dict"):
# Compare the serialized forms
assert reconstructed_value.to_dict() == value.to_dict()
# Special handling for lists that may contain dicts converted to objects
elif isinstance(value, list) and value and isinstance(reconstructed_value, list):
# Check if this is a list of objects that were created from dicts
if isinstance(value[0], dict) and hasattr(reconstructed_value[0], "to_dict"):
# Compare each item by serializing the reconstructed object
assert len(reconstructed_value) == len(value)
else:
assert reconstructed_value == value
# Special handling for dicts that get converted to objects (like UsageDetails, FunctionCallContent)
elif isinstance(value, dict) and hasattr(reconstructed_value, "to_dict"):
# Compare the dict with the serialized form of the object, excluding 'type' key
reconstructed_dict = reconstructed_value.to_dict()
assert len(reconstructed_dict) == len(value)
else:
assert reconstructed_value == value
def test_text_content_with_annotations_serialization():
"""Test TextContent with CitationAnnotation and TextSpanRegion roundtrip serialization."""
# Create TextSpanRegion
region = TextSpanRegion(start_index=0, end_index=5)
# Create CitationAnnotation with region
citation = CitationAnnotation(
title="Test Citation",
url="http://example.com/citation",
file_id="file-123",
tool_name="test_tool",
snippet="This is a test snippet",
annotated_regions=[region],
additional_properties={"custom": "value"},
)
# Create TextContent with annotation
content = TextContent(
text="Hello world", annotations=[citation], additional_properties={"content_key": "content_val"}
)
# Serialize to dict
content_dict = content.to_dict()
# Verify structure
assert content_dict["type"] == "text"
assert content_dict["text"] == "Hello world"
assert content_dict["content_key"] == "content_val"
assert len(content_dict["annotations"]) == 1
# Verify annotation structure
annotation_dict = content_dict["annotations"][0]
assert annotation_dict["type"] == "citation"
assert annotation_dict["title"] == "Test Citation"
assert annotation_dict["url"] == "http://example.com/citation"
assert annotation_dict["file_id"] == "file-123"
assert annotation_dict["tool_name"] == "test_tool"
assert annotation_dict["snippet"] == "This is a test snippet"
assert annotation_dict["custom"] == "value"
# Verify region structure
assert len(annotation_dict["annotated_regions"]) == 1
region_dict = annotation_dict["annotated_regions"][0]
assert region_dict["type"] == "text_span"
assert region_dict["start_index"] == 0
assert region_dict["end_index"] == 5
# Deserialize from dict
reconstructed = TextContent.from_dict(content_dict)
# Verify reconstructed content
assert isinstance(reconstructed, TextContent)
assert reconstructed.text == "Hello world"
assert reconstructed.type == "text"
assert reconstructed.additional_properties == {"content_key": "content_val"}
# Verify reconstructed annotation
assert len(reconstructed.annotations) == 1 # type: ignore[arg-type]
recon_annotation = reconstructed.annotations[0] # type: ignore[index]
assert isinstance(recon_annotation, CitationAnnotation)
assert recon_annotation.title == "Test Citation"
assert recon_annotation.url == "http://example.com/citation"
assert recon_annotation.file_id == "file-123"
assert recon_annotation.tool_name == "test_tool"
assert recon_annotation.snippet == "This is a test snippet"
assert recon_annotation.additional_properties == {"custom": "value"}
# Verify reconstructed region
assert len(recon_annotation.annotated_regions) == 1 # type: ignore[arg-type]
recon_region = recon_annotation.annotated_regions[0] # type: ignore[index]
assert isinstance(recon_region, TextSpanRegion)
assert recon_region.start_index == 0
assert recon_region.end_index == 5
assert recon_region.type == "text_span"
def test_text_content_with_multiple_annotations_serialization():
"""Test TextContent with multiple annotations roundtrip serialization."""
# Create multiple regions
region1 = TextSpanRegion(start_index=0, end_index=5)
region2 = TextSpanRegion(start_index=6, end_index=11)
# Create multiple citations
citation1 = CitationAnnotation(title="Citation 1", url="http://example.com/1", annotated_regions=[region1])
citation2 = CitationAnnotation(title="Citation 2", url="http://example.com/2", annotated_regions=[region2])
# Create TextContent with multiple annotations
content = TextContent(text="Hello world", annotations=[citation1, citation2])
# Serialize
content_dict = content.to_dict()
# Verify we have 2 annotations
assert len(content_dict["annotations"]) == 2
assert content_dict["annotations"][0]["title"] == "Citation 1"
assert content_dict["annotations"][1]["title"] == "Citation 2"
# Deserialize
reconstructed = TextContent.from_dict(content_dict)
# Verify reconstruction
assert len(reconstructed.annotations) == 2
assert all(isinstance(ann, CitationAnnotation) for ann in reconstructed.annotations)
assert reconstructed.annotations[0].title == "Citation 1"
assert reconstructed.annotations[1].title == "Citation 2"
assert all(isinstance(ann.annotated_regions[0], TextSpanRegion) for ann in reconstructed.annotations)
@@ -46,23 +46,26 @@ skip_if_openai_integration_tests_disabled = pytest.mark.skipif(
def create_test_openai_assistants_client(
mock_async_openai: MagicMock,
ai_model_id: str | None = None,
model_id: str | None = None,
assistant_id: str | None = None,
assistant_name: str | None = None,
thread_id: str | None = None,
should_delete_assistant: bool = False,
) -> OpenAIAssistantsClient:
"""Helper function to create OpenAIAssistantsClient instances for testing, bypassing Pydantic validation."""
return OpenAIAssistantsClient.model_construct(
ai_model_id=ai_model_id or "gpt-4",
"""Helper function to create OpenAIAssistantsClient instances for testing."""
client = OpenAIAssistantsClient(
model_id=model_id or "gpt-4",
assistant_id=assistant_id,
assistant_name=assistant_name,
thread_id=thread_id,
api_key="test-api-key",
org_id="test-org-id",
client=mock_async_openai,
_should_delete_assistant=should_delete_assistant,
async_client=mock_async_openai,
)
# Set the _should_delete_assistant flag directly if needed
if should_delete_assistant:
object.__setattr__(client, "_should_delete_assistant", True)
return client
async def create_vector_store(client: OpenAIAssistantsClient) -> tuple[str, HostedVectorStoreContent]:
@@ -117,11 +120,11 @@ def mock_async_openai() -> MagicMock:
def test_openai_assistants_client_init_with_client(mock_async_openai: MagicMock) -> None:
"""Test OpenAIAssistantsClient initialization with existing client."""
chat_client = create_test_openai_assistants_client(
mock_async_openai, ai_model_id="gpt-4", assistant_id="existing-assistant-id", thread_id="test-thread-id"
mock_async_openai, model_id="gpt-4", assistant_id="existing-assistant-id", thread_id="test-thread-id"
)
assert chat_client.client is mock_async_openai
assert chat_client.ai_model_id == "gpt-4"
assert chat_client.model_id == "gpt-4"
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
@@ -133,19 +136,16 @@ def test_openai_assistants_client_init_auto_create_client(
mock_async_openai: MagicMock,
) -> None:
"""Test OpenAIAssistantsClient initialization with auto-created client."""
chat_client = OpenAIAssistantsClient.model_construct(
ai_model_id=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
assistant_id=None,
chat_client = OpenAIAssistantsClient(
model_id=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
assistant_name="TestAssistant",
thread_id=None,
api_key=openai_unit_test_env["OPENAI_API_KEY"],
org_id=openai_unit_test_env["OPENAI_ORG_ID"],
client=mock_async_openai,
_should_delete_assistant=False,
async_client=mock_async_openai,
)
assert chat_client.client is mock_async_openai
assert chat_client.ai_model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"]
assert chat_client.model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"]
assert chat_client.assistant_id is None
assert chat_client.assistant_name == "TestAssistant"
assert not chat_client._should_delete_assistant # type: ignore
@@ -155,7 +155,7 @@ def test_openai_assistants_client_init_validation_fail() -> None:
"""Test OpenAIAssistantsClient initialization with validation failure."""
with pytest.raises(ServiceInitializationError):
# Force failure by providing invalid model ID type - this should cause validation to fail
OpenAIAssistantsClient(ai_model_id=123, api_key="valid-key") # type: ignore
OpenAIAssistantsClient(model_id=123, api_key="valid-key") # type: ignore
@pytest.mark.parametrize("exclude_list", [["OPENAI_CHAT_MODEL_ID"]], indirect=True)
@@ -171,7 +171,7 @@ def test_openai_assistants_client_init_missing_model_id(openai_unit_test_env: di
def test_openai_assistants_client_init_missing_api_key(openai_unit_test_env: dict[str, str]) -> None:
"""Test OpenAIAssistantsClient initialization with missing API key."""
with pytest.raises(ServiceInitializationError):
OpenAIAssistantsClient(ai_model_id="gpt-4", env_file_path="nonexistent.env")
OpenAIAssistantsClient(model_id="gpt-4", env_file_path="nonexistent.env")
def test_openai_assistants_client_init_with_default_headers(openai_unit_test_env: dict[str, str]) -> None:
@@ -179,12 +179,12 @@ def test_openai_assistants_client_init_with_default_headers(openai_unit_test_env
default_headers = {"X-Unit-Test": "test-guid"}
chat_client = OpenAIAssistantsClient(
ai_model_id="gpt-4",
model_id="gpt-4",
api_key=openai_unit_test_env["OPENAI_API_KEY"],
default_headers=default_headers,
)
assert chat_client.ai_model_id == "gpt-4"
assert chat_client.model_id == "gpt-4"
assert isinstance(chat_client, ChatClientProtocol)
# Assert that the default header we added is present in the client's default headers
@@ -211,7 +211,7 @@ async def test_openai_assistants_client_get_assistant_id_or_create_create_new(
) -> None:
"""Test _get_assistant_id_or_create when creating a new assistant."""
chat_client = create_test_openai_assistants_client(
mock_async_openai, ai_model_id="gpt-4", assistant_name="TestAssistant"
mock_async_openai, model_id="gpt-4", assistant_name="TestAssistant"
)
assistant_id = await chat_client._get_assistant_id_or_create() # type: ignore
@@ -269,7 +269,7 @@ def test_openai_assistants_client_serialize(openai_unit_test_env: dict[str, str]
# Test basic initialization and to_dict
chat_client = OpenAIAssistantsClient(
ai_model_id="gpt-4",
model_id="gpt-4",
assistant_id="test-assistant-id",
assistant_name="TestAssistant",
thread_id="test-thread-id",
@@ -280,11 +280,10 @@ def test_openai_assistants_client_serialize(openai_unit_test_env: dict[str, str]
dumped_settings = chat_client.to_dict()
assert dumped_settings["ai_model_id"] == "gpt-4"
assert dumped_settings["model_id"] == "gpt-4"
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"] == openai_unit_test_env["OPENAI_API_KEY"]
assert dumped_settings["org_id"] == openai_unit_test_env["OPENAI_ORG_ID"]
# Assert that the default header we added is present in the dumped_settings default headers
@@ -915,6 +914,7 @@ def get_weather(
return f"The weather in {location} is sunny with a high of 25°C."
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_client_get_response() -> None:
"""Test OpenAI Assistants Client response."""
@@ -939,6 +939,7 @@ async def test_openai_assistants_client_get_response() -> None:
assert any(word in response.text.lower() for word in ["sunny", "25", "weather", "seattle"])
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_client_get_response_tools() -> None:
"""Test OpenAI Assistants Client response with tools."""
@@ -960,6 +961,7 @@ async def test_openai_assistants_client_get_response_tools() -> None:
assert any(word in response.text.lower() for word in ["sunny", "25", "weather"])
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_client_streaming() -> None:
"""Test OpenAI Assistants Client streaming response."""
@@ -990,6 +992,7 @@ async def test_openai_assistants_client_streaming() -> None:
assert any(word in full_message.lower() for word in ["sunny", "25", "weather", "seattle"])
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_client_streaming_tools() -> None:
"""Test OpenAI Assistants Client streaming response with tools."""
@@ -1016,6 +1019,7 @@ async def test_openai_assistants_client_streaming_tools() -> None:
assert any(word in full_message.lower() for word in ["sunny", "25", "weather"])
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_client_with_existing_assistant() -> None:
"""Test OpenAI Assistants Client with existing assistant ID."""
@@ -1028,7 +1032,7 @@ async def test_openai_assistants_client_with_existing_assistant() -> None:
# Now test using the existing assistant
async with OpenAIAssistantsClient(
ai_model_id="gpt-4o-mini", assistant_id=assistant_id
model_id="gpt-4o-mini", assistant_id=assistant_id
) as openai_assistants_client:
assert isinstance(openai_assistants_client, ChatClientProtocol)
assert openai_assistants_client.assistant_id == assistant_id
@@ -1043,6 +1047,7 @@ async def test_openai_assistants_client_with_existing_assistant() -> None:
assert len(response.text) > 0
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
@pytest.mark.skip(reason="OpenAI file search functionality is currently broken - tracked in GitHub issue")
async def test_openai_assistants_client_file_search() -> None:
@@ -1066,6 +1071,7 @@ async def test_openai_assistants_client_file_search() -> None:
assert any(word in response.text.lower() for word in ["sunny", "25", "weather"])
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
@pytest.mark.skip(reason="OpenAI file search functionality is currently broken - tracked in GitHub issue")
async def test_openai_assistants_client_file_search_streaming() -> None:
@@ -1096,6 +1102,7 @@ async def test_openai_assistants_client_file_search_streaming() -> None:
assert any(word in full_message.lower() for word in ["sunny", "25", "weather"])
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_agent_basic_run():
"""Test ChatAgent basic run functionality with OpenAIAssistantsClient."""
@@ -1112,6 +1119,7 @@ async def test_openai_assistants_agent_basic_run():
assert "Hello World" in response.text
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_agent_basic_run_streaming():
"""Test ChatAgent basic streaming functionality with OpenAIAssistantsClient."""
@@ -1131,6 +1139,7 @@ async def test_openai_assistants_agent_basic_run_streaming():
assert "streaming response test" in full_message.lower()
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_agent_thread_persistence():
"""Test ChatAgent thread persistence across runs with OpenAIAssistantsClient."""
@@ -1159,6 +1168,7 @@ async def test_openai_assistants_agent_thread_persistence():
assert thread.service_thread_id is not None
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_agent_existing_thread_id():
"""Test ChatAgent with existing thread ID to continue conversations across agent instances."""
@@ -1203,6 +1213,7 @@ async def test_openai_assistants_agent_existing_thread_id():
assert "paris" in response2.text.lower()
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_agent_code_interpreter():
"""Test ChatAgent with code interpreter through OpenAIAssistantsClient."""
@@ -1222,6 +1233,7 @@ async def test_openai_assistants_agent_code_interpreter():
assert "120" in response.text or "factorial" in response.text.lower()
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_client_agent_level_tool_persistence():
"""Test that agent-level tools persist across multiple runs with OpenAI Assistants Client."""
@@ -41,22 +41,22 @@ def test_init(openai_unit_test_env: dict[str, str]) -> None:
# Test successful initialization
open_ai_chat_completion = OpenAIChatClient()
assert open_ai_chat_completion.ai_model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"]
assert open_ai_chat_completion.model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"]
assert isinstance(open_ai_chat_completion, ChatClientProtocol)
def test_init_validation_fail() -> None:
# Test successful initialization
with pytest.raises(ServiceInitializationError):
OpenAIChatClient(api_key="34523", ai_model_id={"test": "dict"}) # type: ignore
OpenAIChatClient(api_key="34523", model_id={"test": "dict"}) # type: ignore
def test_init_ai_model_id_constructor(openai_unit_test_env: dict[str, str]) -> None:
def test_init_model_id_constructor(openai_unit_test_env: dict[str, str]) -> None:
# Test successful initialization
ai_model_id = "test_model_id"
open_ai_chat_completion = OpenAIChatClient(ai_model_id=ai_model_id)
model_id = "test_model_id"
open_ai_chat_completion = OpenAIChatClient(model_id=model_id)
assert open_ai_chat_completion.ai_model_id == ai_model_id
assert open_ai_chat_completion.model_id == model_id
assert isinstance(open_ai_chat_completion, ChatClientProtocol)
@@ -68,7 +68,7 @@ def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None:
default_headers=default_headers,
)
assert open_ai_chat_completion.ai_model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"]
assert open_ai_chat_completion.model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"]
assert isinstance(open_ai_chat_completion, ChatClientProtocol)
# Assert that the default header we added is present in the client's default headers
@@ -95,7 +95,7 @@ def test_init_base_url_from_settings_env() -> None:
},
):
client = OpenAIChatClient()
assert client.ai_model_id == "gpt-5"
assert client.model_id == "gpt-5"
assert str(client.client.base_url) == "https://custom-openai-endpoint.com/v1/"
@@ -109,11 +109,11 @@ def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None:
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
def test_init_with_empty_api_key(openai_unit_test_env: dict[str, str]) -> None:
ai_model_id = "test_model_id"
model_id = "test_model_id"
with pytest.raises(ServiceInitializationError):
OpenAIChatClient(
ai_model_id=ai_model_id,
model_id=model_id,
env_file_path="test.env",
)
@@ -122,15 +122,14 @@ def test_serialize(openai_unit_test_env: dict[str, str]) -> None:
default_headers = {"X-Unit-Test": "test-guid"}
settings = {
"ai_model_id": openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
"model_id": openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
"api_key": openai_unit_test_env["OPENAI_API_KEY"],
"default_headers": default_headers,
}
open_ai_chat_completion = OpenAIChatClient.from_dict(settings)
dumped_settings = open_ai_chat_completion.to_dict()
assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"]
assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"]
assert dumped_settings["model_id"] == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"]
# 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,18 +140,17 @@ def test_serialize(openai_unit_test_env: dict[str, str]) -> None:
def test_serialize_with_org_id(openai_unit_test_env: dict[str, str]) -> None:
settings = {
"ai_model_id": openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
"model_id": openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
"api_key": openai_unit_test_env["OPENAI_API_KEY"],
"org_id": openai_unit_test_env["OPENAI_ORG_ID"],
}
open_ai_chat_completion = OpenAIChatClient.from_dict(settings)
dumped_settings = open_ai_chat_completion.to_dict()
assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"]
assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"]
assert dumped_settings["model_id"] == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"]
assert dumped_settings["org_id"] == openai_unit_test_env["OPENAI_ORG_ID"]
# Assert that the 'User-Agent' header is not present in the dumped_settings default headers
assert "User-Agent" not in dumped_settings["default_headers"]
assert "User-Agent" not in dumped_settings.get("default_headers", {})
async def test_content_filter_exception_handling(openai_unit_test_env: dict[str, str]) -> None:
@@ -210,6 +208,7 @@ def get_weather(location: str) -> str:
return f"The weather in {location} is sunny and 72°F."
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_chat_completion_response() -> None:
"""Test OpenAI chat completion responses."""
@@ -237,6 +236,7 @@ async def test_openai_chat_completion_response() -> None:
assert "scientists" in response.text
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_chat_completion_response_tools() -> None:
"""Test OpenAI chat completion responses."""
@@ -259,6 +259,7 @@ async def test_openai_chat_completion_response_tools() -> None:
assert "scientists" in response.text
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_chat_client_streaming() -> None:
"""Test Azure OpenAI chat completion responses."""
@@ -294,6 +295,7 @@ async def test_openai_chat_client_streaming() -> None:
assert "scientists" in full_message
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_chat_client_streaming_tools() -> None:
"""Test AzureOpenAI chat completion responses."""
@@ -321,10 +323,11 @@ async def test_openai_chat_client_streaming_tools() -> None:
assert "scientists" in full_message
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_chat_client_web_search() -> None:
# Currently only a select few models support web search tool calls
openai_chat_client = OpenAIChatClient(ai_model_id="gpt-4o-search-preview")
openai_chat_client = OpenAIChatClient(model_id="gpt-4o-search-preview")
assert isinstance(openai_chat_client, ChatClientProtocol)
@@ -361,9 +364,10 @@ async def test_openai_chat_client_web_search() -> None:
assert response.text is not None
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_chat_client_web_search_streaming() -> None:
openai_chat_client = OpenAIChatClient(ai_model_id="gpt-4o-search-preview")
openai_chat_client = OpenAIChatClient(model_id="gpt-4o-search-preview")
assert isinstance(openai_chat_client, ChatClientProtocol)
@@ -414,11 +418,12 @@ async def test_openai_chat_client_web_search_streaming() -> None:
assert full_message is not None
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_chat_client_agent_basic_run():
"""Test OpenAI chat client agent basic run functionality with OpenAIChatClient."""
async with ChatAgent(
chat_client=OpenAIChatClient(ai_model_id="gpt-4o-search-preview"),
chat_client=OpenAIChatClient(model_id="gpt-4o-search-preview"),
) as agent:
# Test basic run
response = await agent.run("Hello! Please respond with 'Hello World' exactly.")
@@ -429,11 +434,12 @@ async def test_openai_chat_client_agent_basic_run():
assert "hello world" in response.text.lower()
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_chat_client_agent_basic_run_streaming():
"""Test OpenAI chat client agent basic streaming functionality with OpenAIChatClient."""
async with ChatAgent(
chat_client=OpenAIChatClient(ai_model_id="gpt-4o-search-preview"),
chat_client=OpenAIChatClient(model_id="gpt-4o-search-preview"),
) as agent:
# Test streaming run
full_text = ""
@@ -446,11 +452,12 @@ async def test_openai_chat_client_agent_basic_run_streaming():
assert "streaming response test" in full_text.lower()
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_chat_client_agent_thread_persistence():
"""Test OpenAI chat client agent thread persistence across runs with OpenAIChatClient."""
async with ChatAgent(
chat_client=OpenAIChatClient(ai_model_id="gpt-4o-search-preview"),
chat_client=OpenAIChatClient(model_id="gpt-4o-search-preview"),
instructions="You are a helpful assistant with good memory.",
) as agent:
# Create a new thread that will be reused
@@ -470,6 +477,7 @@ async def test_openai_chat_client_agent_thread_persistence():
assert "alice" in response2.text.lower()
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_chat_client_agent_existing_thread():
"""Test OpenAI chat client agent with existing thread to continue conversations across agent instances."""
@@ -477,7 +485,7 @@ async def test_openai_chat_client_agent_existing_thread():
preserved_thread = None
async with ChatAgent(
chat_client=OpenAIChatClient(ai_model_id="gpt-4o-search-preview"),
chat_client=OpenAIChatClient(model_id="gpt-4o-search-preview"),
instructions="You are a helpful assistant with good memory.",
) as first_agent:
# Start a conversation and capture the thread
@@ -493,7 +501,7 @@ async def test_openai_chat_client_agent_existing_thread():
# Second conversation - reuse the thread in a new agent instance
if preserved_thread:
async with ChatAgent(
chat_client=OpenAIChatClient(ai_model_id="gpt-4o-search-preview"),
chat_client=OpenAIChatClient(model_id="gpt-4o-search-preview"),
instructions="You are a helpful assistant with good memory.",
) as second_agent:
# Reuse the preserved thread
@@ -504,12 +512,13 @@ async def test_openai_chat_client_agent_existing_thread():
assert "alice" in second_response.text.lower()
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_chat_client_agent_level_tool_persistence():
"""Test that agent-level tools persist across multiple runs with OpenAI Chat Client."""
async with ChatAgent(
chat_client=OpenAIChatClient(ai_model_id="gpt-4.1"),
chat_client=OpenAIChatClient(model_id="gpt-4.1"),
instructions="You are a helpful assistant that uses available tools.",
tools=[get_weather], # Agent-level tool
) as agent:
@@ -530,6 +539,7 @@ async def test_openai_chat_client_agent_level_tool_persistence():
assert any(term in second_response.text.lower() for term in ["miami", "sunny", "72"])
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_chat_client_run_level_tool_isolation():
"""Test that run-level tools are isolated to specific runs and don't persist with OpenAI Chat Client."""
@@ -544,7 +554,7 @@ async def test_openai_chat_client_run_level_tool_isolation():
return f"The weather in {location} is sunny and 72°F."
async with ChatAgent(
chat_client=OpenAIChatClient(ai_model_id="gpt-4.1"),
chat_client=OpenAIChatClient(model_id="gpt-4.1"),
instructions="You are a helpful assistant.",
) as agent:
# First run - use run-level tool
@@ -571,7 +581,7 @@ async def test_openai_chat_client_run_level_tool_isolation():
async def test_exception_message_includes_original_error_details() -> None:
"""Test that exception messages include original error details in the new format."""
client = OpenAIChatClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIChatClient(model_id="test-model", api_key="test-key")
messages = [ChatMessage(role="user", text="test message")]
mock_response = MagicMock()
@@ -96,22 +96,22 @@ def test_init(openai_unit_test_env: dict[str, str]) -> None:
# Test successful initialization
openai_responses_client = OpenAIResponsesClient()
assert openai_responses_client.ai_model_id == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"]
assert openai_responses_client.model_id == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"]
assert isinstance(openai_responses_client, ChatClientProtocol)
def test_init_validation_fail() -> None:
# Test successful initialization
with pytest.raises(ServiceInitializationError):
OpenAIResponsesClient(api_key="34523", ai_model_id={"test": "dict"}) # type: ignore
OpenAIResponsesClient(api_key="34523", model_id={"test": "dict"}) # type: ignore
def test_init_ai_model_id_constructor(openai_unit_test_env: dict[str, str]) -> None:
def test_init_model_id_constructor(openai_unit_test_env: dict[str, str]) -> None:
# Test successful initialization
ai_model_id = "test_model_id"
openai_responses_client = OpenAIResponsesClient(ai_model_id=ai_model_id)
model_id = "test_model_id"
openai_responses_client = OpenAIResponsesClient(model_id=model_id)
assert openai_responses_client.ai_model_id == ai_model_id
assert openai_responses_client.model_id == model_id
assert isinstance(openai_responses_client, ChatClientProtocol)
@@ -123,7 +123,7 @@ def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None:
default_headers=default_headers,
)
assert openai_responses_client.ai_model_id == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"]
assert openai_responses_client.model_id == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"]
assert isinstance(openai_responses_client, ChatClientProtocol)
# Assert that the default header we added is present in the client's default headers
@@ -142,11 +142,11 @@ def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None:
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
def test_init_with_empty_api_key(openai_unit_test_env: dict[str, str]) -> None:
ai_model_id = "test_model_id"
model_id = "test_model_id"
with pytest.raises(ServiceInitializationError):
OpenAIResponsesClient(
ai_model_id=ai_model_id,
model_id=model_id,
env_file_path="test.env",
)
@@ -155,15 +155,14 @@ def test_serialize(openai_unit_test_env: dict[str, str]) -> None:
default_headers = {"X-Unit-Test": "test-guid"}
settings = {
"ai_model_id": openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"],
"model_id": openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"],
"api_key": openai_unit_test_env["OPENAI_API_KEY"],
"default_headers": default_headers,
}
openai_responses_client = OpenAIResponsesClient.from_dict(settings)
dumped_settings = openai_responses_client.to_dict()
assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"]
assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"]
assert dumped_settings["model_id"] == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"]
# 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"]
@@ -174,24 +173,23 @@ def test_serialize(openai_unit_test_env: dict[str, str]) -> None:
def test_serialize_with_org_id(openai_unit_test_env: dict[str, str]) -> None:
settings = {
"ai_model_id": openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"],
"model_id": openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"],
"api_key": openai_unit_test_env["OPENAI_API_KEY"],
"org_id": openai_unit_test_env["OPENAI_ORG_ID"],
}
openai_responses_client = OpenAIResponsesClient.from_dict(settings)
dumped_settings = openai_responses_client.to_dict()
assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"]
assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"]
assert dumped_settings["model_id"] == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"]
assert dumped_settings["org_id"] == openai_unit_test_env["OPENAI_ORG_ID"]
# Assert that the 'User-Agent' header is not present in the dumped_settings default headers
assert "User-Agent" not in dumped_settings["default_headers"]
assert "User-Agent" not in dumped_settings.get("default_headers", {})
def test_get_response_with_invalid_input() -> None:
"""Test get_response with invalid inputs to trigger exception handling."""
client = OpenAIResponsesClient(ai_model_id="invalid-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="invalid-model", api_key="test-key")
# Test with empty messages which should trigger ServiceInvalidRequestError
with pytest.raises(ServiceInvalidRequestError, match="Messages are required"):
@@ -200,7 +198,7 @@ def test_get_response_with_invalid_input() -> None:
def test_get_response_with_all_parameters() -> None:
"""Test get_response with all possible parameters to cover parameter handling logic."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test with comprehensive parameter set - should fail due to invalid API key
with pytest.raises(ServiceResponseException):
@@ -232,7 +230,7 @@ def test_get_response_with_all_parameters() -> None:
def test_web_search_tool_with_location() -> None:
"""Test HostedWebSearchTool with location parameters."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test web search tool with location
web_search_tool = HostedWebSearchTool(
@@ -254,7 +252,7 @@ def test_web_search_tool_with_location() -> None:
def test_file_search_tool_with_invalid_inputs() -> None:
"""Test HostedFileSearchTool with invalid vector store inputs."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test with invalid inputs type (should trigger ValueError)
file_search_tool = HostedFileSearchTool(inputs=[HostedFileContent(file_id="invalid")])
@@ -268,7 +266,7 @@ def test_file_search_tool_with_invalid_inputs() -> None:
def test_code_interpreter_tool_variations() -> None:
"""Test HostedCodeInterpreterTool with and without file inputs."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test code interpreter without files
code_tool_empty = HostedCodeInterpreterTool()
@@ -293,7 +291,7 @@ def test_code_interpreter_tool_variations() -> None:
def test_content_filter_exception() -> None:
"""Test that content filter errors in get_response are properly handled."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Mock a BadRequestError with content_filter code
mock_error = BadRequestError(
@@ -313,7 +311,7 @@ def test_content_filter_exception() -> None:
def test_hosted_file_search_tool_validation() -> None:
"""Test get_response HostedFileSearchTool validation."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test HostedFileSearchTool without inputs (should raise ValueError)
empty_file_search_tool = HostedFileSearchTool()
@@ -326,7 +324,7 @@ def test_hosted_file_search_tool_validation() -> None:
def test_chat_message_parsing_with_function_calls() -> None:
"""Test get_response message preparation with function call and result content types in conversation flow."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Create messages with function call and result content
function_call = FunctionCallContent(
@@ -351,7 +349,7 @@ def test_chat_message_parsing_with_function_calls() -> None:
async def test_response_format_parse_path() -> None:
"""Test get_response response_format parsing path."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Mock successful parse response
mock_parsed_response = MagicMock()
@@ -375,7 +373,7 @@ async def test_response_format_parse_path() -> None:
async def test_bad_request_error_non_content_filter() -> None:
"""Test get_response BadRequestError without content_filter."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Mock a BadRequestError without content_filter code
mock_error = BadRequestError(
@@ -396,7 +394,7 @@ async def test_bad_request_error_non_content_filter() -> None:
async def test_streaming_content_filter_exception_handling() -> None:
"""Test that content filter errors in get_streaming_response are properly handled."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Mock the OpenAI client to raise a BadRequestError with content_filter code
with patch.object(client.client.responses, "create") as mock_create:
@@ -413,10 +411,11 @@ async def test_streaming_content_filter_exception_handling() -> None:
break
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_get_streaming_response_with_all_parameters() -> None:
"""Test get_streaming_response with all possible parameters."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Should fail due to invalid API key
with pytest.raises(ServiceResponseException):
@@ -449,7 +448,7 @@ async def test_get_streaming_response_with_all_parameters() -> None:
def test_response_content_creation_with_annotations() -> None:
"""Test _create_response_content with different annotation types."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Create a mock response with annotated text content
mock_response = MagicMock()
@@ -489,7 +488,7 @@ def test_response_content_creation_with_annotations() -> None:
def test_response_content_creation_with_refusal() -> None:
"""Test _create_response_content with refusal content."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Create a mock response with refusal content
mock_response = MagicMock()
@@ -519,7 +518,7 @@ def test_response_content_creation_with_refusal() -> None:
def test_response_content_creation_with_reasoning() -> None:
"""Test _create_response_content with reasoning content."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Create a mock response with reasoning content
mock_response = MagicMock()
@@ -550,7 +549,7 @@ def test_response_content_creation_with_reasoning() -> None:
def test_response_content_creation_with_code_interpreter() -> None:
"""Test _create_response_content with code interpreter outputs."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Create a mock response with code interpreter outputs
mock_response = MagicMock()
@@ -588,7 +587,7 @@ def test_response_content_creation_with_code_interpreter() -> None:
def test_response_content_creation_with_function_call() -> None:
"""Test _create_response_content with function call content."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Create a mock response with function call
mock_response = MagicMock()
@@ -620,7 +619,7 @@ def test_response_content_creation_with_function_call() -> None:
def test_tools_to_response_tools_with_hosted_mcp() -> None:
"""Test that HostedMCPTool is converted to the correct response tool dict."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
tool = HostedMCPTool(
name="My MCP",
@@ -650,7 +649,7 @@ def test_tools_to_response_tools_with_hosted_mcp() -> None:
def test_create_response_content_with_mcp_approval_request() -> None:
"""Test that a non-streaming mcp_approval_request is parsed into FunctionApprovalRequestContent."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
mock_response = MagicMock()
mock_response.output_parsed = None
@@ -681,7 +680,7 @@ def test_create_response_content_with_mcp_approval_request() -> None:
def test_tools_to_response_tools_with_raw_image_generation() -> None:
"""Test that raw image_generation tool dict is handled correctly with parameter mapping."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test with raw tool dict using user-friendly parameter names
tool = {
@@ -710,7 +709,7 @@ def test_tools_to_response_tools_with_raw_image_generation() -> None:
def test_tools_to_response_tools_with_raw_image_generation_openai_responses_params() -> None:
"""Test raw image_generation tool with OpenAI-specific parameters."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test with OpenAI-specific parameters
tool = {
@@ -742,7 +741,7 @@ def test_tools_to_response_tools_with_raw_image_generation_openai_responses_para
def test_tools_to_response_tools_with_raw_image_generation_minimal() -> None:
"""Test raw image_generation tool with minimal configuration."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test with minimal parameters (just type)
tool = {"type": "image_generation"}
@@ -760,7 +759,7 @@ def test_tools_to_response_tools_with_raw_image_generation_minimal() -> None:
def test_create_streaming_response_content_with_mcp_approval_request() -> None:
"""Test that a streaming mcp_approval_request event is parsed into FunctionApprovalRequestContent."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
@@ -787,7 +786,7 @@ def test_end_to_end_mcp_approval_flow(span_exporter) -> None:
"""End-to-end mocked test:
model issues an mcp_approval_request, user approves, client sends mcp_approval_response.
"""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# First mocked response: model issues an mcp_approval_request
mock_response1 = MagicMock()
@@ -851,7 +850,7 @@ def test_end_to_end_mcp_approval_flow(span_exporter) -> None:
def test_usage_details_basic() -> None:
"""Test _usage_details_from_openai without cached or reasoning tokens."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
mock_usage = MagicMock()
mock_usage.input_tokens = 100
@@ -869,7 +868,7 @@ def test_usage_details_basic() -> None:
def test_usage_details_with_cached_tokens() -> None:
"""Test _usage_details_from_openai with cached input tokens."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
mock_usage = MagicMock()
mock_usage.input_tokens = 200
@@ -887,7 +886,7 @@ def test_usage_details_with_cached_tokens() -> None:
def test_usage_details_with_reasoning_tokens() -> None:
"""Test _usage_details_from_openai with reasoning tokens."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
mock_usage = MagicMock()
mock_usage.input_tokens = 150
@@ -905,7 +904,7 @@ def test_usage_details_with_reasoning_tokens() -> None:
def test_get_metadata_from_response() -> None:
"""Test the _get_metadata_from_response method."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test with logprobs
mock_output_with_logprobs = MagicMock()
@@ -925,7 +924,7 @@ def test_get_metadata_from_response() -> None:
def test_streaming_response_basic_structure() -> None:
"""Test that _create_streaming_response_content returns proper structure."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
chat_options = ChatOptions(store=True)
function_call_ids: dict[int, tuple[str, str]] = {}
@@ -942,6 +941,7 @@ def test_streaming_response_basic_structure() -> None:
assert response.raw_representation is mock_event
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_response() -> None:
"""Test OpenAI chat completion responses."""
@@ -986,6 +986,7 @@ async def test_openai_responses_client_response() -> None:
assert output.weather is not None
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_response_tools() -> None:
"""Test OpenAI chat completion responses."""
@@ -1025,6 +1026,7 @@ async def test_openai_responses_client_response_tools() -> None:
assert "sunny" in output.weather.lower()
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_streaming() -> None:
"""Test OpenAI chat completion responses."""
@@ -1071,6 +1073,7 @@ async def test_openai_responses_client_streaming() -> None:
assert output.weather is not None
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_streaming_tools() -> None:
"""Test OpenAI chat completion responses."""
@@ -1118,6 +1121,7 @@ async def test_openai_responses_client_streaming_tools() -> None:
assert "sunny" in output.weather.lower()
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_web_search() -> None:
openai_responses_client = OpenAIResponsesClient()
@@ -1157,6 +1161,7 @@ async def test_openai_responses_client_web_search() -> None:
assert response.text is not None
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_web_search_streaming() -> None:
openai_responses_client = OpenAIResponsesClient()
@@ -1210,6 +1215,7 @@ async def test_openai_responses_client_web_search_streaming() -> None:
assert full_message is not None
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_file_search() -> None:
openai_responses_client = OpenAIResponsesClient()
@@ -1234,6 +1240,7 @@ async def test_openai_responses_client_file_search() -> None:
assert "75" in response.text
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_streaming_file_search() -> None:
openai_responses_client = OpenAIResponsesClient()
@@ -1268,6 +1275,7 @@ async def test_openai_responses_client_streaming_file_search() -> None:
assert "75" in full_message
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_agent_basic_run():
"""Test OpenAI Responses Client agent basic run functionality with OpenAIResponsesClient."""
@@ -1284,6 +1292,7 @@ async def test_openai_responses_client_agent_basic_run():
assert "hello world" in response.text.lower()
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_agent_basic_run_streaming():
"""Test OpenAI Responses Client agent basic streaming functionality with OpenAIResponsesClient."""
@@ -1301,6 +1310,7 @@ async def test_openai_responses_client_agent_basic_run_streaming():
assert "streaming response test" in full_text.lower()
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_agent_thread_persistence():
"""Test OpenAI Responses Client agent thread persistence across runs with OpenAIResponsesClient."""
@@ -1324,6 +1334,7 @@ async def test_openai_responses_client_agent_thread_persistence():
assert second_response.text is not None
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_agent_thread_storage_with_store_true():
"""Test OpenAI Responses Client agent with store=True to verify service_thread_id is returned."""
@@ -1355,6 +1366,7 @@ async def test_openai_responses_client_agent_thread_storage_with_store_true():
assert len(thread.service_thread_id) > 0
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_agent_existing_thread():
"""Test OpenAI Responses Client agent with existing thread to continue conversations across agent instances."""
@@ -1389,6 +1401,7 @@ async def test_openai_responses_client_agent_existing_thread():
assert "photography" in second_response.text.lower()
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_agent_hosted_code_interpreter_tool():
"""Test OpenAI Responses Client agent with HostedCodeInterpreterTool through OpenAIResponsesClient."""
@@ -1410,6 +1423,7 @@ async def test_openai_responses_client_agent_hosted_code_interpreter_tool():
assert contains_relevant_content or len(response.text.strip()) > 10
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_agent_raw_image_generation_tool():
"""Test OpenAI Responses Client agent with raw image_generation tool through OpenAIResponsesClient."""
@@ -1446,6 +1460,7 @@ async def test_openai_responses_client_agent_raw_image_generation_tool():
assert image_content_found, "Expected to find image content in response"
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_agent_level_tool_persistence():
"""Test that agent-level tools persist across multiple runs with OpenAI Responses Client."""
@@ -1472,6 +1487,7 @@ async def test_openai_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_openai_integration_tests_disabled
async def test_openai_responses_client_run_level_tool_isolation():
"""Test that run-level tools are isolated to specific runs and don't persist with OpenAI Responses Client."""
@@ -1511,6 +1527,7 @@ async def test_openai_responses_client_run_level_tool_isolation():
assert call_count == 1
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_agent_chat_options_run_level() -> None:
"""Integration test for comprehensive ChatOptions parameter coverage with OpenAI Response Agent."""
@@ -1534,6 +1551,7 @@ async def test_openai_responses_client_agent_chat_options_run_level() -> None:
assert len(response.text) > 0
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_agent_chat_options_agent_level() -> None:
"""Integration test for comprehensive ChatOptions parameter coverage with OpenAI Response Agent."""
@@ -1557,6 +1575,7 @@ async def test_openai_responses_client_agent_chat_options_agent_level() -> None:
assert len(response.text) > 0
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_agent_hosted_mcp_tool() -> None:
"""Integration test for HostedMCPTool with OpenAI Response Agent using Microsoft Learn MCP."""
@@ -1587,7 +1606,7 @@ async def test_openai_responses_client_agent_hosted_mcp_tool() -> None:
def test_service_response_exception_includes_original_error_details() -> None:
"""Test that ServiceResponseException messages include original error details in the new format."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
messages = [ChatMessage(role="user", text="test message")]
mock_response = MagicMock()
@@ -1612,7 +1631,7 @@ def test_service_response_exception_includes_original_error_details() -> None:
def test_get_streaming_response_with_response_format() -> None:
"""Test get_streaming_response with response_format."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
messages = [ChatMessage(role="user", text="Test streaming with format")]
# It will fail due to invalid API key, but exercises the code path
@@ -1627,7 +1646,7 @@ def test_get_streaming_response_with_response_format() -> None:
def test_openai_content_parser_image_content() -> None:
"""Test _openai_content_parser with image content variations."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test image content with detail parameter and file_id
image_content_with_detail = UriContent(
@@ -1651,7 +1670,7 @@ def test_openai_content_parser_image_content() -> None:
def test_openai_content_parser_audio_content() -> None:
"""Test _openai_content_parser with audio content variations."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test WAV audio content
wav_content = UriContent(uri="data:audio/wav;base64,abc123", media_type="audio/wav")
@@ -1669,7 +1688,7 @@ def test_openai_content_parser_audio_content() -> None:
def test_openai_content_parser_unsupported_content() -> None:
"""Test _openai_content_parser with unsupported content types."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test unsupported audio format
unsupported_audio = UriContent(uri="data:audio/ogg;base64,ghi789", media_type="audio/ogg")
@@ -1684,7 +1703,7 @@ def test_openai_content_parser_unsupported_content() -> None:
def test_create_streaming_response_content_code_interpreter() -> None:
"""Test _create_streaming_response_content with code_interpreter_call."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
@@ -1708,7 +1727,7 @@ def test_create_streaming_response_content_code_interpreter() -> None:
def test_create_streaming_response_content_reasoning() -> None:
"""Test _create_streaming_response_content with reasoning content."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
@@ -1732,7 +1751,7 @@ def test_create_streaming_response_content_reasoning() -> None:
def test_openai_content_parser_text_reasoning_comprehensive() -> None:
"""Test _openai_content_parser with TextReasoningContent all additional properties."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test TextReasoningContent with all additional properties
comprehensive_reasoning = TextReasoningContent(
@@ -1754,7 +1773,7 @@ def test_openai_content_parser_text_reasoning_comprehensive() -> None:
def test_streaming_reasoning_text_delta_event() -> None:
"""Test reasoning text delta event creates TextReasoningContent."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
@@ -1779,7 +1798,7 @@ def test_streaming_reasoning_text_delta_event() -> None:
def test_streaming_reasoning_text_done_event() -> None:
"""Test reasoning text done event creates TextReasoningContent with complete text."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
@@ -1805,7 +1824,7 @@ def test_streaming_reasoning_text_done_event() -> None:
def test_streaming_reasoning_summary_text_delta_event() -> None:
"""Test reasoning summary text delta event creates TextReasoningContent."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
@@ -1830,7 +1849,7 @@ def test_streaming_reasoning_summary_text_delta_event() -> None:
def test_streaming_reasoning_summary_text_done_event() -> None:
"""Test reasoning summary text done event creates TextReasoningContent with complete text."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
@@ -1856,7 +1875,7 @@ def test_streaming_reasoning_summary_text_done_event() -> None:
def test_streaming_reasoning_events_preserve_metadata() -> None:
"""Test that reasoning events preserve metadata like regular text events."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
@@ -1894,7 +1913,7 @@ def test_streaming_reasoning_events_preserve_metadata() -> None:
def test_create_response_content_image_generation_raw_base64():
"""Test image generation response parsing with raw base64 string."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Create a mock response with raw base64 image data (PNG signature)
mock_response = MagicMock()
@@ -1928,7 +1947,7 @@ def test_create_response_content_image_generation_raw_base64():
def test_create_response_content_image_generation_existing_data_uri():
"""Test image generation response parsing with existing data URI."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Create a mock response with existing data URI
mock_response = MagicMock()
@@ -1961,7 +1980,7 @@ def test_create_response_content_image_generation_existing_data_uri():
def test_create_response_content_image_generation_format_detection():
"""Test different image format detection from base64 data."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test JPEG detection
jpeg_signature = b"\xff\xd8\xff"
@@ -2014,7 +2033,7 @@ def test_create_response_content_image_generation_format_detection():
def test_create_response_content_image_generation_fallback():
"""Test image generation with invalid base64 falls back to PNG."""
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Create a mock response with invalid base64
mock_response = MagicMock()
@@ -2046,7 +2065,7 @@ def test_create_response_content_image_generation_fallback():
def test_prepare_options_store_parameter_handling() -> None:
client = OpenAIResponsesClient(ai_model_id="test-model", api_key="test-key")
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
messages = [ChatMessage(role="user", text="Test message")]
test_conversation_id = "test-conversation-123"
@@ -5,8 +5,6 @@ from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
import pytest
from agent_framework._workflows._checkpoint import CheckpointStorage, WorkflowCheckpoint
from agent_framework._workflows._events import RequestInfoEvent, WorkflowEvent
from agent_framework._workflows._executor import (
@@ -295,7 +293,6 @@ def test_restore_state_falls_back_to_base_request_type() -> None:
assert isinstance(restored.data, RequestInfoMessage)
@pytest.mark.asyncio
async def test_run_persists_pending_requests_in_runner_state() -> None:
shared_state = SharedState()
runner_ctx = _StubRunnerContext()
@@ -117,7 +117,6 @@ async def test_sequential_with_custom_executor_summary() -> None:
assert msgs[2].role == Role.ASSISTANT and msgs[2].text.startswith("Summary of users:")
@pytest.mark.asyncio
async def test_sequential_checkpoint_resume_round_trip() -> None:
storage = InMemoryCheckpointStorage()