Python: [BREAKING]: Introducing Options as TypedDict and Generic (#3140)

* WIP typeddict for options

* updated all clients and ChatAgents

* updated everything

* added ADR

* fix mypy

* proper typevar imports

* fixed import

* fixed other imports

* slight update in the sample

* updated from feedback

* fixes

* fixed missing covariants and test fixes

* fixed typing

* updated anthropic thinking config

* ruff fixes

* fixed int tests

* fix tests and mypy

* updated integration tests

* updated docstring and test fix

* improved options handling in obser

* mypy fix

* updated a host of integration tests

* fix tests

* bedrock fix
This commit is contained in:
Eduard van Valkenburg
2026-01-13 16:41:05 +00:00
committed by GitHub
parent 5faa2851bb
commit 3e97425245
111 changed files with 6141 additions and 4715 deletions
@@ -17,7 +17,6 @@ from agent_framework import (
ChatAgent,
ChatClientProtocol,
ChatMessage,
ChatOptions,
ChatResponse,
ChatResponseUpdate,
FunctionCallContent,
@@ -27,7 +26,6 @@ from agent_framework import (
HostedVectorStoreContent,
Role,
TextContent,
ToolMode,
UriContent,
UsageContent,
ai_function,
@@ -43,6 +41,8 @@ skip_if_openai_integration_tests_disabled = pytest.mark.skipif(
else "Integration tests are disabled.",
)
INTEGRATION_TEST_MODEL = "gpt-4.1-nano"
def create_test_openai_assistants_client(
mock_async_openai: MagicMock,
@@ -117,7 +117,7 @@ def mock_async_openai() -> MagicMock:
return mock_client
def test_openai_assistants_client_init_with_client(mock_async_openai: MagicMock) -> None:
def test_init_with_client(mock_async_openai: MagicMock) -> None:
"""Test OpenAIAssistantsClient initialization with existing client."""
chat_client = create_test_openai_assistants_client(
mock_async_openai, model_id="gpt-4", assistant_id="existing-assistant-id", thread_id="test-thread-id"
@@ -131,7 +131,7 @@ def test_openai_assistants_client_init_with_client(mock_async_openai: MagicMock)
assert isinstance(chat_client, ChatClientProtocol)
def test_openai_assistants_client_init_auto_create_client(
def test_init_auto_create_client(
openai_unit_test_env: dict[str, str],
mock_async_openai: MagicMock,
) -> None:
@@ -151,7 +151,7 @@ def test_openai_assistants_client_init_auto_create_client(
assert not chat_client._should_delete_assistant # type: ignore
def test_openai_assistants_client_init_validation_fail() -> None:
def test_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
@@ -159,7 +159,7 @@ def test_openai_assistants_client_init_validation_fail() -> None:
@pytest.mark.parametrize("exclude_list", [["OPENAI_CHAT_MODEL_ID"]], indirect=True)
def test_openai_assistants_client_init_missing_model_id(openai_unit_test_env: dict[str, str]) -> None:
def test_init_missing_model_id(openai_unit_test_env: dict[str, str]) -> None:
"""Test OpenAIAssistantsClient initialization with missing model ID."""
with pytest.raises(ServiceInitializationError):
OpenAIAssistantsClient(
@@ -168,13 +168,13 @@ def test_openai_assistants_client_init_missing_model_id(openai_unit_test_env: di
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
def test_openai_assistants_client_init_missing_api_key(openai_unit_test_env: dict[str, str]) -> None:
def test_init_missing_api_key(openai_unit_test_env: dict[str, str]) -> None:
"""Test OpenAIAssistantsClient initialization with missing API key."""
with pytest.raises(ServiceInitializationError):
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:
def test_init_with_default_headers(openai_unit_test_env: dict[str, str]) -> None:
"""Test OpenAIAssistantsClient initialization with default headers."""
default_headers = {"X-Unit-Test": "test-guid"}
@@ -193,7 +193,7 @@ def test_openai_assistants_client_init_with_default_headers(openai_unit_test_env
assert chat_client.client.default_headers[key] == value
async def test_openai_assistants_client_get_assistant_id_or_create_existing_assistant(
async def test_get_assistant_id_or_create_existing_assistant(
mock_async_openai: MagicMock,
) -> None:
"""Test _get_assistant_id_or_create when assistant_id is already provided."""
@@ -206,7 +206,7 @@ async def test_openai_assistants_client_get_assistant_id_or_create_existing_assi
mock_async_openai.beta.assistants.create.assert_not_called()
async def test_openai_assistants_client_get_assistant_id_or_create_create_new(
async def test_get_assistant_id_or_create_create_new(
mock_async_openai: MagicMock,
) -> None:
"""Test _get_assistant_id_or_create when creating a new assistant."""
@@ -221,7 +221,7 @@ async def test_openai_assistants_client_get_assistant_id_or_create_create_new(
mock_async_openai.beta.assistants.create.assert_called_once()
async def test_openai_assistants_client_aclose_should_not_delete(
async def test_aclose_should_not_delete(
mock_async_openai: MagicMock,
) -> None:
"""Test close when assistant should not be deleted."""
@@ -236,7 +236,7 @@ async def test_openai_assistants_client_aclose_should_not_delete(
assert not chat_client._should_delete_assistant # type: ignore
async def test_openai_assistants_client_aclose_should_delete(mock_async_openai: MagicMock) -> None:
async def test_aclose_should_delete(mock_async_openai: MagicMock) -> None:
"""Test close method calls cleanup."""
chat_client = create_test_openai_assistants_client(
mock_async_openai, assistant_id="assistant-to-delete", should_delete_assistant=True
@@ -249,7 +249,7 @@ async def test_openai_assistants_client_aclose_should_delete(mock_async_openai:
assert not chat_client._should_delete_assistant # type: ignore
async def test_openai_assistants_client_async_context_manager(mock_async_openai: MagicMock) -> None:
async def test_async_context_manager(mock_async_openai: MagicMock) -> None:
"""Test async context manager functionality."""
chat_client = create_test_openai_assistants_client(
mock_async_openai, assistant_id="assistant-to-delete", should_delete_assistant=True
@@ -263,7 +263,7 @@ async def test_openai_assistants_client_async_context_manager(mock_async_openai:
mock_async_openai.beta.assistants.delete.assert_called_once_with("assistant-to-delete")
def test_openai_assistants_client_serialize(openai_unit_test_env: dict[str, str]) -> None:
def test_serialize(openai_unit_test_env: dict[str, str]) -> None:
"""Test serialization of OpenAIAssistantsClient."""
default_headers = {"X-Unit-Test": "test-guid"}
@@ -294,7 +294,7 @@ def test_openai_assistants_client_serialize(openai_unit_test_env: dict[str, str]
assert "User-Agent" not in dumped_settings["default_headers"]
async def test_openai_assistants_client_get_active_thread_run_none_thread_id(mock_async_openai: MagicMock) -> None:
async def test_get_active_thread_run_none_thread_id(mock_async_openai: MagicMock) -> None:
"""Test _get_active_thread_run with None thread_id returns None."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -305,7 +305,7 @@ async def test_openai_assistants_client_get_active_thread_run_none_thread_id(moc
mock_async_openai.beta.threads.runs.list.assert_not_called()
async def test_openai_assistants_client_get_active_thread_run_with_active_run(mock_async_openai: MagicMock) -> None:
async def test_get_active_thread_run_with_active_run(mock_async_openai: MagicMock) -> None:
"""Test _get_active_thread_run finds an active run."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -326,7 +326,7 @@ async def test_openai_assistants_client_get_active_thread_run_with_active_run(mo
mock_async_openai.beta.threads.runs.list.assert_called_once_with(thread_id="thread-123", limit=1, order="desc")
async def test_openai_assistants_client_prepare_thread_create_new(mock_async_openai: MagicMock) -> None:
async def test_prepare_thread_create_new(mock_async_openai: MagicMock) -> None:
"""Test _prepare_thread creates new thread when thread_id is None."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -353,7 +353,7 @@ async def test_openai_assistants_client_prepare_thread_create_new(mock_async_ope
)
async def test_openai_assistants_client_prepare_thread_cancel_existing_run(mock_async_openai: MagicMock) -> None:
async def test_prepare_thread_cancel_existing_run(mock_async_openai: MagicMock) -> None:
"""Test _prepare_thread cancels existing run when provided."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -369,7 +369,7 @@ async def test_openai_assistants_client_prepare_thread_cancel_existing_run(mock_
mock_async_openai.beta.threads.runs.cancel.assert_called_once_with(run_id="run-456", thread_id="thread-123")
async def test_openai_assistants_client_prepare_thread_existing_no_run(mock_async_openai: MagicMock) -> None:
async def test_prepare_thread_existing_no_run(mock_async_openai: MagicMock) -> None:
"""Test _prepare_thread with existing thread_id but no active run."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -382,7 +382,7 @@ async def test_openai_assistants_client_prepare_thread_existing_no_run(mock_asyn
mock_async_openai.beta.threads.runs.cancel.assert_not_called()
async def test_openai_assistants_client_process_stream_events_thread_run_created(mock_async_openai: MagicMock) -> None:
async def test_process_stream_events_thread_run_created(mock_async_openai: MagicMock) -> None:
"""Test _process_stream_events with thread.run.created event."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -415,7 +415,7 @@ async def test_openai_assistants_client_process_stream_events_thread_run_created
assert update.raw_representation == mock_response.data
async def test_openai_assistants_client_process_stream_events_message_delta_text(mock_async_openai: MagicMock) -> None:
async def test_process_stream_events_message_delta_text(mock_async_openai: MagicMock) -> None:
"""Test _process_stream_events with thread.message.delta event containing text."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -459,7 +459,7 @@ async def test_openai_assistants_client_process_stream_events_message_delta_text
assert update.raw_representation == mock_message_delta
async def test_openai_assistants_client_process_stream_events_requires_action(mock_async_openai: MagicMock) -> None:
async def test_process_stream_events_requires_action(mock_async_openai: MagicMock) -> None:
"""Test _process_stream_events with thread.run.requires_action event."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -502,7 +502,7 @@ async def test_openai_assistants_client_process_stream_events_requires_action(mo
chat_client._parse_function_calls_from_assistants.assert_called_once_with(mock_run, None) # type: ignore
async def test_openai_assistants_client_process_stream_events_run_step_created(mock_async_openai: MagicMock) -> None:
async def test_process_stream_events_run_step_created(mock_async_openai: MagicMock) -> None:
"""Test _process_stream_events with thread.run.step.created event."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -534,7 +534,7 @@ async def test_openai_assistants_client_process_stream_events_run_step_created(m
assert len(updates) == 0
async def test_openai_assistants_client_process_stream_events_run_completed_with_usage(
async def test_process_stream_events_run_completed_with_usage(
mock_async_openai: MagicMock,
) -> None:
"""Test _process_stream_events with thread.run.completed event containing usage."""
@@ -585,7 +585,7 @@ async def test_openai_assistants_client_process_stream_events_run_completed_with
assert update.raw_representation == mock_run
def test_openai_assistants_client_parse_function_calls_from_assistants_basic(mock_async_openai: MagicMock) -> None:
def test_parse_function_calls_from_assistants_basic(mock_async_openai: MagicMock) -> None:
"""Test _parse_function_calls_from_assistants with a simple function call."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -614,22 +614,22 @@ def test_openai_assistants_client_parse_function_calls_from_assistants_basic(moc
assert contents[0].arguments == {"location": "Seattle"}
def test_openai_assistants_client_prepare_options_basic(mock_async_openai: MagicMock) -> None:
def test_prepare_options_basic(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with basic chat options."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
# Create basic chat options
chat_options = ChatOptions(
max_tokens=100,
model_id="gpt-4",
temperature=0.7,
top_p=0.9,
)
# Create basic chat options as a dict
options = {
"max_tokens": 100,
"model_id": "gpt-4",
"temperature": 0.7,
"top_p": 0.9,
}
messages = [ChatMessage(role=Role.USER, text="Hello")]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, chat_options) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore
# Check basic options were set
assert run_options["max_completion_tokens"] == 100
@@ -639,7 +639,7 @@ def test_openai_assistants_client_prepare_options_basic(mock_async_openai: Magic
assert tool_results is None
def test_openai_assistants_client_prepare_options_with_ai_function_tool(mock_async_openai: MagicMock) -> None:
def test_prepare_options_with_ai_function_tool(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with AIFunction tool."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -650,15 +650,15 @@ def test_openai_assistants_client_prepare_options_with_ai_function_tool(mock_asy
"""A test function."""
return f"Result for {query}"
chat_options = ChatOptions(
tools=[test_function],
tool_choice="auto",
)
options = {
"tools": [test_function],
"tool_choice": "auto",
}
messages = [ChatMessage(role=Role.USER, text="Hello")]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, chat_options) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore
# Check tools were set correctly
assert "tools" in run_options
@@ -668,22 +668,22 @@ def test_openai_assistants_client_prepare_options_with_ai_function_tool(mock_asy
assert run_options["tool_choice"] == "auto"
def test_openai_assistants_client_prepare_options_with_code_interpreter(mock_async_openai: MagicMock) -> None:
def test_prepare_options_with_code_interpreter(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with HostedCodeInterpreterTool."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
# Create a real HostedCodeInterpreterTool
code_tool = HostedCodeInterpreterTool()
chat_options = ChatOptions(
tools=[code_tool],
tool_choice="auto",
)
options = {
"tools": [code_tool],
"tool_choice": "auto",
}
messages = [ChatMessage(role=Role.USER, text="Calculate something")]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, chat_options) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore
# Check code interpreter tool was set correctly
assert "tools" in run_options
@@ -692,39 +692,39 @@ def test_openai_assistants_client_prepare_options_with_code_interpreter(mock_asy
assert run_options["tool_choice"] == "auto"
def test_openai_assistants_client_prepare_options_tool_choice_none(mock_async_openai: MagicMock) -> None:
def test_prepare_options_tool_choice_none(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with tool_choice set to 'none'."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
chat_options = ChatOptions(
tool_choice="none",
)
options = {
"tool_choice": "none",
}
messages = [ChatMessage(role=Role.USER, text="Hello")]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, chat_options) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore
# Should set tool_choice to none and not include tools
assert run_options["tool_choice"] == "none"
assert "tools" not in run_options
def test_openai_assistants_client_prepare_options_required_function(mock_async_openai: MagicMock) -> None:
def test_prepare_options_required_function(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with required function tool choice."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
# Create a required function tool choice
tool_choice = ToolMode(mode="required", required_function_name="specific_function")
# Create a required function tool choice as dict
tool_choice = {"mode": "required", "required_function_name": "specific_function"}
chat_options = ChatOptions(
tool_choice=tool_choice,
)
options = {
"tool_choice": tool_choice,
}
messages = [ChatMessage(role=Role.USER, text="Hello")]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, chat_options) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore
# Check required function tool choice was set correctly
expected_tool_choice = {
@@ -734,7 +734,7 @@ def test_openai_assistants_client_prepare_options_required_function(mock_async_o
assert run_options["tool_choice"] == expected_tool_choice
def test_openai_assistants_client_prepare_options_with_file_search_tool(mock_async_openai: MagicMock) -> None:
def test_prepare_options_with_file_search_tool(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with HostedFileSearchTool."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -742,15 +742,15 @@ def test_openai_assistants_client_prepare_options_with_file_search_tool(mock_asy
# Create a HostedFileSearchTool with max_results
file_search_tool = HostedFileSearchTool(max_results=10)
chat_options = ChatOptions(
tools=[file_search_tool],
tool_choice="auto",
)
options = {
"tools": [file_search_tool],
"tool_choice": "auto",
}
messages = [ChatMessage(role=Role.USER, text="Search for information")]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, chat_options) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore
# Check file search tool was set correctly
assert "tools" in run_options
@@ -760,22 +760,22 @@ def test_openai_assistants_client_prepare_options_with_file_search_tool(mock_asy
assert run_options["tool_choice"] == "auto"
def test_openai_assistants_client_prepare_options_with_mapping_tool(mock_async_openai: MagicMock) -> None:
def test_prepare_options_with_mapping_tool(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with MutableMapping tool."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
# Create a tool as a MutableMapping (dict)
mapping_tool = {"type": "custom_tool", "parameters": {"setting": "value"}}
chat_options = ChatOptions(
tools=[mapping_tool], # type: ignore
tool_choice="auto",
)
options = {
"tools": [mapping_tool], # type: ignore
"tool_choice": "auto",
}
messages = [ChatMessage(role=Role.USER, text="Use custom tool")]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, chat_options) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore
# Check mapping tool was set correctly
assert "tools" in run_options
@@ -784,7 +784,7 @@ def test_openai_assistants_client_prepare_options_with_mapping_tool(mock_async_o
assert run_options["tool_choice"] == "auto"
def test_openai_assistants_client_prepare_options_with_system_message(mock_async_openai: MagicMock) -> None:
def test_prepare_options_with_system_message(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with system message converted to instructions."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -794,7 +794,7 @@ def test_openai_assistants_client_prepare_options_with_system_message(mock_async
]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, None) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, {}) # type: ignore
# Check that additional_messages only contains the user message
# System message should be converted to instructions (though this is handled internally)
@@ -803,7 +803,7 @@ def test_openai_assistants_client_prepare_options_with_system_message(mock_async
assert run_options["additional_messages"][0]["role"] == "user"
def test_openai_assistants_client_prepare_options_with_image_content(mock_async_openai: MagicMock) -> None:
def test_prepare_options_with_image_content(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with image content."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -813,7 +813,7 @@ def test_openai_assistants_client_prepare_options_with_image_content(mock_async_
messages = [ChatMessage(role=Role.USER, contents=[image_content])]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, None) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, {}) # type: ignore
# Check that image content was processed
assert "additional_messages" in run_options
@@ -825,7 +825,7 @@ def test_openai_assistants_client_prepare_options_with_image_content(mock_async_
assert message["content"][0]["image_url"]["url"] == "https://example.com/image.jpg"
def test_openai_assistants_client_prepare_tool_outputs_for_assistants_empty(mock_async_openai: MagicMock) -> None:
def test_prepare_tool_outputs_for_assistants_empty(mock_async_openai: MagicMock) -> None:
"""Test _prepare_tool_outputs_for_assistants with empty list."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -835,7 +835,7 @@ def test_openai_assistants_client_prepare_tool_outputs_for_assistants_empty(mock
assert tool_outputs is None
def test_openai_assistants_client_prepare_tool_outputs_for_assistants_valid(mock_async_openai: MagicMock) -> None:
def test_prepare_tool_outputs_for_assistants_valid(mock_async_openai: MagicMock) -> None:
"""Test _prepare_tool_outputs_for_assistants with valid function results."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -851,7 +851,7 @@ def test_openai_assistants_client_prepare_tool_outputs_for_assistants_valid(mock
assert tool_outputs[0].get("output") == "Function executed successfully"
def test_openai_assistants_client_prepare_tool_outputs_for_assistants_mismatched_run_ids(
def test_prepare_tool_outputs_for_assistants_mismatched_run_ids(
mock_async_openai: MagicMock,
) -> None:
"""Test _prepare_tool_outputs_for_assistants with mismatched run IDs."""
@@ -872,7 +872,7 @@ def test_openai_assistants_client_prepare_tool_outputs_for_assistants_mismatched
assert tool_outputs[0].get("tool_call_id") == "call-456"
def test_openai_assistants_client_update_agent_name_and_description(mock_async_openai: MagicMock) -> None:
def test_update_agent_name_and_description(mock_async_openai: MagicMock) -> None:
"""Test _update_agent_name_and_description method updates assistant_name when not already set."""
# Test updating agent name when assistant_name is None
chat_client = create_test_openai_assistants_client(mock_async_openai, assistant_name=None)
@@ -883,7 +883,7 @@ def test_openai_assistants_client_update_agent_name_and_description(mock_async_o
assert chat_client.assistant_name == "New Assistant Name"
def test_openai_assistants_client_update_agent_name_and_description_existing(mock_async_openai: MagicMock) -> None:
def test_update_agent_name_and_description_existing(mock_async_openai: MagicMock) -> None:
"""Test _update_agent_name_and_description method doesn't override existing assistant_name."""
# Test that existing assistant_name is not overridden
chat_client = create_test_openai_assistants_client(mock_async_openai, assistant_name="Existing Assistant")
@@ -895,7 +895,7 @@ def test_openai_assistants_client_update_agent_name_and_description_existing(moc
assert chat_client.assistant_name == "Existing Assistant"
def test_openai_assistants_client_update_agent_name_and_description_none(mock_async_openai: MagicMock) -> None:
def test_update_agent_name_and_description_none(mock_async_openai: MagicMock) -> None:
"""Test _update_agent_name_and_description method with None agent_name parameter."""
# Test that None agent_name doesn't change anything
chat_client = create_test_openai_assistants_client(mock_async_openai, assistant_name=None)
@@ -916,9 +916,9 @@ def get_weather(
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_client_get_response() -> None:
async def test_get_response() -> None:
"""Test OpenAI Assistants Client response."""
async with OpenAIAssistantsClient() as openai_assistants_client:
async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client:
assert isinstance(openai_assistants_client, ChatClientProtocol)
messages: list[ChatMessage] = []
@@ -941,9 +941,9 @@ async def test_openai_assistants_client_get_response() -> None:
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_client_get_response_tools() -> None:
async def test_get_response_tools() -> None:
"""Test OpenAI Assistants Client response with tools."""
async with OpenAIAssistantsClient() as openai_assistants_client:
async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client:
assert isinstance(openai_assistants_client, ChatClientProtocol)
messages: list[ChatMessage] = []
@@ -952,8 +952,7 @@ async def test_openai_assistants_client_get_response_tools() -> None:
# Test that the client can be used to get a response
response = await openai_assistants_client.get_response(
messages=messages,
tools=[get_weather],
tool_choice="auto",
options={"tools": [get_weather], "tool_choice": "auto"},
)
assert response is not None
@@ -963,9 +962,9 @@ async def test_openai_assistants_client_get_response_tools() -> None:
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_client_streaming() -> None:
async def test_streaming() -> None:
"""Test OpenAI Assistants Client streaming response."""
async with OpenAIAssistantsClient() as openai_assistants_client:
async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client:
assert isinstance(openai_assistants_client, ChatClientProtocol)
messages: list[ChatMessage] = []
@@ -994,9 +993,9 @@ async def test_openai_assistants_client_streaming() -> None:
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_client_streaming_tools() -> None:
async def test_streaming_tools() -> None:
"""Test OpenAI Assistants Client streaming response with tools."""
async with OpenAIAssistantsClient() as openai_assistants_client:
async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client:
assert isinstance(openai_assistants_client, ChatClientProtocol)
messages: list[ChatMessage] = []
@@ -1005,8 +1004,10 @@ async def test_openai_assistants_client_streaming_tools() -> None:
# Test that the client can be used to get a response
response = openai_assistants_client.get_streaming_response(
messages=messages,
tools=[get_weather],
tool_choice="auto",
options={
"tools": [get_weather],
"tool_choice": "auto",
},
)
full_message: str = ""
async for chunk in response:
@@ -1021,10 +1022,10 @@ async def test_openai_assistants_client_streaming_tools() -> None:
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_client_with_existing_assistant() -> None:
async def test_with_existing_assistant() -> None:
"""Test OpenAI Assistants Client with existing assistant ID."""
# First create an assistant to use in the test
async with OpenAIAssistantsClient() as temp_client:
async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as temp_client:
# Get the assistant ID by triggering assistant creation
messages = [ChatMessage(role="user", text="Hello")]
await temp_client.get_response(messages=messages)
@@ -1032,7 +1033,7 @@ async def test_openai_assistants_client_with_existing_assistant() -> None:
# Now test using the existing assistant
async with OpenAIAssistantsClient(
model_id="gpt-4o-mini", assistant_id=assistant_id
model_id=INTEGRATION_TEST_MODEL, assistant_id=assistant_id
) as openai_assistants_client:
assert isinstance(openai_assistants_client, ChatClientProtocol)
assert openai_assistants_client.assistant_id == assistant_id
@@ -1050,9 +1051,9 @@ async def test_openai_assistants_client_with_existing_assistant() -> None:
@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:
async def test_file_search() -> None:
"""Test OpenAI Assistants Client response."""
async with OpenAIAssistantsClient() as openai_assistants_client:
async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client:
assert isinstance(openai_assistants_client, ChatClientProtocol)
messages: list[ChatMessage] = []
@@ -1061,8 +1062,10 @@ async def test_openai_assistants_client_file_search() -> None:
file_id, vector_store = await create_vector_store(openai_assistants_client)
response = await openai_assistants_client.get_response(
messages=messages,
tools=[HostedFileSearchTool()],
tool_resources={"file_search": {"vector_store_ids": [vector_store.vector_store_id]}},
options={
"tools": [HostedFileSearchTool()],
"tool_resources": {"file_search": {"vector_store_ids": [vector_store.vector_store_id]}},
},
)
await delete_vector_store(openai_assistants_client, file_id, vector_store.vector_store_id)
@@ -1074,9 +1077,9 @@ async def test_openai_assistants_client_file_search() -> None:
@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:
async def test_file_search_streaming() -> None:
"""Test OpenAI Assistants Client response."""
async with OpenAIAssistantsClient() as openai_assistants_client:
async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as openai_assistants_client:
assert isinstance(openai_assistants_client, ChatClientProtocol)
messages: list[ChatMessage] = []
@@ -1085,8 +1088,10 @@ async def test_openai_assistants_client_file_search_streaming() -> None:
file_id, vector_store = await create_vector_store(openai_assistants_client)
response = openai_assistants_client.get_streaming_response(
messages=messages,
tools=[HostedFileSearchTool()],
tool_resources={"file_search": {"vector_store_ids": [vector_store.vector_store_id]}},
options={
"tools": [HostedFileSearchTool()],
"tool_resources": {"file_search": {"vector_store_ids": [vector_store.vector_store_id]}},
},
)
assert response is not None
@@ -1107,7 +1112,7 @@ async def test_openai_assistants_client_file_search_streaming() -> None:
async def test_openai_assistants_agent_basic_run():
"""Test ChatAgent basic run functionality with OpenAIAssistantsClient."""
async with ChatAgent(
chat_client=OpenAIAssistantsClient(),
chat_client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL),
) as agent:
# Run a simple query
response = await agent.run("Hello! Please respond with 'Hello World' exactly.")
@@ -1124,7 +1129,7 @@ async def test_openai_assistants_agent_basic_run():
async def test_openai_assistants_agent_basic_run_streaming():
"""Test ChatAgent basic streaming functionality with OpenAIAssistantsClient."""
async with ChatAgent(
chat_client=OpenAIAssistantsClient(),
chat_client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL),
) as agent:
# Run streaming query
full_message: str = ""
@@ -1144,7 +1149,7 @@ async def test_openai_assistants_agent_basic_run_streaming():
async def test_openai_assistants_agent_thread_persistence():
"""Test ChatAgent thread persistence across runs with OpenAIAssistantsClient."""
async with ChatAgent(
chat_client=OpenAIAssistantsClient(),
chat_client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL),
instructions="You are a helpful assistant with good memory.",
) as agent:
# Create a new thread that will be reused
@@ -1176,7 +1181,7 @@ async def test_openai_assistants_agent_existing_thread_id():
existing_thread_id = None
async with ChatAgent(
chat_client=OpenAIAssistantsClient(),
chat_client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL),
instructions="You are a helpful weather agent.",
tools=[get_weather],
) as agent:
@@ -1219,7 +1224,7 @@ async def test_openai_assistants_agent_code_interpreter():
"""Test ChatAgent with code interpreter through OpenAIAssistantsClient."""
async with ChatAgent(
chat_client=OpenAIAssistantsClient(),
chat_client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL),
instructions="You are a helpful assistant that can write and execute Python code.",
tools=[HostedCodeInterpreterTool()],
) as agent:
@@ -1235,11 +1240,11 @@ async def test_openai_assistants_agent_code_interpreter():
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_assistants_client_agent_level_tool_persistence():
async def test_agent_level_tool_persistence():
"""Test that agent-level tools persist across multiple runs with OpenAI Assistants Client."""
async with ChatAgent(
chat_client=OpenAIAssistantsClient(),
chat_client=OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL),
instructions="You are a helpful assistant that uses available tools.",
tools=[get_weather], # Agent-level tool
) as agent:
@@ -1261,7 +1266,7 @@ async def test_openai_assistants_client_agent_level_tool_persistence():
# Callable API Key Tests
def test_openai_assistants_client_with_callable_api_key() -> None:
def test_with_callable_api_key() -> None:
"""Test OpenAIAssistantsClient initialization with callable API key."""
async def get_api_key() -> str:
@@ -1,25 +1,22 @@
# Copyright (c) Microsoft. All rights reserved.
import json
import os
from typing import Annotated
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
from openai import BadRequestError
from pydantic import BaseModel
from pytest import param
from agent_framework import (
AgentRunResponse,
AgentRunResponseUpdate,
ChatAgent,
ChatClientProtocol,
ChatMessage,
ChatOptions,
ChatResponse,
ChatResponseUpdate,
DataContent,
FunctionResultContent,
HostedWebSearchTool,
TextContent,
ToolProtocol,
ai_function,
prepare_function_call_results,
@@ -170,7 +167,7 @@ async def test_content_filter_exception_handling(openai_unit_test_env: dict[str,
patch.object(client.client.chat.completions, "create", side_effect=mock_error),
pytest.raises(OpenAIContentFilterException),
):
await client._inner_get_response(messages=messages, chat_options=ChatOptions()) # type: ignore
await client._inner_get_response(messages=messages, options={}) # type: ignore
def test_unsupported_tool_handling(openai_unit_test_env: dict[str, str]) -> None:
@@ -183,12 +180,12 @@ def test_unsupported_tool_handling(openai_unit_test_env: dict[str, str]) -> None
# This should ignore the unsupported ToolProtocol and return empty list
result = client._prepare_tools_for_openai([unsupported_tool]) # type: ignore
assert result == []
assert result == {}
# Also test with a non-ToolProtocol that should be converted to dict
dict_tool = {"type": "function", "name": "test"}
result = client._prepare_tools_for_openai([dict_tool]) # type: ignore
assert result == [dict_tool]
assert result["tools"] == [dict_tool]
@ai_function
@@ -208,407 +205,6 @@ 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."""
openai_chat_client = OpenAIChatClient()
assert isinstance(openai_chat_client, ChatClientProtocol)
messages: list[ChatMessage] = []
messages.append(
ChatMessage(
role="user",
text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. "
"Bonded by their love for the natural world and shared curiosity, they uncovered a "
"groundbreaking phenomenon in glaciology that could potentially reshape our understanding "
"of climate change.",
)
)
messages.append(ChatMessage(role="user", text="who are Emily and David?"))
# Test that the client can be used to get a response
response = await openai_chat_client.get_response(messages=messages)
assert response is not None
assert isinstance(response, ChatResponse)
assert "scientists" in response.text
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_openai_chat_completion_response_params() -> None:
"""Test OpenAI chat completion responses."""
openai_chat_client = OpenAIChatClient()
assert isinstance(openai_chat_client, ChatClientProtocol)
messages: list[ChatMessage] = []
messages.append(
ChatMessage(
role="user",
text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. "
"Bonded by their love for the natural world and shared curiosity, they uncovered a "
"groundbreaking phenomenon in glaciology that could potentially reshape our understanding "
"of climate change.",
)
)
messages.append(ChatMessage(role="user", text="who are Emily and David?"))
# Test that the client can be used to get a response
response = await openai_chat_client.get_response(
messages=messages, chat_options=ChatOptions(max_tokens=150, temperature=0.7, top_p=0.9)
)
assert response is not None
assert isinstance(response, ChatResponse)
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."""
openai_chat_client = OpenAIChatClient()
assert isinstance(openai_chat_client, ChatClientProtocol)
messages: list[ChatMessage] = []
messages.append(ChatMessage(role="user", text="who are Emily and David?"))
# Test that the client can be used to get a response
response = await openai_chat_client.get_response(
messages=messages,
tools=[get_story_text],
tool_choice="auto",
)
assert response is not None
assert isinstance(response, ChatResponse)
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."""
openai_chat_client = OpenAIChatClient()
assert isinstance(openai_chat_client, ChatClientProtocol)
messages: list[ChatMessage] = []
messages.append(
ChatMessage(
role="user",
text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. "
"Bonded by their love for the natural world and shared curiosity, they uncovered a "
"groundbreaking phenomenon in glaciology that could potentially reshape our understanding "
"of climate change.",
)
)
messages.append(ChatMessage(role="user", text="who are Emily and David?"))
# Test that the client can be used to get a response
response = openai_chat_client.get_streaming_response(messages=messages)
full_message: str = ""
async for chunk in response:
assert chunk is not None
assert isinstance(chunk, ChatResponseUpdate)
assert chunk.message_id is not None
assert chunk.response_id is not None
for content in chunk.contents:
if isinstance(content, TextContent) and content.text:
full_message += content.text
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."""
openai_chat_client = OpenAIChatClient()
assert isinstance(openai_chat_client, ChatClientProtocol)
messages: list[ChatMessage] = []
messages.append(ChatMessage(role="user", text="who are Emily and David?"))
# Test that the client can be used to get a response
response = openai_chat_client.get_streaming_response(
messages=messages,
tools=[get_story_text],
tool_choice="auto",
)
full_message: str = ""
async for chunk in response:
assert chunk is not None
assert isinstance(chunk, ChatResponseUpdate)
for content in chunk.contents:
if isinstance(content, TextContent) and content.text:
full_message += content.text
assert "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(model_id="gpt-4o-search-preview")
assert isinstance(openai_chat_client, ChatClientProtocol)
# Test that the client will use the web search tool
response = await openai_chat_client.get_response(
messages=[
ChatMessage(
role="user",
text="Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.",
)
],
tools=[HostedWebSearchTool()],
tool_choice="auto",
)
assert response is not None
assert isinstance(response, ChatResponse)
assert "Rumi" in response.text
assert "Mira" in response.text
assert "Zoey" in response.text
# Test that the client will use the web search tool with location
additional_properties = {
"user_location": {
"country": "US",
"city": "Seattle",
}
}
response = await openai_chat_client.get_response(
messages=[ChatMessage(role="user", text="What is the current weather? Do not ask for my current location.")],
tools=[HostedWebSearchTool(additional_properties=additional_properties)],
tool_choice="auto",
)
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(model_id="gpt-4o-search-preview")
assert isinstance(openai_chat_client, ChatClientProtocol)
# Test that the client will use the web search tool
response = openai_chat_client.get_streaming_response(
messages=[
ChatMessage(
role="user",
text="Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.",
)
],
tools=[HostedWebSearchTool()],
tool_choice="auto",
)
assert response is not None
full_message: str = ""
async for chunk in response:
assert chunk is not None
assert isinstance(chunk, ChatResponseUpdate)
for content in chunk.contents:
if isinstance(content, TextContent) and content.text:
full_message += content.text
assert "Rumi" in full_message
assert "Mira" in full_message
assert "Zoey" in full_message
# Test that the client will use the web search tool with location
additional_properties = {
"user_location": {
"country": "US",
"city": "Seattle",
}
}
response = openai_chat_client.get_streaming_response(
messages=[ChatMessage(role="user", text="What is the current weather? Do not ask for my current location.")],
tools=[HostedWebSearchTool(additional_properties=additional_properties)],
tool_choice="auto",
)
assert response is not None
full_message: str = ""
async for chunk in response:
assert chunk is not None
assert isinstance(chunk, ChatResponseUpdate)
for content in chunk.contents:
if isinstance(content, TextContent) and content.text:
full_message += content.text
assert 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(model_id="gpt-4o-search-preview"),
) as agent:
# Test basic run
response = await agent.run("Hello! Please respond with 'Hello World' exactly.")
assert isinstance(response, AgentRunResponse)
assert response.text is not None
assert len(response.text) > 0
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(model_id="gpt-4o-search-preview"),
) as agent:
# Test streaming run
full_text = ""
async for chunk in agent.run_stream("Please respond with exactly: 'This is a streaming response test.'"):
assert isinstance(chunk, AgentRunResponseUpdate)
if chunk.text:
full_text += chunk.text
assert len(full_text) > 0
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(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
thread = agent.get_new_thread()
# First interaction
response1 = await agent.run("My name is Alice. Remember this.", thread=thread)
assert isinstance(response1, AgentRunResponse)
assert response1.text is not None
# Second interaction - test memory
response2 = await agent.run("What is my name?", thread=thread)
assert isinstance(response2, AgentRunResponse)
assert response2.text is not None
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."""
# First conversation - capture the thread
preserved_thread = None
async with ChatAgent(
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
thread = first_agent.get_new_thread()
first_response = await first_agent.run("My name is Alice. Remember this.", thread=thread)
assert isinstance(first_response, AgentRunResponse)
assert first_response.text is not None
# Preserve the thread for reuse
preserved_thread = thread
# Second conversation - reuse the thread in a new agent instance
if preserved_thread:
async with ChatAgent(
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
second_response = await second_agent.run("What is my name?", thread=preserved_thread)
assert isinstance(second_response, AgentRunResponse)
assert second_response.text is not None
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(model_id="gpt-4.1"),
instructions="You are a helpful assistant that uses available tools.",
tools=[get_weather], # Agent-level tool
) as agent:
# First run - agent-level tool should be available
first_response = await agent.run("What's the weather like in Chicago?")
assert isinstance(first_response, AgentRunResponse)
assert first_response.text is not None
# Should use the agent-level weather tool
assert any(term in first_response.text.lower() for term in ["chicago", "sunny", "72"])
# Second run - agent-level tool should still be available (persistence test)
second_response = await agent.run("What's the weather in Miami?")
assert isinstance(second_response, AgentRunResponse)
assert second_response.text is not None
# Should use the agent-level weather tool again
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."""
# Counter to track how many times the weather tool is called
call_count = 0
@ai_function
async def get_weather_with_counter(location: Annotated[str, "The location as a city name"]) -> str:
"""Get the current weather in a given location."""
nonlocal call_count
call_count += 1
return f"The weather in {location} is sunny and 72°F."
async with ChatAgent(
chat_client=OpenAIChatClient(model_id="gpt-4.1"),
instructions="You are a helpful assistant.",
) as agent:
# First run - use run-level tool
first_response = await agent.run(
"What's the weather like in Chicago?",
tools=[get_weather_with_counter], # Run-level tool
)
assert isinstance(first_response, AgentRunResponse)
assert first_response.text is not None
# Should use the run-level weather tool (call count should be 1)
assert call_count == 1
assert any(term in first_response.text.lower() for term in ["chicago", "sunny", "72"])
# Second run - run-level tool should NOT persist (key isolation test)
second_response = await agent.run("What's the weather like in Miami?")
assert isinstance(second_response, AgentRunResponse)
assert second_response.text is not None
# Should NOT use the weather tool since it was only run-level in previous call
# Call count should still be 1 (no additional calls)
assert call_count == 1
async def test_exception_message_includes_original_error_details() -> None:
"""Test that exception messages include original error details in the new format."""
client = OpenAIChatClient(model_id="test-model", api_key="test-key")
@@ -627,7 +223,7 @@ async def test_exception_message_includes_original_error_details() -> None:
patch.object(client.client.chat.completions, "create", side_effect=mock_error),
pytest.raises(ServiceResponseException) as exc_info,
):
await client._inner_get_response(messages=messages, chat_options=ChatOptions()) # type: ignore
await client._inner_get_response(messages=messages, options={}) # type: ignore
exception_message = str(exc_info.value)
assert "service failed to complete the prompt:" in exception_message
@@ -667,7 +263,7 @@ def test_chat_response_content_order_text_before_tool_calls(openai_unit_test_env
)
client = OpenAIChatClient()
response = client._parse_response_from_openai(mock_response, ChatOptions())
response = client._parse_response_from_openai(mock_response, {})
# Verify we have both text and tool call content
assert len(response.messages) == 1
@@ -894,3 +490,191 @@ def test_prepare_content_for_openai_document_file_mapping(openai_unit_test_env:
assert result["type"] == "file"
assert "filename" not in result["file"] # None filename should be omitted
# region Integration Tests
class OutputStruct(BaseModel):
"""A structured output for testing purposes."""
location: str
weather: str | None = None
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
@pytest.mark.parametrize(
"option_name,option_value,needs_validation",
[
# Simple ChatOptions - just verify they don't fail
param("temperature", 0.7, False, id="temperature"),
param("top_p", 0.9, False, id="top_p"),
param("max_tokens", 500, False, id="max_tokens"),
param("seed", 123, False, id="seed"),
param("user", "test-user-id", False, id="user"),
param("frequency_penalty", 0.5, False, id="frequency_penalty"),
param("presence_penalty", 0.3, False, id="presence_penalty"),
param("stop", ["END"], False, id="stop"),
param("allow_multiple_tool_calls", True, False, id="allow_multiple_tool_calls"),
# OpenAIChatOptions - just verify they don't fail
param("logit_bias", {"50256": -1}, False, id="logit_bias"),
param("prediction", {"type": "content", "content": "hello world"}, False, id="prediction"),
# Complex options requiring output validation
param("tools", [get_weather], True, id="tools_function"),
param("tool_choice", "auto", True, id="tool_choice_auto"),
param("tool_choice", "none", True, id="tool_choice_none"),
param("tool_choice", "required", True, id="tool_choice_required_any"),
param(
"tool_choice",
{"mode": "required", "required_function_name": "get_weather"},
True,
id="tool_choice_required",
),
param("response_format", OutputStruct, True, id="response_format_pydantic"),
param(
"response_format",
{
"type": "json_schema",
"json_schema": {
"name": "WeatherDigest",
"strict": True,
"schema": {
"title": "WeatherDigest",
"type": "object",
"properties": {
"location": {"type": "string"},
"conditions": {"type": "string"},
"temperature_c": {"type": "number"},
"advisory": {"type": "string"},
},
"required": ["location", "conditions", "temperature_c", "advisory"],
"additionalProperties": False,
},
},
},
True,
id="response_format_runtime_json_schema",
),
],
)
async def test_integration_options(
option_name: str,
option_value: Any,
needs_validation: bool,
) -> None:
"""Parametrized test covering all ChatOptions and OpenAIChatOptions.
Tests both streaming and non-streaming modes for each option to ensure
they don't cause failures. Options marked with needs_validation also
check that the feature actually works correctly.
"""
client = OpenAIChatClient()
# to ensure toolmode required does not endlessly loop
client.function_invocation_configuration.max_iterations = 1
for streaming in [False, True]:
# Prepare test message
if option_name.startswith("tools") or option_name.startswith("tool_choice"):
# Use weather-related prompt for tool tests
messages = [ChatMessage(role="user", text="What is the weather in Seattle?")]
elif option_name.startswith("response_format"):
# Use prompt that works well with structured output
messages = [ChatMessage(role="user", text="The weather in Seattle is sunny")]
messages.append(ChatMessage(role="user", text="What is the weather in Seattle?"))
else:
# Generic prompt for simple options
messages = [ChatMessage(role="user", text="Say 'Hello World' briefly.")]
# Build options dict
options: dict[str, Any] = {option_name: option_value}
# Add tools if testing tool_choice to avoid errors
if option_name.startswith("tool_choice"):
options["tools"] = [get_weather]
if streaming:
# Test streaming mode
response_gen = client.get_streaming_response(
messages=messages,
options=options,
)
output_format = option_value if option_name.startswith("response_format") else None
response = await ChatResponse.from_chat_response_generator(response_gen, output_format_type=output_format)
else:
# Test non-streaming mode
response = await client.get_response(
messages=messages,
options=options,
)
assert response is not None
assert isinstance(response, ChatResponse)
assert response.text is not None, f"No text in response for option '{option_name}'"
assert len(response.text) > 0, f"Empty response for option '{option_name}'"
# Validate based on option type
if needs_validation:
if option_name.startswith("tools") or option_name.startswith("tool_choice"):
# Should have called the weather function
text = response.text.lower()
assert "sunny" in text or "seattle" in text, f"Tool not invoked for {option_name}"
elif option_name.startswith("response_format"):
if option_value == OutputStruct:
# Should have structured output
assert response.value is not None, "No structured output"
assert isinstance(response.value, OutputStruct)
assert "seattle" in response.value.location.lower()
else:
# Runtime JSON schema
assert response.value is None, "No structured output, can't parse any json."
response_value = json.loads(response.text)
assert isinstance(response_value, dict)
assert "location" in response_value
assert "seattle" in response_value["location"].lower()
@pytest.mark.flaky
@skip_if_openai_integration_tests_disabled
async def test_integration_web_search() -> None:
client = OpenAIChatClient(model_id="gpt-4o-search-preview")
for streaming in [False, True]:
content = {
"messages": "Who are the main characters of Kpop Demon Hunters? Do a web search to find the answer.",
"options": {
"tool_choice": "auto",
"tools": [HostedWebSearchTool()],
},
}
if streaming:
response = await ChatResponse.from_chat_response_generator(client.get_streaming_response(**content))
else:
response = await client.get_response(**content)
assert response is not None
assert isinstance(response, ChatResponse)
assert "Rumi" in response.text
assert "Mira" in response.text
assert "Zoey" in response.text
# Test that the client will use the web search tool with location
additional_properties = {
"user_location": {
"country": "US",
"city": "Seattle",
}
}
content = {
"messages": "What is the current weather? Do not ask for my current location.",
"options": {
"tool_choice": "auto",
"tools": [HostedWebSearchTool(additional_properties=additional_properties)],
},
}
if streaming:
response = await ChatResponse.from_chat_response_generator(client.get_streaming_response(**content))
else:
response = await client.get_response(**content)
assert response.text is not None
@@ -115,7 +115,6 @@ async def test_cmc_no_fcc_in_response(
openai_chat_completion = OpenAIChatClient()
await openai_chat_completion.get_response(
messages=chat_history,
arguments={},
)
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
@@ -199,7 +198,7 @@ async def test_cmc_additional_properties(
chat_history.append(ChatMessage(role="user", text="hello world"))
openai_chat_completion = OpenAIChatClient()
await openai_chat_completion.get_response(messages=chat_history, additional_properties={"reasoning_effort": "low"})
await openai_chat_completion.get_response(messages=chat_history, options={"reasoning_effort": "low"})
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
stream=False,
@@ -382,8 +381,6 @@ def test_chat_response_created_at_uses_utc(openai_unit_test_env: dict[str, str])
This is a regression test for the issue where created_at was using local time
but labeling it as UTC (with 'Z' suffix).
"""
from agent_framework import ChatOptions
# Use a specific Unix timestamp: 1733011890 = 2024-12-01T00:31:30Z (UTC)
# This ensures we test that the timestamp is actually converted to UTC
utc_timestamp = 1733011890
@@ -399,7 +396,7 @@ def test_chat_response_created_at_uses_utc(openai_unit_test_env: dict[str, str])
)
client = OpenAIChatClient()
response = client._parse_response_from_openai(mock_response, ChatOptions())
response = client._parse_response_from_openai(mock_response, {})
# Verify that created_at is correctly formatted as UTC
assert response.created_at is not None
File diff suppressed because it is too large Load Diff