From 000b5d8cb04dfdf214e1e7c93c6b0bdb5f056f84 Mon Sep 17 00:00:00 2001 From: yetuge <182123929+yetuge@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:54:36 +0800 Subject: [PATCH] fix(tencent-wsa): report a format error for Pages with no usable page objects (#6799) `_parse_results` skipped malformed page entries but returned the collected list even when it was empty, so a non-empty `Pages` container holding no usable page fell through to "No results found". That tells the agent the query legitimately matched nothing instead of surfacing the malformed payload -- the failure mode the sibling providers already contain (brave #6550, groundroute #6603, tavily #6645, sofya #6719), and the rule the tavily changelog entry already lists `tencent_wsa` as applying. A missing or null `Pages` stays an empty search, a non-list container keeps the existing structured format error, and individual malformed entries in an otherwise valid container are still skipped so the valid rows survive. Only the non-empty-container-with-no-usable-page case now returns the provider's format error and logs the container, never the query or payload values. RED/GREEN: the 5 new cases fail on main with "No results found" and pass with the fix (stash-compared source-only); test_tencent_wsa_tools.py 33 passed, the five provider suites 281 passed, ruff check and format clean. Co-authored-by: Willem Jiang --- README.md | 2 + .../deerflow/community/tencent_wsa/tools.py | 13 ++++ backend/tests/test_tencent_wsa_tools.py | 64 +++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/README.md b/README.md index 6ef9a648a6..e1cf4c1d55 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,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. + 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. + Jina, Browserless, and InfoQuest web fetches resolve relative links and image sources using the requested page URL (or a usable HTML base URL), so returned Markdown includes complete destinations. Link resolution preserves the surrounding HTML source, including malformed-page formatting. Self-hosted Browserless, Crawl4AI, Firecrawl, and fastCRW fetch backends require outbound isolation from private, loopback, link-local, and cloud-metadata networks before setting `network_isolation_confirmed: true` on each tool. DeerFlow rejects private or unverifiable backends by default because those services handle redirects, DNS resolution, and subresources themselves. `allow_private_addresses` permits intentional internal target URLs and does not bypass the backend check. See [delegated fetch deployment](backend/docs/CONFIGURATION.md#delegated-fetch-backend-isolation); `make doctor` reports backend configurations that would be refused. diff --git a/backend/packages/harness/deerflow/community/tencent_wsa/tools.py b/backend/packages/harness/deerflow/community/tencent_wsa/tools.py index ae9a7ab976..6e824c7e4f 100644 --- a/backend/packages/harness/deerflow/community/tencent_wsa/tools.py +++ b/backend/packages/harness/deerflow/community/tencent_wsa/tools.py @@ -159,6 +159,16 @@ def _get_response(data: dict[str, Any], query: str) -> tuple[dict[str, Any] | No def _parse_results(response: dict[str, Any], *, max_results: int) -> list[dict[str, Any]] | None: + """Return the result rows of a WSA response, or None if malformed. + + A missing or null ``Pages`` is an empty search. A non-list container, or a + non-empty container holding no usable page, is malformed: the caller + reports the provider's format error rather than "No results found", which + would tell the agent the query legitimately matched nothing — the + containment rule the sibling providers already apply (brave, groundroute, + tavily, sofya). Individual malformed entries in an otherwise valid + container are still skipped so the valid rows survive. + """ pages = response.get("Pages") if pages is None: return [] @@ -202,6 +212,9 @@ def _parse_results(response: dict[str, Any], *, max_results: int) -> list[dict[s results.append(result) if len(results) >= max_results: break + if pages and not results: + logger.error("Tencent Cloud WSA returned 'Pages' with no usable page objects") + return None return results diff --git a/backend/tests/test_tencent_wsa_tools.py b/backend/tests/test_tencent_wsa_tools.py index 5c8f6da29a..0d27ce5ec3 100644 --- a/backend/tests/test_tencent_wsa_tools.py +++ b/backend/tests/test_tencent_wsa_tools.py @@ -1,6 +1,7 @@ """Unit tests for the Tencent Cloud Web Search API community provider.""" import json +import logging from unittest.mock import MagicMock, patch import httpx @@ -263,6 +264,69 @@ class TestTencentWsaSearch: "request_id": "request-123", } + @pytest.mark.parametrize("pages", [["junk", 3, None], [12345], [[]], [False], ["not-json"]]) + def test_pages_without_any_usable_objects_return_format_error(self, pages, caplog): + """A non-empty Pages container holding no usable page is malformed, not an empty search. + + Reporting "No results found" for it would tell the agent the query + legitimately matched nothing, hiding a broken upstream payload. + """ + http_response = MagicMock() + http_response.json.return_value = _response(pages) + _, context_manager = _mock_http_client(http_response) + + with ( + patch("deerflow.community.tencent_wsa.tools.get_app_config") as get_config, + patch("deerflow.community.tencent_wsa.tools.httpx.Client", return_value=context_manager), + caplog.at_level(logging.ERROR), + ): + get_config.return_value.get_tool_config.return_value = _tool_config({"api_key": "test-key"}) + + from deerflow.community.tencent_wsa.tools import web_search_tool + + result = json.loads(web_search_tool.run({"query": "腾讯云"})) + + assert result["error"] == "Tencent Cloud WSA returned an unexpected response format" + assert any("no usable page objects" in r.getMessage() for r in caplog.records if r.levelno == logging.ERROR) + + def test_pages_with_some_usable_objects_keep_them(self): + """One usable page among malformed entries keeps the existing skip-and-keep behavior.""" + http_response = MagicMock() + http_response.json.return_value = _response(["not-json", 42, {"title": "Kept", "url": "https://example.com", "content": "Body"}]) + _, context_manager = _mock_http_client(http_response) + + with ( + patch("deerflow.community.tencent_wsa.tools.get_app_config") as get_config, + patch("deerflow.community.tencent_wsa.tools.httpx.Client", return_value=context_manager), + ): + get_config.return_value.get_tool_config.return_value = _tool_config({"api_key": "test-key"}) + + from deerflow.community.tencent_wsa.tools import web_search_tool + + result = json.loads(web_search_tool.run({"query": "腾讯云"})) + + assert result["total_results"] == 1 + assert result["results"][0]["title"] == "Kept" + + def test_empty_and_missing_pages_keep_no_results(self): + """A missing or empty Pages container stays a legitimate empty search.""" + for pages in ([], None): + http_response = MagicMock() + http_response.json.return_value = _response(pages) + _, context_manager = _mock_http_client(http_response) + + with ( + patch("deerflow.community.tencent_wsa.tools.get_app_config") as get_config, + patch("deerflow.community.tencent_wsa.tools.httpx.Client", return_value=context_manager), + ): + get_config.return_value.get_tool_config.return_value = _tool_config({"api_key": "test-key"}) + + from deerflow.community.tencent_wsa.tools import web_search_tool + + result = json.loads(web_search_tool.run({"query": "腾讯云"})) + + assert result == {"error": "No results found", "query": "腾讯云", "request_id": "request-123"} + class TestTencentWsaConfiguration: @pytest.mark.parametrize(