.NET: Python: OpenAI Responses Agent Completeness (#721)

* OpenAI Responses Agent Completeness

* prepare options

* added unit tests

* azure responses test fix

* resolved conflict

* pre commit fix

* Revert "Merge remote changes and resolve conflicts"

This reverts commit 56787f25a4, reversing
changes made to f71a27ebfe.

* Fixes

* azure responses file search fix

* Fix corrupted uv.lock file

---------

Co-authored-by: Giles Odigwe <gilesodigwe@microsoft.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
This commit is contained in:
Giles Odigwe
2025-09-15 09:19:53 -07:00
committed by GitHub
Unverified
parent 674a514cae
commit 0715e0f8d3
6 changed files with 596 additions and 429 deletions
@@ -620,8 +620,8 @@ def test_openai_assistants_client_create_function_call_contents_basic(mock_async
assert contents[0].arguments == {"location": "Seattle"}
def test_openai_assistants_client_create_run_options_basic(mock_async_openai: MagicMock) -> None:
"""Test _create_run_options with basic chat options."""
def test_openai_assistants_client_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
@@ -635,7 +635,7 @@ def test_openai_assistants_client_create_run_options_basic(mock_async_openai: Ma
messages = [ChatMessage(role=Role.USER, text="Hello")]
# Call the method
run_options, tool_results = chat_client._create_run_options(messages, chat_options) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, chat_options) # type: ignore
# Check basic options were set
assert run_options["max_completion_tokens"] == 100
@@ -645,8 +645,8 @@ def test_openai_assistants_client_create_run_options_basic(mock_async_openai: Ma
assert tool_results is None
def test_openai_assistants_client_create_run_options_with_ai_function_tool(mock_async_openai: MagicMock) -> None:
"""Test _create_run_options with AIFunction tool."""
def test_openai_assistants_client_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)
@@ -664,7 +664,7 @@ def test_openai_assistants_client_create_run_options_with_ai_function_tool(mock_
messages = [ChatMessage(role=Role.USER, text="Hello")]
# Call the method
run_options, tool_results = chat_client._create_run_options(messages, chat_options) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, chat_options) # type: ignore
# Check tools were set correctly
assert "tools" in run_options
@@ -674,8 +674,8 @@ def test_openai_assistants_client_create_run_options_with_ai_function_tool(mock_
assert run_options["tool_choice"] == "auto"
def test_openai_assistants_client_create_run_options_with_code_interpreter(mock_async_openai: MagicMock) -> None:
"""Test _create_run_options with HostedCodeInterpreterTool."""
def test_openai_assistants_client_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
@@ -689,7 +689,7 @@ def test_openai_assistants_client_create_run_options_with_code_interpreter(mock_
messages = [ChatMessage(role=Role.USER, text="Calculate something")]
# Call the method
run_options, tool_results = chat_client._create_run_options(messages, chat_options) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, chat_options) # type: ignore
# Check code interpreter tool was set correctly
assert "tools" in run_options
@@ -698,8 +698,8 @@ def test_openai_assistants_client_create_run_options_with_code_interpreter(mock_
assert run_options["tool_choice"] == "auto"
def test_openai_assistants_client_create_run_options_tool_choice_none(mock_async_openai: MagicMock) -> None:
"""Test _create_run_options with tool_choice set to 'none'."""
def test_openai_assistants_client_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(
@@ -709,15 +709,15 @@ def test_openai_assistants_client_create_run_options_tool_choice_none(mock_async
messages = [ChatMessage(role=Role.USER, text="Hello")]
# Call the method
run_options, tool_results = chat_client._create_run_options(messages, chat_options) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, chat_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_create_run_options_required_function(mock_async_openai: MagicMock) -> None:
"""Test _create_run_options with required function tool choice."""
def test_openai_assistants_client_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
@@ -730,7 +730,7 @@ def test_openai_assistants_client_create_run_options_required_function(mock_asyn
messages = [ChatMessage(role=Role.USER, text="Hello")]
# Call the method
run_options, tool_results = chat_client._create_run_options(messages, chat_options) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, chat_options) # type: ignore
# Check required function tool choice was set correctly
expected_tool_choice = {
@@ -740,8 +740,8 @@ def test_openai_assistants_client_create_run_options_required_function(mock_asyn
assert run_options["tool_choice"] == expected_tool_choice
def test_openai_assistants_client_create_run_options_with_file_search_tool(mock_async_openai: MagicMock) -> None:
"""Test _create_run_options with HostedFileSearchTool."""
def test_openai_assistants_client_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)
@@ -756,7 +756,7 @@ def test_openai_assistants_client_create_run_options_with_file_search_tool(mock_
messages = [ChatMessage(role=Role.USER, text="Search for information")]
# Call the method
run_options, tool_results = chat_client._create_run_options(messages, chat_options) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, chat_options) # type: ignore
# Check file search tool was set correctly
assert "tools" in run_options
@@ -766,8 +766,8 @@ def test_openai_assistants_client_create_run_options_with_file_search_tool(mock_
assert run_options["tool_choice"] == "auto"
def test_openai_assistants_client_create_run_options_with_mapping_tool(mock_async_openai: MagicMock) -> None:
"""Test _create_run_options with MutableMapping tool."""
def test_openai_assistants_client_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)
@@ -781,7 +781,7 @@ def test_openai_assistants_client_create_run_options_with_mapping_tool(mock_asyn
messages = [ChatMessage(role=Role.USER, text="Use custom tool")]
# Call the method
run_options, tool_results = chat_client._create_run_options(messages, chat_options) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, chat_options) # type: ignore
# Check mapping tool was set correctly
assert "tools" in run_options
@@ -790,8 +790,8 @@ def test_openai_assistants_client_create_run_options_with_mapping_tool(mock_asyn
assert run_options["tool_choice"] == "auto"
def test_openai_assistants_client_create_run_options_with_system_message(mock_async_openai: MagicMock) -> None:
"""Test _create_run_options with system message converted to instructions."""
def test_openai_assistants_client_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)
messages = [
@@ -800,7 +800,7 @@ def test_openai_assistants_client_create_run_options_with_system_message(mock_as
]
# Call the method
run_options, tool_results = chat_client._create_run_options(messages, None) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, None) # type: ignore
# Check that additional_messages only contains the user message
# System message should be converted to instructions (though this is handled internally)
@@ -809,8 +809,8 @@ def test_openai_assistants_client_create_run_options_with_system_message(mock_as
assert run_options["additional_messages"][0]["role"] == "user"
def test_openai_assistants_client_create_run_options_with_image_content(mock_async_openai: MagicMock) -> None:
"""Test _create_run_options with image content."""
def test_openai_assistants_client_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)
@@ -819,7 +819,7 @@ def test_openai_assistants_client_create_run_options_with_image_content(mock_asy
messages = [ChatMessage(role=Role.USER, contents=[image_content])]
# Call the method
run_options, tool_results = chat_client._create_run_options(messages, None) # type: ignore
run_options, tool_results = chat_client._prepare_options(messages, None) # type: ignore
# Check that image content was processed
assert "additional_messages" in run_options
@@ -186,37 +186,6 @@ def test_serialize_with_org_id(openai_unit_test_env: dict[str, str]) -> None:
assert "User-Agent" not in dumped_settings["default_headers"]
def test_filter_options_method(openai_unit_test_env: dict[str, str]) -> None:
"""Test that the _filter_options method filters out None values correctly."""
client = OpenAIResponsesClient()
# Test with a mix of None and non-None values
filtered = client._filter_options( # type: ignore
include=["usage"],
instructions="Test instruction",
max_tokens=None,
temperature=0.7,
seed=None,
model="test-model",
store=True,
top_p=None,
)
# Should only contain non-None values
expected = {
"include": ["usage"],
"instructions": "Test instruction",
"temperature": 0.7,
"model": "test-model",
"store": True,
}
assert filtered == expected
assert "max_tokens" not in filtered
assert "seed" not in filtered
assert "top_p" not in filtered
def test_get_response_with_invalid_input() -> None:
"""Test get_response with invalid inputs to trigger exception handling."""
@@ -977,7 +946,7 @@ async def test_openai_responses_client_response_tools() -> None:
@skip_if_openai_integration_tests_disabled
async def test_openai_responses_client_streaming() -> None:
"""Test Azure OpenAI chat completion responses."""
"""Test OpenAI chat completion responses."""
openai_responses_client = OpenAIResponsesClient()
assert isinstance(openai_responses_client, ChatClientProtocol)
@@ -1161,7 +1130,6 @@ async def test_openai_responses_client_web_search_streaming() -> None:
@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_responses_client_file_search() -> None:
openai_responses_client = OpenAIResponsesClient()
@@ -1186,7 +1154,6 @@ async def test_openai_responses_client_file_search() -> None:
@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_responses_client_streaming_file_search() -> None:
openai_responses_client = OpenAIResponsesClient()
@@ -1427,6 +1394,81 @@ async def test_openai_responses_client_run_level_tool_isolation():
assert call_count == 1
@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."""
async with ChatAgent(
chat_client=OpenAIResponsesClient(),
instructions="You are a helpful assistant.",
) as agent:
response = await agent.run(
"Provide a brief, helpful response.",
max_tokens=100,
temperature=0.7,
top_p=0.9,
seed=123,
user="comprehensive-test-user",
tools=[get_weather],
tool_choice="auto",
)
assert isinstance(response, AgentRunResponse)
assert response.text is not None
assert len(response.text) > 0
@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."""
async with ChatAgent(
chat_client=OpenAIResponsesClient(),
instructions="You are a helpful assistant.",
max_tokens=100,
temperature=0.7,
top_p=0.9,
seed=123,
user="comprehensive-test-user",
tools=[get_weather],
tool_choice="auto",
) as agent:
response = await agent.run(
"Provide a brief, helpful response.",
)
assert isinstance(response, AgentRunResponse)
assert response.text is not None
assert len(response.text) > 0
@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."""
# Use the same MCP server as the Foundry example
mcp_tool = HostedMCPTool(
name="Microsoft Learn MCP",
url="https://learn.microsoft.com/api/mcp",
description="A Microsoft Learn MCP server for documentation questions",
approval_mode="never_require",
)
async with ChatAgent(
chat_client=OpenAIResponsesClient(),
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
tools=[mcp_tool],
) as agent:
# Use the same query as the Foundry example
response = await agent.run(
"How to create an Azure storage account using az cli?",
max_tokens=200,
)
assert isinstance(response, AgentRunResponse)
assert response.text is not None
assert len(response.text) > 0
# Should contain Azure-related content since it's asking about Azure CLI
assert any(term in response.text.lower() for term in ["azure", "storage", "account", "cli"])
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")
@@ -1452,6 +1494,148 @@ def test_service_response_exception_includes_original_error_details() -> None:
assert original_error_message in exception_message
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")
messages = [ChatMessage(role="user", text="Test streaming with format")]
# It will fail due to invalid API key, but exercises the code path
with pytest.raises(ServiceResponseException):
async def run_streaming():
async for _ in client.get_streaming_response(messages=messages, response_format=OutputStruct):
pass
asyncio.run(run_streaming())
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")
# Test image content with detail parameter and file_id
image_content_with_detail = UriContent(
uri="https://example.com/image.jpg",
media_type="image/jpeg",
additional_properties={"detail": "high", "file_id": "file_123"},
)
result = client._openai_content_parser(Role.USER, image_content_with_detail, {}) # type: ignore
assert result["type"] == "input_image"
assert result["image_url"] == "https://example.com/image.jpg"
assert result["detail"] == "high"
assert result["file_id"] == "file_123"
# Test image content without additional properties (defaults)
image_content_basic = UriContent(uri="https://example.com/basic.png", media_type="image/png")
result = client._openai_content_parser(Role.USER, image_content_basic, {}) # type: ignore
assert result["type"] == "input_image"
assert result["detail"] == "auto"
assert result["file_id"] is 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")
# Test WAV audio content
wav_content = UriContent(uri="data:audio/wav;base64,abc123", media_type="audio/wav")
result = client._openai_content_parser(Role.USER, wav_content, {}) # type: ignore
assert result["type"] == "input_audio"
assert result["input_audio"]["data"] == "data:audio/wav;base64,abc123"
assert result["input_audio"]["format"] == "wav"
# Test MP3 audio content
mp3_content = UriContent(uri="data:audio/mp3;base64,def456", media_type="audio/mp3")
result = client._openai_content_parser(Role.USER, mp3_content, {}) # type: ignore
assert result["type"] == "input_audio"
assert result["input_audio"]["format"] == "mp3"
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")
# Test unsupported audio format
unsupported_audio = UriContent(uri="data:audio/ogg;base64,ghi789", media_type="audio/ogg")
result = client._openai_content_parser(Role.USER, unsupported_audio, {}) # type: ignore
assert result == {}
# Test non-media content
text_uri_content = UriContent(uri="https://example.com/document.txt", media_type="text/plain")
result = client._openai_content_parser(Role.USER, text_uri_content, {}) # type: ignore
assert result == {}
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")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
mock_event_image = MagicMock()
mock_event_image.type = "response.output_item.added"
mock_item_image = MagicMock()
mock_item_image.type = "code_interpreter_call"
mock_image_output = MagicMock()
mock_image_output.type = "image"
mock_image_output.url = "https://example.com/plot.png"
mock_item_image.outputs = [mock_image_output]
mock_item_image.code = None
mock_event_image.item = mock_item_image
result = client._create_streaming_response_content(mock_event_image, chat_options, function_call_ids) # type: ignore
assert len(result.contents) == 1
assert isinstance(result.contents[0], UriContent)
assert result.contents[0].uri == "https://example.com/plot.png"
assert result.contents[0].media_type == "image"
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")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
mock_event_reasoning = MagicMock()
mock_event_reasoning.type = "response.output_item.added"
mock_item_reasoning = MagicMock()
mock_item_reasoning.type = "reasoning"
mock_reasoning_content = MagicMock()
mock_reasoning_content.text = "Analyzing the problem step by step..."
mock_item_reasoning.content = [mock_reasoning_content]
mock_item_reasoning.summary = ["Problem analysis summary"]
mock_event_reasoning.item = mock_item_reasoning
result = client._create_streaming_response_content(mock_event_reasoning, chat_options, function_call_ids) # type: ignore
assert len(result.contents) == 1
assert isinstance(result.contents[0], TextReasoningContent)
assert result.contents[0].text == "Analyzing the problem step by step..."
if result.contents[0].additional_properties:
assert result.contents[0].additional_properties["summary"] == "Problem analysis summary"
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")
# Test TextReasoningContent with all additional properties
comprehensive_reasoning = TextReasoningContent(
text="Comprehensive reasoning summary",
additional_properties={
"status": "in_progress",
"reasoning_text": "Step-by-step analysis",
"encrypted_content": "secure_data_456",
},
)
result = client._openai_content_parser(Role.ASSISTANT, comprehensive_reasoning, {}) # type: ignore
assert result["type"] == "reasoning"
assert result["summary"]["text"] == "Comprehensive reasoning summary"
assert result["status"] == "in_progress"
assert result["content"]["type"] == "reasoning_text"
assert result["content"]["text"] == "Step-by-step analysis"
assert result["encrypted_content"] == "secure_data_456"
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")