From c73df74a966d60c9f9fab2ee50b8af310238d090 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Thu, 26 Feb 2026 11:47:45 -0800 Subject: [PATCH 1/8] feat(python): allow @tool functions to return rich content (images, audio) Add support for tool functions to return Content objects that the model can perceive natively. Closes #4272 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework_anthropic/_chat_client.py | 39 ++++++-- .../agent_framework_azure_ai/_chat_client.py | 5 + .../agent_framework_bedrock/_chat_client.py | 8 +- python/packages/core/agent_framework/_mcp.py | 75 ++++++++------ .../packages/core/agent_framework/_tools.py | 77 +++++++++++---- .../packages/core/agent_framework/_types.py | 19 +++- .../agent_framework/openai/_chat_client.py | 18 +++- .../openai/_responses_client.py | 13 +++ python/packages/core/tests/core/test_mcp.py | 45 ++++++--- python/packages/core/tests/core/test_types.py | 97 ++++++++++++++++++- .../agent_framework_ollama/_chat_client.py | 15 ++- 11 files changed, 329 insertions(+), 82 deletions(-) diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index acfc1b0180..0692db892d 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -659,12 +659,39 @@ def _prepare_message_for_anthropic(self, message: Message) -> dict[str, Any]: "input": content.parse_arguments(), }) case "function_result": - a_content.append({ - "type": "tool_result", - "tool_use_id": content.call_id, - "content": content.result if content.result is not None else "", - "is_error": content.exception is not None, - }) + if content.items: + # Rich content: build array with text + image blocks + tool_content: list[dict[str, Any]] = [] + if content.result: + tool_content.append({"type": "text", "text": content.result}) + for item in content.items: + if item.type == "data" and item.has_top_level_media_type("image"): + tool_content.append({ + "type": "image", + "source": { + "data": _get_data_bytes_as_str(item), # type: ignore[attr-defined] + "media_type": item.media_type, + "type": "base64", + }, + }) + elif item.type == "uri" and item.has_top_level_media_type("image"): + tool_content.append({ + "type": "image", + "source": {"type": "url", "url": item.uri}, + }) + a_content.append({ + "type": "tool_result", + "tool_use_id": content.call_id, + "content": tool_content, + "is_error": content.exception is not None, + }) + else: + a_content.append({ + "type": "tool_result", + "tool_use_id": content.call_id, + "content": content.result if content.result is not None else "", + "is_error": content.exception is not None, + }) case "mcp_server_tool_call": mcp_call: dict[str, Any] = { "type": "mcp_tool_use", diff --git a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py index 7590111bac..cccae17aa2 100644 --- a/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py +++ b/python/packages/azure-ai/agent_framework_azure_ai/_chat_client.py @@ -1391,6 +1391,11 @@ def _prepare_tool_outputs_for_azure_ai( call_id = run_and_call_ids[1] if content.type == "function_result": + if content.items: + logger.warning( + "Azure AI Agents does not support rich content (images, audio) in tool results. " + "Rich content items will be omitted." + ) if tool_outputs is None: tool_outputs = [] tool_outputs.append( diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index b0d87fe8cc..4e6c60b54b 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -503,10 +503,16 @@ def _convert_content_to_bedrock_block(self, content: Content) -> dict[str, Any] } } case "function_result": + tool_result_blocks = self._convert_tool_result_to_blocks(content.result) + if content.items: + logger.warning( + "Bedrock does not support rich content (images, audio) in tool results. " + "Rich content items will be omitted." + ) tool_result_block = { "toolResult": { "toolUseId": content.call_id, - "content": self._convert_tool_result_to_blocks(content.result), + "content": tool_result_blocks, "status": "error" if content.exception else "success", } } diff --git a/python/packages/core/agent_framework/_mcp.py b/python/packages/core/agent_framework/_mcp.py index 0c241cb89a..b455f1b696 100644 --- a/python/packages/core/agent_framework/_mcp.py +++ b/python/packages/core/agent_framework/_mcp.py @@ -142,38 +142,44 @@ def _parse_message_from_mcp( def _parse_tool_result_from_mcp( mcp_type: types.CallToolResult, -) -> str: - """Parse an MCP CallToolResult directly into a string representation. +) -> str | list[Content]: + """Parse an MCP CallToolResult into a string or rich content list. - Converts each content item in the MCP result to its string form and combines them. - This skips the intermediate Content object step for tool results. + Converts each content item in the MCP result to its appropriate form. + Text-only results are returned as strings. When the result contains + image or audio content, returns a list of Content objects so the + framework can forward the rich media to the model. Args: mcp_type: The MCP CallToolResult object to convert. Returns: - A string representation of the tool result — either plain text or serialized JSON. + A string for text-only results, or a list of Content for rich media results. """ import json - parts: list[str] = [] + text_parts: list[str] = [] + rich_items: list[Content] = [] for item in mcp_type.content: match item: case types.TextContent(): - parts.append(item.text) - case types.ImageContent() | types.AudioContent(): - parts.append( - json.dumps( - { - "type": "image" if isinstance(item, types.ImageContent) else "audio", - "data": item.data, - "mimeType": item.mimeType, - }, - default=str, + text_parts.append(item.text) + case types.ImageContent(): + rich_items.append( + Content.from_uri( + uri=f"data:{item.mimeType};base64,{item.data}", + media_type=item.mimeType, + ) + ) + case types.AudioContent(): + rich_items.append( + Content.from_uri( + uri=f"data:{item.mimeType};base64,{item.data}", + media_type=item.mimeType, ) ) case types.ResourceLink(): - parts.append( + text_parts.append( json.dumps( { "type": "resource_link", @@ -186,9 +192,9 @@ def _parse_tool_result_from_mcp( case types.EmbeddedResource(): match item.resource: case types.TextResourceContents(): - parts.append(item.resource.text) + text_parts.append(item.resource.text) case types.BlobResourceContents(): - parts.append( + text_parts.append( json.dumps( { "type": "blob", @@ -199,12 +205,21 @@ def _parse_tool_result_from_mcp( ) ) case _: - parts.append(str(item)) - if not parts: + text_parts.append(str(item)) + + if rich_items: + # Return rich content list with text items included + result: list[Content] = [] + for text in text_parts: + result.append(Content.from_text(text)) + result.extend(rich_items) + return result + + if not text_parts: return "" - if len(parts) == 1: - return parts[0] - return json.dumps(parts, default=str) + if len(text_parts) == 1: + return text_parts[0] + return json.dumps(text_parts, default=str) def _parse_content_from_mcp( @@ -425,7 +440,7 @@ def __init__( approval_mode: (Literal["always_require", "never_require"] | MCPSpecificApproval | None) = None, allowed_tools: Collection[str] | None = None, load_tools: bool = True, - parse_tool_results: Callable[[types.CallToolResult], str] | None = None, + parse_tool_results: Callable[[types.CallToolResult], str | list[Content]] | None = None, load_prompts: bool = True, parse_prompt_results: Callable[[types.GetPromptResult], str] | None = None, session: ClientSession | None = None, @@ -850,7 +865,7 @@ async def _ensure_connected(self) -> None: inner_exception=ex, ) from ex - async def call_tool(self, tool_name: str, **kwargs: Any) -> str: + async def call_tool(self, tool_name: str, **kwargs: Any) -> str | list[Content]: """Call a tool with the given arguments. Args: @@ -860,7 +875,7 @@ async def call_tool(self, tool_name: str, **kwargs: Any) -> str: kwargs: Arguments to pass to the tool. Returns: - A string representation of the tool result — either plain text or serialized JSON. + A string for text-only results, or a list of Content for rich media results. Raises: ToolExecutionException: If the MCP server is not connected, tools are not loaded, @@ -1053,7 +1068,7 @@ def __init__( command: str, *, load_tools: bool = True, - parse_tool_results: Callable[[types.CallToolResult], str] | None = None, + parse_tool_results: Callable[[types.CallToolResult], str | list[Content]] | None = None, load_prompts: bool = True, parse_prompt_results: Callable[[types.GetPromptResult], str] | None = None, request_timeout: int | None = None, @@ -1178,7 +1193,7 @@ def __init__( url: str, *, load_tools: bool = True, - parse_tool_results: Callable[[types.CallToolResult], str] | None = None, + parse_tool_results: Callable[[types.CallToolResult], str | list[Content]] | None = None, load_prompts: bool = True, parse_prompt_results: Callable[[types.GetPromptResult], str] | None = None, request_timeout: int | None = None, @@ -1297,7 +1312,7 @@ def __init__( url: str, *, load_tools: bool = True, - parse_tool_results: Callable[[types.CallToolResult], str] | None = None, + parse_tool_results: Callable[[types.CallToolResult], str | list[Content]] | None = None, load_prompts: bool = True, parse_prompt_results: Callable[[types.GetPromptResult], str] | None = None, request_timeout: int | None = None, diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 3ec167d4f7..bc9bddd5e4 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -242,7 +242,7 @@ def __init__( additional_properties: dict[str, Any] | None = None, func: Callable[..., Any] | None = None, input_model: type[BaseModel] | Mapping[str, Any] | None = None, - result_parser: Callable[[Any], str] | None = None, + result_parser: Callable[[Any], str | list[Content]] | None = None, **kwargs: Any, ) -> None: """Initialize the FunctionTool. @@ -438,19 +438,19 @@ async def invoke( *, arguments: BaseModel | Mapping[str, Any] | None = None, **kwargs: Any, - ) -> str: + ) -> str | list[Content]: """Run the AI function with the provided arguments as a Pydantic model. The raw return value of the wrapped function is automatically parsed into a ``str`` - (either plain text or serialized JSON) using :meth:`parse_result` or the custom - ``result_parser`` if one was provided. + (either plain text or serialized JSON) or a ``list[Content]`` (for rich content like + images) using :meth:`parse_result` or the custom ``result_parser`` if one was provided. Keyword Args: arguments: A mapping or model instance containing the arguments for the function. kwargs: Keyword arguments to pass to the function, will not be used if ``arguments`` is provided. Returns: - The parsed result as a string — either plain text or serialized JSON. + The parsed result as a string, or a list of Content items for rich results. Raises: TypeError: If arguments is not mapping-like or fails schema checks. @@ -556,8 +556,9 @@ async def invoke( parsed = str(result) logger.info(f"Function {self.name} succeeded.") if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED: # type: ignore[name-defined] - span.set_attribute(OtelAttr.TOOL_RESULT, parsed) - logger.debug(f"Function result: {parsed}") + result_str = parsed if isinstance(parsed, str) else str(parsed) + span.set_attribute(OtelAttr.TOOL_RESULT, result_str) + logger.debug(f"Function result: {result_str}") return parsed finally: duration = (end_time_stamp or perf_counter()) - start_time_stamp @@ -609,10 +610,13 @@ def _make_dumpable(value: Any) -> Any: return value @staticmethod - def parse_result(result: Any) -> str: - """Convert a raw function return value to a string representation. + def parse_result(result: Any) -> str | list[Content]: + """Convert a raw function return value to a string or rich content list. + + Returns a ``str`` for text-only results, or a ``list[Content]`` when the + function produced rich content (images, audio, files) that should be + forwarded to the model as visual/multi-modal input. - The return value is always a ``str`` — either plain text or serialized JSON. This is called automatically by :meth:`invoke` before returning the result, ensuring that the result stored in ``Content.from_function_result`` is already in a form that can be passed directly to LLM APIs. @@ -621,12 +625,22 @@ def parse_result(result: Any) -> str: result: The raw return value from the wrapped function. Returns: - A string representation of the result, either plain text or serialized JSON. + A string representation, or a list of Content items for rich results. """ + from ._types import Content + if result is None: return "" if isinstance(result, str): return result + # Preserve rich Content (images, audio, files) instead of serializing to JSON + if isinstance(result, Content): + if result.type in ("data", "uri"): + return [result] + if result.type == "text" and result.text: + return result.text + if isinstance(result, list) and any(isinstance(item, Content) for item in result): + return [item if isinstance(item, Content) else Content.from_text(str(item)) for item in result] dumpable = FunctionTool._make_dumpable(result) if isinstance(dumpable, str): return dumpable @@ -1080,7 +1094,7 @@ def tool( max_invocations: int | None = None, max_invocation_exceptions: int | None = None, additional_properties: dict[str, Any] | None = None, - result_parser: Callable[[Any], str] | None = None, + result_parser: Callable[[Any], str | list[Content]] | None = None, ) -> FunctionTool: ... @@ -1095,7 +1109,7 @@ def tool( max_invocations: int | None = None, max_invocation_exceptions: int | None = None, additional_properties: dict[str, Any] | None = None, - result_parser: Callable[[Any], str] | None = None, + result_parser: Callable[[Any], str | list[Content]] | None = None, ) -> Callable[[Callable[..., Any]], FunctionTool]: ... @@ -1109,7 +1123,7 @@ def tool( max_invocations: int | None = None, max_invocation_exceptions: int | None = None, additional_properties: dict[str, Any] | None = None, - result_parser: Callable[[Any], str] | None = None, + result_parser: Callable[[Any], str | list[Content]] | None = None, ) -> FunctionTool | Callable[[Callable[..., Any]], FunctionTool]: """Decorate a function to turn it into a FunctionTool that can be passed to models and executed automatically. @@ -1343,6 +1357,33 @@ def normalize_function_invocation_configuration( return normalized +def _build_function_result(call_id: str, function_result: str | list[Content]) -> Content: + """Build a function_result Content from a parsed tool result. + + When the tool returned rich content (list of Content items), the text + items are concatenated as the text result and media items are stored + in the ``items`` field so providers can forward them to the model. + + Args: + call_id: The function call ID this result corresponds to. + function_result: The parsed result from FunctionTool.invoke. + + Returns: + A Content with type ``function_result``. + """ + from ._types import Content + + if isinstance(function_result, list): + text_parts = [c.text for c in function_result if c.type == "text" and c.text] + rich_items = [c for c in function_result if c.type in ("data", "uri")] + return Content.from_function_result( + call_id=call_id, + result="\n".join(text_parts) if text_parts else "", + items=rich_items or None, + ) + return Content.from_function_result(call_id=call_id, result=function_result) + + async def _auto_invoke_function( function_call_content: Content, custom_args: dict[str, Any] | None = None, @@ -1440,9 +1481,9 @@ async def _auto_invoke_function( tool_call_id=function_call_content.call_id, **runtime_kwargs if getattr(tool, "_forward_runtime_kwargs", False) else {}, ) - return Content.from_function_result( + return _build_function_result( call_id=function_call_content.call_id, # type: ignore[arg-type] - result=function_result, + function_result=function_result, ) except Exception as exc: message = "Error: Function failed." @@ -1474,9 +1515,9 @@ async def final_function_handler(context_obj: Any) -> Any: # MiddlewareTermination bubbles up to signal loop termination try: function_result = await middleware_pipeline.execute(middleware_context, final_function_handler) - return Content.from_function_result( + return _build_function_result( call_id=function_call_content.call_id, # type: ignore[arg-type] - result=function_result, + function_result=function_result, ) except MiddlewareTermination as term_exc: # Re-raise to signal loop termination, but first capture any result set by middleware diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index 37ee9f1138..3943004c6e 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -468,6 +468,7 @@ def __init__( arguments: str | Mapping[str, Any] | None = None, exception: str | None = None, result: Any = None, + items: Sequence[Content] | None = None, # Hosted file/vector store fields file_id: str | None = None, vector_store_id: str | None = None, @@ -513,6 +514,7 @@ def __init__( self.arguments = arguments self.exception = exception self.result = result + self.items = items self.file_id = file_id self.vector_store_id = vector_store_id self.inputs = inputs @@ -756,16 +758,30 @@ def from_function_result( call_id: str, *, result: Any = None, + items: Sequence[Content] | None = None, exception: str | None = None, annotations: Sequence[Annotation] | None = None, additional_properties: MutableMapping[str, Any] | None = None, raw_representation: Any = None, ) -> ContentT: - """Create function result content.""" + """Create function result content. + + Args: + call_id: The ID of the function call this result corresponds to. + + Keyword Args: + result: The text result of the function call. + items: Optional rich content items (e.g. images, audio) produced by the tool. + exception: The exception message if the function call failed. + annotations: Optional annotations for the content. + additional_properties: Optional additional properties. + raw_representation: Optional raw representation from the provider. + """ return cls( "function_result", call_id=call_id, result=result, + items=list(items) if items else None, exception=exception, annotations=annotations, additional_properties=additional_properties, @@ -1029,6 +1045,7 @@ def to_dict(self, *, exclude_none: bool = True, exclude: set[str] | None = None) "arguments", "exception", "result", + "items", "file_id", "vector_store_id", "inputs", diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py index f08d80e990..205aa6075c 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/core/agent_framework/openai/_chat_client.py @@ -571,9 +571,21 @@ def _prepare_message_for_openai(self, message: Message) -> list[dict[str, Any]]: args["tool_calls"] = [self._prepare_content_for_openai(content)] # type: ignore case "function_result": args["tool_call_id"] = content.call_id - # Always include content for tool results - API requires it even if empty - # Functions returning None should still have a tool result message - args["content"] = content.result if content.result is not None else "" + if content.items: + # Multi-part: text result + rich content (images, audio, files) + content_parts: list[dict[str, Any]] = [] + text_result = content.result if content.result else "" + if text_result: + content_parts.append({"type": "text", "text": text_result}) + for item in content.items: + prepared = self._prepare_content_for_openai(item) + if prepared: + content_parts.append(prepared) + args["content"] = content_parts + else: + # Always include content for tool results - API requires it even if empty + # Functions returning None should still have a tool result message + args["content"] = content.result if content.result is not None else "" case "text_reasoning" if (protected_data := content.protected_data) is not None: all_messages[-1]["reasoning_details"] = json.loads(protected_data) case _: diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py index fa140ee0b7..661648e46d 100644 --- a/python/packages/core/agent_framework/openai/_responses_client.py +++ b/python/packages/core/agent_framework/openai/_responses_client.py @@ -926,6 +926,19 @@ def _prepare_message_for_openai( new_args.update(self._prepare_content_for_openai(message.role, content, call_id_to_id)) # type: ignore[arg-type] if new_args: all_messages.append(new_args) + # Forward rich content items (images, audio, files) as a user message + if content.items: + rich_parts = [ + self._prepare_content_for_openai("user", item, call_id_to_id) # type: ignore[arg-type] + for item in content.items + ] + rich_parts = [p for p in rich_parts if p] + if rich_parts: + all_messages.append({ + "type": "message", + "role": "user", + "content": rich_parts, + }) case "function_call": function_call = self._prepare_content_for_openai(message.role, content, call_id_to_id) # type: ignore[arg-type] if function_call: diff --git a/python/packages/core/tests/core/test_mcp.py b/python/packages/core/tests/core/test_mcp.py index 65b4015093..50d8e4e4ec 100644 --- a/python/packages/core/tests/core/test_mcp.py +++ b/python/packages/core/tests/core/test_mcp.py @@ -64,7 +64,7 @@ def test_mcp_prompt_message_to_ai_content(): def test_parse_tool_result_from_mcp(): - """Test conversion from MCP tool result to string representation.""" + """Test conversion from MCP tool result with images returns rich content list.""" mcp_result = types.CallToolResult( content=[ types.TextContent(type="text", text="Result text"), @@ -74,20 +74,19 @@ def test_parse_tool_result_from_mcp(): ) result = _parse_tool_result_from_mcp(mcp_result) - # Multiple items produce a JSON array of strings - assert isinstance(result, str) - import json - - parsed = json.loads(result) - assert len(parsed) == 3 - assert parsed[0] == "Result text" - # Image items are JSON-encoded strings within the array - img1 = json.loads(parsed[1]) - assert img1["type"] == "image" - assert img1["data"] == "eHl6" - img2 = json.loads(parsed[2]) - assert img2["type"] == "image" - assert img2["data"] == "YWJj" + # Results with images return a list of Content objects + assert isinstance(result, list) + assert len(result) == 3 + # First item is the text content + assert result[0].type == "text" + assert result[0].text == "Result text" + # Image items are preserved as data Content objects (data URI) + assert result[1].type == "data" + assert result[1].media_type == "image/png" + assert "eHl6" in result[1].uri + assert result[2].type == "data" + assert result[2].media_type == "image/webp" + assert "YWJj" in result[2].uri def test_parse_tool_result_from_mcp_single_text(): @@ -117,6 +116,22 @@ def test_parse_tool_result_from_mcp_empty_content(): assert result == "" +def test_parse_tool_result_from_mcp_audio_content(): + """Test conversion from MCP tool result with audio returns rich content list.""" + mcp_result = types.CallToolResult( + content=[ + types.AudioContent(type="audio", data="YXVkaW8=", mimeType="audio/wav"), + ] + ) + result = _parse_tool_result_from_mcp(mcp_result) + + assert isinstance(result, list) + assert len(result) == 1 + assert result[0].type == "data" + assert result[0].media_type == "audio/wav" + assert "YXVkaW8=" in result[0].uri + + def test_mcp_content_types_to_ai_content_text(): """Test conversion of MCP text content to AI content.""" mcp_content = types.TextContent(type="text", text="Sample text") diff --git a/python/packages/core/tests/core/test_types.py b/python/packages/core/tests/core/test_types.py index 8a8885b919..8a250ea981 100644 --- a/python/packages/core/tests/core/test_types.py +++ b/python/packages/core/tests/core/test_types.py @@ -2210,12 +2210,103 @@ def test_parse_result_content_object(): def test_parse_result_list_of_content(): - """Test that list[Content] is serialized to JSON.""" + """Test that list[Content] with text-only items is returned as list[Content].""" contents = [Content.from_text("hello"), Content.from_text("world")] result = FunctionTool.parse_result(contents) + assert isinstance(result, list) + assert len(result) == 2 + assert result[0].text == "hello" + assert result[1].text == "world" + + +def test_parse_result_single_image_content(): + """Test that a single image Content is preserved as list[Content].""" + image_content = Content.from_data(data=b"fake_png_bytes", media_type="image/png") + result = FunctionTool.parse_result(image_content) + assert isinstance(result, list) + assert len(result) == 1 + assert result[0].type == "data" + assert result[0].media_type == "image/png" + + +def test_parse_result_single_text_content(): + """Test that a single text Content returns its text string.""" + text_content = Content.from_text("just text") + result = FunctionTool.parse_result(text_content) assert isinstance(result, str) - assert "hello" in result - assert "world" in result + assert result == "just text" + + +def test_parse_result_mixed_content_list(): + """Test that list with text and image Content is preserved.""" + contents = [ + Content.from_text("Chart rendered."), + Content.from_data(data=b"image_bytes", media_type="image/png"), + ] + result = FunctionTool.parse_result(contents) + assert isinstance(result, list) + assert len(result) == 2 + assert result[0].type == "text" + assert result[1].type == "data" + + +def test_build_function_result_with_rich_content(): + """Test _build_function_result separates text and rich items.""" + from agent_framework._tools import _build_function_result + + content_list = [ + Content.from_text("Chart rendered."), + Content.from_data(data=b"image_bytes", media_type="image/png"), + ] + result = _build_function_result(call_id="test-123", function_result=content_list) + assert result.type == "function_result" + assert result.call_id == "test-123" + assert result.result == "Chart rendered." + assert result.items is not None + assert len(result.items) == 1 + assert result.items[0].type == "data" + assert result.items[0].media_type == "image/png" + + +def test_build_function_result_with_string(): + """Test _build_function_result with plain string result.""" + from agent_framework._tools import _build_function_result + + result = _build_function_result(call_id="test-123", function_result="just text") + assert result.type == "function_result" + assert result.call_id == "test-123" + assert result.result == "just text" + assert result.items is None + + +def test_content_from_function_result_with_items(): + """Test Content.from_function_result with items parameter.""" + image = Content.from_data(data=b"png_data", media_type="image/png") + result = Content.from_function_result( + call_id="call-1", + result="Screenshot captured.", + items=[image], + ) + assert result.type == "function_result" + assert result.call_id == "call-1" + assert result.result == "Screenshot captured." + assert result.items is not None + assert len(result.items) == 1 + assert result.items[0].media_type == "image/png" + + +def test_content_from_function_result_items_in_to_dict(): + """Test that items are included in to_dict serialization.""" + image = Content.from_data(data=b"png_data", media_type="image/png") + result = Content.from_function_result( + call_id="call-1", + result="done", + items=[image], + ) + d = result.to_dict() + assert "items" in d + assert len(d["items"]) == 1 + assert d["items"][0]["type"] == "data" # endregion diff --git a/python/packages/ollama/agent_framework_ollama/_chat_client.py b/python/packages/ollama/agent_framework_ollama/_chat_client.py index cc7fc0c9a7..1d4d7bf307 100644 --- a/python/packages/ollama/agent_framework_ollama/_chat_client.py +++ b/python/packages/ollama/agent_framework_ollama/_chat_client.py @@ -500,11 +500,16 @@ def _format_assistant_message(self, message: Message) -> list[OllamaMessage]: def _format_tool_message(self, message: Message) -> list[OllamaMessage]: # Ollama does not support multiple tool results in a single message, so we create a separate - return [ - OllamaMessage(role="tool", content=str(item.result), tool_name=item.call_id) - for item in message.contents - if item.type == "function_result" - ] + messages: list[OllamaMessage] = [] + for item in message.contents: + if item.type == "function_result": + if item.items: + logger.warning( + "Ollama does not support rich content (images, audio) in tool results. " + "Rich content items will be omitted." + ) + messages.append(OllamaMessage(role="tool", content=str(item.result), tool_name=item.call_id)) + return messages def _parse_contents_from_ollama(self, response: OllamaChatResponse) -> list[Content]: contents: list[Content] = [] From 7c97a9119d4cec5ee61ee245943679501c3b8cbf Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Thu, 26 Feb 2026 13:50:41 -0800 Subject: [PATCH 2/8] Anthropic logging + mypy fix --- .../anthropic/agent_framework_anthropic/_chat_client.py | 5 +++++ .../packages/bedrock/agent_framework_bedrock/_chat_client.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 0692db892d..f8e08c6982 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -679,6 +679,11 @@ def _prepare_message_for_anthropic(self, message: Message) -> dict[str, Any]: "type": "image", "source": {"type": "url", "url": item.uri}, }) + else: + logger.debug( + "Ignoring unsupported rich content media type in tool result: %s", + item.media_type, + ) a_content.append({ "type": "tool_result", "tool_use_id": content.call_id, diff --git a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py index 4e6c60b54b..f4c3b35b76 100644 --- a/python/packages/bedrock/agent_framework_bedrock/_chat_client.py +++ b/python/packages/bedrock/agent_framework_bedrock/_chat_client.py @@ -534,6 +534,8 @@ def _convert_content_to_bedrock_block(self, content: Content) -> dict[str, Any] def _convert_tool_result_to_blocks(self, result: Any) -> list[dict[str, Any]]: prepared_result = result if isinstance(result, str) else FunctionTool.parse_result(result) + if not isinstance(prepared_result, str): + return [{"text": str(prepared_result)}] try: parsed_result = json.loads(prepared_result) except json.JSONDecodeError: From 9bd8a155e6b967d6a82816752191fb24d89ed9e5 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Mon, 2 Mar 2026 10:43:33 -0800 Subject: [PATCH 3/8] Address PR review: fix MCP ordering, fold helper into from_function_result, fix Chat client - Preserve original content order in MCP tool results instead of text-first - Move _build_function_result logic into Content.from_function_result() - Chat Completions: inject user message for rich items (API only supports string tool content) - Update tests for ordering and new from_function_result behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/packages/core/agent_framework/_mcp.py | 16 ++++++--- .../packages/core/agent_framework/_tools.py | 35 +++---------------- .../packages/core/agent_framework/_types.py | 18 +++++++++- .../agent_framework/openai/_chat_client.py | 29 +++++++-------- python/packages/core/tests/core/test_mcp.py | 18 +++++----- python/packages/core/tests/core/test_types.py | 16 ++++----- 6 files changed, 64 insertions(+), 68 deletions(-) diff --git a/python/packages/core/agent_framework/_mcp.py b/python/packages/core/agent_framework/_mcp.py index b455f1b696..65d57e0859 100644 --- a/python/packages/core/agent_framework/_mcp.py +++ b/python/packages/core/agent_framework/_mcp.py @@ -208,11 +208,19 @@ def _parse_tool_result_from_mcp( text_parts.append(str(item)) if rich_items: - # Return rich content list with text items included + # Return rich content list preserving original order result: list[Content] = [] - for text in text_parts: - result.append(Content.from_text(text)) - result.extend(rich_items) + text_idx = 0 + rich_idx = 0 + for item in mcp_type.content: + match item: + case types.ImageContent() | types.AudioContent(): + result.append(rich_items[rich_idx]) + rich_idx += 1 + case _: + if text_idx < len(text_parts): + result.append(Content.from_text(text_parts[text_idx])) + text_idx += 1 return result if not text_parts: diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index bc9bddd5e4..f4bb2e91bc 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -1357,33 +1357,6 @@ def normalize_function_invocation_configuration( return normalized -def _build_function_result(call_id: str, function_result: str | list[Content]) -> Content: - """Build a function_result Content from a parsed tool result. - - When the tool returned rich content (list of Content items), the text - items are concatenated as the text result and media items are stored - in the ``items`` field so providers can forward them to the model. - - Args: - call_id: The function call ID this result corresponds to. - function_result: The parsed result from FunctionTool.invoke. - - Returns: - A Content with type ``function_result``. - """ - from ._types import Content - - if isinstance(function_result, list): - text_parts = [c.text for c in function_result if c.type == "text" and c.text] - rich_items = [c for c in function_result if c.type in ("data", "uri")] - return Content.from_function_result( - call_id=call_id, - result="\n".join(text_parts) if text_parts else "", - items=rich_items or None, - ) - return Content.from_function_result(call_id=call_id, result=function_result) - - async def _auto_invoke_function( function_call_content: Content, custom_args: dict[str, Any] | None = None, @@ -1481,9 +1454,9 @@ async def _auto_invoke_function( tool_call_id=function_call_content.call_id, **runtime_kwargs if getattr(tool, "_forward_runtime_kwargs", False) else {}, ) - return _build_function_result( + return Content.from_function_result( call_id=function_call_content.call_id, # type: ignore[arg-type] - function_result=function_result, + result=function_result, ) except Exception as exc: message = "Error: Function failed." @@ -1515,9 +1488,9 @@ async def final_function_handler(context_obj: Any) -> Any: # MiddlewareTermination bubbles up to signal loop termination try: function_result = await middleware_pipeline.execute(middleware_context, final_function_handler) - return _build_function_result( + return Content.from_function_result( call_id=function_call_content.call_id, # type: ignore[arg-type] - function_result=function_result, + result=function_result, ) except MiddlewareTermination as term_exc: # Re-raise to signal loop termination, but first capture any result set by middleware diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index 0a624120af..b62fd892e3 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -770,13 +770,29 @@ def from_function_result( call_id: The ID of the function call this result corresponds to. Keyword Args: - result: The text result of the function call. + result: The text result, or a list of Content items. When a list is + provided, text items are concatenated as the text result and + media items (images, audio, files) are stored in ``items``. items: Optional rich content items (e.g. images, audio) produced by the tool. + Ignored when ``result`` is a list (items are extracted from it instead). exception: The exception message if the function call failed. annotations: Optional annotations for the content. additional_properties: Optional additional properties. raw_representation: Optional raw representation from the provider. """ + if isinstance(result, list): + text_parts = [c.text for c in result if c.type == "text" and c.text] + rich_items = [c for c in result if c.type in ("data", "uri")] + return cls( + "function_result", + call_id=call_id, + result="\n".join(text_parts) if text_parts else "", + items=rich_items or None, + exception=exception, + annotations=annotations, + additional_properties=additional_properties, + raw_representation=raw_representation, + ) return cls( "function_result", call_id=call_id, diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py index 205aa6075c..04b93101f1 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/core/agent_framework/openai/_chat_client.py @@ -571,21 +571,22 @@ def _prepare_message_for_openai(self, message: Message) -> list[dict[str, Any]]: args["tool_calls"] = [self._prepare_content_for_openai(content)] # type: ignore case "function_result": args["tool_call_id"] = content.call_id + # Always include content for tool results - API requires it even if empty + # Functions returning None should still have a tool result message + args["content"] = content.result if content.result is not None else "" + if args: + all_messages.append(args) + # Chat Completions API only supports string content in tool messages. + # Forward rich items as a follow-up user message (same as Responses client). if content.items: - # Multi-part: text result + rich content (images, audio, files) - content_parts: list[dict[str, Any]] = [] - text_result = content.result if content.result else "" - if text_result: - content_parts.append({"type": "text", "text": text_result}) - for item in content.items: - prepared = self._prepare_content_for_openai(item) - if prepared: - content_parts.append(prepared) - args["content"] = content_parts - else: - # Always include content for tool results - API requires it even if empty - # Functions returning None should still have a tool result message - args["content"] = content.result if content.result is not None else "" + rich_parts = [self._prepare_content_for_openai(item) for item in content.items] + rich_parts = [p for p in rich_parts if p] + if rich_parts: + all_messages.append({ + "role": "user", + "content": rich_parts, + }) + continue case "text_reasoning" if (protected_data := content.protected_data) is not None: all_messages[-1]["reasoning_details"] = json.loads(protected_data) case _: diff --git a/python/packages/core/tests/core/test_mcp.py b/python/packages/core/tests/core/test_mcp.py index 50d8e4e4ec..8200eceda5 100644 --- a/python/packages/core/tests/core/test_mcp.py +++ b/python/packages/core/tests/core/test_mcp.py @@ -64,29 +64,31 @@ def test_mcp_prompt_message_to_ai_content(): def test_parse_tool_result_from_mcp(): - """Test conversion from MCP tool result with images returns rich content list.""" + """Test conversion from MCP tool result with images preserves original order.""" mcp_result = types.CallToolResult( content=[ types.TextContent(type="text", text="Result text"), types.ImageContent(type="image", data="eHl6", mimeType="image/png"), + types.TextContent(type="text", text="After image"), types.ImageContent(type="image", data="YWJj", mimeType="image/webp"), ] ) result = _parse_tool_result_from_mcp(mcp_result) - # Results with images return a list of Content objects + # Results with images return a list of Content objects in original order assert isinstance(result, list) - assert len(result) == 3 - # First item is the text content + assert len(result) == 4 + # Order is preserved: text, image, text, image assert result[0].type == "text" assert result[0].text == "Result text" - # Image items are preserved as data Content objects (data URI) assert result[1].type == "data" assert result[1].media_type == "image/png" assert "eHl6" in result[1].uri - assert result[2].type == "data" - assert result[2].media_type == "image/webp" - assert "YWJj" in result[2].uri + assert result[2].type == "text" + assert result[2].text == "After image" + assert result[3].type == "data" + assert result[3].media_type == "image/webp" + assert "YWJj" in result[3].uri def test_parse_tool_result_from_mcp_single_text(): diff --git a/python/packages/core/tests/core/test_types.py b/python/packages/core/tests/core/test_types.py index 8a250ea981..080cd0fa66 100644 --- a/python/packages/core/tests/core/test_types.py +++ b/python/packages/core/tests/core/test_types.py @@ -2250,15 +2250,13 @@ def test_parse_result_mixed_content_list(): assert result[1].type == "data" -def test_build_function_result_with_rich_content(): - """Test _build_function_result separates text and rich items.""" - from agent_framework._tools import _build_function_result - +def test_from_function_result_with_content_list(): + """Test Content.from_function_result separates text and rich items from a list.""" content_list = [ Content.from_text("Chart rendered."), Content.from_data(data=b"image_bytes", media_type="image/png"), ] - result = _build_function_result(call_id="test-123", function_result=content_list) + result = Content.from_function_result(call_id="test-123", result=content_list) assert result.type == "function_result" assert result.call_id == "test-123" assert result.result == "Chart rendered." @@ -2268,11 +2266,9 @@ def test_build_function_result_with_rich_content(): assert result.items[0].media_type == "image/png" -def test_build_function_result_with_string(): - """Test _build_function_result with plain string result.""" - from agent_framework._tools import _build_function_result - - result = _build_function_result(call_id="test-123", function_result="just text") +def test_from_function_result_with_string(): + """Test Content.from_function_result with plain string result.""" + result = Content.from_function_result(call_id="test-123", result="just text") assert result.type == "function_result" assert result.call_id == "test-123" assert result.result == "just text" From 8e7352d51bee9f8fc21c93ecbe2bcce7b0a90977 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Mon, 2 Mar 2026 20:02:37 -0800 Subject: [PATCH 4/8] Use native Responses API multi-part output, warn+omit for Chat client - Responses client: put rich items directly in function_call_output's output field as list (native API support) instead of user message injection - Chat client: warn and omit rich items (API doesn't support multi-part tool results), matching Ollama/Bedrock pattern - Unify test image: use sample_image.jpg across all integration tests - Add Azure OpenAI Responses integration test - Assert model describes house image to verify perception Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../anthropic/tests/test_anthropic_client.py | 279 +++++++--- .../agent_framework/openai/_chat_client.py | 15 +- .../openai/_responses_client.py | 26 +- .../core/tests/assets/sample_image.jpg | Bin 0 -> 182161 bytes .../tests/azure/test_azure_chat_client.py | 171 ++++-- .../azure/test_azure_responses_client.py | 180 +++++-- .../tests/openai/test_openai_chat_client.py | 259 ++++++--- .../openai/test_openai_responses_client.py | 497 ++++++++++++++---- 8 files changed, 1079 insertions(+), 348 deletions(-) create mode 100644 python/packages/core/tests/assets/sample_image.jpg diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index d7c4c9afc7..017a3fd487 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -14,6 +14,8 @@ tool, ) from agent_framework._settings import load_settings +from agent_framework_anthropic import AnthropicClient +from agent_framework_anthropic._chat_client import AnthropicSettings from anthropic.types.beta import ( BetaMessage, BetaTextBlock, @@ -22,9 +24,6 @@ ) from pydantic import BaseModel, Field -from agent_framework_anthropic import AnthropicClient -from agent_framework_anthropic._chat_client import AnthropicSettings - # Test constants VALID_PNG_BASE64 = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" @@ -70,8 +69,13 @@ def test_anthropic_settings_init(anthropic_unit_test_env: dict[str, str]) -> Non settings = load_settings(AnthropicSettings, env_prefix="ANTHROPIC_") assert settings["api_key"] is not None - assert settings["api_key"].get_secret_value() == anthropic_unit_test_env["ANTHROPIC_API_KEY"] - assert settings["chat_model_id"] == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"] + assert ( + settings["api_key"].get_secret_value() + == anthropic_unit_test_env["ANTHROPIC_API_KEY"] + ) + assert ( + settings["chat_model_id"] == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"] + ) def test_anthropic_settings_init_with_explicit_values() -> None: @@ -89,11 +93,15 @@ def test_anthropic_settings_init_with_explicit_values() -> None: @pytest.mark.parametrize("exclude_list", [["ANTHROPIC_API_KEY"]], indirect=True) -def test_anthropic_settings_missing_api_key(anthropic_unit_test_env: dict[str, str]) -> None: +def test_anthropic_settings_missing_api_key( + anthropic_unit_test_env: dict[str, str], +) -> None: """Test AnthropicSettings when API key is missing.""" settings = load_settings(AnthropicSettings, env_prefix="ANTHROPIC_") assert settings["api_key"] is None - assert settings["chat_model_id"] == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"] + assert ( + settings["chat_model_id"] == anthropic_unit_test_env["ANTHROPIC_CHAT_MODEL_ID"] + ) # Client Initialization Tests @@ -101,14 +109,18 @@ def test_anthropic_settings_missing_api_key(anthropic_unit_test_env: dict[str, s def test_anthropic_client_init_with_client(mock_anthropic_client: MagicMock) -> None: """Test AnthropicClient initialization with existing anthropic_client.""" - client = create_test_anthropic_client(mock_anthropic_client, model_id="claude-3-5-sonnet-20241022") + client = create_test_anthropic_client( + mock_anthropic_client, model_id="claude-3-5-sonnet-20241022" + ) assert client.anthropic_client is mock_anthropic_client assert client.model_id == "claude-3-5-sonnet-20241022" assert isinstance(client, SupportsChatGetResponse) -def test_anthropic_client_init_auto_create_client(anthropic_unit_test_env: dict[str, str]) -> None: +def test_anthropic_client_init_auto_create_client( + anthropic_unit_test_env: dict[str, str], +) -> None: """Test AnthropicClient initialization with auto-created anthropic_client.""" client = AnthropicClient( api_key=anthropic_unit_test_env["ANTHROPIC_API_KEY"], @@ -122,7 +134,10 @@ def test_anthropic_client_init_auto_create_client(anthropic_unit_test_env: dict[ def test_anthropic_client_init_missing_api_key() -> None: """Test AnthropicClient initialization when API key is missing.""" with patch("agent_framework_anthropic._chat_client.load_settings") as mock_load: - mock_load.return_value = {"api_key": None, "chat_model_id": "claude-3-5-sonnet-20241022"} + mock_load.return_value = { + "api_key": None, + "chat_model_id": "claude-3-5-sonnet-20241022", + } with pytest.raises(ValueError, match="Anthropic API key is required"): AnthropicClient() @@ -150,7 +165,9 @@ def test_prepare_message_for_anthropic_text(mock_anthropic_client: MagicMock) -> assert result["content"][0]["text"] == "Hello, world!" -def test_prepare_message_for_anthropic_function_call(mock_anthropic_client: MagicMock) -> None: +def test_prepare_message_for_anthropic_function_call( + mock_anthropic_client: MagicMock, +) -> None: """Test converting function call message to Anthropic format.""" client = create_test_anthropic_client(mock_anthropic_client) message = Message( @@ -174,7 +191,9 @@ def test_prepare_message_for_anthropic_function_call(mock_anthropic_client: Magi assert result["content"][0]["input"] == {"location": "San Francisco"} -def test_prepare_message_for_anthropic_function_result(mock_anthropic_client: MagicMock) -> None: +def test_prepare_message_for_anthropic_function_result( + mock_anthropic_client: MagicMock, +) -> None: """Test converting function result message to Anthropic format.""" client = create_test_anthropic_client(mock_anthropic_client) message = Message( @@ -199,7 +218,9 @@ def test_prepare_message_for_anthropic_function_result(mock_anthropic_client: Ma assert result["content"][0]["is_error"] is False -def test_prepare_message_for_anthropic_text_reasoning(mock_anthropic_client: MagicMock) -> None: +def test_prepare_message_for_anthropic_text_reasoning( + mock_anthropic_client: MagicMock, +) -> None: """Test converting text reasoning message to Anthropic format.""" client = create_test_anthropic_client(mock_anthropic_client) message = Message( @@ -216,12 +237,18 @@ def test_prepare_message_for_anthropic_text_reasoning(mock_anthropic_client: Mag assert "signature" not in result["content"][0] -def test_prepare_message_for_anthropic_text_reasoning_with_signature(mock_anthropic_client: MagicMock) -> None: +def test_prepare_message_for_anthropic_text_reasoning_with_signature( + mock_anthropic_client: MagicMock, +) -> None: """Test converting text reasoning message with signature to Anthropic format.""" client = create_test_anthropic_client(mock_anthropic_client) message = Message( role="assistant", - contents=[Content.from_text_reasoning(text="Let me think about this...", protected_data="sig_abc123")], + contents=[ + Content.from_text_reasoning( + text="Let me think about this...", protected_data="sig_abc123" + ) + ], ) result = client._prepare_message_for_anthropic(message) @@ -233,7 +260,9 @@ def test_prepare_message_for_anthropic_text_reasoning_with_signature(mock_anthro assert result["content"][0]["signature"] == "sig_abc123" -def test_prepare_message_for_anthropic_mcp_server_tool_call(mock_anthropic_client: MagicMock) -> None: +def test_prepare_message_for_anthropic_mcp_server_tool_call( + mock_anthropic_client: MagicMock, +) -> None: """Test converting MCP server tool call message to Anthropic format.""" client = create_test_anthropic_client(mock_anthropic_client) message = Message( @@ -259,7 +288,9 @@ def test_prepare_message_for_anthropic_mcp_server_tool_call(mock_anthropic_clien assert result["content"][0]["input"] == {"query": "Azure Functions"} -def test_prepare_message_for_anthropic_mcp_server_tool_call_no_server_name(mock_anthropic_client: MagicMock) -> None: +def test_prepare_message_for_anthropic_mcp_server_tool_call_no_server_name( + mock_anthropic_client: MagicMock, +) -> None: """Test converting MCP server tool call with no server name defaults to empty string.""" client = create_test_anthropic_client(mock_anthropic_client) message = Message( @@ -284,7 +315,9 @@ def test_prepare_message_for_anthropic_mcp_server_tool_call_no_server_name(mock_ assert result["content"][0]["input"] == {} -def test_prepare_message_for_anthropic_mcp_server_tool_result(mock_anthropic_client: MagicMock) -> None: +def test_prepare_message_for_anthropic_mcp_server_tool_result( + mock_anthropic_client: MagicMock, +) -> None: """Test converting MCP server tool result message to Anthropic format.""" client = create_test_anthropic_client(mock_anthropic_client) message = Message( @@ -306,7 +339,9 @@ def test_prepare_message_for_anthropic_mcp_server_tool_result(mock_anthropic_cli assert result["content"][0]["content"] == "Found 3 results for Azure Functions." -def test_prepare_message_for_anthropic_mcp_server_tool_result_none_output(mock_anthropic_client: MagicMock) -> None: +def test_prepare_message_for_anthropic_mcp_server_tool_result_none_output( + mock_anthropic_client: MagicMock, +) -> None: """Test converting MCP server tool result with None output defaults to empty string.""" client = create_test_anthropic_client(mock_anthropic_client) message = Message( @@ -328,7 +363,9 @@ def test_prepare_message_for_anthropic_mcp_server_tool_result_none_output(mock_a assert result["content"][0]["content"] == "" -def test_prepare_messages_for_anthropic_with_system(mock_anthropic_client: MagicMock) -> None: +def test_prepare_messages_for_anthropic_with_system( + mock_anthropic_client: MagicMock, +) -> None: """Test converting messages list with system message.""" client = create_test_anthropic_client(mock_anthropic_client) messages = [ @@ -344,7 +381,9 @@ def test_prepare_messages_for_anthropic_with_system(mock_anthropic_client: Magic assert result[0]["content"][0]["text"] == "Hello!" -def test_prepare_messages_for_anthropic_without_system(mock_anthropic_client: MagicMock) -> None: +def test_prepare_messages_for_anthropic_without_system( + mock_anthropic_client: MagicMock, +) -> None: """Test converting messages list without system message.""" client = create_test_anthropic_client(mock_anthropic_client) messages = [ @@ -367,7 +406,9 @@ def test_prepare_tools_for_anthropic_tool(mock_anthropic_client: MagicMock) -> N client = create_test_anthropic_client(mock_anthropic_client) @tool(approval_mode="never_require") - def get_weather(location: Annotated[str, Field(description="Location to get weather for")]) -> str: + def get_weather( + location: Annotated[str, Field(description="Location to get weather for")], + ) -> str: """Get weather for a location.""" return f"Weather for {location}" @@ -382,7 +423,9 @@ def get_weather(location: Annotated[str, Field(description="Location to get weat assert "Get weather for a location" in result["tools"][0]["description"] -def test_prepare_tools_for_anthropic_web_search(mock_anthropic_client: MagicMock) -> None: +def test_prepare_tools_for_anthropic_web_search( + mock_anthropic_client: MagicMock, +) -> None: """Test converting web_search dict tool to Anthropic format.""" client = create_test_anthropic_client(mock_anthropic_client) chat_options = ChatOptions(tools=[client.get_web_search_tool()]) @@ -396,7 +439,9 @@ def test_prepare_tools_for_anthropic_web_search(mock_anthropic_client: MagicMock assert result["tools"][0]["name"] == "web_search" -def test_prepare_tools_for_anthropic_code_interpreter(mock_anthropic_client: MagicMock) -> None: +def test_prepare_tools_for_anthropic_code_interpreter( + mock_anthropic_client: MagicMock, +) -> None: """Test converting code_interpreter dict tool to Anthropic format.""" client = create_test_anthropic_client(mock_anthropic_client) chat_options = ChatOptions(tools=[client.get_code_interpreter_tool()]) @@ -413,7 +458,9 @@ def test_prepare_tools_for_anthropic_code_interpreter(mock_anthropic_client: Mag def test_prepare_tools_for_anthropic_mcp_tool(mock_anthropic_client: MagicMock) -> None: """Test converting MCP dict tool to Anthropic format.""" client = create_test_anthropic_client(mock_anthropic_client) - chat_options = ChatOptions(tools=[client.get_mcp_tool(name="test-mcp", url="https://example.com/mcp")]) + chat_options = ChatOptions( + tools=[client.get_mcp_tool(name="test-mcp", url="https://example.com/mcp")] + ) result = client._prepare_tools_for_anthropic(chat_options) @@ -425,7 +472,9 @@ def test_prepare_tools_for_anthropic_mcp_tool(mock_anthropic_client: MagicMock) assert result["mcp_servers"][0]["url"] == "https://example.com/mcp" -def test_prepare_tools_for_anthropic_mcp_with_auth(mock_anthropic_client: MagicMock) -> None: +def test_prepare_tools_for_anthropic_mcp_with_auth( + mock_anthropic_client: MagicMock, +) -> None: """Test converting MCP dict tool with authorization token.""" client = create_test_anthropic_client(mock_anthropic_client) # Use the static method with authorization_token @@ -445,10 +494,16 @@ def test_prepare_tools_for_anthropic_mcp_with_auth(mock_anthropic_client: MagicM assert result["mcp_servers"][0]["authorization_token"] == "Bearer token123" -def test_prepare_tools_for_anthropic_dict_tool(mock_anthropic_client: MagicMock) -> None: +def test_prepare_tools_for_anthropic_dict_tool( + mock_anthropic_client: MagicMock, +) -> None: """Test converting dict tool to Anthropic format.""" client = create_test_anthropic_client(mock_anthropic_client) - chat_options = ChatOptions(tools=[{"type": "custom", "name": "custom_tool", "description": "A custom tool"}]) + chat_options = ChatOptions( + tools=[ + {"type": "custom", "name": "custom_tool", "description": "A custom tool"} + ] + ) result = client._prepare_tools_for_anthropic(chat_options) @@ -486,7 +541,9 @@ async def test_prepare_options_basic(mock_anthropic_client: MagicMock) -> None: assert "messages" in run_options -async def test_prepare_options_with_system_message(mock_anthropic_client: MagicMock) -> None: +async def test_prepare_options_with_system_message( + mock_anthropic_client: MagicMock, +) -> None: """Test _prepare_options with system message.""" client = create_test_anthropic_client(mock_anthropic_client) @@ -502,7 +559,9 @@ async def test_prepare_options_with_system_message(mock_anthropic_client: MagicM assert len(run_options["messages"]) == 1 # System message not in messages list -async def test_prepare_options_with_tool_choice_auto(mock_anthropic_client: MagicMock) -> None: +async def test_prepare_options_with_tool_choice_auto( + mock_anthropic_client: MagicMock, +) -> None: """Test _prepare_options with auto tool choice.""" client = create_test_anthropic_client(mock_anthropic_client) @@ -516,13 +575,17 @@ async def test_prepare_options_with_tool_choice_auto(mock_anthropic_client: Magi assert "allow_multiple_tool_calls" not in run_options -async def test_prepare_options_with_tool_choice_required(mock_anthropic_client: MagicMock) -> None: +async def test_prepare_options_with_tool_choice_required( + mock_anthropic_client: MagicMock, +) -> None: """Test _prepare_options with required tool choice.""" client = create_test_anthropic_client(mock_anthropic_client) messages = [Message(role="user", text="Hello")] # For required with specific function, need to pass as dict - chat_options = ChatOptions(tool_choice={"mode": "required", "required_function_name": "get_weather"}) + chat_options = ChatOptions( + tool_choice={"mode": "required", "required_function_name": "get_weather"} + ) run_options = client._prepare_options(messages, chat_options) @@ -530,7 +593,9 @@ async def test_prepare_options_with_tool_choice_required(mock_anthropic_client: assert run_options["tool_choice"]["name"] == "get_weather" -async def test_prepare_options_with_tool_choice_none(mock_anthropic_client: MagicMock) -> None: +async def test_prepare_options_with_tool_choice_none( + mock_anthropic_client: MagicMock, +) -> None: """Test _prepare_options with none tool choice.""" client = create_test_anthropic_client(mock_anthropic_client) @@ -560,7 +625,9 @@ def get_weather(location: str) -> str: assert len(run_options["tools"]) == 1 -async def test_prepare_options_with_stop_sequences(mock_anthropic_client: MagicMock) -> None: +async def test_prepare_options_with_stop_sequences( + mock_anthropic_client: MagicMock, +) -> None: """Test _prepare_options with stop sequences.""" client = create_test_anthropic_client(mock_anthropic_client) @@ -584,7 +651,9 @@ async def test_prepare_options_with_top_p(mock_anthropic_client: MagicMock) -> N assert run_options["top_p"] == 0.9 -async def test_prepare_options_excludes_stream_option(mock_anthropic_client: MagicMock) -> None: +async def test_prepare_options_excludes_stream_option( + mock_anthropic_client: MagicMock, +) -> None: """Test _prepare_options excludes stream when stream is provided in options.""" client = create_test_anthropic_client(mock_anthropic_client) @@ -596,7 +665,9 @@ async def test_prepare_options_excludes_stream_option(mock_anthropic_client: Mag assert "stream" not in run_options -async def test_prepare_options_filters_internal_kwargs(mock_anthropic_client: MagicMock) -> None: +async def test_prepare_options_filters_internal_kwargs( + mock_anthropic_client: MagicMock, +) -> None: """Test _prepare_options filters internal framework kwargs. Internal kwargs like _function_middleware_pipeline, thread, and middleware @@ -715,7 +786,9 @@ def test_parse_contents_from_anthropic_text(mock_anthropic_client: MagicMock) -> assert result[0].text == "Hello!" -def test_parse_contents_from_anthropic_tool_use(mock_anthropic_client: MagicMock) -> None: +def test_parse_contents_from_anthropic_tool_use( + mock_anthropic_client: MagicMock, +) -> None: """Test _parse_contents_from_anthropic with tool use.""" client = create_test_anthropic_client(mock_anthropic_client) @@ -735,7 +808,9 @@ def test_parse_contents_from_anthropic_tool_use(mock_anthropic_client: MagicMock assert result[0].name == "get_weather" -def test_parse_contents_from_anthropic_input_json_delta_no_duplicate_name(mock_anthropic_client: MagicMock) -> None: +def test_parse_contents_from_anthropic_input_json_delta_no_duplicate_name( + mock_anthropic_client: MagicMock, +) -> None: """Test that input_json_delta events have empty name to prevent duplicate ToolCallStartEvents. When streaming tool calls, the initial tool_use event provides the name, @@ -825,7 +900,9 @@ async def test_inner_get_response(mock_anthropic_client: MagicMock) -> None: assert len(response.messages) == 1 -async def test_inner_get_response_ignores_options_stream_non_streaming(mock_anthropic_client: MagicMock) -> None: +async def test_inner_get_response_ignores_options_stream_non_streaming( + mock_anthropic_client: MagicMock, +) -> None: """Test stream option in options does not conflict in non-streaming mode.""" client = create_test_anthropic_client(mock_anthropic_client) @@ -846,7 +923,9 @@ async def test_inner_get_response_ignores_options_stream_non_streaming(mock_anth ) assert mock_anthropic_client.beta.messages.create.call_count == 1 - assert mock_anthropic_client.beta.messages.create.call_args.kwargs["stream"] is False + assert ( + mock_anthropic_client.beta.messages.create.call_args.kwargs["stream"] is False + ) async def test_inner_get_response_streaming(mock_anthropic_client: MagicMock) -> None: @@ -875,7 +954,9 @@ async def mock_stream(): assert isinstance(chunks, list) -async def test_inner_get_response_ignores_options_stream_streaming(mock_anthropic_client: MagicMock) -> None: +async def test_inner_get_response_ignores_options_stream_streaming( + mock_anthropic_client: MagicMock, +) -> None: """Test stream option in options does not conflict in streaming mode.""" client = create_test_anthropic_client(mock_anthropic_client) @@ -939,7 +1020,9 @@ async def test_anthropic_client_integration_streaming_chat() -> None: messages = [Message(role="user", text="Count from 1 to 5.")] chunks = [] - async for chunk in client.get_response(messages=messages, stream=True, options={"max_tokens": 50}): + async for chunk in client.get_response( + messages=messages, stream=True, options={"max_tokens": 50} + ): chunks.append(chunk) assert len(chunks) > 0 @@ -963,7 +1046,11 @@ async def test_anthropic_client_integration_function_calling() -> None: assert response is not None # Should contain function call - has_function_call = any(content.type == "function_call" for msg in response.messages for content in msg.contents) + has_function_call = any( + content.type == "function_call" + for msg in response.messages + for content in msg.contents + ) assert has_function_call @@ -1102,7 +1189,9 @@ def test_prepare_response_format_openai_style(mock_anthropic_client: MagicMock) assert result["schema"]["properties"]["name"]["type"] == "string" -def test_prepare_response_format_direct_schema(mock_anthropic_client: MagicMock) -> None: +def test_prepare_response_format_direct_schema( + mock_anthropic_client: MagicMock, +) -> None: """Test response_format with direct schema key.""" client = create_test_anthropic_client(mock_anthropic_client) @@ -1136,7 +1225,9 @@ def test_prepare_response_format_raw_schema(mock_anthropic_client: MagicMock) -> assert result["schema"]["properties"]["count"]["type"] == "integer" -def test_prepare_response_format_pydantic_model(mock_anthropic_client: MagicMock) -> None: +def test_prepare_response_format_pydantic_model( + mock_anthropic_client: MagicMock, +) -> None: """Test response_format with Pydantic BaseModel.""" client = create_test_anthropic_client(mock_anthropic_client) @@ -1179,7 +1270,11 @@ def test_prepare_message_with_image_uri(mock_anthropic_client: MagicMock) -> Non message = Message( role="user", - contents=[Content.from_uri(uri="https://example.com/image.jpg", media_type="image/jpeg")], + contents=[ + Content.from_uri( + uri="https://example.com/image.jpg", media_type="image/jpeg" + ) + ], ) result = client._prepare_message_for_anthropic(message) @@ -1209,13 +1304,19 @@ def test_prepare_message_with_unsupported_data_type( assert len(result["content"]) == 0 -def test_prepare_message_with_unsupported_uri_type(mock_anthropic_client: MagicMock) -> None: +def test_prepare_message_with_unsupported_uri_type( + mock_anthropic_client: MagicMock, +) -> None: """Test preparing messages with unsupported URI content type.""" client = create_test_anthropic_client(mock_anthropic_client) message = Message( role="user", - contents=[Content.from_uri(uri="https://example.com/video.mp4", media_type="video/mp4")], + contents=[ + Content.from_uri( + uri="https://example.com/video.mp4", media_type="video/mp4" + ) + ], ) result = client._prepare_message_for_anthropic(message) @@ -1346,7 +1447,9 @@ def test_parse_contents_mcp_tool_result_object_content( assert result[0].type == "mcp_server_tool_result" -def test_parse_contents_web_search_tool_result(mock_anthropic_client: MagicMock) -> None: +def test_parse_contents_web_search_tool_result( + mock_anthropic_client: MagicMock, +) -> None: """Test parsing web search tool result.""" client = create_test_anthropic_client(mock_anthropic_client) client._last_call_id_name = ("call_789", "web_search") @@ -1399,7 +1502,9 @@ def test_get_mcp_tool_with_allowed_tools() -> None: def test_get_mcp_tool_without_allowed_tools() -> None: """Test get_mcp_tool without allowed_tools parameter.""" - result = AnthropicClient.get_mcp_tool(name="Test Server", url="https://example.com/mcp") + result = AnthropicClient.get_mcp_tool( + name="Test Server", url="https://example.com/mcp" + ) assert result["type"] == "mcp" assert result["server_label"] == "Test_Server" @@ -1476,7 +1581,9 @@ def test_func() -> str: assert result["tool_choice"]["type"] == "any" -def test_tool_choice_required_specific_function(mock_anthropic_client: MagicMock) -> None: +def test_tool_choice_required_specific_function( + mock_anthropic_client: MagicMock, +) -> None: """Test tool_choice required mode with specific function.""" client = create_test_anthropic_client(mock_anthropic_client) @@ -1516,7 +1623,9 @@ def test_func() -> str: assert result["tool_choice"]["type"] == "none" -def test_tool_choice_required_allows_parallel_use(mock_anthropic_client: MagicMock) -> None: +def test_tool_choice_required_allows_parallel_use( + mock_anthropic_client: MagicMock, +) -> None: """Test tool choice required mode with allow_multiple=True.""" client = create_test_anthropic_client(mock_anthropic_client) @@ -1636,7 +1745,9 @@ def test_parse_usage_with_cache_tokens(mock_anthropic_client: MagicMock) -> None # Code Execution Result Tests -def test_parse_code_execution_result_with_error(mock_anthropic_client: MagicMock) -> None: +def test_parse_code_execution_result_with_error( + mock_anthropic_client: MagicMock, +) -> None: """Test parsing code execution result with error.""" client = create_test_anthropic_client(mock_anthropic_client) client._last_call_id_name = ("call_code1", "code_execution_tool") @@ -1659,7 +1770,9 @@ def test_parse_code_execution_result_with_error(mock_anthropic_client: MagicMock assert result[0].type == "code_interpreter_tool_result" -def test_parse_code_execution_result_with_stdout(mock_anthropic_client: MagicMock) -> None: +def test_parse_code_execution_result_with_stdout( + mock_anthropic_client: MagicMock, +) -> None: """Test parsing code execution result with stdout.""" client = create_test_anthropic_client(mock_anthropic_client) client._last_call_id_name = ("call_code2", "code_execution_tool") @@ -1681,7 +1794,9 @@ def test_parse_code_execution_result_with_stdout(mock_anthropic_client: MagicMoc assert result[0].type == "code_interpreter_tool_result" -def test_parse_code_execution_result_with_stderr(mock_anthropic_client: MagicMock) -> None: +def test_parse_code_execution_result_with_stderr( + mock_anthropic_client: MagicMock, +) -> None: """Test parsing code execution result with stderr.""" client = create_test_anthropic_client(mock_anthropic_client) client._last_call_id_name = ("call_code3", "code_execution_tool") @@ -1703,7 +1818,9 @@ def test_parse_code_execution_result_with_stderr(mock_anthropic_client: MagicMoc assert result[0].type == "code_interpreter_tool_result" -def test_parse_code_execution_result_with_files(mock_anthropic_client: MagicMock) -> None: +def test_parse_code_execution_result_with_files( + mock_anthropic_client: MagicMock, +) -> None: """Test parsing code execution result with file outputs.""" client = create_test_anthropic_client(mock_anthropic_client) client._last_call_id_name = ("call_code4", "code_execution_tool") @@ -1732,7 +1849,9 @@ def test_parse_code_execution_result_with_files(mock_anthropic_client: MagicMock # Bash Execution Result Tests -def test_parse_bash_execution_result_with_stdout(mock_anthropic_client: MagicMock) -> None: +def test_parse_bash_execution_result_with_stdout( + mock_anthropic_client: MagicMock, +) -> None: """Test parsing bash execution result with stdout.""" client = create_test_anthropic_client(mock_anthropic_client) client._last_call_id_name = ("call_bash2", "bash_code_execution") @@ -1754,7 +1873,9 @@ def test_parse_bash_execution_result_with_stdout(mock_anthropic_client: MagicMoc assert result[0].type == "function_result" -def test_parse_bash_execution_result_with_stderr(mock_anthropic_client: MagicMock) -> None: +def test_parse_bash_execution_result_with_stderr( + mock_anthropic_client: MagicMock, +) -> None: """Test parsing bash execution result with stderr.""" client = create_test_anthropic_client(mock_anthropic_client) client._last_call_id_name = ("call_bash3", "bash_code_execution") @@ -1970,7 +2091,9 @@ def test_parse_citations_page_location(mock_anthropic_client: MagicMock) -> None assert len(result) > 0 -def test_parse_citations_content_block_location(mock_anthropic_client: MagicMock) -> None: +def test_parse_citations_content_block_location( + mock_anthropic_client: MagicMock, +) -> None: """Test parsing citations with content_block_location.""" client = create_test_anthropic_client(mock_anthropic_client) @@ -2015,7 +2138,9 @@ def test_parse_citations_web_search_location(mock_anthropic_client: MagicMock) - assert len(result) > 0 -def test_parse_citations_search_result_location(mock_anthropic_client: MagicMock) -> None: +def test_parse_citations_search_result_location( + mock_anthropic_client: MagicMock, +) -> None: """Test parsing citations with search_result_location.""" client = create_test_anthropic_client(mock_anthropic_client) @@ -2037,3 +2162,37 @@ def test_parse_citations_search_result_location(mock_anthropic_client: MagicMock result = client._parse_citations_from_anthropic(mock_block) assert len(result) > 0 + + +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_anthropic_integration_tests_disabled +async def test_anthropic_client_integration_tool_rich_content_image() -> None: + """Integration test: a tool returns an image and the model describes it.""" + image_path = Path(__file__).parent / "assets" / "sample_image.jpg" + image_bytes = image_path.read_bytes() + + @tool(approval_mode="never_require") + def get_test_image() -> Content: + """Return a test image for analysis.""" + return Content.from_data(data=image_bytes, media_type="image/jpeg") + + client = AnthropicClient() + client.function_invocation_configuration["max_iterations"] = 2 + + messages = [ + Message( + role="user", text="Call the get_test_image tool and describe what you see." + ) + ] + + response = await client.get_response( + messages=messages, + options={"tools": [get_test_image], "tool_choice": "auto", "max_tokens": 200}, + ) + + print("Model response:", response.text) + assert response is not None + assert response.text is not None + assert len(response.text) > 0 + assert "house" in response.text.lower(), f"Model did not describe the house image. Response: {response.text}" diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py index 04b93101f1..c4d4a0839a 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/core/agent_framework/openai/_chat_client.py @@ -574,18 +574,13 @@ def _prepare_message_for_openai(self, message: Message) -> list[dict[str, Any]]: # Always include content for tool results - API requires it even if empty # Functions returning None should still have a tool result message args["content"] = content.result if content.result is not None else "" + if content.items: + logger.warning( + "OpenAI Chat Completions API does not support rich content (images, audio) in tool results. " + "Rich content items will be omitted. Use the Responses API client for rich tool results." + ) if args: all_messages.append(args) - # Chat Completions API only supports string content in tool messages. - # Forward rich items as a follow-up user message (same as Responses client). - if content.items: - rich_parts = [self._prepare_content_for_openai(item) for item in content.items] - rich_parts = [p for p in rich_parts if p] - if rich_parts: - all_messages.append({ - "role": "user", - "content": rich_parts, - }) continue case "text_reasoning" if (protected_data := content.protected_data) is not None: all_messages[-1]["reasoning_details"] = json.loads(protected_data) diff --git a/python/packages/core/agent_framework/openai/_responses_client.py b/python/packages/core/agent_framework/openai/_responses_client.py index c164f6d8f9..5fb59b88c8 100644 --- a/python/packages/core/agent_framework/openai/_responses_client.py +++ b/python/packages/core/agent_framework/openai/_responses_client.py @@ -931,19 +931,6 @@ def _prepare_message_for_openai( new_args.update(self._prepare_content_for_openai(message.role, content, call_id_to_id)) # type: ignore[arg-type] if new_args: all_messages.append(new_args) - # Forward rich content items (images, audio, files) as a user message - if content.items: - rich_parts = [ - self._prepare_content_for_openai("user", item, call_id_to_id) # type: ignore[arg-type] - for item in content.items - ] - rich_parts = [p for p in rich_parts if p] - if rich_parts: - all_messages.append({ - "type": "message", - "role": "user", - "content": rich_parts, - }) case "function_call": function_call = self._prepare_content_for_openai(message.role, content, call_id_to_id) # type: ignore[arg-type] if function_call: @@ -1058,10 +1045,21 @@ def _prepare_content_for_openai( } case "function_result": # call_id for the result needs to be the same as the call_id for the function call + output: str | list[dict[str, Any]] = content.result if content.result is not None else "" + if content.items: + output_parts: list[dict[str, Any]] = [] + if content.result: + output_parts.append({"type": "input_text", "text": content.result}) + for item in content.items: + part = self._prepare_content_for_openai("user", item, call_id_to_id) # type: ignore[arg-type] + if part: + output_parts.append(part) + if output_parts: + output = output_parts args: dict[str, Any] = { "call_id": content.call_id, "type": "function_call_output", - "output": content.result if content.result is not None else "", + "output": output, } return args case "function_approval_request": diff --git a/python/packages/core/tests/assets/sample_image.jpg b/python/packages/core/tests/assets/sample_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ea6486656fd5b603af043e29b941c99845baea7a GIT binary patch literal 182161 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%0Dk-Pm;7s}kHuGa7vE${d#SHv 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 UcKRcD&9zq&ZLV4!^WzTS~D|(tTOER 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 B_m}_x literal 0 HcmV?d00001 diff --git a/python/packages/core/tests/azure/test_azure_chat_client.py b/python/packages/core/tests/azure/test_azure_chat_client.py index 3e88504493..f479f17beb 100644 --- a/python/packages/core/tests/azure/test_azure_chat_client.py +++ b/python/packages/core/tests/azure/test_azure_chat_client.py @@ -6,16 +6,6 @@ import openai import pytest -from azure.identity import AzureCliCredential -from httpx import Request, Response -from openai import AsyncAzureOpenAI, AsyncStream -from openai.resources.chat.completions import AsyncCompletions as AsyncChatCompletions -from openai.types.chat import ChatCompletion, ChatCompletionChunk -from openai.types.chat.chat_completion import Choice -from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice -from openai.types.chat.chat_completion_chunk import ChoiceDelta as ChunkChoiceDelta -from openai.types.chat.chat_completion_message import ChatCompletionMessage - from agent_framework import ( Agent, AgentResponse, @@ -33,6 +23,15 @@ ContentFilterResultSeverity, OpenAIContentFilterException, ) +from azure.identity import AzureCliCredential +from httpx import Request, Response +from openai import AsyncAzureOpenAI, AsyncStream +from openai.resources.chat.completions import AsyncCompletions as AsyncChatCompletions +from openai.types.chat import ChatCompletion, ChatCompletionChunk +from openai.types.chat.chat_completion import Choice +from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice +from openai.types.chat.chat_completion_chunk import ChoiceDelta as ChunkChoiceDelta +from openai.types.chat.chat_completion_message import ChatCompletionMessage # region Service Setup @@ -48,7 +47,10 @@ def test_init(azure_openai_unit_test_env: dict[str, str]) -> None: assert azure_chat_client.client is not None assert isinstance(azure_chat_client.client, AsyncAzureOpenAI) - assert azure_chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] + assert ( + azure_chat_client.model_id + == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] + ) assert isinstance(azure_chat_client, SupportsChatGetResponse) @@ -71,7 +73,10 @@ def test_init_base_url(azure_openai_unit_test_env: dict[str, str]) -> None: assert azure_chat_client.client is not None assert isinstance(azure_chat_client.client, AsyncAzureOpenAI) - assert azure_chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] + assert ( + azure_chat_client.model_id + == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] + ) assert isinstance(azure_chat_client, SupportsChatGetResponse) for key, value in default_headers.items(): assert key in azure_chat_client.client.default_headers @@ -84,23 +89,38 @@ def test_init_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None: assert azure_chat_client.client is not None assert isinstance(azure_chat_client.client, AsyncAzureOpenAI) - assert azure_chat_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] + assert ( + azure_chat_client.model_id + == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"] + ) assert isinstance(azure_chat_client, SupportsChatGetResponse) -@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]], indirect=True) -def test_init_with_empty_deployment_name(azure_openai_unit_test_env: dict[str, str]) -> None: +@pytest.mark.parametrize( + "exclude_list", [["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"]], indirect=True +) +def test_init_with_empty_deployment_name( + azure_openai_unit_test_env: dict[str, str], +) -> None: with pytest.raises(ValueError): AzureOpenAIChatClient() -@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_BASE_URL"]], indirect=True) -def test_init_with_empty_endpoint_and_base_url(azure_openai_unit_test_env: dict[str, str]) -> None: +@pytest.mark.parametrize( + "exclude_list", [["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_BASE_URL"]], indirect=True +) +def test_init_with_empty_endpoint_and_base_url( + azure_openai_unit_test_env: dict[str, str], +) -> None: with pytest.raises(ValueError): AzureOpenAIChatClient() -@pytest.mark.parametrize("override_env_param_dict", [{"AZURE_OPENAI_ENDPOINT": "http://test.com"}], indirect=True) +@pytest.mark.parametrize( + "override_env_param_dict", + [{"AZURE_OPENAI_ENDPOINT": "http://test.com"}], + indirect=True, +) def test_init_with_invalid_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None: # Note: URL scheme validation was previously handled by pydantic's HTTPsUrl type. # After migrating to load_settings with TypedDict, endpoint is a plain string and no longer @@ -114,7 +134,9 @@ def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None: default_headers = {"X-Test": "test"} settings = { - "deployment_name": azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], + "deployment_name": azure_openai_unit_test_env[ + "AZURE_OPENAI_CHAT_DEPLOYMENT_NAME" + ], "endpoint": azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"], "api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"], "api_version": azure_openai_unit_test_env["AZURE_OPENAI_API_VERSION"], @@ -147,7 +169,11 @@ def mock_chat_completion_response() -> ChatCompletion: return ChatCompletion( id="test_id", choices=[ - Choice(index=0, message=ChatCompletionMessage(content="test", role="assistant"), finish_reason="stop") + Choice( + index=0, + message=ChatCompletionMessage(content="test", role="assistant"), + finish_reason="stop", + ) ], created=0, model="test", @@ -159,7 +185,13 @@ def mock_chat_completion_response() -> ChatCompletion: def mock_streaming_chat_completion_response() -> AsyncStream[ChatCompletionChunk]: content = ChatCompletionChunk( id="test_id", - choices=[ChunkChoice(index=0, delta=ChunkChoiceDelta(content="test", role="assistant"), finish_reason="stop")], + choices=[ + ChunkChoice( + index=0, + delta=ChunkChoiceDelta(content="test", role="assistant"), + finish_reason="stop", + ) + ], created=0, model="test", object="chat.completion.chunk", @@ -205,7 +237,9 @@ async def test_cmc_with_logit_bias( azure_chat_client = AzureOpenAIChatClient() - await azure_chat_client.get_response(messages=chat_history, options={"logit_bias": token_bias}) + await azure_chat_client.get_response( + messages=chat_history, options={"logit_bias": token_bias} + ) mock_create.assert_awaited_once_with( model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"], @@ -323,18 +357,20 @@ async def test_azure_on_your_data_string( message=ChatCompletionMessage( content="test", role="assistant", - context=json.dumps({ # type: ignore - "citations": [ - { - "content": "test content", - "title": "test title", - "url": "test url", - "filepath": "test filepath", - "chunk_id": "test chunk_id", - } - ], - "intent": "query used", - }), + context=json.dumps( + { # type: ignore + "citations": [ + { + "content": "test content", + "title": "test title", + "url": "test url", + "filepath": "test filepath", + "chunk_id": "test chunk_id", + } + ], + "intent": "query used", + } + ), ), finish_reason="stop", ) @@ -484,7 +520,9 @@ async def test_content_filtering_raises_correct_exception( azure_chat_client = AzureOpenAIChatClient() - with pytest.raises(OpenAIContentFilterException, match="service encountered a content error") as exc_info: + with pytest.raises( + OpenAIContentFilterException, match="service encountered a content error" + ) as exc_info: await azure_chat_client.get_response( messages=chat_history, ) @@ -492,7 +530,10 @@ async def test_content_filtering_raises_correct_exception( content_filter_exc = exc_info.value assert content_filter_exc.param == "prompt" assert content_filter_exc.content_filter_result["hate"].filtered - assert content_filter_exc.content_filter_result["hate"].severity == ContentFilterResultSeverity.HIGH + assert ( + content_filter_exc.content_filter_result["hate"].severity + == ContentFilterResultSeverity.HIGH + ) @patch.object(AsyncChatCompletions, "create") @@ -528,7 +569,9 @@ async def test_content_filtering_without_response_code_raises_with_default_code( azure_chat_client = AzureOpenAIChatClient() - with pytest.raises(OpenAIContentFilterException, match="service encountered a content error"): + with pytest.raises( + OpenAIContentFilterException, match="service encountered a content error" + ): await azure_chat_client.get_response( messages=chat_history, ) @@ -546,12 +589,16 @@ async def test_bad_request_non_content_filter( test_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") assert test_endpoint is not None mock_create.side_effect = openai.BadRequestError( - "The request was bad.", response=Response(400, request=Request("POST", test_endpoint)), body={} + "The request was bad.", + response=Response(400, request=Request("POST", test_endpoint)), + body={}, ) azure_chat_client = AzureOpenAIChatClient() - with pytest.raises(ChatClientException, match="service failed to complete the prompt"): + with pytest.raises( + ChatClientException, match="service failed to complete the prompt" + ): await azure_chat_client.get_response( messages=chat_history, ) @@ -594,7 +641,9 @@ async def test_streaming_with_none_delta( ) -> None: """Test streaming handles None delta from async content filtering.""" # First chunk has None delta (simulates async filtering) - chunk_choice_with_none = ChunkChoice.model_construct(index=0, delta=None, finish_reason=None) + chunk_choice_with_none = ChunkChoice.model_construct( + index=0, delta=None, finish_reason=None + ) chunk_with_none_delta = ChatCompletionChunk.model_construct( id="test_id", choices=[chunk_choice_with_none], @@ -605,7 +654,13 @@ async def test_streaming_with_none_delta( # Second chunk has actual content chunk_with_content = ChatCompletionChunk( id="test_id", - choices=[ChunkChoice(index=0, delta=ChunkChoiceDelta(content="test", role="assistant"), finish_reason="stop")], + choices=[ + ChunkChoice( + index=0, + delta=ChunkChoiceDelta(content="test", role="assistant"), + finish_reason="stop", + ) + ], created=0, model="test", object="chat.completion.chunk", @@ -622,7 +677,11 @@ async def test_streaming_with_none_delta( results.append(msg) assert len(results) > 0 - assert any(content.type == "text" and content.text == "test" for msg in results for content in msg.contents) + assert any( + content.type == "text" and content.text == "test" + for msg in results + for content in msg.contents + ) assert any(msg.contents for msg in results) @@ -670,7 +729,8 @@ async def test_azure_openai_chat_client_response() -> None: assert isinstance(response, ChatResponse) # Check for any relevant keywords that indicate the AI understood the context assert any( - word in response.text.lower() for word in ["scientists", "research", "antarctica", "glaciology", "climate"] + word in response.text.lower() + for word in ["scientists", "research", "antarctica", "glaciology", "climate"] ) @@ -769,7 +829,9 @@ async def test_azure_openai_chat_client_agent_basic_run(): client=AzureOpenAIChatClient(credential=AzureCliCredential()), ) as agent: # Test basic run - response = await agent.run("Please respond with exactly: 'This is a response test.'") + response = await agent.run( + "Please respond with exactly: 'This is a response test.'" + ) assert isinstance(response, AgentResponse) assert response.text is not None @@ -787,7 +849,10 @@ async def test_azure_openai_chat_client_agent_basic_run_streaming(): ) as agent: # Test streaming run full_text = "" - async for chunk in agent.run("Please respond with exactly: 'This is a streaming response test.'", stream=True): + async for chunk in agent.run( + "Please respond with exactly: 'This is a streaming response test.'", + stream=True, + ): assert isinstance(chunk, AgentResponseUpdate) if chunk.text: full_text += chunk.text @@ -836,7 +901,9 @@ async def test_azure_openai_chat_client_agent_existing_session(): ) as first_agent: # Start a conversation and capture the session session = first_agent.create_session() - first_response = await first_agent.run("My name is Alice. Remember this.", session=session) + first_response = await first_agent.run( + "My name is Alice. Remember this.", session=session + ) assert isinstance(first_response, AgentResponse) assert first_response.text is not None @@ -851,7 +918,9 @@ async def test_azure_openai_chat_client_agent_existing_session(): instructions="You are a helpful assistant with good memory.", ) as second_agent: # Reuse the preserved session - second_response = await second_agent.run("What is my name?", session=preserved_session) + second_response = await second_agent.run( + "What is my name?", session=preserved_session + ) assert isinstance(second_response, AgentResponse) assert second_response.text is not None @@ -875,7 +944,9 @@ async def test_azure_chat_client_agent_level_tool_persistence(): assert isinstance(first_response, AgentResponse) 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"]) + 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?") @@ -883,4 +954,6 @@ async def test_azure_chat_client_agent_level_tool_persistence(): assert isinstance(second_response, AgentResponse) 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"]) + assert any( + term in second_response.text.lower() for term in ["miami", "sunny", "72"] + ) diff --git a/python/packages/core/tests/azure/test_azure_responses_client.py b/python/packages/core/tests/azure/test_azure_responses_client.py index 37efff16ca..6465957ae8 100644 --- a/python/packages/core/tests/azure/test_azure_responses_client.py +++ b/python/packages/core/tests/azure/test_azure_responses_client.py @@ -3,14 +3,11 @@ import json import logging import os +from pathlib import Path from typing import Annotated, Any from unittest.mock import MagicMock import pytest -from azure.identity import AzureCliCredential -from pydantic import BaseModel -from pytest import param - from agent_framework import ( Agent, AgentResponse, @@ -21,6 +18,9 @@ tool, ) from agent_framework.azure import AzureOpenAIResponsesClient +from azure.identity import AzureCliCredential +from pydantic import BaseModel +from pytest import param skip_if_azure_integration_tests_disabled = pytest.mark.skipif( os.getenv("AZURE_OPENAI_ENDPOINT", "") in ("", "https://test-endpoint.com"), @@ -44,23 +44,32 @@ 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: AzureOpenAIResponsesClient) -> tuple[str, Content]: +async def create_vector_store( + client: AzureOpenAIResponsesClient, +) -> tuple[str, Content]: """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" + 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) + 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}") + raise Exception( + f"Vector store file processing failed with status: {result.last_error.message}" + ) return file.id, Content.from_hosted_vector_store(vector_store_id=vector_store.id) -async def delete_vector_store(client: AzureOpenAIResponsesClient, file_id: str, vector_store_id: str) -> None: +async def delete_vector_store( + client: AzureOpenAIResponsesClient, 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) @@ -71,7 +80,10 @@ def test_init(azure_openai_unit_test_env: dict[str, str]) -> None: # Test successful initialization azure_responses_client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) - assert azure_responses_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] + assert ( + azure_responses_client.model_id + == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] + ) assert isinstance(azure_responses_client, SupportsChatGetResponse) @@ -98,9 +110,13 @@ def test_init_model_id_kwarg(azure_openai_unit_test_env: dict[str, str]) -> None assert isinstance(azure_responses_client, SupportsChatGetResponse) -def test_init_model_id_kwarg_does_not_override_deployment_name(azure_openai_unit_test_env: dict[str, str]) -> None: +def test_init_model_id_kwarg_does_not_override_deployment_name( + azure_openai_unit_test_env: dict[str, str], +) -> None: """Test that deployment_name takes precedence over model_id kwarg (issue #4299).""" - azure_responses_client = AzureOpenAIResponsesClient(deployment_name="my-deployment", model_id="gpt-4o") + azure_responses_client = AzureOpenAIResponsesClient( + deployment_name="my-deployment", model_id="gpt-4o" + ) assert azure_responses_client.model_id == "my-deployment" assert isinstance(azure_responses_client, SupportsChatGetResponse) @@ -110,7 +126,10 @@ def test_init_model_id_kwarg_none(azure_openai_unit_test_env: dict[str, str]) -> """Test that model_id=None does not override the env-var deployment name.""" azure_responses_client = AzureOpenAIResponsesClient(model_id=None) - assert azure_responses_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] + assert ( + azure_responses_client.model_id + == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] + ) def test_init_with_default_header(azure_openai_unit_test_env: dict[str, str]) -> None: @@ -121,7 +140,10 @@ def test_init_with_default_header(azure_openai_unit_test_env: dict[str, str]) -> default_headers=default_headers, ) - assert azure_responses_client.model_id == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] + assert ( + azure_responses_client.model_id + == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] + ) assert isinstance(azure_responses_client, SupportsChatGetResponse) # Assert that the default header we added is present in the client's default headers @@ -130,7 +152,9 @@ def test_init_with_default_header(azure_openai_unit_test_env: dict[str, str]) -> assert azure_responses_client.client.default_headers[key] == value -@pytest.mark.parametrize("exclude_list", [["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]], indirect=True) +@pytest.mark.parametrize( + "exclude_list", [["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"]], indirect=True +) def test_init_with_empty_model_id(azure_openai_unit_test_env: dict[str, str]) -> None: with pytest.raises(ValueError): AzureOpenAIResponsesClient() @@ -214,7 +238,9 @@ def test_create_client_from_project_with_endpoint() -> None: mock_openai_client = MagicMock(spec=AsyncOpenAI) mock_credential = MagicMock() - with patch("agent_framework.azure._responses_client.AIProjectClient") as MockAIProjectClient: + with patch( + "agent_framework.azure._responses_client.AIProjectClient" + ) as MockAIProjectClient: mock_instance = MockAIProjectClient.return_value mock_instance.get_openai_client.return_value = mock_openai_client @@ -253,14 +279,19 @@ def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None: default_headers = {"X-Unit-Test": "test-guid"} settings = { - "deployment_name": azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"], + "deployment_name": azure_openai_unit_test_env[ + "AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME" + ], "api_key": azure_openai_unit_test_env["AZURE_OPENAI_API_KEY"], "default_headers": default_headers, } azure_responses_client = AzureOpenAIResponsesClient.from_dict(settings) dumped_settings = azure_responses_client.to_dict() - assert dumped_settings["deployment_name"] == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] + assert ( + dumped_settings["deployment_name"] + == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME"] + ) assert "api_key" not in dumped_settings # Assert that the default header we added is present in the dumped_settings default headers for key, value in default_headers.items(): @@ -323,7 +354,12 @@ def test_serialize(azure_openai_unit_test_env: dict[str, str]) -> None: "temperature_c": {"type": "number"}, "advisory": {"type": "string"}, }, - "required": ["location", "conditions", "temperature_c", "advisory"], + "required": [ + "location", + "conditions", + "temperature_c", + "advisory", + ], "additionalProperties": False, }, }, @@ -388,7 +424,9 @@ async def test_integration_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 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 @@ -396,7 +434,9 @@ async def test_integration_options( if option_name == "tools" or option_name == "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}" + assert "sunny" in text or "seattle" in text, ( + f"Tool not invoked for {option_name}" + ) elif option_name == "response_format": if option_value == OutputStruct: # Should have structured output @@ -405,7 +445,9 @@ async def test_integration_options( assert "seattle" in response.value.location.lower() else: # Runtime JSON schema - assert response.value is None, "No structured output, can't parse any json." + 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 @@ -445,11 +487,18 @@ async def test_integration_web_search() -> None: # Test that the client will use the web search tool with location content = { - "messages": [Message(role="user", text="What is the current weather? Do not ask for my current location.")], + "messages": [ + Message( + role="user", + text="What is the current weather? Do not ask for my current location.", + ) + ], "options": { "tool_choice": "auto", "tools": [ - AzureOpenAIResponsesClient.get_web_search_tool(user_location={"country": "US", "city": "Seattle"}) + AzureOpenAIResponsesClient.get_web_search_tool( + user_location={"country": "US", "city": "Seattle"} + ) ], }, "stream": streaming, @@ -479,7 +528,9 @@ async def test_integration_client_file_search() -> None: ], options={ "tools": [ - AzureOpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id]) + AzureOpenAIResponsesClient.get_file_search_tool( + vector_store_ids=[vector_store.vector_store_id] + ) ], "tool_choice": "auto", }, @@ -488,7 +539,9 @@ async def test_integration_client_file_search() -> None: assert "sunny" in response.text.lower() assert "75" in response.text finally: - await delete_vector_store(azure_responses_client, file_id, vector_store.vector_store_id) + await delete_vector_store( + azure_responses_client, file_id, vector_store.vector_store_id + ) @pytest.mark.flaky @@ -510,7 +563,9 @@ async def test_integration_client_file_search_streaming() -> None: stream=True, options={ "tools": [ - AzureOpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id]) + AzureOpenAIResponsesClient.get_file_search_tool( + vector_store_ids=[vector_store.vector_store_id] + ) ], "tool_choice": "auto", }, @@ -520,7 +575,9 @@ async def test_integration_client_file_search_streaming() -> None: assert "sunny" in full_response.text.lower() assert "75" in full_response.text finally: - await delete_vector_store(azure_responses_client, file_id, vector_store.vector_store_id) + await delete_vector_store( + azure_responses_client, file_id, vector_store.vector_store_id + ) @pytest.mark.flaky @@ -530,7 +587,11 @@ async def test_integration_client_agent_hosted_mcp_tool() -> None: """Integration test for MCP tool with Azure Response Agent using Microsoft Learn MCP.""" client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) response = await client.get_response( - messages=[Message(role="user", text="How to create an Azure storage account using az cli?")], + messages=[ + Message( + role="user", text="How to create an Azure storage account using az cli?" + ) + ], options={ # this needs to be high enough to handle the full MCP tool response. "max_tokens": 5000, @@ -545,7 +606,9 @@ async def test_integration_client_agent_hosted_mcp_tool() -> None: if not response.text: pytest.skip("MCP server returned empty response - service-side issue") # 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"]) + assert any( + term in response.text.lower() for term in ["azure", "storage", "account", "cli"] + ) @pytest.mark.flaky @@ -556,14 +619,20 @@ async def test_integration_client_agent_hosted_code_interpreter_tool(): client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) response = await client.get_response( - messages=[Message(role="user", text="Calculate the sum of numbers from 1 to 10 using Python code.")], + messages=[ + Message( + role="user", + text="Calculate the sum of numbers from 1 to 10 using Python code.", + ) + ], options={ "tools": [AzureOpenAIResponsesClient.get_code_interpreter_tool()], }, ) # Should contain calculation result (sum of 1-10 = 55) or code execution content contains_relevant_content = any( - term in response.text.lower() for term in ["55", "sum", "code", "python", "calculate", "10"] + term in response.text.lower() + for term in ["55", "sum", "code", "python", "calculate", "10"] ) assert contains_relevant_content or len(response.text.strip()) > 10 @@ -582,7 +651,9 @@ async def test_integration_client_agent_existing_session(): ) as first_agent: # Start a conversation and capture the session session = first_agent.create_session() - first_response = await first_agent.run("My hobby is photography. Remember this.", session=session, store=True) + first_response = await first_agent.run( + "My hobby is photography. Remember this.", session=session, store=True + ) assert isinstance(first_response, AgentResponse) assert first_response.text is not None @@ -597,8 +668,49 @@ async def test_integration_client_agent_existing_session(): instructions="You are a helpful assistant with good memory.", ) as second_agent: # Reuse the preserved session - second_response = await second_agent.run("What is my hobby?", session=preserved_session) + second_response = await second_agent.run( + "What is my hobby?", session=preserved_session + ) assert isinstance(second_response, AgentResponse) assert second_response.text is not None assert "photography" in second_response.text.lower() + + +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_azure_integration_tests_disabled +async def test_azure_openai_responses_client_tool_rich_content_image() -> None: + """Test that Azure OpenAI Responses client can handle tool results containing images.""" + image_path = Path(__file__).parent.parent / "assets" / "sample_image.jpg" + image_bytes = image_path.read_bytes() + + @tool(approval_mode="never_require") + def get_test_image() -> Content: + """Return a test image for analysis.""" + return Content.from_data(data=image_bytes, media_type="image/jpeg") + + client = AzureOpenAIResponsesClient(credential=AzureCliCredential()) + client.function_invocation_configuration["max_iterations"] = 2 + + for streaming in [False, True]: + messages = [ + Message( + role="user", + text="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) + + assert response is not None + assert isinstance(response, ChatResponse) + assert response.text is not None + assert len(response.text) > 0 + assert "house" in response.text.lower(), f"Model did not describe the house image. Response: {response.text}" diff --git a/python/packages/core/tests/openai/test_openai_chat_client.py b/python/packages/core/tests/openai/test_openai_chat_client.py index fae303ed22..870c5dcf22 100644 --- a/python/packages/core/tests/openai/test_openai_chat_client.py +++ b/python/packages/core/tests/openai/test_openai_chat_client.py @@ -6,12 +6,6 @@ from unittest.mock import MagicMock, patch import pytest -from openai import BadRequestError -from openai.types.chat.chat_completion import ChatCompletion, Choice -from openai.types.chat.chat_completion_message import ChatCompletionMessage -from pydantic import BaseModel -from pytest import param - from agent_framework import ( ChatResponse, Content, @@ -22,6 +16,11 @@ from agent_framework.exceptions import ChatClientException from agent_framework.openai import OpenAIChatClient from agent_framework.openai._exceptions import OpenAIContentFilterException +from openai import BadRequestError +from openai.types.chat.chat_completion import ChatCompletion, Choice +from openai.types.chat.chat_completion_message import ChatCompletionMessage +from pydantic import BaseModel +from pytest import param skip_if_openai_integration_tests_disabled = pytest.mark.skipif( os.getenv("OPENAI_API_KEY", "") in ("", "test-dummy-key"), @@ -33,7 +32,9 @@ def test_init(openai_unit_test_env: dict[str, str]) -> None: # Test successful initialization open_ai_chat_completion = OpenAIChatClient() - assert open_ai_chat_completion.model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + assert ( + open_ai_chat_completion.model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + ) assert isinstance(open_ai_chat_completion, SupportsChatGetResponse) @@ -60,7 +61,9 @@ def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None: default_headers=default_headers, ) - assert open_ai_chat_completion.model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + assert ( + open_ai_chat_completion.model_id == openai_unit_test_env["OPENAI_CHAT_MODEL_ID"] + ) assert isinstance(open_ai_chat_completion, SupportsChatGetResponse) # Assert that the default header we added is present in the client's default headers @@ -142,7 +145,9 @@ def test_serialize_with_org_id(openai_unit_test_env: dict[str, str]) -> None: assert "User-Agent" not in dumped_settings.get("default_headers", {}) -async def test_content_filter_exception_handling(openai_unit_test_env: dict[str, str]) -> None: +async def test_content_filter_exception_handling( + openai_unit_test_env: dict[str, str], +) -> None: """Test that content filter errors are properly handled.""" client = OpenAIChatClient() messages = [Message(role="user", text="test message")] @@ -150,7 +155,9 @@ async def test_content_filter_exception_handling(openai_unit_test_env: dict[str, # Create a mock BadRequestError with content_filter code mock_response = MagicMock() mock_error = BadRequestError( - message="Content filter error", response=mock_response, body={"error": {"code": "content_filter"}} + message="Content filter error", + response=mock_response, + body={"error": {"code": "content_filter"}}, ) mock_error.code = "content_filter" @@ -184,7 +191,9 @@ class UnsupportedTool: assert result["tools"] == [dict_tool] -def test_prepare_tools_with_single_function_tool(openai_unit_test_env: dict[str, str]) -> None: +def test_prepare_tools_with_single_function_tool( + openai_unit_test_env: dict[str, str], +) -> None: """Test that a single FunctionTool is accepted for tool preparation.""" client = OpenAIChatClient() @@ -241,12 +250,17 @@ async def test_exception_message_includes_original_error_details() -> None: assert original_error_message in exception_message -def test_chat_response_content_order_text_before_tool_calls(openai_unit_test_env: dict[str, str]): +def test_chat_response_content_order_text_before_tool_calls( + openai_unit_test_env: dict[str, str], +): """Test that text content appears before tool calls in ChatResponse contents.""" # Import locally to avoid break other tests when the import changes from openai.types.chat.chat_completion import ChatCompletion, Choice from openai.types.chat.chat_completion_message import ChatCompletionMessage - from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall, Function + from openai.types.chat.chat_completion_message_tool_call import ( + ChatCompletionMessageToolCall, + Function, + ) # Create a mock OpenAI response with both text and tool calls mock_response = ChatCompletion( @@ -264,7 +278,9 @@ def test_chat_response_content_order_text_before_tool_calls(openai_unit_test_env ChatCompletionMessageToolCall( id="call-123", type="function", - function=Function(name="calculate", arguments='{"x": 5, "y": 3}'), + function=Function( + name="calculate", arguments='{"x": 5, "y": 3}' + ), ) ], ), @@ -298,7 +314,8 @@ def test_function_result_falsy_values_handling(openai_unit_test_env: dict[str, s # Test with empty list serialized as JSON string (as FunctionTool.invoke would produce) message_with_empty_list = Message( - role="tool", contents=[Content.from_function_result(call_id="call-123", result="[]")] + role="tool", + contents=[Content.from_function_result(call_id="call-123", result="[]")], ) openai_messages = client._prepare_message_for_openai(message_with_empty_list) @@ -307,7 +324,8 @@ def test_function_result_falsy_values_handling(openai_unit_test_env: dict[str, s # Test with empty string (falsy but not None) message_with_empty_string = Message( - role="tool", contents=[Content.from_function_result(call_id="call-456", result="")] + role="tool", + contents=[Content.from_function_result(call_id="call-456", result="")], ) openai_messages = client._prepare_message_for_openai(message_with_empty_string) @@ -316,7 +334,8 @@ def test_function_result_falsy_values_handling(openai_unit_test_env: dict[str, s # Test with False serialized as JSON string (as FunctionTool.invoke would produce) message_with_false = Message( - role="tool", contents=[Content.from_function_result(call_id="call-789", result="false")] + role="tool", + contents=[Content.from_function_result(call_id="call-789", result="false")], ) openai_messages = client._prepare_message_for_openai(message_with_false) @@ -336,7 +355,11 @@ def test_function_result_exception_handling(openai_unit_test_env: dict[str, str] message_with_exception = Message( role="tool", contents=[ - Content.from_function_result(call_id="call-123", result="Error: Function failed.", exception=test_exception) + Content.from_function_result( + call_id="call-123", + result="Error: Function failed.", + exception=test_exception, + ) ], ) @@ -355,7 +378,9 @@ def test_parse_result_string_passthrough(): assert isinstance(result, str) -def test_prepare_content_for_openai_data_content_image(openai_unit_test_env: dict[str, str]) -> None: +def test_prepare_content_for_openai_data_content_image( + openai_unit_test_env: dict[str, str], +) -> None: """Test _prepare_content_for_openai converts DataContent with image media type to OpenAI format.""" client = OpenAIChatClient() @@ -372,7 +397,9 @@ def test_prepare_content_for_openai_data_content_image(openai_unit_test_env: dic assert result["image_url"]["url"] == image_data_content.uri # Test DataContent with non-image media type should use default model_dump - text_data_content = Content.from_uri(uri="data:text/plain;base64,SGVsbG8gV29ybGQ=", media_type="text/plain") + text_data_content = Content.from_uri( + uri="data:text/plain;base64,SGVsbG8gV29ybGQ=", media_type="text/plain" + ) result = client._prepare_content_for_openai(text_data_content) # type: ignore @@ -392,12 +419,16 @@ def test_prepare_content_for_openai_data_content_image(openai_unit_test_env: dic # Should convert to OpenAI input_audio format assert result["type"] == "input_audio" # Data should contain just the base64 part, not the full data URI - assert result["input_audio"]["data"] == "UklGRjBEAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQwEAAAAAAAAAAAA" + assert ( + result["input_audio"]["data"] + == "UklGRjBEAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQwEAAAAAAAAAAAA" + ) assert result["input_audio"]["format"] == "wav" # Test DataContent with MP3 audio mp3_data_content = Content.from_uri( - uri="data:audio/mp3;base64,//uQAAAAWGluZwAAAA8AAAACAAACcQ==", media_type="audio/mp3" + uri="data:audio/mp3;base64,//uQAAAAWGluZwAAAA8AAAACAAACcQ==", + media_type="audio/mp3", ) result = client._prepare_content_for_openai(mp3_data_content) # type: ignore @@ -409,7 +440,9 @@ def test_prepare_content_for_openai_data_content_image(openai_unit_test_env: dic assert result["input_audio"]["format"] == "mp3" -def test_prepare_content_for_openai_document_file_mapping(openai_unit_test_env: dict[str, str]) -> None: +def test_prepare_content_for_openai_document_file_mapping( + openai_unit_test_env: dict[str, str], +) -> None: """Test _prepare_content_for_openai converts document files (PDF, DOCX, etc.) to OpenAI file format.""" client = OpenAIChatClient() @@ -423,7 +456,9 @@ def test_prepare_content_for_openai_document_file_mapping(openai_unit_test_env: # Should convert to OpenAI file format without filename assert result["type"] == "file" - assert "filename" not in result["file"] # No filename provided, so none should be set + assert ( + "filename" not in result["file"] + ) # No filename provided, so none should be set assert "file_data" in result["file"] # Base64 data should be the full data URI (OpenAI requirement) assert result["file"]["file_data"].startswith("data:application/pdf;base64,") @@ -473,7 +508,9 @@ def test_prepare_content_for_openai_document_file_mapping(openai_unit_test_env: # All application/* types should now be mapped to file format assert result["type"] == "file" - assert "filename" not in result["file"] # Should omit filename when not provided + assert ( + "filename" not in result["file"] + ) # Should omit filename when not provided assert result["file"]["file_data"] == doc_content.uri # Test with filename - should now use file format with filename @@ -515,7 +552,9 @@ def test_prepare_content_for_openai_document_file_mapping(openai_unit_test_env: assert "filename" not in result["file"] # None filename should be omitted -def test_parse_text_reasoning_content_from_response(openai_unit_test_env: dict[str, str]) -> None: +def test_parse_text_reasoning_content_from_response( + openai_unit_test_env: dict[str, str], +) -> None: """Test that TextReasoningContent is correctly parsed from OpenAI response with reasoning_details.""" client = OpenAIChatClient() @@ -563,7 +602,9 @@ def test_parse_text_reasoning_content_from_response(openai_unit_test_env: dict[s assert parsed_details == mock_reasoning_details -def test_parse_text_reasoning_content_from_streaming_chunk(openai_unit_test_env: dict[str, str]) -> None: +def test_parse_text_reasoning_content_from_streaming_chunk( + openai_unit_test_env: dict[str, str], +) -> None: """Test that TextReasoningContent is correctly parsed from streaming OpenAI chunk with reasoning_details.""" from openai.types.chat.chat_completion_chunk import ChatCompletionChunk from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice @@ -611,7 +652,9 @@ def test_parse_text_reasoning_content_from_streaming_chunk(openai_unit_test_env: assert parsed_details == mock_reasoning_details -def test_prepare_message_with_text_reasoning_content(openai_unit_test_env: dict[str, str]) -> None: +def test_prepare_message_with_text_reasoning_content( + openai_unit_test_env: dict[str, str], +) -> None: """Test that TextReasoningContent with protected_data is correctly prepared for OpenAI.""" client = OpenAIChatClient() @@ -622,7 +665,9 @@ def test_prepare_message_with_text_reasoning_content(openai_unit_test_env: dict[ "summary": "Quick analysis", } - reasoning_content = Content.from_text_reasoning(text=None, protected_data=json.dumps(mock_reasoning_data)) + reasoning_content = Content.from_text_reasoning( + text=None, protected_data=json.dumps(mock_reasoning_data) + ) # Message must have other content first for reasoning to attach to message = Message( @@ -643,7 +688,9 @@ def test_prepare_message_with_text_reasoning_content(openai_unit_test_env: dict[ assert prepared[0]["content"] == "The answer is 42." -def test_function_approval_content_is_skipped_in_preparation(openai_unit_test_env: dict[str, str]) -> None: +def test_function_approval_content_is_skipped_in_preparation( + openai_unit_test_env: dict[str, str], +) -> None: """Test that function approval request and response content are skipped.""" client = OpenAIChatClient() @@ -689,7 +736,9 @@ def test_function_approval_content_is_skipped_in_preparation(openai_unit_test_en assert prepared_mixed[0]["content"] == "I need approval for this action." -def test_usage_content_in_streaming_response(openai_unit_test_env: dict[str, str]) -> None: +def test_usage_content_in_streaming_response( + openai_unit_test_env: dict[str, str], +) -> None: """Test that UsageContent is correctly parsed from streaming response with usage data.""" from openai.types.chat.chat_completion_chunk import ChatCompletionChunk from openai.types.completion_usage import CompletionUsage @@ -725,13 +774,19 @@ def test_usage_content_in_streaming_response(openai_unit_test_env: dict[str, str assert usage_content.usage_details["total_token_count"] == 150 -def test_streaming_chunk_with_usage_and_text(openai_unit_test_env: dict[str, str]) -> None: +def test_streaming_chunk_with_usage_and_text( + openai_unit_test_env: dict[str, str], +) -> None: """Test that text content is not lost when usage data is in the same chunk. Some providers (e.g. Gemini) include both usage and text content in the same streaming chunk. See https://github.com/microsoft/agent-framework/issues/3434 """ - from openai.types.chat.chat_completion_chunk import ChatCompletionChunk, Choice, ChoiceDelta + from openai.types.chat.chat_completion_chunk import ( + ChatCompletionChunk, + Choice, + ChoiceDelta, + ) from openai.types.completion_usage import CompletionUsage client = OpenAIChatClient() @@ -755,7 +810,9 @@ def test_streaming_chunk_with_usage_and_text(openai_unit_test_env: dict[str, str # Should have BOTH text and usage content content_types = [c.type for c in update.contents] - assert "text" in content_types, "Text content should not be lost when usage is present" + assert "text" in content_types, ( + "Text content should not be lost when usage is present" + ) assert "usage" in content_types, "Usage content should still be present" text_content = next(c for c in update.contents if c.type == "text") @@ -815,11 +872,15 @@ def test_prepare_options_without_messages(openai_unit_test_env: dict[str, str]) client = OpenAIChatClient() - with pytest.raises(ChatClientInvalidRequestException, match="Messages are required"): + with pytest.raises( + ChatClientInvalidRequestException, match="Messages are required" + ): client._prepare_options([], {}) -def test_prepare_tools_with_web_search_no_location(openai_unit_test_env: dict[str, str]) -> None: +def test_prepare_tools_with_web_search_no_location( + openai_unit_test_env: dict[str, str], +) -> None: """Test preparing web search tool without user location.""" client = OpenAIChatClient() @@ -833,7 +894,9 @@ def test_prepare_tools_with_web_search_no_location(openai_unit_test_env: dict[st assert result["web_search_options"] == {} -def test_prepare_options_with_instructions(openai_unit_test_env: dict[str, str]) -> None: +def test_prepare_options_with_instructions( + openai_unit_test_env: dict[str, str], +) -> None: """Test that instructions are prepended as system message.""" client = OpenAIChatClient() @@ -865,7 +928,9 @@ def test_prepare_message_with_author_name(openai_unit_test_env: dict[str, str]) assert prepared[0]["name"] == "TestUser" -def test_prepare_message_with_tool_result_author_name(openai_unit_test_env: dict[str, str]) -> None: +def test_prepare_message_with_tool_result_author_name( + openai_unit_test_env: dict[str, str], +) -> None: """Test that author_name is not included for TOOL role messages.""" client = OpenAIChatClient() @@ -883,7 +948,9 @@ def test_prepare_message_with_tool_result_author_name(openai_unit_test_env: dict assert "name" not in prepared[0] -def test_prepare_system_message_content_is_string(openai_unit_test_env: dict[str, str]) -> None: +def test_prepare_system_message_content_is_string( + openai_unit_test_env: dict[str, str], +) -> None: """Test that system message content is a plain string, not a list. Some OpenAI-compatible endpoints (e.g. NVIDIA NIM) reject system messages @@ -891,7 +958,9 @@ def test_prepare_system_message_content_is_string(openai_unit_test_env: dict[str """ client = OpenAIChatClient() - message = Message(role="system", contents=[Content.from_text(text="You are a helpful assistant.")]) + message = Message( + role="system", contents=[Content.from_text(text="You are a helpful assistant.")] + ) prepared = client._prepare_message_for_openai(message) @@ -901,11 +970,15 @@ def test_prepare_system_message_content_is_string(openai_unit_test_env: dict[str assert prepared[0]["content"] == "You are a helpful assistant." -def test_prepare_developer_message_content_is_string(openai_unit_test_env: dict[str, str]) -> None: +def test_prepare_developer_message_content_is_string( + openai_unit_test_env: dict[str, str], +) -> None: """Test that developer message content is a plain string, not a list.""" client = OpenAIChatClient() - message = Message(role="developer", contents=[Content.from_text(text="Follow these rules.")]) + message = Message( + role="developer", contents=[Content.from_text(text="Follow these rules.")] + ) prepared = client._prepare_message_for_openai(message) @@ -915,7 +988,9 @@ def test_prepare_developer_message_content_is_string(openai_unit_test_env: dict[ assert prepared[0]["content"] == "Follow these rules." -def test_prepare_system_message_multiple_text_contents_joined(openai_unit_test_env: dict[str, str]) -> None: +def test_prepare_system_message_multiple_text_contents_joined( + openai_unit_test_env: dict[str, str], +) -> None: """Test that system messages with multiple text contents are joined into a single string.""" client = OpenAIChatClient() @@ -935,7 +1010,9 @@ def test_prepare_system_message_multiple_text_contents_joined(openai_unit_test_e assert prepared[0]["content"] == "You are a helpful assistant.\nBe concise." -def test_prepare_user_message_text_content_is_string(openai_unit_test_env: dict[str, str]) -> None: +def test_prepare_user_message_text_content_is_string( + openai_unit_test_env: dict[str, str], +) -> None: """Test that text-only user message content is flattened to a plain string. Some OpenAI-compatible endpoints (e.g. Foundry Local) cannot deserialize @@ -953,7 +1030,9 @@ def test_prepare_user_message_text_content_is_string(openai_unit_test_env: dict[ assert prepared[0]["content"] == "Hello" -def test_prepare_user_message_multimodal_content_remains_list(openai_unit_test_env: dict[str, str]) -> None: +def test_prepare_user_message_multimodal_content_remains_list( + openai_unit_test_env: dict[str, str], +) -> None: """Test that multimodal user message content remains a list.""" client = OpenAIChatClient() @@ -961,7 +1040,9 @@ def test_prepare_user_message_multimodal_content_remains_list(openai_unit_test_e role="user", contents=[ Content.from_text(text="What's in this image?"), - Content.from_uri(uri="https://example.com/image.png", media_type="image/png"), + Content.from_uri( + uri="https://example.com/image.png", media_type="image/png" + ), ], ) @@ -972,11 +1053,15 @@ def test_prepare_user_message_multimodal_content_remains_list(openai_unit_test_e assert has_list_content -def test_prepare_assistant_message_text_content_is_string(openai_unit_test_env: dict[str, str]) -> None: +def test_prepare_assistant_message_text_content_is_string( + openai_unit_test_env: dict[str, str], +) -> None: """Test that text-only assistant message content is flattened to a plain string.""" client = OpenAIChatClient() - message = Message(role="assistant", contents=[Content.from_text(text="Sure, I can help.")]) + message = Message( + role="assistant", contents=[Content.from_text(text="Sure, I can help.")] + ) prepared = client._prepare_message_for_openai(message) @@ -986,7 +1071,9 @@ def test_prepare_assistant_message_text_content_is_string(openai_unit_test_env: assert prepared[0]["content"] == "Sure, I can help." -def test_tool_choice_required_with_function_name(openai_unit_test_env: dict[str, str]) -> None: +def test_tool_choice_required_with_function_name( + openai_unit_test_env: dict[str, str], +) -> None: """Test that tool_choice with required mode and function name is correctly prepared.""" client = OpenAIChatClient() @@ -1021,7 +1108,9 @@ def test_response_format_dict_passthrough(openai_unit_test_env: dict[str, str]) assert prepared_options["response_format"] == custom_format -def test_multiple_function_calls_in_single_message(openai_unit_test_env: dict[str, str]) -> None: +def test_multiple_function_calls_in_single_message( + openai_unit_test_env: dict[str, str], +) -> None: """Test that multiple function calls in a message are correctly prepared.""" client = OpenAIChatClient() @@ -1029,8 +1118,12 @@ def test_multiple_function_calls_in_single_message(openai_unit_test_env: dict[st message = Message( role="assistant", contents=[ - Content.from_function_call(call_id="call_1", name="func_1", arguments='{"a": 1}'), - Content.from_function_call(call_id="call_2", name="func_2", arguments='{"b": 2}'), + Content.from_function_call( + call_id="call_1", name="func_1", arguments='{"a": 1}' + ), + Content.from_function_call( + call_id="call_2", name="func_2", arguments='{"b": 2}' + ), ], ) @@ -1044,7 +1137,9 @@ def test_multiple_function_calls_in_single_message(openai_unit_test_env: dict[st assert prepared[0]["tool_calls"][1]["id"] == "call_2" -def test_prepare_options_removes_parallel_tool_calls_when_no_tools(openai_unit_test_env: dict[str, str]) -> None: +def test_prepare_options_removes_parallel_tool_calls_when_no_tools( + openai_unit_test_env: dict[str, str], +) -> None: """Test that parallel_tool_calls is removed when no tools are present.""" client = OpenAIChatClient() @@ -1057,7 +1152,9 @@ def test_prepare_options_removes_parallel_tool_calls_when_no_tools(openai_unit_t assert "parallel_tool_calls" not in prepared_options -async def test_streaming_exception_handling(openai_unit_test_env: dict[str, str]) -> None: +async def test_streaming_exception_handling( + openai_unit_test_env: dict[str, str], +) -> None: """Test that streaming errors are properly handled.""" client = OpenAIChatClient() messages = [Message(role="user", text="test")] @@ -1069,7 +1166,9 @@ async def test_streaming_exception_handling(openai_unit_test_env: dict[str, str] patch.object(client.client.chat.completions, "create", side_effect=mock_error), pytest.raises(ChatClientException), ): - async for _ in client._inner_get_response(messages=messages, stream=True, options={}): # type: ignore + async for _ in client._inner_get_response( + messages=messages, stream=True, options={} + ): # type: ignore pass @@ -1101,7 +1200,12 @@ class OutputStruct(BaseModel): 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"), + 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"), @@ -1130,7 +1234,12 @@ class OutputStruct(BaseModel): "temperature_c": {"type": "number"}, "advisory": {"type": "string"}, }, - "required": ["location", "conditions", "temperature_c", "advisory"], + "required": [ + "location", + "conditions", + "temperature_c", + "advisory", + ], "additionalProperties": False, }, }, @@ -1163,7 +1272,9 @@ async def test_integration_options( elif option_name.startswith("response_format"): # Use prompt that works well with structured output messages = [Message(role="user", text="The weather in Seattle is sunny")] - messages.append(Message(role="user", text="What is the weather in Seattle?")) + messages.append( + Message(role="user", text="What is the weather in Seattle?") + ) else: # Generic prompt for simple options messages = [Message(role="user", text="Say 'Hello World' briefly.")] @@ -1196,9 +1307,14 @@ async def test_integration_options( assert response.messages is not None if not option_name.startswith("tool_choice") and ( (isinstance(option_value, str) and option_value != "required") - or (isinstance(option_value, dict) and option_value.get("mode") != "required") + or ( + isinstance(option_value, dict) + and option_value.get("mode") != "required" + ) ): - assert response.text is not None, f"No text in response for option '{option_name}'" + 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 @@ -1206,7 +1322,9 @@ async def test_integration_options( 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}" + 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 @@ -1215,7 +1333,9 @@ async def test_integration_options( assert "seattle" in response.value.location.lower() else: # Runtime JSON schema - assert response.value is None, "No structured output, can't parse any json." + 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 @@ -1244,7 +1364,9 @@ async def test_integration_web_search() -> None: }, } if streaming: - response = await client.get_response(stream=True, **content).get_final_response() + response = await client.get_response( + stream=True, **content + ).get_final_response() else: response = await client.get_response(**content) @@ -1264,14 +1386,21 @@ async def test_integration_web_search() -> None: } ) content = { - "messages": [Message(role="user", text="What is the current weather? Do not ask for my current location.")], + "messages": [ + Message( + role="user", + text="What is the current weather? Do not ask for my current location.", + ) + ], "options": { "tool_choice": "auto", "tools": [web_search_tool_with_location], }, } if streaming: - response = await client.get_response(stream=True, **content).get_final_response() + response = await client.get_response( + stream=True, **content + ).get_final_response() else: response = await client.get_response(**content) assert response.text is not None diff --git a/python/packages/core/tests/openai/test_openai_responses_client.py b/python/packages/core/tests/openai/test_openai_responses_client.py index 7eaae1e776..e830c7affb 100644 --- a/python/packages/core/tests/openai/test_openai_responses_client.py +++ b/python/packages/core/tests/openai/test_openai_responses_client.py @@ -4,10 +4,26 @@ import json import os from datetime import datetime, timezone +from pathlib import Path from typing import Annotated, Any from unittest.mock import MagicMock, patch import pytest +from agent_framework import ( + ChatOptions, + ChatResponse, + ChatResponseUpdate, + Content, + Message, + SupportsChatGetResponse, + tool, +) +from agent_framework.exceptions import ( + ChatClientException, + ChatClientInvalidRequestException, +) +from agent_framework.openai import OpenAIResponsesClient +from agent_framework.openai._exceptions import OpenAIContentFilterException from openai import BadRequestError from openai.types.responses.response_reasoning_item import Summary from openai.types.responses.response_reasoning_summary_text_delta_event import ( @@ -26,19 +42,6 @@ from pydantic import BaseModel from pytest import param -from agent_framework import ( - ChatOptions, - ChatResponse, - ChatResponseUpdate, - Content, - Message, - SupportsChatGetResponse, - tool, -) -from agent_framework.exceptions import ChatClientException, ChatClientInvalidRequestException -from agent_framework.openai import OpenAIResponsesClient -from agent_framework.openai._exceptions import OpenAIContentFilterException - skip_if_openai_integration_tests_disabled = pytest.mark.skipif( os.getenv("OPENAI_API_KEY", "") in ("", "test-dummy-key"), reason="No real OPENAI_API_KEY provided; skipping integration tests.", @@ -70,12 +73,16 @@ async def create_vector_store( poll_interval_ms=1000, ) if result.last_error is not None: - raise Exception(f"Vector store file processing failed with status: {result.last_error.message}") + raise Exception( + f"Vector store file processing failed with status: {result.last_error.message}" + ) return file.id, Content.from_hosted_vector_store(vector_store_id=vector_store.id) -async def delete_vector_store(client: OpenAIResponsesClient, file_id: str, vector_store_id: str) -> None: +async def delete_vector_store( + client: OpenAIResponsesClient, 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) @@ -93,7 +100,10 @@ def test_init(openai_unit_test_env: dict[str, str]) -> None: # Test successful initialization openai_responses_client = OpenAIResponsesClient() - assert openai_responses_client.model_id == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"] + assert ( + openai_responses_client.model_id + == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"] + ) assert isinstance(openai_responses_client, SupportsChatGetResponse) @@ -120,7 +130,10 @@ def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None: default_headers=default_headers, ) - assert openai_responses_client.model_id == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"] + assert ( + openai_responses_client.model_id + == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"] + ) assert isinstance(openai_responses_client, SupportsChatGetResponse) # Assert that the default header we added is present in the client's default headers @@ -156,7 +169,9 @@ def test_serialize(openai_unit_test_env: dict[str, str]) -> None: openai_responses_client = OpenAIResponsesClient.from_dict(settings) dumped_settings = openai_responses_client.to_dict() - assert dumped_settings["model_id"] == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"] + assert ( + dumped_settings["model_id"] == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"] + ) # Assert that the default header we added is present in the dumped_settings default headers for key, value in default_headers.items(): assert key in dumped_settings["default_headers"] @@ -174,7 +189,9 @@ def test_serialize_with_org_id(openai_unit_test_env: dict[str, str]) -> None: openai_responses_client = OpenAIResponsesClient.from_dict(settings) dumped_settings = openai_responses_client.to_dict() - assert dumped_settings["model_id"] == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"] + assert ( + dumped_settings["model_id"] == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"] + ) assert dumped_settings["org_id"] == openai_unit_test_env["OPENAI_ORG_ID"] # Assert that the 'User-Agent' header is not present in the dumped_settings default headers assert "User-Agent" not in dumped_settings.get("default_headers", {}) @@ -186,7 +203,9 @@ async def test_get_response_with_invalid_input() -> None: client = OpenAIResponsesClient(model_id="invalid-model", api_key="test-key") # Test with empty messages which should trigger ChatClientInvalidRequestException - with pytest.raises(ChatClientInvalidRequestException, match="Messages are required"): + with pytest.raises( + ChatClientInvalidRequestException, match="Messages are required" + ): await client.get_response(messages=[]) @@ -258,7 +277,9 @@ async def test_code_interpreter_tool_variations() -> None: ) # Test code interpreter with files using static method - code_tool_with_files = OpenAIResponsesClient.get_code_interpreter_tool(file_ids=["file1", "file2"]) + code_tool_with_files = OpenAIResponsesClient.get_code_interpreter_tool( + file_ids=["file1", "file2"] + ) with pytest.raises(ChatClientException): await client.get_response( @@ -281,7 +302,9 @@ async def test_content_filter_exception() -> None: with patch.object(client.client.responses, "create", side_effect=mock_error): with pytest.raises(OpenAIContentFilterException) as exc_info: - await client.get_response(messages=[Message(role="user", text="Test message")]) + await client.get_response( + messages=[Message(role="user", text="Test message")] + ) assert "content error" in str(exc_info.value) @@ -293,10 +316,14 @@ async def test_hosted_file_search_tool_validation() -> None: client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") # Test file search tool with vector store IDs - file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=["vs_123"]) + file_search_tool = OpenAIResponsesClient.get_file_search_tool( + vector_store_ids=["vs_123"] + ) # Test using file search tool - may raise various exceptions depending on API response - with pytest.raises((ValueError, ChatClientInvalidRequestException, ChatClientException)): + with pytest.raises( + (ValueError, ChatClientInvalidRequestException, ChatClientException) + ): await client.get_response( messages=[Message("user", ["Test"])], options={"tools": [file_search_tool]}, @@ -315,7 +342,9 @@ async def test_chat_message_parsing_with_function_calls() -> None: additional_properties={"fc_id": "test-fc-id"}, ) - function_result = Content.from_function_result(call_id="test-call-id", result="Function executed successfully") + function_result = Content.from_function_result( + call_id="test-call-id", result="Function executed successfully" + ) messages = [ Message(role="user", text="Call a function"), @@ -344,7 +373,9 @@ async def test_response_format_parse_path() -> None: mock_parsed_response.finish_reason = None mock_parsed_response.conversation = None # No conversation object - with patch.object(client.client.responses, "parse", return_value=mock_parsed_response): + with patch.object( + client.client.responses, "parse", return_value=mock_parsed_response + ): response = await client.get_response( messages=[Message(role="user", text="Test message")], options={"response_format": OutputStruct, "store": True}, @@ -371,7 +402,9 @@ async def test_response_format_parse_path_with_conversation_id() -> None: mock_parsed_response.conversation = MagicMock() mock_parsed_response.conversation.id = "conversation_456" - with patch.object(client.client.responses, "parse", return_value=mock_parsed_response): + with patch.object( + client.client.responses, "parse", return_value=mock_parsed_response + ): response = await client.get_response( messages=[Message(role="user", text="Test message")], options={"response_format": OutputStruct, "store": True}, @@ -416,8 +449,12 @@ async def test_streaming_content_filter_exception_handling() -> None: ) mock_create.side_effect.code = "content_filter" - with pytest.raises(OpenAIContentFilterException, match="service encountered a content error"): - response_stream = client.get_response(stream=True, messages=[Message(role="user", text="Test")]) + with pytest.raises( + OpenAIContentFilterException, match="service encountered a content error" + ): + response_stream = client.get_response( + stream=True, messages=[Message(role="user", text="Test")] + ) async for _ in response_stream: break @@ -683,7 +720,9 @@ def test_prepare_content_for_openai_text_uses_role_specific_type() -> None: assert assistant_result["text"] == "hello" -def test_prepare_messages_for_openai_assistant_history_uses_output_text_with_annotations() -> None: +def test_prepare_messages_for_openai_assistant_history_uses_output_text_with_annotations() -> ( + None +): """Assistant history should be output_text and include required annotations.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") @@ -763,7 +802,9 @@ def test_parse_chunk_from_openai_with_mcp_call_result() -> None: function_call_ids: dict[int, tuple[str, str]] = {} - update = client._parse_chunk_from_openai(mock_event, options={}, function_call_ids=function_call_ids) + update = client._parse_chunk_from_openai( + mock_event, options={}, function_call_ids=function_call_ids + ) # Should have both call and result in contents assert len(update.contents) == 2 @@ -840,7 +881,9 @@ def test_prepare_message_for_openai_includes_reasoning_with_function_call() -> N reasoning_item = next(item for item in result if item["type"] == "reasoning") assert reasoning_item["summary"][0]["text"] == "Let me analyze the request" - assert reasoning_item["id"] == "rs_abc123", "Reasoning id must be preserved for the API" + assert reasoning_item["id"] == "rs_abc123", ( + "Reasoning id must be preserved for the API" + ) def test_prepare_messages_for_openai_full_conversation_with_reasoning() -> None: @@ -879,7 +922,10 @@ def test_prepare_messages_for_openai_full_conversation_with_reasoning() -> None: ), ], ), - Message(role="assistant", contents=[Content.from_text(text="I found hotels for you")]), + Message( + role="assistant", + contents=[Content.from_text(text="I found hotels for you")], + ), ] result = client._prepare_messages_for_openai(messages) @@ -988,11 +1034,19 @@ def test_response_format_with_conflicting_definitions() -> None: client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") # Mock response_format and text_config that conflict - response_format = {"type": "json_schema", "format": {"type": "json_schema", "name": "Test", "schema": {}}} + response_format = { + "type": "json_schema", + "format": {"type": "json_schema", "name": "Test", "schema": {}}, + } text_config = {"format": {"type": "json_object"}} - with pytest.raises(ChatClientInvalidRequestException, match="Conflicting response_format definitions"): - client._prepare_response_and_text_format(response_format=response_format, text_config=text_config) + with pytest.raises( + ChatClientInvalidRequestException, + match="Conflicting response_format definitions", + ): + client._prepare_response_and_text_format( + response_format=response_format, text_config=text_config + ) def test_response_format_json_object_type() -> None: @@ -1001,7 +1055,9 @@ def test_response_format_json_object_type() -> None: response_format = {"type": "json_object"} - _, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None) + _, text_config = client._prepare_response_and_text_format( + response_format=response_format, text_config=None + ) assert text_config is not None assert text_config["format"]["type"] == "json_object" @@ -1013,7 +1069,9 @@ def test_response_format_text_type() -> None: response_format = {"type": "text"} - _, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None) + _, text_config = client._prepare_response_and_text_format( + response_format=response_format, text_config=None + ) assert text_config is not None assert text_config["format"]["type"] == "text" @@ -1023,9 +1081,17 @@ def test_response_format_with_format_key() -> None: """Test response_format that already has a format key.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") - response_format = {"format": {"type": "json_schema", "name": "MySchema", "schema": {"type": "object"}}} + response_format = { + "format": { + "type": "json_schema", + "name": "MySchema", + "schema": {"type": "object"}, + } + } - _, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None) + _, text_config = client._prepare_response_and_text_format( + response_format=response_format, text_config=None + ) assert text_config is not None assert text_config["format"]["type"] == "json_schema" @@ -1038,10 +1104,14 @@ def test_response_format_json_schema_no_name_uses_title() -> None: response_format = { "type": "json_schema", - "json_schema": {"schema": {"title": "MyTitle", "type": "object", "properties": {}}}, + "json_schema": { + "schema": {"title": "MyTitle", "type": "object", "properties": {}} + }, } - _, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None) + _, text_config = client._prepare_response_and_text_format( + response_format=response_format, text_config=None + ) assert text_config is not None assert text_config["format"]["name"] == "MyTitle" @@ -1053,10 +1123,16 @@ def test_response_format_json_schema_with_strict() -> None: response_format = { "type": "json_schema", - "json_schema": {"name": "StrictSchema", "schema": {"type": "object"}, "strict": True}, + "json_schema": { + "name": "StrictSchema", + "schema": {"type": "object"}, + "strict": True, + }, } - _, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None) + _, text_config = client._prepare_response_and_text_format( + response_format=response_format, text_config=None + ) assert text_config is not None assert text_config["format"]["strict"] is True @@ -1075,7 +1151,9 @@ def test_response_format_json_schema_with_description() -> None: }, } - _, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None) + _, text_config = client._prepare_response_and_text_format( + response_format=response_format, text_config=None + ) assert text_config is not None assert text_config["format"]["description"] == "A test schema" @@ -1087,8 +1165,13 @@ def test_response_format_json_schema_missing_schema() -> None: response_format = {"type": "json_schema", "json_schema": {"name": "NoSchema"}} - with pytest.raises(ChatClientInvalidRequestException, match="json_schema response_format requires a schema"): - client._prepare_response_and_text_format(response_format=response_format, text_config=None) + with pytest.raises( + ChatClientInvalidRequestException, + match="json_schema response_format requires a schema", + ): + client._prepare_response_and_text_format( + response_format=response_format, text_config=None + ) def test_response_format_unsupported_type() -> None: @@ -1097,8 +1180,12 @@ def test_response_format_unsupported_type() -> None: response_format = {"type": "unsupported_format"} - with pytest.raises(ChatClientInvalidRequestException, match="Unsupported response_format"): - client._prepare_response_and_text_format(response_format=response_format, text_config=None) + with pytest.raises( + ChatClientInvalidRequestException, match="Unsupported response_format" + ): + client._prepare_response_and_text_format( + response_format=response_format, text_config=None + ) def test_response_format_invalid_type() -> None: @@ -1107,8 +1194,13 @@ def test_response_format_invalid_type() -> None: response_format = "invalid" # Not a Pydantic model or mapping - with pytest.raises(ChatClientInvalidRequestException, match="response_format must be a Pydantic model or mapping"): - client._prepare_response_and_text_format(response_format=response_format, text_config=None) # type: ignore + with pytest.raises( + ChatClientInvalidRequestException, + match="response_format must be a Pydantic model or mapping", + ): + client._prepare_response_and_text_format( + response_format=response_format, text_config=None + ) # type: ignore def test_parse_response_with_store_false() -> None: @@ -1157,7 +1249,9 @@ def test_streaming_chunk_with_usage_only() -> None: mock_event.response.usage.input_tokens_details = None mock_event.response.usage.output_tokens_details = None - update = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + update = client._parse_chunk_from_openai( + mock_event, chat_options, function_call_ids + ) # Should have usage content assert len(update.contents) == 1 @@ -1343,7 +1437,9 @@ def test_prepare_tools_for_openai_with_raw_image_generation() -> None: assert image_tool["output_quality"] == 75 -def test_prepare_tools_for_openai_with_raw_image_generation_openai_responses_params() -> None: +def test_prepare_tools_for_openai_with_raw_image_generation_openai_responses_params() -> ( + None +): """Test raw image_generation tool with OpenAI-specific parameters.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") @@ -1429,7 +1525,9 @@ def test_parse_chunk_from_openai_with_mcp_approval_request() -> None: mock_item.server_label = "My_MCP" mock_event.item = mock_item - update = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + update = client._parse_chunk_from_openai( + mock_event, chat_options, function_call_ids + ) assert any(c.type == "function_approval_request" for c in update.contents) fa = next(c for c in update.contents if c.type == "function_approval_request") assert fa.id == "approval-stream-1" @@ -1478,15 +1576,21 @@ async def test_end_to_end_mcp_approval_flow(span_exporter) -> None: mock_response2.output = [mock_text_item] # Patch the create call to return the two mocked responses in sequence - with patch.object(client.client.responses, "create", side_effect=[mock_response1, mock_response2]) as mock_create: + with patch.object( + client.client.responses, "create", side_effect=[mock_response1, mock_response2] + ) as mock_create: # First call: get the approval request - response = await client.get_response(messages=[Message(role="user", text="Trigger approval")]) + response = await client.get_response( + messages=[Message(role="user", text="Trigger approval")] + ) assert response.messages[0].contents[0].type == "function_approval_request" req = response.messages[0].contents[0] assert req.id == "approval-1" # Build a user approval and send it (include required function_call) - approval = Content.from_function_approval_response(approved=True, id=req.id, function_call=req.function_call) + approval = Content.from_function_approval_response( + approved=True, id=req.id, function_call=req.function_call + ) approval_message = Message(role="user", contents=[approval]) _ = await client.get_response(messages=[approval_message]) @@ -1577,7 +1681,9 @@ def test_streaming_response_basic_structure() -> None: # Test with a basic mock event to ensure the method returns proper structure mock_event = MagicMock() - response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) # type: ignore + response = client._parse_chunk_from_openai( + mock_event, chat_options, function_call_ids + ) # type: ignore # Should get a valid ChatResponseUpdate structure assert isinstance(response, ChatResponseUpdate) @@ -1600,7 +1706,9 @@ def test_streaming_response_created_type() -> None: mock_event.response.conversation = MagicMock() mock_event.response.conversation.id = "conv_5678" - response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + response = client._parse_chunk_from_openai( + mock_event, chat_options, function_call_ids + ) assert response.response_id == "resp_1234" assert response.conversation_id == "conv_5678" @@ -1619,7 +1727,9 @@ def test_streaming_response_in_progress_type() -> None: mock_event.response.conversation = MagicMock() mock_event.response.conversation.id = "conv_5678" - response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + response = client._parse_chunk_from_openai( + mock_event, chat_options, function_call_ids + ) assert response.response_id == "resp_1234" assert response.conversation_id == "conv_5678" @@ -1640,7 +1750,9 @@ def test_streaming_annotation_added_with_file_path() -> None: "index": 42, } - response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + response = client._parse_chunk_from_openai( + mock_event, chat_options, function_call_ids + ) assert len(response.contents) == 1 content = response.contents[0] @@ -1667,7 +1779,9 @@ def test_streaming_annotation_added_with_file_citation() -> None: "index": 15, } - response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + response = client._parse_chunk_from_openai( + mock_event, chat_options, function_call_ids + ) assert len(response.contents) == 1 content = response.contents[0] @@ -1696,7 +1810,9 @@ def test_streaming_annotation_added_with_container_file_citation() -> None: "end_index": 50, } - response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + response = client._parse_chunk_from_openai( + mock_event, chat_options, function_call_ids + ) assert len(response.contents) == 1 content = response.contents[0] @@ -1723,7 +1839,9 @@ def test_streaming_annotation_added_with_unknown_type() -> None: "url": "https://example.com", } - response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + response = client._parse_chunk_from_openai( + mock_event, chat_options, function_call_ids + ) # url_citation should not produce HostedFileContent assert len(response.contents) == 0 @@ -1747,7 +1865,9 @@ async def test_service_response_exception_includes_original_error_details() -> N patch.object(client.client.responses, "parse", side_effect=mock_error), pytest.raises(ChatClientException) as exc_info, ): - await client.get_response(messages=messages, options={"response_format": OutputStruct}) + await client.get_response( + messages=messages, options={"response_format": OutputStruct} + ) exception_message = str(exc_info.value) assert "service failed to complete the prompt:" in exception_message @@ -1764,7 +1884,9 @@ async def test_get_response_streaming_with_response_format() -> None: async def run_streaming(): async for _ in client.get_response( - stream=True, messages=messages, options={"response_format": OutputStruct} + stream=True, + messages=messages, + options={"response_format": OutputStruct}, ): pass @@ -1788,7 +1910,9 @@ def test_prepare_content_for_openai_image_content() -> None: assert result["file_id"] == "file_123" # Test image content without additional properties (defaults) - image_content_basic = Content.from_uri(uri="https://example.com/basic.png", media_type="image/png") + image_content_basic = Content.from_uri( + uri="https://example.com/basic.png", media_type="image/png" + ) result = client._prepare_content_for_openai("user", image_content_basic, {}) # type: ignore assert result["type"] == "input_image" assert result["detail"] == "auto" @@ -1800,14 +1924,18 @@ def test_prepare_content_for_openai_audio_content() -> None: client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") # Test WAV audio content - wav_content = Content.from_uri(uri="data:audio/wav;base64,abc123", media_type="audio/wav") + wav_content = Content.from_uri( + uri="data:audio/wav;base64,abc123", media_type="audio/wav" + ) result = client._prepare_content_for_openai("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 = Content.from_uri(uri="data:audio/mp3;base64,def456", media_type="audio/mp3") + mp3_content = Content.from_uri( + uri="data:audio/mp3;base64,def456", media_type="audio/mp3" + ) result = client._prepare_content_for_openai("user", mp3_content, {}) # type: ignore assert result["type"] == "input_audio" assert result["input_audio"]["format"] == "mp3" @@ -1818,12 +1946,16 @@ def test_prepare_content_for_openai_unsupported_content() -> None: client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") # Test unsupported audio format - unsupported_audio = Content.from_uri(uri="data:audio/ogg;base64,ghi789", media_type="audio/ogg") + unsupported_audio = Content.from_uri( + uri="data:audio/ogg;base64,ghi789", media_type="audio/ogg" + ) result = client._prepare_content_for_openai("user", unsupported_audio, {}) # type: ignore assert result == {} # Test non-media content - text_uri_content = Content.from_uri(uri="https://example.com/document.txt", media_type="text/plain") + text_uri_content = Content.from_uri( + uri="https://example.com/document.txt", media_type="text/plain" + ) result = client._prepare_content_for_openai("user", text_uri_content, {}) # type: ignore assert result == {} @@ -1845,11 +1977,16 @@ def test_parse_chunk_from_openai_code_interpreter() -> None: mock_item_image.code = None mock_event_image.item = mock_item_image - result = client._parse_chunk_from_openai(mock_event_image, chat_options, function_call_ids) # type: ignore + result = client._parse_chunk_from_openai( + mock_event_image, chat_options, function_call_ids + ) # type: ignore assert len(result.contents) == 1 assert result.contents[0].type == "code_interpreter_tool_result" assert result.contents[0].outputs - assert any(out.type == "uri" and out.uri == "https://example.com/plot.png" for out in result.contents[0].outputs) + assert any( + out.type == "uri" and out.uri == "https://example.com/plot.png" + for out in result.contents[0].outputs + ) def test_parse_chunk_from_openai_code_interpreter_delta() -> None: @@ -1868,7 +2005,9 @@ def test_parse_chunk_from_openai_code_interpreter_delta() -> None: mock_delta_event.call_id = None # Ensure fallback to item_id mock_delta_event.id = None - result = client._parse_chunk_from_openai(mock_delta_event, chat_options, function_call_ids) # type: ignore + result = client._parse_chunk_from_openai( + mock_delta_event, chat_options, function_call_ids + ) # type: ignore assert len(result.contents) == 1 assert result.contents[0].type == "code_interpreter_tool_call" assert result.contents[0].call_id == "ci_123" @@ -1891,13 +2030,17 @@ def test_parse_chunk_from_openai_code_interpreter_done() -> None: mock_done_event = MagicMock() mock_done_event.type = "response.code_interpreter_call_code.done" mock_done_event.item_id = "ci_456" - mock_done_event.code = "import pandas as pd\ndf = pd.DataFrame({'a': [1, 2, 3]})\nprint(df)" + mock_done_event.code = ( + "import pandas as pd\ndf = pd.DataFrame({'a': [1, 2, 3]})\nprint(df)" + ) mock_done_event.output_index = 0 mock_done_event.sequence_number = 5 mock_done_event.call_id = None # Ensure fallback to item_id mock_done_event.id = None - result = client._parse_chunk_from_openai(mock_done_event, chat_options, function_call_ids) # type: ignore + result = client._parse_chunk_from_openai( + mock_done_event, chat_options, function_call_ids + ) # type: ignore assert len(result.contents) == 1 assert result.contents[0].type == "code_interpreter_tool_call" assert result.contents[0].call_id == "ci_456" @@ -1926,12 +2069,17 @@ def test_parse_chunk_from_openai_reasoning() -> None: mock_item_reasoning.summary = ["Problem analysis summary"] mock_event_reasoning.item = mock_item_reasoning - result = client._parse_chunk_from_openai(mock_event_reasoning, chat_options, function_call_ids) # type: ignore + result = client._parse_chunk_from_openai( + mock_event_reasoning, chat_options, function_call_ids + ) # type: ignore assert len(result.contents) == 1 assert result.contents[0].type == "text_reasoning" 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" + assert ( + result.contents[0].additional_properties["summary"] + == "Problem analysis summary" + ) def test_prepare_content_for_openai_text_reasoning_comprehensive() -> None: @@ -1948,7 +2096,9 @@ def test_prepare_content_for_openai_text_reasoning_comprehensive() -> None: "encrypted_content": "secure_data_456", }, ) - result = client._prepare_content_for_openai("assistant", comprehensive_reasoning, {}) # type: ignore + result = client._prepare_content_for_openai( + "assistant", comprehensive_reasoning, {} + ) # type: ignore assert result["type"] == "reasoning" assert result["id"] == "rs_comprehensive" assert result["summary"][0]["text"] == "Comprehensive reasoning summary" @@ -1973,8 +2123,12 @@ def test_streaming_reasoning_text_delta_event() -> None: delta="reasoning delta", ) - with patch.object(client, "_get_metadata_from_response", return_value={}) as mock_metadata: - response = client._parse_chunk_from_openai(event, chat_options, function_call_ids) # type: ignore + with patch.object( + client, "_get_metadata_from_response", return_value={} + ) as mock_metadata: + response = client._parse_chunk_from_openai( + event, chat_options, function_call_ids + ) # type: ignore assert len(response.contents) == 1 assert response.contents[0].type == "text_reasoning" @@ -1999,8 +2153,12 @@ def test_streaming_reasoning_text_done_event() -> None: text="complete reasoning", ) - with patch.object(client, "_get_metadata_from_response", return_value={"test": "data"}) as mock_metadata: - response = client._parse_chunk_from_openai(event, chat_options, function_call_ids) # type: ignore + with patch.object( + client, "_get_metadata_from_response", return_value={"test": "data"} + ) as mock_metadata: + response = client._parse_chunk_from_openai( + event, chat_options, function_call_ids + ) # type: ignore assert len(response.contents) == 1 assert response.contents[0].type == "text_reasoning" @@ -2025,8 +2183,12 @@ def test_streaming_reasoning_summary_text_delta_event() -> None: delta="summary delta", ) - with patch.object(client, "_get_metadata_from_response", return_value={}) as mock_metadata: - response = client._parse_chunk_from_openai(event, chat_options, function_call_ids) # type: ignore + with patch.object( + client, "_get_metadata_from_response", return_value={} + ) as mock_metadata: + response = client._parse_chunk_from_openai( + event, chat_options, function_call_ids + ) # type: ignore assert len(response.contents) == 1 assert response.contents[0].type == "text_reasoning" @@ -2050,8 +2212,12 @@ def test_streaming_reasoning_summary_text_done_event() -> None: text="complete summary", ) - with patch.object(client, "_get_metadata_from_response", return_value={"custom": "meta"}) as mock_metadata: - response = client._parse_chunk_from_openai(event, chat_options, function_call_ids) # type: ignore + with patch.object( + client, "_get_metadata_from_response", return_value={"custom": "meta"} + ) as mock_metadata: + response = client._parse_chunk_from_openai( + event, chat_options, function_call_ids + ) # type: ignore assert len(response.contents) == 1 assert response.contents[0].type == "text_reasoning" @@ -2086,9 +2252,15 @@ def test_streaming_reasoning_events_preserve_metadata() -> None: delta="reasoning", ) - with patch.object(client, "_get_metadata_from_response", return_value={"test": "metadata"}): - text_response = client._parse_chunk_from_openai(text_event, chat_options, function_call_ids) # type: ignore - reasoning_response = client._parse_chunk_from_openai(reasoning_event, chat_options, function_call_ids) # type: ignore + with patch.object( + client, "_get_metadata_from_response", return_value={"test": "metadata"} + ): + text_response = client._parse_chunk_from_openai( + text_event, chat_options, function_call_ids + ) # type: ignore + reasoning_response = client._parse_chunk_from_openai( + reasoning_event, chat_options, function_call_ids + ) # type: ignore # Both should preserve metadata assert text_response.additional_properties == {"test": "metadata"} @@ -2196,7 +2368,9 @@ def test_parse_response_from_openai_image_generation_format_detection(): mock_response_jpeg.output = [mock_item_jpeg] with patch.object(client, "_get_metadata_from_response", return_value={}): - response_jpeg = client._parse_response_from_openai(mock_response_jpeg, options={}) # type: ignore + response_jpeg = client._parse_response_from_openai( + mock_response_jpeg, options={} + ) # type: ignore result_contents = response_jpeg.messages[0].contents assert result_contents[1].type == "image_generation_tool_result" outputs = result_contents[1].outputs @@ -2222,7 +2396,9 @@ def test_parse_response_from_openai_image_generation_format_detection(): mock_response_webp.output = [mock_item_webp] with patch.object(client, "_get_metadata_from_response", return_value={}): - response_webp = client._parse_response_from_openai(mock_response_webp, options={}) # type: ignore + response_webp = client._parse_response_from_openai( + mock_response_webp, options={} + ) # type: ignore outputs_webp = response_webp.messages[0].contents[1].outputs assert outputs_webp and outputs_webp.type == "data" assert outputs_webp.media_type == "image/webp" @@ -2296,7 +2472,9 @@ async def test_conversation_id_precedence_kwargs_over_options() -> None: # options has a stale response id, kwargs carries the freshest one opts = {"conversation_id": "resp_old_123"} - run_opts = await client._prepare_options(messages, opts, conversation_id="resp_new_456") # type: ignore + run_opts = await client._prepare_options( + messages, opts, conversation_id="resp_new_456" + ) # type: ignore # Verify kwargs takes precedence and maps to previous_response_id for resp_* IDs assert run_opts.get("previous_response_id") == "resp_new_456" @@ -2330,7 +2508,9 @@ async def test_instructions_sent_first_turn_then_skipped_for_continuation() -> N client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") mock_response = _create_mock_responses_text_response(response_id="resp_123") - with patch.object(client.client.responses, "create", return_value=mock_response) as mock_create: + with patch.object( + client.client.responses, "create", return_value=mock_response + ) as mock_create: await client.get_response( messages=[Message(role="user", text="Hello")], options={"instructions": "Reply in uppercase."}, @@ -2339,12 +2519,17 @@ async def test_instructions_sent_first_turn_then_skipped_for_continuation() -> N first_input_messages = mock_create.call_args.kwargs["input"] assert len(first_input_messages) == 2 assert first_input_messages[0]["role"] == "system" - assert any("Reply in uppercase" in str(c) for c in first_input_messages[0]["content"]) + assert any( + "Reply in uppercase" in str(c) for c in first_input_messages[0]["content"] + ) assert first_input_messages[1]["role"] == "user" await client.get_response( messages=[Message(role="user", text="Tell me a joke")], - options={"instructions": "Reply in uppercase.", "conversation_id": "resp_123"}, + options={ + "instructions": "Reply in uppercase.", + "conversation_id": "resp_123", + }, ) second_input_messages = mock_create.call_args.kwargs["input"] @@ -2354,11 +2539,15 @@ async def test_instructions_sent_first_turn_then_skipped_for_continuation() -> N @pytest.mark.parametrize("conversation_id", ["resp_456", "conv_abc123"]) -async def test_instructions_not_repeated_for_continuation_ids(conversation_id: str) -> None: +async def test_instructions_not_repeated_for_continuation_ids( + conversation_id: str, +) -> None: client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") mock_response = _create_mock_responses_text_response(response_id="resp_456") - with patch.object(client.client.responses, "create", return_value=mock_response) as mock_create: + with patch.object( + client.client.responses, "create", return_value=mock_response + ) as mock_create: await client.get_response( messages=[Message(role="user", text="Continue conversation")], options={"instructions": "Be helpful.", "conversation_id": conversation_id}, @@ -2374,7 +2563,9 @@ async def test_instructions_included_without_conversation_id() -> None: client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") mock_response = _create_mock_responses_text_response(response_id="resp_new") - with patch.object(client.client.responses, "create", return_value=mock_response) as mock_create: + with patch.object( + client.client.responses, "create", return_value=mock_response + ) as mock_create: await client.get_response( messages=[Message(role="user", text="Hello")], options={"instructions": "You are a helpful assistant."}, @@ -2455,7 +2646,12 @@ async def get_api_key() -> str: "temperature_c": {"type": "number"}, "advisory": {"type": "string"}, }, - "required": ["location", "conditions", "temperature_c", "advisory"], + "required": [ + "location", + "conditions", + "temperature_c", + "advisory", + ], "additionalProperties": False, }, }, @@ -2488,7 +2684,9 @@ async def test_integration_options( elif option_name.startswith("response_format"): # Use prompt that works well with structured output messages = [Message(role="user", text="The weather in Seattle is sunny")] - messages.append(Message(role="user", text="What is the weather in Seattle?")) + messages.append( + Message(role="user", text="What is the weather in Seattle?") + ) else: # Generic prompt for simple options messages = [Message(role="user", text="Say 'Hello World' briefly.")] @@ -2518,7 +2716,9 @@ async def test_integration_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 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 @@ -2526,7 +2726,9 @@ async def test_integration_options( 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}" + 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 @@ -2535,7 +2737,9 @@ async def test_integration_options( assert "seattle" in response.value.location.lower() else: # Runtime JSON schema - assert response.value is None, "No structured output, can't parse any json." + 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 @@ -2565,7 +2769,9 @@ async def test_integration_web_search() -> None: }, } if streaming: - response = await client.get_response(stream=True, **content).get_final_response() + response = await client.get_response( + stream=True, **content + ).get_final_response() else: response = await client.get_response(**content) @@ -2580,14 +2786,21 @@ async def test_integration_web_search() -> None: user_location={"country": "US", "city": "Seattle"}, ) content = { - "messages": [Message(role="user", text="What is the current weather? Do not ask for my current location.")], + "messages": [ + Message( + role="user", + text="What is the current weather? Do not ask for my current location.", + ) + ], "options": { "tool_choice": "auto", "tools": [web_search_tool_with_location], }, } if streaming: - response = await client.get_response(stream=True, **content).get_final_response() + response = await client.get_response( + stream=True, **content + ).get_final_response() else: response = await client.get_response(**content) assert response.text is not None @@ -2607,7 +2820,9 @@ async def test_integration_file_search() -> None: file_id, vector_store = await create_vector_store(openai_responses_client) # Use static method for file search tool - file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id]) + file_search_tool = OpenAIResponsesClient.get_file_search_tool( + vector_store_ids=[vector_store.vector_store_id] + ) # Test that the client will use the file search tool response = await openai_responses_client.get_response( messages=[ @@ -2622,7 +2837,9 @@ async def test_integration_file_search() -> None: }, ) - await delete_vector_store(openai_responses_client, file_id, vector_store.vector_store_id) + await delete_vector_store( + openai_responses_client, file_id, vector_store.vector_store_id + ) assert "sunny" in response.text.lower() assert "75" in response.text @@ -2641,7 +2858,9 @@ async def test_integration_streaming_file_search() -> None: file_id, vector_store = await create_vector_store(openai_responses_client) # Use static method for file search tool - file_search_tool = OpenAIResponsesClient.get_file_search_tool(vector_store_ids=[vector_store.vector_store_id]) + file_search_tool = OpenAIResponsesClient.get_file_search_tool( + vector_store_ids=[vector_store.vector_store_id] + ) # Test that the client will use the web search tool response = openai_responses_client.get_streaming_response( messages=[ @@ -2665,13 +2884,51 @@ async def test_integration_streaming_file_search() -> None: if content.type == "text" and content.text: full_message += content.text - await delete_vector_store(openai_responses_client, file_id, vector_store.vector_store_id) + await delete_vector_store( + openai_responses_client, file_id, vector_store.vector_store_id + ) assert "sunny" in full_message.lower() assert "75" in full_message -# region Background Response / ContinuationToken Tests +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_openai_integration_tests_disabled +async def test_integration_tool_rich_content_image() -> None: + """Integration test: a tool returns an image and the model describes it.""" + image_path = Path(__file__).parent.parent / "assets" / "sample_image.jpg" + image_bytes = image_path.read_bytes() + + @tool(approval_mode="never_require") + def get_test_image() -> Content: + """Return a test image for analysis.""" + return Content.from_data(data=image_bytes, media_type="image/jpeg") + + client = OpenAIResponsesClient() + client.function_invocation_configuration["max_iterations"] = 2 + + for streaming in [False, True]: + messages = [ + Message( + role="user", + text="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) + + assert response is not None + assert isinstance(response, ChatResponse) + assert response.text is not None + assert len(response.text) > 0 + assert "house" in response.text.lower(), f"Model did not describe the house image. Response: {response.text}" def test_continuation_token_json_serializable() -> None: @@ -2846,13 +3103,17 @@ def test_streaming_response_in_progress_sets_continuation_token() -> None: mock_event.response.conversation.id = "conv_456" mock_event.response.status = "in_progress" - update = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + update = client._parse_chunk_from_openai( + mock_event, chat_options, function_call_ids + ) assert update.continuation_token is not None assert update.continuation_token["response_id"] == "resp_stream_123" -def test_streaming_response_created_with_in_progress_status_sets_continuation_token() -> None: +def test_streaming_response_created_with_in_progress_status_sets_continuation_token() -> ( + None +): """Test that response.created with in_progress status sets continuation_token.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") chat_options: dict[str, Any] = {} @@ -2866,7 +3127,9 @@ def test_streaming_response_created_with_in_progress_status_sets_continuation_to mock_event.response.conversation.id = "conv_789" mock_event.response.status = "in_progress" - update = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + update = client._parse_chunk_from_openai( + mock_event, chat_options, function_call_ids + ) assert update.continuation_token is not None assert update.continuation_token["response_id"] == "resp_created_123" @@ -2887,7 +3150,9 @@ def test_streaming_response_completed_no_continuation_token() -> None: mock_event.response.model = "test-model" mock_event.response.usage = None - update = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) + update = client._parse_chunk_from_openai( + mock_event, chat_options, function_call_ids + ) assert update.continuation_token is None From 6869d0289414d819e36b717214f9ecd2e46e3aa7 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Mon, 2 Mar 2026 20:05:06 -0800 Subject: [PATCH 5/8] Fix lint: remove print statement, wrap long line Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/packages/anthropic/tests/test_anthropic_client.py | 1 - python/packages/core/agent_framework/openai/_chat_client.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 017a3fd487..6641aebbdd 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -2191,7 +2191,6 @@ def get_test_image() -> Content: options={"tools": [get_test_image], "tool_choice": "auto", "max_tokens": 200}, ) - print("Model response:", response.text) assert response is not None assert response.text is not None assert len(response.text) > 0 diff --git a/python/packages/core/agent_framework/openai/_chat_client.py b/python/packages/core/agent_framework/openai/_chat_client.py index c4d4a0839a..a0f661855c 100644 --- a/python/packages/core/agent_framework/openai/_chat_client.py +++ b/python/packages/core/agent_framework/openai/_chat_client.py @@ -576,8 +576,9 @@ def _prepare_message_for_openai(self, message: Message) -> list[dict[str, Any]]: args["content"] = content.result if content.result is not None else "" if content.items: logger.warning( - "OpenAI Chat Completions API does not support rich content (images, audio) in tool results. " - "Rich content items will be omitted. Use the Responses API client for rich tool results." + "OpenAI Chat Completions API does not support rich content (images, audio) " + "in tool results. Rich content items will be omitted. " + "Use the Responses API client for rich tool results." ) if args: all_messages.append(args) From fbc7756c17c12ab5b5c43701f8bd0125698e6461 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Thu, 5 Mar 2026 11:06:54 -0800 Subject: [PATCH 6/8] Address review feedback: bug fixes, single-pass MCP, unit tests - Add isinstance guard in from_function_result for non-Content lists - Fix Anthropic empty tool_content fallback to string result - Fix Content(type='text', text=None) edge case in parse_result - Rewrite MCP _parse_tool_result_from_mcp as single-pass (no index counters) - Add Anthropic unit tests: data image, uri image, unsupported media, all-unsupported - Add OpenAI Chat unit test: rich items warning and omission - Add OpenAI Responses unit tests: function_result with/without items - Add test_types tests: only-rich-items list, non-Content list fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agent_framework_anthropic/_chat_client.py | 7 +- .../anthropic/tests/test_anthropic_client.py | 106 ++++++++++++++++++ python/packages/core/agent_framework/_mcp.py | 70 ++++++------ .../packages/core/agent_framework/_tools.py | 4 +- .../packages/core/agent_framework/_types.py | 11 ++ python/packages/core/tests/core/test_types.py | 21 ++++ .../tests/openai/test_openai_chat_client.py | 31 +++++ .../openai/test_openai_responses_client.py | 39 +++++++ 8 files changed, 248 insertions(+), 41 deletions(-) diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index ace2498b44..8d731744b9 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -741,10 +741,15 @@ def _prepare_message_for_anthropic(self, message: Message) -> dict[str, Any]: "Ignoring unsupported rich content media type in tool result: %s", item.media_type, ) + tool_result_content = ( + tool_content + if tool_content + else (content.result if content.result is not None else "") + ) a_content.append({ "type": "tool_result", "tool_use_id": content.call_id, - "content": tool_content, + "content": tool_result_content, "is_error": content.exception is not None, }) else: diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index d63458058e..22d6cd4ed0 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -217,6 +217,112 @@ def test_prepare_message_for_anthropic_function_result( assert result["content"][0]["is_error"] is False +def test_prepare_message_for_anthropic_function_result_with_data_image( + mock_anthropic_client: MagicMock, +) -> None: + """Test function result with a data-type image item produces a base64 image block.""" + client = create_test_anthropic_client(mock_anthropic_client) + image_content = Content.from_data(data=b"fake_image_bytes", media_type="image/png") + message = Message( + role="tool", + contents=[ + Content.from_function_result( + call_id="call_img", + result=[Content.from_text("Here is the image"), image_content], + ) + ], + ) + + result = client._prepare_message_for_anthropic(message) + + assert result["role"] == "user" + tool_result = result["content"][0] + assert tool_result["type"] == "tool_result" + assert tool_result["tool_use_id"] == "call_img" + content = tool_result["content"] + assert len(content) == 2 + assert content[0]["type"] == "text" + assert content[0]["text"] == "Here is the image" + assert content[1]["type"] == "image" + assert content[1]["source"]["type"] == "base64" + assert content[1]["source"]["media_type"] == "image/png" + + +def test_prepare_message_for_anthropic_function_result_with_uri_image( + mock_anthropic_client: MagicMock, +) -> None: + """Test function result with a uri-type image item produces a URL image block.""" + client = create_test_anthropic_client(mock_anthropic_client) + uri_content = Content.from_uri(uri="https://example.com/image.png", media_type="image/png") + message = Message( + role="tool", + contents=[ + Content.from_function_result( + call_id="call_uri", + result=[uri_content], + ) + ], + ) + + result = client._prepare_message_for_anthropic(message) + + tool_result = result["content"][0] + content = tool_result["content"] + assert len(content) == 1 + assert content[0]["type"] == "image" + assert content[0]["source"]["type"] == "url" + assert content[0]["source"]["url"] == "https://example.com/image.png" + + +def test_prepare_message_for_anthropic_function_result_with_unsupported_media( + mock_anthropic_client: MagicMock, +) -> None: + """Test function result with unsupported media type skips the item.""" + client = create_test_anthropic_client(mock_anthropic_client) + audio_content = Content.from_data(data=b"audio_bytes", media_type="audio/wav") + message = Message( + role="tool", + contents=[ + Content.from_function_result( + call_id="call_audio", + result=[Content.from_text("Some text"), audio_content], + ) + ], + ) + + result = client._prepare_message_for_anthropic(message) + + tool_result = result["content"][0] + content = tool_result["content"] + # Audio should be skipped, only text remains + assert len(content) == 1 + assert content[0]["type"] == "text" + assert content[0]["text"] == "Some text" + + +def test_prepare_message_for_anthropic_function_result_all_unsupported_media( + mock_anthropic_client: MagicMock, +) -> None: + """Test function result where all items are unsupported falls back to string result.""" + client = create_test_anthropic_client(mock_anthropic_client) + audio_content = Content.from_data(data=b"audio_bytes", media_type="audio/wav") + message = Message( + role="tool", + contents=[ + Content.from_function_result( + call_id="call_all_unsupported", + result=[audio_content], + ) + ], + ) + + result = client._prepare_message_for_anthropic(message) + + tool_result = result["content"][0] + # All items unsupported → tool_content is empty → falls back to string result + assert tool_result["content"] == "" + + def test_prepare_message_for_anthropic_text_reasoning( mock_anthropic_client: MagicMock, ) -> None: diff --git a/python/packages/core/agent_framework/_mcp.py b/python/packages/core/agent_framework/_mcp.py index 65d57e0859..7f54cc73bf 100644 --- a/python/packages/core/agent_framework/_mcp.py +++ b/python/packages/core/agent_framework/_mcp.py @@ -158,71 +158,65 @@ def _parse_tool_result_from_mcp( """ import json - text_parts: list[str] = [] - rich_items: list[Content] = [] + result: list[Content] = [] + has_rich = False for item in mcp_type.content: match item: case types.TextContent(): - text_parts.append(item.text) + result.append(Content.from_text(item.text)) case types.ImageContent(): - rich_items.append( + has_rich = True + result.append( Content.from_uri( uri=f"data:{item.mimeType};base64,{item.data}", media_type=item.mimeType, ) ) case types.AudioContent(): - rich_items.append( + has_rich = True + result.append( Content.from_uri( uri=f"data:{item.mimeType};base64,{item.data}", media_type=item.mimeType, ) ) case types.ResourceLink(): - text_parts.append( - json.dumps( - { - "type": "resource_link", - "uri": str(item.uri), - "mimeType": item.mimeType, - }, - default=str, + result.append( + Content.from_text( + json.dumps( + { + "type": "resource_link", + "uri": str(item.uri), + "mimeType": item.mimeType, + }, + default=str, + ) ) ) case types.EmbeddedResource(): match item.resource: case types.TextResourceContents(): - text_parts.append(item.resource.text) + result.append(Content.from_text(item.resource.text)) case types.BlobResourceContents(): - text_parts.append( - json.dumps( - { - "type": "blob", - "data": item.resource.blob, - "mimeType": item.resource.mimeType, - }, - default=str, + result.append( + Content.from_text( + json.dumps( + { + "type": "blob", + "data": item.resource.blob, + "mimeType": item.resource.mimeType, + }, + default=str, + ) ) ) case _: - text_parts.append(str(item)) - - if rich_items: - # Return rich content list preserving original order - result: list[Content] = [] - text_idx = 0 - rich_idx = 0 - for item in mcp_type.content: - match item: - case types.ImageContent() | types.AudioContent(): - result.append(rich_items[rich_idx]) - rich_idx += 1 - case _: - if text_idx < len(text_parts): - result.append(Content.from_text(text_parts[text_idx])) - text_idx += 1 + result.append(Content.from_text(str(item))) + + if has_rich: return result + text_parts = [c.text for c in result if c.text] if not text_parts: return "" if len(text_parts) == 1: diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 1d754f0274..5f6d49ef6a 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -649,8 +649,8 @@ def parse_result(result: Any) -> str | list[Content]: if isinstance(result, Content): if result.type in ("data", "uri"): return [result] - if result.type == "text" and result.text: - return result.text + if result.type == "text": + return result.text or "" if isinstance(result, list) and any(isinstance(item, Content) for item in result): return [item if isinstance(item, Content) else Content.from_text(str(item)) for item in result] dumpable = FunctionTool._make_dumpable(result) diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index 82f109d5df..fb30c1b298 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -801,6 +801,17 @@ def from_function_result( raw_representation: Optional raw representation from the provider. """ if isinstance(result, list): + if not all(isinstance(c, Content) for c in result): + return cls( + "function_result", + call_id=call_id, + result=str(result), + items=list(items) if items else None, + exception=exception, + annotations=annotations, + additional_properties=additional_properties, + raw_representation=raw_representation, + ) text_parts = [c.text for c in result if c.type == "text" and c.text] rich_items = [c for c in result if c.type in ("data", "uri")] return cls( diff --git a/python/packages/core/tests/core/test_types.py b/python/packages/core/tests/core/test_types.py index 5536170339..e868aa9124 100644 --- a/python/packages/core/tests/core/test_types.py +++ b/python/packages/core/tests/core/test_types.py @@ -2427,6 +2427,27 @@ def test_content_from_function_result_items_in_to_dict(): assert d["items"][0]["type"] == "data" +def test_from_function_result_with_only_rich_content_list(): + """Test Content.from_function_result with only image items and no text.""" + content_list = [ + Content.from_data(data=b"image_bytes", media_type="image/png"), + ] + result = Content.from_function_result(call_id="test-456", result=content_list) + assert result.type == "function_result" + assert result.result == "" + assert result.items is not None + assert len(result.items) == 1 + assert result.items[0].type == "data" + + +def test_from_function_result_with_non_content_list(): + """Test Content.from_function_result with a list of non-Content objects falls back to str.""" + result = Content.from_function_result(call_id="test-789", result=["hello", "world"]) + assert result.type == "function_result" + assert result.result == "['hello', 'world']" + assert result.items is None + + # endregion diff --git a/python/packages/core/tests/openai/test_openai_chat_client.py b/python/packages/core/tests/openai/test_openai_chat_client.py index 5fe8457505..6243b61896 100644 --- a/python/packages/core/tests/openai/test_openai_chat_client.py +++ b/python/packages/core/tests/openai/test_openai_chat_client.py @@ -364,6 +364,37 @@ def test_function_result_exception_handling(openai_unit_test_env: dict[str, str] assert openai_messages[0]["tool_call_id"] == "call-123" +def test_function_result_with_rich_items_warns_and_omits( + openai_unit_test_env: dict[str, str], +) -> None: + """Test that function_result with items logs a warning and omits rich items.""" + + client = OpenAIChatClient() + image_content = Content.from_data(data=b"image_bytes", media_type="image/png") + message = Message( + role="tool", + contents=[ + Content.from_function_result( + call_id="call_rich", + result=[Content.from_text("Result text"), image_content], + ) + ], + ) + + with patch("agent_framework.openai._chat_client.logger") as mock_logger: + openai_messages = client._prepare_message_for_openai(message) + + # Warning should be logged + mock_logger.warning.assert_called_once() + assert "does not support rich content" in mock_logger.warning.call_args[0][0] + + # Tool message should still be emitted with text result + assert len(openai_messages) == 1 + assert openai_messages[0]["role"] == "tool" + assert openai_messages[0]["tool_call_id"] == "call_rich" + assert openai_messages[0]["content"] == "Result text" + + def test_parse_result_string_passthrough(): """Test that string values are passed through directly without JSON encoding.""" from agent_framework import FunctionTool diff --git a/python/packages/core/tests/openai/test_openai_responses_client.py b/python/packages/core/tests/openai/test_openai_responses_client.py index b5e845a586..d9dd0830de 100644 --- a/python/packages/core/tests/openai/test_openai_responses_client.py +++ b/python/packages/core/tests/openai/test_openai_responses_client.py @@ -2241,6 +2241,45 @@ def test_prepare_content_for_openai_unsupported_content() -> None: assert result == {} +def test_prepare_content_for_openai_function_result_with_rich_items() -> None: + """Test _prepare_content_for_openai with function_result containing rich items.""" + client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + + image_content = Content.from_data(data=b"image_bytes", media_type="image/png") + content = Content.from_function_result( + call_id="call_rich", + result=[Content.from_text("Result text"), image_content], + ) + + result = client._prepare_content_for_openai("user", content, {}) # type: ignore + + assert result["type"] == "function_call_output" + assert result["call_id"] == "call_rich" + # Output should be a list with text and image parts + output = result["output"] + assert isinstance(output, list) + assert len(output) == 2 + assert output[0]["type"] == "input_text" + assert output[0]["text"] == "Result text" + assert output[1]["type"] == "input_image" + + +def test_prepare_content_for_openai_function_result_without_items() -> None: + """Test _prepare_content_for_openai with plain string function_result.""" + client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") + + content = Content.from_function_result( + call_id="call_plain", + result="Simple result", + ) + + result = client._prepare_content_for_openai("user", content, {}) # type: ignore + + assert result["type"] == "function_call_output" + assert result["call_id"] == "call_plain" + assert result["output"] == "Simple result" + + def test_parse_chunk_from_openai_code_interpreter() -> None: """Test _parse_chunk_from_openai with code_interpreter_call.""" client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") From 888ff30eb62d913e22991983085b1d48600c83ff Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Thu, 5 Mar 2026 11:20:34 -0800 Subject: [PATCH 7/8] Fix pyright errors: add type ignore comments for Any list iteration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/packages/core/agent_framework/_tools.py | 4 ++-- python/packages/core/agent_framework/_types.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 5f6d49ef6a..e7f007228f 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -651,8 +651,8 @@ def parse_result(result: Any) -> str | list[Content]: return [result] if result.type == "text": return result.text or "" - if isinstance(result, list) and any(isinstance(item, Content) for item in result): - return [item if isinstance(item, Content) else Content.from_text(str(item)) for item in result] + if isinstance(result, list) and any(isinstance(item, Content) for item in result): # type: ignore[reportUnknownVariableType] + return [item if isinstance(item, Content) else Content.from_text(str(item)) for item in result] # type: ignore[reportUnknownVariableType, reportUnknownArgumentType] dumpable = FunctionTool._make_dumpable(result) if isinstance(dumpable, str): return dumpable diff --git a/python/packages/core/agent_framework/_types.py b/python/packages/core/agent_framework/_types.py index fb30c1b298..60ec77b9d0 100644 --- a/python/packages/core/agent_framework/_types.py +++ b/python/packages/core/agent_framework/_types.py @@ -801,24 +801,24 @@ def from_function_result( raw_representation: Optional raw representation from the provider. """ if isinstance(result, list): - if not all(isinstance(c, Content) for c in result): + if not all(isinstance(c, Content) for c in result): # type: ignore[reportUnknownVariableType] return cls( "function_result", call_id=call_id, - result=str(result), + result=str(result), # type: ignore[reportUnknownArgumentType] items=list(items) if items else None, exception=exception, annotations=annotations, additional_properties=additional_properties, raw_representation=raw_representation, ) - text_parts = [c.text for c in result if c.type == "text" and c.text] - rich_items = [c for c in result if c.type in ("data", "uri")] + text_parts = [c.text for c in result if c.type == "text" and c.text] # type: ignore[reportUnknownVariableType, reportUnknownMemberType] + rich_items = [c for c in result if c.type in ("data", "uri")] # type: ignore[reportUnknownVariableType, reportUnknownMemberType] return cls( "function_result", call_id=call_id, - result="\n".join(text_parts) if text_parts else "", - items=rich_items or None, + result="\n".join(text_parts) if text_parts else "", # type: ignore[reportUnknownArgumentType] + items=rich_items or None, # type: ignore[reportUnknownArgumentType] exception=exception, annotations=annotations, additional_properties=additional_properties, From 099ceb660851cf85a674524858260b29d31d3df6 Mon Sep 17 00:00:00 2001 From: Giles Odigwe Date: Fri, 6 Mar 2026 09:20:26 -0800 Subject: [PATCH 8/8] Fix mypy/pyright: ensure ToolExecutionException receives str Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/packages/core/agent_framework/_mcp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/packages/core/agent_framework/_mcp.py b/python/packages/core/agent_framework/_mcp.py index 4c3ec41ced..e35e1d6fb3 100644 --- a/python/packages/core/agent_framework/_mcp.py +++ b/python/packages/core/agent_framework/_mcp.py @@ -919,7 +919,8 @@ async def call_tool(self, tool_name: str, **kwargs: Any) -> str | list[Content]: try: result = await self.session.call_tool(tool_name, arguments=filtered_kwargs, meta=otel_meta) # type: ignore if result.isError: - raise ToolExecutionException(parser(result)) + parsed = parser(result) + raise ToolExecutionException(str(parsed) if not isinstance(parsed, str) else parsed) return parser(result) except ToolExecutionException: raise