mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(sofya): report a format error for a results list with no usable objects (#6719)
* fix(sofya): report a format error for results with no usable objects `_response_results` skipped non-object entries but returned the filtered list even when it was empty, so a non-empty `results` array holding no objects fell through to "No results found" -- telling the agent the query legitimately matched nothing instead of surfacing the malformed payload. Treat a non-empty list with no object entries as malformed, matching the containment rule #6603 documents for this provider family (brave #6550, groundroute #6603, tavily #6645, webz). Both `web_search` and `web_fetch` share this helper, so both now report the provider's format error. Missing, null, and empty `results` keep returning "No results found", and mixed lists still keep their valid entries in order. * test(sofya): cover mixed fetch result entries --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
17f774b442
commit
62588f3675
@ -178,6 +178,8 @@ streams simulate chunks from a non-streaming response, while no-tool streams sta
|
||||
|
||||
Tavily web search and fetch contain malformed HTTP-success payloads the same way: a non-object payload, or a `results`/`failed_results` container that is not a list, returns the provider's format error, and non-object entries in an otherwise valid list are skipped in order. Missing, null, or empty results keep the existing response (`[]` for search, `Error: No results found` for fetch); missing or null search fields (`title`, `url`, `content`) normalize to empty text, and a non-string fetch `raw_content` is coerced to text before the truncation limit.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@ -117,14 +117,24 @@ def _clip(value: object, limit: int) -> str:
|
||||
|
||||
|
||||
def _response_results(data: dict) -> list[dict] | None:
|
||||
"""Return the result dicts of a Sofya response, or None if malformed."""
|
||||
"""Return the result dicts of a Sofya response, or None if malformed.
|
||||
|
||||
A missing or null ``results`` is an empty search. A non-list container, or a
|
||||
non-empty list holding no objects, is malformed: the caller reports a format
|
||||
error rather than "No results found", which would tell the agent the query
|
||||
legitimately matched nothing.
|
||||
"""
|
||||
results = data.get("results")
|
||||
if results is None:
|
||||
return []
|
||||
if not isinstance(results, list):
|
||||
logger.error("Sofya returned an unexpected 'results' payload type: %s", type(results).__name__)
|
||||
return None
|
||||
return [item for item in results if isinstance(item, dict)]
|
||||
usable = [item for item in results if isinstance(item, dict)]
|
||||
if results and not usable:
|
||||
logger.error("Sofya returned 'results' with no usable result objects")
|
||||
return None
|
||||
return usable
|
||||
|
||||
|
||||
@tool("web_search", parse_docstring=True)
|
||||
|
||||
@ -437,6 +437,33 @@ class TestWebSearchTool:
|
||||
|
||||
assert parsed["error"] == "Sofya returned an unexpected response format"
|
||||
|
||||
@pytest.mark.parametrize("results", [["a", "b"], [None, None], [1, 2], [[]], [False]])
|
||||
def test_results_without_any_objects_returns_format_error(self, mock_config_with_key, results, caplog):
|
||||
"""A non-empty `results` holding no objects is malformed, not an empty search."""
|
||||
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
||||
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_search_response(results)
|
||||
|
||||
from deerflow.community.sofya.tools import web_search_tool
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
parsed = json.loads(web_search_tool.invoke({"query": "test"}))
|
||||
|
||||
assert parsed["error"] == "Sofya returned an unexpected response format"
|
||||
assert any("no usable result objects" in r.getMessage() for r in caplog.records if r.levelno == logging.ERROR)
|
||||
|
||||
def test_mixed_results_keep_the_object_entries(self, mock_config_with_key):
|
||||
results = ["junk", {"title": "Kept", "url": "https://example.com/1", "content": "Body"}, 7]
|
||||
|
||||
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
||||
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_search_response(results)
|
||||
|
||||
from deerflow.community.sofya.tools import web_search_tool
|
||||
|
||||
parsed = json.loads(web_search_tool.invoke({"query": "test"}))
|
||||
|
||||
assert parsed["total_results"] == 1
|
||||
assert parsed["results"][0] == {"title": "Kept", "url": "https://example.com/1", "content": "Body"}
|
||||
|
||||
def test_http_error_returns_error_json(self, mock_config_with_key):
|
||||
request = httpx.Request("POST", "https://sofya.co/v1/search")
|
||||
response = httpx.Response(402, text="Insufficient credits", request=request)
|
||||
@ -569,6 +596,36 @@ class TestWebFetchTool:
|
||||
|
||||
assert result == "Error: No results found"
|
||||
|
||||
def test_results_without_any_objects_returns_format_error(self, mock_config_with_key):
|
||||
"""A non-empty `results` holding no objects is malformed, not an empty fetch."""
|
||||
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
||||
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_fetch_response(["a", 3])
|
||||
|
||||
from deerflow.community.sofya.tools import web_fetch_tool
|
||||
|
||||
result = web_fetch_tool.invoke({"url": "https://example.com"})
|
||||
|
||||
assert result == "Error: Sofya returned an unexpected response format"
|
||||
|
||||
def test_mixed_results_returns_first_object_content(self, mock_config_with_key):
|
||||
results = [
|
||||
"junk",
|
||||
None,
|
||||
{"title": "First page", "content": "First body", "success": True},
|
||||
7,
|
||||
{"title": "Second page", "content": "Second body", "success": True},
|
||||
False,
|
||||
]
|
||||
|
||||
with patch("deerflow.community.sofya.tools.httpx.Client") as mock_client_cls:
|
||||
mock_client_cls.return_value.__enter__.return_value.post.return_value = _make_fetch_response(results)
|
||||
|
||||
from deerflow.community.sofya.tools import web_fetch_tool
|
||||
|
||||
result = web_fetch_tool.invoke({"url": "https://example.com"})
|
||||
|
||||
assert result == "# First page\n\nFirst body"
|
||||
|
||||
def test_http_error_returns_error_string(self, mock_config_with_key):
|
||||
request = httpx.Request("POST", "https://sofya.co/v1/fetch")
|
||||
response = httpx.Response(401, text="Invalid API key", request=request)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user