From 2398b3440d017c40e9ae61c6bb1dcab376c97588 Mon Sep 17 00:00:00 2001 From: elmaroun07-hub Date: Wed, 11 Nov 2026 11:10:57 +0000 Subject: [PATCH] fix(tools): tell the model when a RestApiTool body is empty or not JSON When an API answered 2xx with an empty or non-JSON body, RestApiTool returned only {"text": response.text}. An empty body reached the model as {"text": ""}, which it cannot tell apart from real data. Keep "text" as it is and add a "message" saying the body was empty or was not valid JSON, with the status code. The call still counts as a success, so nothing is reported under "error". JSON bodies and error statuses are unchanged. --- .../openapi_spec_parser/rest_api_tool.py | 12 +- .../openapi_spec_parser/test_rest_api_tool.py | 107 ++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py b/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py index 18dbb70b259..8112cc4d347 100644 --- a/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +++ b/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py @@ -816,7 +816,17 @@ async def call( ) except ValueError: self._logger.debug("API Response (non-JSON): %s", response.text) - return {"text": response.text} # Return text if not JSON + # A bare "text" leaves the model unable to tell an empty or non-JSON + # body from data, so say which one came back. The call still succeeded, + # so this goes under "message", never "error". + if response.text.strip(): + message = "The response body was not valid JSON." + else: + message = "The response body was empty." + return { + "text": response.text, # Return text if not JSON + "message": f"{message} Status Code: {response.status_code}", + } def _format_error_response(self, error_message: str) -> dict[str, str]: """Formats a tool execution error response dict.""" diff --git a/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py b/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py index d44949ae5ed..b1832ed82a6 100644 --- a/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py +++ b/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py @@ -521,6 +521,113 @@ async def test_call_http_failure_decodes_body_with_declared_charset( "Status Code: 404, Commande introuvable : échec" ) + @pytest.mark.parametrize( + "content", + [b"", b" \n\t "], + ids=["empty", "whitespace"], + ) + @patch( + "google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request" + ) + @pytest.mark.asyncio + async def test_call_empty_body_says_the_body_was_empty( + self, + mock_request, + content, + mock_tool_context, + sample_endpoint, + sample_operation, + ): + """An empty 2xx body is labelled so the model does not read it as data.""" + mock_request.return_value = httpx.Response( + status_code=200, + request=httpx.Request("GET", "https://example.com/test"), + content=content, + ) + tool = RestApiTool( + name="test_tool", + description="Test Tool", + endpoint=sample_endpoint, + operation=sample_operation, + ) + + result = await tool.call(args={}, tool_context=mock_tool_context) + + assert result == { + "text": content.decode(), + "message": "The response body was empty. Status Code: 200", + } + + @pytest.mark.parametrize( + "content, content_type", + [ + (b"Saved", "text/html"), + (b"Saved", "text/plain"), + ], + ids=["html", "plain_text"], + ) + @patch( + "google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request" + ) + @pytest.mark.asyncio + async def test_call_non_json_body_says_the_body_was_not_json( + self, + mock_request, + content, + content_type, + mock_tool_context, + sample_endpoint, + sample_operation, + ): + """A non-JSON 2xx body keeps its text and is labelled as not JSON.""" + mock_request.return_value = httpx.Response( + status_code=200, + request=httpx.Request("GET", "https://example.com/test"), + content=content, + headers={"content-type": content_type}, + ) + tool = RestApiTool( + name="test_tool", + description="Test Tool", + endpoint=sample_endpoint, + operation=sample_operation, + ) + + result = await tool.call(args={}, tool_context=mock_tool_context) + + assert result == { + "text": content.decode(), + "message": "The response body was not valid JSON. Status Code: 200", + } + + @patch( + "google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request" + ) + @pytest.mark.asyncio + async def test_call_json_body_is_returned_unchanged( + self, + mock_request, + mock_tool_context, + sample_endpoint, + sample_operation, + ): + """A JSON 2xx body reaches the model as decoded, with nothing added.""" + mock_request.return_value = httpx.Response( + status_code=200, + request=httpx.Request("GET", "https://example.com/test"), + json={"result": "ok"}, + ) + tool = RestApiTool( + name="test_tool", + description="Test Tool", + endpoint=sample_endpoint, + operation=sample_operation, + ) + + result = await tool.call(args={}, tool_context=mock_tool_context) + + assert result == {"result": "ok"} + @patch( "google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request" )