fix(serper): report a format error for results with no usable objects (#6779)

* fix(serper): report a format error for results with no usable objects

`_response_items` skipped non-object entries but returned the filtered
list even when it was empty, so a non-empty `organic`/`images` array
holding no objects fell through to "No results found" / "No images
found" -- telling the agent the query legitimately matched nothing
instead of surfacing the malformed payload.

Treat a non-empty container with no object entries as malformed, matching
the containment rule #6603 documents for this provider family (brave
#6550, groundroute #6603, tavily #6645, webz). `web_search` and
`image_search` share this helper, so both now report the provider's
format error. Missing, null, and empty containers keep reading as an
empty search, and mixed lists still keep their valid entries in order.

* fix: address review and validation findings for PR 6779
This commit is contained in:
D41910 2026-10-15 11:37:59 +08:00 committed by GitHub
parent ac9ed11b8b
commit 3195ed1f09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 44 additions and 1 deletions

View File

@ -182,6 +182,8 @@ streams simulate chunks from a non-streaming response, while no-tool streams sta
Sofya web search and fetch contain malformed HTTP-success payloads the same way: a non-object payload, a `results` container that is not a list, or a non-empty list with no object entries returns the provider's format error, while non-object entries in an otherwise valid list are skipped in order. Missing, null, or empty results keep the existing "No results found" response.
Serper web and image search preserve valid entries in mixed result lists. A malformed `organic`/`images` container, or a non-empty list holding no object entries, returns Serper's format error; missing, null, or empty containers keep the existing "No results found" / "No images found" response. Format errors log the container field name only, without queries, credentials, or payload values.
Tencent Cloud WSA web search contains malformed HTTP-success payloads the same way: a `Pages` container that is not a list, or a non-empty container holding no usable page, returns the provider's format error, while malformed page entries in an otherwise valid container are skipped so the valid rows survive. Missing, null, or empty `Pages` keep the existing "No results found" response.
Serply web search contains malformed HTTP-success payloads the same way in every vertical: a `results`/`entries`/`articles` container that is not a list, or a non-empty list with no object entries, returns the provider's format error, while non-object entries in an otherwise valid list are skipped in order. Missing, null, or empty containers keep the existing "No results found" response.

View File

@ -90,7 +90,14 @@ def _response_items(data: dict, field: str, query: str) -> tuple[list[dict] | No
if not isinstance(items, list):
logger.error("Serper returned unexpected '%s' payload type: %s", field, type(items).__name__)
return None, _unexpected_format_error(query)
return [item for item in items if isinstance(item, dict)], None
usable = [item for item in items if isinstance(item, dict)]
if items and not usable:
# Same containment rule as groundroute/tavily: a populated container
# holding nothing usable is malformed, not an empty search. Reporting
# "No results found" tells the agent the query matched nothing.
logger.error("Serper returned '%s' with no usable result objects", field)
return None, _unexpected_format_error(query)
return usable, None
def _clean_query(query: str) -> str:

View File

@ -776,6 +776,25 @@ class TestWebSearchTool:
assert parsed["total_results"] == 1
assert parsed["results"][0]["title"] == "T"
def test_organic_with_no_usable_objects_returns_error(self, mock_config_with_key):
"""A populated container holding nothing usable is malformed, not an empty search.
Mirrors groundroute/tavily: reporting "No results found" would tell the
agent the query legitimately matched nothing.
"""
mock_resp = _make_serper_response(["bad", 3, None, ["nested"]])
with patch("deerflow.community.serper.tools.httpx.Client") as mock_client_cls:
mock_client_cls.return_value.__enter__.return_value.post.return_value = mock_resp
from deerflow.community.serper.tools import web_search_tool
result = web_search_tool.invoke({"query": "test"})
parsed = json.loads(result)
assert parsed["error"] == "Serper returned an unexpected response format"
assert parsed["query"] == "test"
def test_timeout_returns_error(self, mock_config_with_key):
with patch("deerflow.community.serper.tools.httpx.Client") as mock_client_cls:
mock_client_cls.return_value.__enter__.return_value.post.side_effect = httpx.TimeoutException("Read timed out")
@ -991,6 +1010,21 @@ class TestImageSearchTool:
assert parsed["error"] == "No images found"
assert parsed["query"] == "no results"
def test_images_with_no_usable_objects_returns_error(self, mock_config_with_key):
"""Same containment rule on the images container, which shares the helper."""
mock_resp = _make_serper_images_response(["https://x/y.png", 7])
with patch("deerflow.community.serper.tools.httpx.Client") as mock_client_cls:
mock_client_cls.return_value.__enter__.return_value.post.return_value = mock_resp
from deerflow.community.serper.tools import image_search_tool
result = image_search_tool.invoke({"query": "no results"})
parsed = json.loads(result)
assert parsed["error"] == "Serper returned an unexpected response format"
assert parsed["query"] == "no results"
def test_missing_api_key_returns_error_json(self, mock_config_no_key):
with patch.dict("os.environ", {}, clear=True):
import os