mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.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 commit56787f25a4, reversing changes made tof71a27ebfe. * 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:
committed by
GitHub
Unverified
parent
674a514cae
commit
0715e0f8d3
@@ -14,6 +14,9 @@ from agent_framework import (
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
HostedCodeInterpreterTool,
|
||||
HostedFileSearchTool,
|
||||
HostedMCPTool,
|
||||
HostedVectorStoreContent,
|
||||
TextContent,
|
||||
ai_function,
|
||||
)
|
||||
@@ -45,6 +48,29 @@ async def get_weather(location: Annotated[str, "The location as a city name"]) -
|
||||
return f"The weather in {location} is sunny and 72°F."
|
||||
|
||||
|
||||
async def create_vector_store(client: AzureResponsesClient) -> tuple[str, HostedVectorStoreContent]:
|
||||
"""Create a vector store with sample documents for testing."""
|
||||
file = await client.client.files.create(
|
||||
file=("todays_weather.txt", b"The weather today is sunny with a high of 75F."), purpose="assistants"
|
||||
)
|
||||
vector_store = await client.client.vector_stores.create(
|
||||
name="knowledge_base",
|
||||
expires_after={"anchor": "last_active_at", "days": 1},
|
||||
)
|
||||
result = await client.client.vector_stores.files.create_and_poll(vector_store_id=vector_store.id, file_id=file.id)
|
||||
if result.last_error is not None:
|
||||
raise Exception(f"Vector store file processing failed with status: {result.last_error.message}")
|
||||
|
||||
return file.id, HostedVectorStoreContent(vector_store_id=vector_store.id)
|
||||
|
||||
|
||||
async def delete_vector_store(client: AzureResponsesClient, file_id: str, vector_store_id: str) -> None:
|
||||
"""Delete the vector store after tests."""
|
||||
|
||||
await client.client.vector_stores.delete(vector_store_id=vector_store_id)
|
||||
await client.client.files.delete(file_id=file_id)
|
||||
|
||||
|
||||
def test_init(azure_openai_unit_test_env: dict[str, str]) -> None:
|
||||
# Test successful initialization
|
||||
azure_responses_client = AzureResponsesClient()
|
||||
@@ -459,3 +485,140 @@ async def test_azure_responses_client_agent_level_tool_persistence():
|
||||
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"])
|
||||
|
||||
|
||||
@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."""
|
||||
async with ChatAgent(
|
||||
chat_client=AzureResponsesClient(credential=AzureCliCredential()),
|
||||
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_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."""
|
||||
async with ChatAgent(
|
||||
chat_client=AzureResponsesClient(credential=AzureCliCredential()),
|
||||
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_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."""
|
||||
# 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=AzureResponsesClient(credential=AzureCliCredential()),
|
||||
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"])
|
||||
|
||||
|
||||
@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:
|
||||
"""Test Azure responses client with file search tool."""
|
||||
azure_responses_client = AzureResponsesClient(credential=AzureCliCredential())
|
||||
|
||||
assert isinstance(azure_responses_client, ChatClientProtocol)
|
||||
|
||||
file_id, vector_store = await create_vector_store(azure_responses_client)
|
||||
# Test that the client will use the web search tool
|
||||
response = await azure_responses_client.get_response(
|
||||
messages=[
|
||||
ChatMessage(
|
||||
role="user",
|
||||
text="What is the weather today? Do a file search to find the answer.",
|
||||
)
|
||||
],
|
||||
tools=[HostedFileSearchTool(inputs=vector_store)],
|
||||
tool_choice="auto",
|
||||
)
|
||||
|
||||
await delete_vector_store(azure_responses_client, file_id, vector_store.vector_store_id)
|
||||
assert "sunny" in response.text.lower()
|
||||
assert "75" in response.text
|
||||
|
||||
|
||||
@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:
|
||||
"""Test Azure responses client with file search tool and streaming."""
|
||||
azure_responses_client = AzureResponsesClient(credential=AzureCliCredential())
|
||||
|
||||
assert isinstance(azure_responses_client, ChatClientProtocol)
|
||||
|
||||
file_id, vector_store = await create_vector_store(azure_responses_client)
|
||||
# Test that the client will use the web search tool
|
||||
response = azure_responses_client.get_streaming_response(
|
||||
messages=[
|
||||
ChatMessage(
|
||||
role="user",
|
||||
text="What is the weather today? Do a file search to find the answer.",
|
||||
)
|
||||
],
|
||||
tools=[HostedFileSearchTool(inputs=vector_store)],
|
||||
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
|
||||
|
||||
await delete_vector_store(azure_responses_client, file_id, vector_store.vector_store_id)
|
||||
|
||||
assert "sunny" in full_message.lower()
|
||||
assert "75" in full_message
|
||||
|
||||
@@ -162,7 +162,7 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
# Extract necessary state from messages and options
|
||||
run_options, tool_results = self._create_run_options(messages, chat_options, **kwargs)
|
||||
run_options, tool_results = self._prepare_options(messages, chat_options, **kwargs)
|
||||
|
||||
# Get the thread ID
|
||||
thread_id: str | None = (
|
||||
@@ -349,7 +349,7 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
|
||||
return contents
|
||||
|
||||
def _create_run_options(
|
||||
def _prepare_options(
|
||||
self,
|
||||
messages: MutableSequence[ChatMessage],
|
||||
chat_options: ChatOptions | None,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, Callable, Mapping, MutableMapping, MutableSequence, Sequence
|
||||
from collections.abc import AsyncIterable, Mapping, MutableMapping, MutableSequence, Sequence
|
||||
from datetime import datetime
|
||||
from itertools import chain
|
||||
from typing import TYPE_CHECKING, Any, Literal, TypeVar
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from openai import AsyncOpenAI, BadRequestError
|
||||
from openai.types.responses.file_search_tool_param import FileSearchToolParam
|
||||
@@ -67,17 +66,6 @@ from ..telemetry import use_telemetry
|
||||
from ._exceptions import OpenAIContentFilterException
|
||||
from ._shared import OpenAIBase, OpenAIConfigMixin, OpenAISettings, prepare_function_call_results
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override # type: ignore # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import override # type: ignore[import] # pragma: no cover
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openai.types.responses.response_includable import ResponseIncludable
|
||||
|
||||
from .._types import ChatToolMode
|
||||
|
||||
|
||||
logger = get_logger("agent_framework.openai")
|
||||
|
||||
__all__ = ["OpenAIResponsesClient"]
|
||||
@@ -90,192 +78,6 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
|
||||
FILE_SEARCH_MAX_RESULTS: int = 50
|
||||
|
||||
def _filter_options(self, **kwargs: Any) -> dict[str, Any]:
|
||||
"""Filter options for the responses call."""
|
||||
# The responses call does not support all the options that the chat completion call does.
|
||||
# We filter out the unsupported options.
|
||||
return {key: value for key, value in kwargs.items() if value is not None}
|
||||
|
||||
@override
|
||||
async def get_response(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage],
|
||||
*,
|
||||
include: list["ResponseIncludable"] | None = None,
|
||||
instructions: str | None = None,
|
||||
max_tokens: int | None = None,
|
||||
parallel_tool_calls: bool | None = None,
|
||||
model: str | None = None,
|
||||
previous_response_id: str | None = None,
|
||||
reasoning: dict[str, str] | None = None,
|
||||
service_tier: str | None = None,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
seed: int | None = None,
|
||||
store: bool | None = None,
|
||||
temperature: float | None = None,
|
||||
tool_choice: "ChatToolMode" | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto",
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
top_p: float | None = None,
|
||||
user: str | None = None,
|
||||
truncation: str | None = None,
|
||||
timeout: float | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
"""Get a response from the OpenAI API.
|
||||
|
||||
Args:
|
||||
messages: the message or messages to send to the model
|
||||
include: additional output data to include in the model response.
|
||||
instructions: a system (or developer) message inserted into the model's context.
|
||||
max_tokens: The maximum number of tokens to generate.
|
||||
parallel_tool_calls: Whether to enable parallel tool calls.
|
||||
model: The model to use for the agent.
|
||||
previous_response_id: The ID of the previous response.
|
||||
reasoning: The reasoning to use for the response.
|
||||
service_tier: The service tier to use for the response.
|
||||
response_format: The format of the response.
|
||||
seed: The random seed to use for the response.
|
||||
store: whether to store the response.
|
||||
temperature: the sampling temperature to use.
|
||||
tool_choice: the tool choice for the request.
|
||||
tools: the tools to use for the request.
|
||||
top_p: the nucleus sampling probability to use.
|
||||
user: the user to associate with the request.
|
||||
truncation: the truncation strategy to use.
|
||||
timeout: the timeout for the request.
|
||||
additional_properties: additional properties to include in the request.
|
||||
kwargs: any additional keyword arguments,
|
||||
will only be passed to functions that are called.
|
||||
|
||||
Returns:
|
||||
A chat response from the model.
|
||||
"""
|
||||
additional_properties = additional_properties or {}
|
||||
additional_properties.update(
|
||||
self._filter_options(
|
||||
include=include,
|
||||
instructions=instructions,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
model=model,
|
||||
previous_response_id=previous_response_id,
|
||||
reasoning=reasoning,
|
||||
service_tier=service_tier,
|
||||
truncation=truncation,
|
||||
timeout=timeout,
|
||||
)
|
||||
)
|
||||
|
||||
return await super().get_response(
|
||||
messages=messages,
|
||||
max_tokens=max_tokens,
|
||||
response_format=response_format,
|
||||
seed=seed,
|
||||
store=store,
|
||||
temperature=temperature,
|
||||
tool_choice=tool_choice,
|
||||
tools=tools, # type: ignore
|
||||
top_p=top_p,
|
||||
user=user,
|
||||
additional_properties=additional_properties,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@override
|
||||
async def get_streaming_response(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage],
|
||||
*,
|
||||
# TODO(peterychang): enable this option. background: bool | None = None,
|
||||
include: list["ResponseIncludable"] | None = None,
|
||||
instructions: str | None = None,
|
||||
max_tokens: int | None = None,
|
||||
parallel_tool_calls: bool | None = None,
|
||||
model: str | None = None,
|
||||
previous_response_id: str | None = None,
|
||||
reasoning: dict[str, str] | None = None,
|
||||
service_tier: str | None = None,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
seed: int | None = None,
|
||||
store: bool | None = None,
|
||||
temperature: float | None = None,
|
||||
tool_choice: "ChatToolMode" | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto",
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
top_p: float | None = None,
|
||||
user: str | None = None,
|
||||
truncation: str | None = None,
|
||||
timeout: float | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
"""Get a streaming response from the OpenAI API.
|
||||
|
||||
Args:
|
||||
messages: the message or messages to send to the model
|
||||
include: additional output data to include in the model response.
|
||||
instructions: a system (or developer) message inserted into the model's context.
|
||||
max_tokens: The maximum number of tokens to generate.
|
||||
parallel_tool_calls: Whether to enable parallel tool calls.
|
||||
model: The model to use for the agent.
|
||||
previous_response_id: The ID of the previous response.
|
||||
reasoning: The reasoning to use for the response.
|
||||
service_tier: The service tier to use for the response.
|
||||
response_format: The format of the response.
|
||||
seed: The random seed to use for the response.
|
||||
store: whether to store the response.
|
||||
temperature: the sampling temperature to use.
|
||||
tool_choice: the tool choice for the request.
|
||||
tools: the tools to use for the request.
|
||||
top_p: the nucleus sampling probability to use.
|
||||
user: the user to associate with the request.
|
||||
truncation: the truncation strategy to use.
|
||||
timeout: the timeout for the request.
|
||||
additional_properties: additional properties to include in the request.
|
||||
kwargs: any additional keyword arguments,
|
||||
will only be passed to functions that are called.
|
||||
|
||||
Returns:
|
||||
A stream representing the response(s) from the LLM.
|
||||
"""
|
||||
additional_properties = additional_properties or {}
|
||||
additional_properties.update(
|
||||
self._filter_options(
|
||||
include=include,
|
||||
instructions=instructions,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
model=model,
|
||||
previous_response_id=previous_response_id,
|
||||
reasoning=reasoning,
|
||||
service_tier=service_tier,
|
||||
truncation=truncation,
|
||||
timeout=timeout,
|
||||
)
|
||||
)
|
||||
|
||||
async for update in super().get_streaming_response(
|
||||
messages=messages,
|
||||
max_tokens=max_tokens,
|
||||
response_format=response_format,
|
||||
seed=seed,
|
||||
store=store,
|
||||
temperature=temperature,
|
||||
tool_choice=tool_choice,
|
||||
tools=tools, # type: ignore
|
||||
top_p=top_p,
|
||||
user=user,
|
||||
additional_properties=additional_properties,
|
||||
**kwargs,
|
||||
):
|
||||
yield update
|
||||
|
||||
# region Inner Methods
|
||||
|
||||
async def _inner_get_response(
|
||||
@@ -474,18 +276,33 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
return response_tools
|
||||
|
||||
def _prepare_options(self, messages: MutableSequence[ChatMessage], chat_options: ChatOptions) -> dict[str, Any]:
|
||||
"""Take ChatOptions and create the specific options for Responses."""
|
||||
options_dict = chat_options.to_provider_settings(exclude={"response_format"})
|
||||
"""Take ChatOptions and create the specific options for Responses API."""
|
||||
options_dict: dict[str, Any] = {}
|
||||
|
||||
if chat_options.max_tokens is not None:
|
||||
options_dict["max_output_tokens"] = chat_options.max_tokens
|
||||
|
||||
if chat_options.temperature is not None:
|
||||
options_dict["temperature"] = chat_options.temperature
|
||||
|
||||
if chat_options.top_p is not None:
|
||||
options_dict["top_p"] = chat_options.top_p
|
||||
|
||||
if chat_options.user is not None:
|
||||
options_dict["user"] = chat_options.user
|
||||
|
||||
# messages
|
||||
request_input = self._prepare_chat_messages_for_request(messages)
|
||||
if not request_input:
|
||||
raise ServiceInvalidRequestError("Messages are required for chat completions")
|
||||
options_dict["input"] = request_input
|
||||
|
||||
# tools
|
||||
if chat_options.tools is None:
|
||||
options_dict.pop("parallel_tool_calls", None)
|
||||
else:
|
||||
options_dict["tools"] = self._tools_to_response_tools(chat_options.tools)
|
||||
|
||||
# other settings
|
||||
if "store" not in options_dict:
|
||||
options_dict["store"] = False
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user