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(