mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Fix single-tool input handling in OpenAIResponsesClient._prepare_tools_for_openai (#4312)
* Fix OpenAIResponsesClient mishandling single-tool inputs (#4304) Use normalize_tools() in _prepare_tools_for_openai to wrap single tools (FunctionTool or dict) in a list before iteration, consistent with the chat client implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR review feedback for #4304 - Use precise type annotation matching normalize_tools/OpenAIChatClient signature instead of collapsed Sequence[Any] | Any | None - Move emptiness guard after normalize_tools() call so single falsy tool objects are not silently swallowed - Import ToolTypes for the type annotation - Expand test_prepare_tools_for_openai_single_function_tool assertions to verify parameters, strict, and parameter schema fields - Add test_prepare_tools_for_openai_none to verify None input returns [] Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
6f7e55c430
commit
ff124c44a9
@@ -43,6 +43,8 @@ from .._tools import (
|
||||
FunctionInvocationConfiguration,
|
||||
FunctionInvocationLayer,
|
||||
FunctionTool,
|
||||
ToolTypes,
|
||||
normalize_tools,
|
||||
)
|
||||
from .._types import (
|
||||
Annotation,
|
||||
@@ -425,21 +427,24 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
|
||||
|
||||
# region Prep methods
|
||||
|
||||
def _prepare_tools_for_openai(self, tools: Sequence[Any] | None) -> list[Any]:
|
||||
def _prepare_tools_for_openai(
|
||||
self, tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None
|
||||
) -> list[Any]:
|
||||
"""Prepare tools for the OpenAI Responses API.
|
||||
|
||||
Converts FunctionTool to Responses API format. All other tools pass through unchanged.
|
||||
|
||||
Args:
|
||||
tools: Sequence of tools to prepare.
|
||||
tools: A single tool or sequence of tools to prepare.
|
||||
|
||||
Returns:
|
||||
List of tool parameters ready for the OpenAI API.
|
||||
"""
|
||||
if not tools:
|
||||
tools_list = normalize_tools(tools)
|
||||
if not tools_list:
|
||||
return []
|
||||
response_tools: list[Any] = []
|
||||
for tool in tools:
|
||||
for tool in tools_list:
|
||||
if isinstance(tool, FunctionTool):
|
||||
params = tool.parameters()
|
||||
params["additionalProperties"] = False
|
||||
|
||||
@@ -1193,6 +1193,52 @@ def test_prepare_tools_for_openai_with_mcp() -> None:
|
||||
assert "require_approval" in mcp
|
||||
|
||||
|
||||
def test_prepare_tools_for_openai_single_function_tool() -> None:
|
||||
"""Test that a single FunctionTool (not wrapped in a list) is handled correctly."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
@tool
|
||||
def hello(name: str) -> str:
|
||||
"""Say hello."""
|
||||
return name
|
||||
|
||||
resp_tools = client._prepare_tools_for_openai(hello)
|
||||
assert isinstance(resp_tools, list)
|
||||
assert len(resp_tools) == 1
|
||||
tool_def = resp_tools[0]
|
||||
assert tool_def["type"] == "function"
|
||||
assert tool_def["name"] == "hello"
|
||||
assert tool_def["strict"] is False
|
||||
assert "parameters" in tool_def
|
||||
params = tool_def["parameters"]
|
||||
assert isinstance(params, dict)
|
||||
assert params.get("type") == "object"
|
||||
assert "properties" in params
|
||||
assert "name" in params["properties"]
|
||||
assert params["properties"]["name"]["type"] == "string"
|
||||
|
||||
|
||||
def test_prepare_tools_for_openai_single_dict_tool() -> None:
|
||||
"""Test that a single dict tool (not wrapped in a list) is handled correctly."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
web_tool = OpenAIResponsesClient.get_web_search_tool(search_context_size="low")
|
||||
resp_tools = client._prepare_tools_for_openai(web_tool)
|
||||
assert isinstance(resp_tools, list)
|
||||
assert len(resp_tools) == 1
|
||||
assert "type" in resp_tools[0]
|
||||
assert resp_tools[0]["search_context_size"] == "low"
|
||||
|
||||
|
||||
def test_prepare_tools_for_openai_none() -> None:
|
||||
"""Test that passing None returns an empty list."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
resp_tools = client._prepare_tools_for_openai(None)
|
||||
assert isinstance(resp_tools, list)
|
||||
assert len(resp_tools) == 0
|
||||
|
||||
|
||||
def test_parse_response_from_openai_with_mcp_approval_request() -> None:
|
||||
"""Test that a non-streaming mcp_approval_request is parsed into FunctionApprovalRequestContent."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
Reference in New Issue
Block a user