mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Add search tool content for OpenAI responses (#5302)
* Add OpenAI search tool content parsing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix typing * simplified oai image test * same for azure * skip az responses api test --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
60af59ba8b
commit
3e54a689fc
@@ -351,6 +351,8 @@ ContentType = Literal[
|
||||
"image_generation_tool_result",
|
||||
"mcp_server_tool_call",
|
||||
"mcp_server_tool_result",
|
||||
"search_tool_call",
|
||||
"search_tool_result",
|
||||
"shell_tool_call",
|
||||
"shell_tool_result",
|
||||
"shell_command_output",
|
||||
@@ -864,6 +866,56 @@ class Content:
|
||||
raw_representation=raw_representation,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_search_tool_call(
|
||||
cls: type[ContentT],
|
||||
call_id: str,
|
||||
*,
|
||||
tool_name: str,
|
||||
arguments: str | Mapping[str, Any] | None = None,
|
||||
status: str | None = None,
|
||||
annotations: Sequence[Annotation] | None = None,
|
||||
additional_properties: MutableMapping[str, Any] | None = None,
|
||||
raw_representation: Any = None,
|
||||
) -> ContentT:
|
||||
"""Create search tool call content."""
|
||||
return cls(
|
||||
"search_tool_call",
|
||||
call_id=call_id,
|
||||
tool_name=tool_name,
|
||||
arguments=arguments,
|
||||
status=status,
|
||||
annotations=annotations,
|
||||
additional_properties=additional_properties,
|
||||
raw_representation=raw_representation,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_search_tool_result(
|
||||
cls: type[ContentT],
|
||||
call_id: str,
|
||||
*,
|
||||
tool_name: str,
|
||||
result: Any = None,
|
||||
items: Sequence[Content] | None = None,
|
||||
status: str | None = None,
|
||||
annotations: Sequence[Annotation] | None = None,
|
||||
additional_properties: MutableMapping[str, Any] | None = None,
|
||||
raw_representation: Any = None,
|
||||
) -> ContentT:
|
||||
"""Create search tool result content."""
|
||||
return cls(
|
||||
"search_tool_result",
|
||||
call_id=call_id,
|
||||
tool_name=tool_name,
|
||||
result=result,
|
||||
items=list(items) if items is not None else None,
|
||||
status=status,
|
||||
annotations=annotations,
|
||||
additional_properties=additional_properties,
|
||||
raw_representation=raw_representation,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_usage(
|
||||
cls: type[ContentT],
|
||||
@@ -1478,7 +1530,7 @@ class Content:
|
||||
return span.lower() == top_level_media_type.lower()
|
||||
|
||||
def parse_arguments(self) -> dict[str, Any | None] | None:
|
||||
"""Parse arguments from function_call or mcp_server_tool_call content.
|
||||
"""Parse arguments from function_call, mcp_server_tool_call, or search_tool_call content.
|
||||
|
||||
If arguments cannot be parsed as JSON or the result is not a dict,
|
||||
they are returned as a dictionary with a single key "raw".
|
||||
|
||||
@@ -549,6 +549,7 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
chunk,
|
||||
options=validated_options,
|
||||
function_call_ids=function_call_ids,
|
||||
seen_reasoning_delta_item_ids=seen_reasoning_delta_item_ids,
|
||||
)
|
||||
else:
|
||||
async for chunk in await client.responses.create(stream=True, **run_options):
|
||||
@@ -556,6 +557,7 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
chunk,
|
||||
options=validated_options,
|
||||
function_call_ids=function_call_ids,
|
||||
seen_reasoning_delta_item_ids=seen_reasoning_delta_item_ids,
|
||||
)
|
||||
except Exception as ex:
|
||||
self._handle_request_error(ex)
|
||||
@@ -1587,6 +1589,54 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
"""Join shell commands into a single executable command string."""
|
||||
return "\n".join(command for command in commands if command).strip()
|
||||
|
||||
@staticmethod
|
||||
def _serialize_provider_payload(value: Any) -> Any:
|
||||
"""Convert OpenAI SDK objects into JSON-serializable Python values."""
|
||||
if isinstance(value, BaseModel):
|
||||
return value.model_dump(mode="json", exclude_none=True)
|
||||
if isinstance(value, Mapping):
|
||||
return {str(key): RawOpenAIChatClient._serialize_provider_payload(item) for key, item in value.items()} # type: ignore[reportUnknownVariableType]
|
||||
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
||||
return [RawOpenAIChatClient._serialize_provider_payload(item) for item in value] # type: ignore[reportUnknownVariableType]
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _get_search_tool_name(item_type: str) -> str:
|
||||
"""Map OpenAI search output item types to unified content tool names."""
|
||||
return "web_search" if item_type == "web_search_call" else "file_search"
|
||||
|
||||
def _parse_search_tool_call_content(self, item: Any) -> Content:
|
||||
"""Create unified search tool call content from an OpenAI search output item."""
|
||||
item_type = getattr(item, "type", "")
|
||||
call_id = getattr(item, "id", None) or getattr(item, "call_id", None) or ""
|
||||
if item_type == "web_search_call":
|
||||
arguments = self._serialize_provider_payload(getattr(item, "action", None))
|
||||
else:
|
||||
arguments = {"queries": list(getattr(item, "queries", []) or [])}
|
||||
return Content.from_search_tool_call(
|
||||
call_id=call_id,
|
||||
tool_name=self._get_search_tool_name(item_type),
|
||||
arguments=arguments,
|
||||
status=getattr(item, "status", None),
|
||||
raw_representation=item,
|
||||
)
|
||||
|
||||
def _parse_search_tool_result_content(self, item: Any) -> Content:
|
||||
"""Create unified search tool result content from an OpenAI search output item."""
|
||||
item_type = getattr(item, "type", "")
|
||||
call_id = getattr(item, "id", None) or getattr(item, "call_id", None) or ""
|
||||
if item_type == "web_search_call":
|
||||
result = {"action": self._serialize_provider_payload(getattr(item, "action", None))}
|
||||
else:
|
||||
result = {"results": self._serialize_provider_payload(getattr(item, "results", None))}
|
||||
return Content.from_search_tool_result(
|
||||
call_id=call_id,
|
||||
tool_name=self._get_search_tool_name(item_type),
|
||||
result=result,
|
||||
status=getattr(item, "status", None),
|
||||
raw_representation=item,
|
||||
)
|
||||
|
||||
# region Parse methods
|
||||
def _parse_response_from_openai(
|
||||
self,
|
||||
@@ -1788,6 +1838,9 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
raw_representation=item,
|
||||
)
|
||||
)
|
||||
case "web_search_call" | "file_search_call":
|
||||
contents.append(self._parse_search_tool_call_content(item))
|
||||
contents.append(self._parse_search_tool_result_content(item))
|
||||
case "mcp_approval_request": # ResponseOutputMcpApprovalRequest
|
||||
contents.append(
|
||||
Content.from_function_approval_request(
|
||||
@@ -2377,8 +2430,19 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
additional_properties=additional_properties_empty or None,
|
||||
)
|
||||
)
|
||||
case "web_search_call" | "file_search_call":
|
||||
contents.append(self._parse_search_tool_call_content(event_item))
|
||||
case _:
|
||||
logger.debug("Unparsed event of type: %s: %s", event.type, event)
|
||||
case (
|
||||
"response.web_search_call.in_progress"
|
||||
| "response.web_search_call.searching"
|
||||
| "response.web_search_call.completed"
|
||||
| "response.file_search_call.in_progress"
|
||||
| "response.file_search_call.searching"
|
||||
| "response.file_search_call.completed"
|
||||
):
|
||||
pass
|
||||
case "response.function_call_arguments.delta":
|
||||
call_id, name = function_call_ids.get(event.output_index, (None, None))
|
||||
if call_id and name:
|
||||
@@ -2514,6 +2578,8 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
raw_representation=done_item,
|
||||
)
|
||||
)
|
||||
elif getattr(done_item, "type", None) in ("web_search_call", "file_search_call"):
|
||||
contents.append(self._parse_search_tool_result_content(done_item))
|
||||
case _:
|
||||
logger.debug("Unparsed event of type: %s: %s", event.type, event)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from agent_framework import (
|
||||
@@ -71,6 +71,35 @@ class OutputStruct(BaseModel):
|
||||
weather: str | None = None
|
||||
|
||||
|
||||
class _FakeAsyncEventStream:
|
||||
def __init__(self, events: list[object]) -> None:
|
||||
self._events = events
|
||||
self._iterator = iter(())
|
||||
|
||||
def __aiter__(self) -> "_FakeAsyncEventStream":
|
||||
self._iterator = iter(self._events)
|
||||
return self
|
||||
|
||||
async def __anext__(self) -> object:
|
||||
try:
|
||||
return next(self._iterator)
|
||||
except StopIteration as exc:
|
||||
raise StopAsyncIteration from exc
|
||||
|
||||
|
||||
class _FakeAsyncEventStreamContext(_FakeAsyncEventStream):
|
||||
async def __aenter__(self) -> "_FakeAsyncEventStreamContext":
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc: BaseException | None,
|
||||
traceback: object | None,
|
||||
) -> None:
|
||||
return None
|
||||
|
||||
|
||||
async def create_vector_store(
|
||||
client: OpenAIChatClient,
|
||||
) -> tuple[str, Content]:
|
||||
@@ -1250,6 +1279,91 @@ def test_response_content_creation_with_function_call() -> None:
|
||||
assert function_call.arguments == '{"location": "Seattle"}'
|
||||
|
||||
|
||||
def test_parse_response_from_openai_with_web_search_call() -> None:
|
||||
"""Test _parse_response_from_openai with web search output."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.output_parsed = None
|
||||
mock_response.metadata = {}
|
||||
mock_response.usage = None
|
||||
mock_response.id = "resp-web"
|
||||
mock_response.model = "test-model"
|
||||
mock_response.created_at = 1000000000
|
||||
|
||||
mock_search_item = MagicMock()
|
||||
mock_search_item.type = "web_search_call"
|
||||
mock_search_item.id = "ws_123"
|
||||
mock_search_item.status = "completed"
|
||||
mock_search_item.action = {
|
||||
"type": "search",
|
||||
"query": "current weather in Seattle",
|
||||
"queries": ["current weather in Seattle"],
|
||||
"sources": [{"title": "Weather", "url": "https://weather.example"}],
|
||||
}
|
||||
|
||||
mock_response.output = [mock_search_item]
|
||||
|
||||
response = client._parse_response_from_openai(mock_response, options={}) # type: ignore
|
||||
|
||||
assert len(response.messages[0].contents) == 2
|
||||
call_content, result_content = response.messages[0].contents
|
||||
assert call_content.type == "search_tool_call"
|
||||
assert call_content.call_id == "ws_123"
|
||||
assert call_content.tool_name == "web_search"
|
||||
assert call_content.status == "completed"
|
||||
assert call_content.arguments == mock_search_item.action
|
||||
assert result_content.type == "search_tool_result"
|
||||
assert result_content.call_id == "ws_123"
|
||||
assert result_content.tool_name == "web_search"
|
||||
assert result_content.status == "completed"
|
||||
assert result_content.result == {"action": mock_search_item.action}
|
||||
|
||||
|
||||
def test_parse_response_from_openai_with_file_search_call() -> None:
|
||||
"""Test _parse_response_from_openai with file search output."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.output_parsed = None
|
||||
mock_response.metadata = {}
|
||||
mock_response.usage = None
|
||||
mock_response.id = "resp-file"
|
||||
mock_response.model = "test-model"
|
||||
mock_response.created_at = 1000000000
|
||||
|
||||
mock_search_item = MagicMock()
|
||||
mock_search_item.type = "file_search_call"
|
||||
mock_search_item.id = "fs_123"
|
||||
mock_search_item.status = "completed"
|
||||
mock_search_item.queries = ["weather history"]
|
||||
mock_search_item.results = [
|
||||
{
|
||||
"file_id": "file_1",
|
||||
"filename": "weather.txt",
|
||||
"score": 0.9,
|
||||
"text": "Seattle was cloudy.",
|
||||
}
|
||||
]
|
||||
|
||||
mock_response.output = [mock_search_item]
|
||||
|
||||
response = client._parse_response_from_openai(mock_response, options={}) # type: ignore
|
||||
|
||||
assert len(response.messages[0].contents) == 2
|
||||
call_content, result_content = response.messages[0].contents
|
||||
assert call_content.type == "search_tool_call"
|
||||
assert call_content.call_id == "fs_123"
|
||||
assert call_content.tool_name == "file_search"
|
||||
assert call_content.status == "completed"
|
||||
assert call_content.arguments == {"queries": ["weather history"]}
|
||||
assert result_content.type == "search_tool_result"
|
||||
assert result_content.call_id == "fs_123"
|
||||
assert result_content.tool_name == "file_search"
|
||||
assert result_content.status == "completed"
|
||||
assert result_content.result == {"results": mock_search_item.results}
|
||||
|
||||
|
||||
def test_prepare_content_for_opentool_approval_response() -> None:
|
||||
"""Test _prepare_content_for_openai with function approval response content."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
@@ -1394,6 +1508,86 @@ def test_parse_response_from_openai_with_mcp_server_tool_result() -> None:
|
||||
assert result_content.output is not None
|
||||
|
||||
|
||||
def test_parse_chunk_from_openai_with_web_search_call_added() -> None:
|
||||
"""Test that response.output_item.added for web_search_call emits search tool call content."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
chat_options = ChatOptions()
|
||||
function_call_ids: dict[int, tuple[str, str]] = {}
|
||||
|
||||
mock_event = MagicMock()
|
||||
mock_event.type = "response.output_item.added"
|
||||
mock_event.output_index = 0
|
||||
|
||||
mock_item = MagicMock()
|
||||
mock_item.type = "web_search_call"
|
||||
mock_item.id = "ws_call_123"
|
||||
mock_item.status = "in_progress"
|
||||
mock_item.action = {"type": "search", "query": "weather in Seattle"}
|
||||
mock_event.item = mock_item
|
||||
|
||||
update = client._parse_chunk_from_openai(mock_event, options=chat_options, function_call_ids=function_call_ids)
|
||||
|
||||
assert len(update.contents) == 1
|
||||
content = update.contents[0]
|
||||
assert content.type == "search_tool_call"
|
||||
assert content.call_id == "ws_call_123"
|
||||
assert content.tool_name == "web_search"
|
||||
assert content.status == "in_progress"
|
||||
assert content.arguments == {"type": "search", "query": "weather in Seattle"}
|
||||
|
||||
|
||||
def test_parse_chunk_from_openai_with_file_search_call_done() -> None:
|
||||
"""Test that response.output_item.done for file_search_call emits search tool result content."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
chat_options = ChatOptions()
|
||||
function_call_ids: dict[int, tuple[str, str]] = {}
|
||||
|
||||
mock_event = MagicMock()
|
||||
mock_event.type = "response.output_item.done"
|
||||
|
||||
mock_item = MagicMock()
|
||||
mock_item.type = "file_search_call"
|
||||
mock_item.id = "fs_call_123"
|
||||
mock_item.status = "completed"
|
||||
mock_item.results = [{"file_id": "file_1", "text": "Seattle was cloudy."}]
|
||||
mock_event.item = mock_item
|
||||
|
||||
update = client._parse_chunk_from_openai(mock_event, options=chat_options, function_call_ids=function_call_ids)
|
||||
|
||||
assert len(update.contents) == 1
|
||||
content = update.contents[0]
|
||||
assert content.type == "search_tool_result"
|
||||
assert content.call_id == "fs_call_123"
|
||||
assert content.tool_name == "file_search"
|
||||
assert content.status == "completed"
|
||||
assert content.result == {"results": [{"file_id": "file_1", "text": "Seattle was cloudy."}]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"event_type",
|
||||
[
|
||||
"response.web_search_call.in_progress",
|
||||
"response.web_search_call.searching",
|
||||
"response.web_search_call.completed",
|
||||
"response.file_search_call.in_progress",
|
||||
"response.file_search_call.searching",
|
||||
"response.file_search_call.completed",
|
||||
],
|
||||
)
|
||||
def test_parse_chunk_from_openai_ignores_search_progress_events(event_type: str) -> None:
|
||||
"""Search progress events should be explicitly ignored instead of logged as unparsed."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
chat_options = ChatOptions()
|
||||
function_call_ids: dict[int, tuple[str, str]] = {}
|
||||
|
||||
mock_event = MagicMock()
|
||||
mock_event.type = event_type
|
||||
|
||||
update = client._parse_chunk_from_openai(mock_event, options=chat_options, function_call_ids=function_call_ids)
|
||||
|
||||
assert update.contents == []
|
||||
|
||||
|
||||
def test_parse_chunk_from_openai_with_mcp_call_added_defers_result() -> None:
|
||||
"""Test that response.output_item.added for mcp_call emits only the call, not the result.
|
||||
|
||||
@@ -2716,6 +2910,48 @@ async def test_get_response_streaming_with_response_format() -> None:
|
||||
await run_streaming()
|
||||
|
||||
|
||||
async def test_inner_get_response_streaming_with_response_format_tracks_reasoning_delta_ids() -> None:
|
||||
"""The responses.stream path should suppress reasoning done events after deltas."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
messages = [Message(role="user", contents=["Test streaming with format"])]
|
||||
item_id = "reasoning_stream"
|
||||
events = [
|
||||
ResponseReasoningTextDeltaEvent(
|
||||
type="response.reasoning_text.delta",
|
||||
content_index=0,
|
||||
item_id=item_id,
|
||||
output_index=0,
|
||||
sequence_number=1,
|
||||
delta="Hello ",
|
||||
),
|
||||
ResponseReasoningTextDoneEvent(
|
||||
type="response.reasoning_text.done",
|
||||
content_index=0,
|
||||
item_id=item_id,
|
||||
output_index=0,
|
||||
sequence_number=2,
|
||||
text="Hello ",
|
||||
),
|
||||
]
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
client,
|
||||
"_prepare_request",
|
||||
new=AsyncMock(return_value=(client.client, {"text_format": OutputStruct}, {})),
|
||||
),
|
||||
patch.object(client.client.responses, "stream", return_value=_FakeAsyncEventStreamContext(events)),
|
||||
patch.object(client, "_get_metadata_from_response", return_value={}),
|
||||
):
|
||||
stream = client._inner_get_response(messages=messages, options={}, stream=True)
|
||||
updates = [update async for update in stream]
|
||||
|
||||
reasoning_chunks = [
|
||||
content.text for update in updates for content in update.contents if content.type == "text_reasoning"
|
||||
]
|
||||
assert reasoning_chunks == ["Hello "]
|
||||
|
||||
|
||||
def test_prepare_content_for_openai_image_content() -> None:
|
||||
"""Test _prepare_content_for_openai with image content variations."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
@@ -3153,6 +3389,44 @@ def test_streaming_reasoning_deltas_then_done_no_duplication() -> None:
|
||||
assert "".join(c.text for c in all_contents) == "Hello world"
|
||||
|
||||
|
||||
async def test_inner_get_response_streaming_create_tracks_reasoning_delta_ids() -> None:
|
||||
"""The responses.create(stream=True) path should suppress reasoning done events after deltas."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
messages = [Message(role="user", contents=["Test streaming"])]
|
||||
item_id = "reasoning_create"
|
||||
events = [
|
||||
ResponseReasoningTextDeltaEvent(
|
||||
type="response.reasoning_text.delta",
|
||||
content_index=0,
|
||||
item_id=item_id,
|
||||
output_index=0,
|
||||
sequence_number=1,
|
||||
delta="Hello ",
|
||||
),
|
||||
ResponseReasoningTextDoneEvent(
|
||||
type="response.reasoning_text.done",
|
||||
content_index=0,
|
||||
item_id=item_id,
|
||||
output_index=0,
|
||||
sequence_number=2,
|
||||
text="Hello ",
|
||||
),
|
||||
]
|
||||
|
||||
with (
|
||||
patch.object(client, "_prepare_request", new=AsyncMock(return_value=(client.client, {}, {}))),
|
||||
patch.object(client.client.responses, "create", new=AsyncMock(return_value=_FakeAsyncEventStream(events))),
|
||||
patch.object(client, "_get_metadata_from_response", return_value={}),
|
||||
):
|
||||
stream = client._inner_get_response(messages=messages, options={}, stream=True)
|
||||
updates = [update async for update in stream]
|
||||
|
||||
reasoning_chunks = [
|
||||
content.text for update in updates for content in update.contents if content.type == "text_reasoning"
|
||||
]
|
||||
assert reasoning_chunks == ["Hello "]
|
||||
|
||||
|
||||
def test_streaming_reasoning_events_preserve_metadata() -> None:
|
||||
"""Test that reasoning events preserve metadata like regular text events."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
@@ -3890,26 +4164,22 @@ async def test_integration_tool_rich_content_image() -> None:
|
||||
client = OpenAIChatClient()
|
||||
client.function_invocation_configuration["max_iterations"] = 2
|
||||
|
||||
for streaming in [False, True]:
|
||||
messages = [
|
||||
Message(
|
||||
role="user",
|
||||
contents=["Call the get_test_image tool and describe what you see."],
|
||||
)
|
||||
]
|
||||
options: dict[str, Any] = {"tools": [get_test_image], "tool_choice": "auto"}
|
||||
messages = [
|
||||
Message(
|
||||
role="user",
|
||||
contents=["Call the get_test_image tool and describe what you see."],
|
||||
)
|
||||
]
|
||||
options: dict[str, Any] = {"tools": [get_test_image], "tool_choice": "auto"}
|
||||
|
||||
if streaming:
|
||||
response = await client.get_response(messages=messages, stream=True, options=options).get_final_response()
|
||||
else:
|
||||
response = await client.get_response(messages=messages, options=options)
|
||||
response = await client.get_response(messages=messages, stream=True, options=options).get_final_response()
|
||||
|
||||
assert response is not None
|
||||
assert isinstance(response, ChatResponse)
|
||||
assert response.text is not None
|
||||
assert len(response.text) > 0
|
||||
# sample_image.jpg contains a photo of a house; the model should mention it.
|
||||
assert "house" in response.text.lower(), f"Model did not describe the house image. Response: {response.text}"
|
||||
assert response is not None
|
||||
assert isinstance(response, ChatResponse)
|
||||
assert response.text is not None
|
||||
assert len(response.text) > 0
|
||||
# sample_image.jpg contains a photo of a house; the model should mention it.
|
||||
assert "house" in response.text.lower(), f"Model did not describe the house image. Response: {response.text}"
|
||||
|
||||
|
||||
@pytest.mark.flaky
|
||||
|
||||
@@ -486,6 +486,7 @@ async def test_integration_client_agent_existing_session() -> None:
|
||||
@pytest.mark.integration
|
||||
@skip_if_azure_openai_integration_tests_disabled
|
||||
@_with_azure_openai_debug()
|
||||
@pytest.mark.skip(reason="Azure OpenAI is flaky when handling image content as function result. Needs investigation.")
|
||||
async def test_azure_openai_chat_client_tool_rich_content_image() -> None:
|
||||
image_path = Path(__file__).parent.parent / "assets" / "sample_image.jpg"
|
||||
image_bytes = image_path.read_bytes()
|
||||
@@ -499,21 +500,12 @@ async def test_azure_openai_chat_client_tool_rich_content_image() -> None:
|
||||
client = OpenAIChatClient(credential=credential)
|
||||
client.function_invocation_configuration["max_iterations"] = 2
|
||||
|
||||
for streaming in [False, True]:
|
||||
messages = [Message(role="user", contents=["Call the get_test_image tool and describe what you see."])]
|
||||
options: dict[str, Any] = {"tools": [get_test_image], "tool_choice": "auto"}
|
||||
response = await client.get_response(
|
||||
messages=[Message(role="user", contents=["Call the get_test_image tool and describe what you see."])],
|
||||
stream=True,
|
||||
options={"tools": [get_test_image], "tool_choice": "auto"},
|
||||
).get_final_response()
|
||||
|
||||
if streaming:
|
||||
response = await client.get_response(
|
||||
messages=messages,
|
||||
stream=True,
|
||||
options=options,
|
||||
).get_final_response()
|
||||
else:
|
||||
response = await client.get_response(messages=messages, options=options)
|
||||
|
||||
assert isinstance(response, ChatResponse)
|
||||
assert response.text is not None
|
||||
assert "house" in response.text.lower(), (
|
||||
f"Model did not describe the house image. Response: {response.text}"
|
||||
)
|
||||
assert isinstance(response, ChatResponse)
|
||||
assert response.text is not None
|
||||
assert "house" in response.text.lower(), f"Model did not describe the house image. Response: {response.text}"
|
||||
|
||||
Reference in New Issue
Block a user