mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
fix(search): coerce DDG result limits from environment variables (#5792)
* fix(search): coerce DDG result limits from environment variables * fix(search): fall back for invalid DDG result limits --------- Co-authored-by: kwsYegar <331533331+kwsYegar@users.noreply.github.com>
This commit is contained in:
parent
887883a58e
commit
434d9647d7
@ -1172,6 +1172,8 @@ Public-skill CI waivers are exact, expiring exceptions in `.github/skill-review-
|
||||
|
||||
Tools follow the same philosophy. DeerFlow comes with a core toolset — web search, web fetch, rendered web capture, file operations, bash execution — and supports custom tools via MCP servers and Python functions. The bundled DDG, Brave, Tavily, and SearXNG search providers accept an optional `time_range` of `day`, `week`, `month`, or `year`; omitting it preserves existing search behavior. For DDG recency searches, DeerFlow excludes DDGS backends that ignore time limits. Swap anything. Add anything.
|
||||
|
||||
For DDG search, `max_results` in `config.yaml` can be a positive integer or an environment-variable reference such as `max_results: $DDG_MAX_RESULTS` with `DDG_MAX_RESULTS=5`. The configured value takes precedence over the tool call's `max_results` argument. Invalid values (for example, `abc`, an empty string, or `3.5`), zero, and negative counts produce a warning and fall back to the default of 5 results.
|
||||
|
||||
Stdio MCP servers can set `cwd` in `extensions_config.json` when their entrypoint
|
||||
or data files depend on a specific working directory. The setting applies to
|
||||
both discovery and tool calls; see [MCP configuration](backend/docs/MCP_SERVER.md#stdio-working-directory).
|
||||
|
||||
@ -13,6 +13,7 @@ from deerflow.config import get_app_config
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_BACKEND = "auto"
|
||||
DEFAULT_MAX_RESULTS = 5
|
||||
DEFAULT_REGION = "wt-wt"
|
||||
DEFAULT_SAFESEARCH = "moderate"
|
||||
DEFAULT_WIKIPEDIA_REGION = "us-en"
|
||||
@ -30,6 +31,18 @@ WIKIPEDIA_LANGUAGE_ALIASES = {
|
||||
}
|
||||
|
||||
|
||||
def _coerce_max_results(value: object) -> int:
|
||||
"""Normalize config/parameter values before passing them to DDGS."""
|
||||
try:
|
||||
count = int(value) # type: ignore[call-overload]
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
count = 0
|
||||
if count <= 0:
|
||||
logger.warning("Invalid DDG Search max_results=%r; using default %s", value, DEFAULT_MAX_RESULTS)
|
||||
return DEFAULT_MAX_RESULTS
|
||||
return count
|
||||
|
||||
|
||||
def _normalize_backend(backend: str | list[str] | tuple[str, ...] | None) -> str:
|
||||
if backend is None:
|
||||
return DEFAULT_BACKEND
|
||||
@ -105,7 +118,7 @@ def _resolve_ddgs_region(query: str, region: str | None, backend: str | list[str
|
||||
|
||||
def _search_text(
|
||||
query: str,
|
||||
max_results: int = 5,
|
||||
max_results: int = DEFAULT_MAX_RESULTS,
|
||||
region: str | None = DEFAULT_REGION,
|
||||
safesearch: str | None = DEFAULT_SAFESEARCH,
|
||||
backend: str | list[str] | tuple[str, ...] | None = DEFAULT_BACKEND,
|
||||
@ -156,7 +169,7 @@ def _search_text(
|
||||
@tool("web_search", parse_docstring=True)
|
||||
def web_search_tool(
|
||||
query: str,
|
||||
max_results: int = 5,
|
||||
max_results: int = DEFAULT_MAX_RESULTS,
|
||||
time_range: SearchTimeRange | None = None,
|
||||
) -> str:
|
||||
"""Search the web for information. Use this tool to find current information, news, articles, and facts from the internet.
|
||||
@ -180,7 +193,7 @@ def web_search_tool(
|
||||
|
||||
results = _search_text(
|
||||
query=query,
|
||||
max_results=max_results,
|
||||
max_results=_coerce_max_results(max_results),
|
||||
region=region,
|
||||
safesearch=safesearch,
|
||||
backend=backend,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"""Unit tests for the DDGS community web search tool."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
@ -10,6 +11,60 @@ import pytest
|
||||
from deerflow.community.ddg_search import tools
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("configured_limit", "env_value", "call_args", "expected_count", "warns"),
|
||||
[
|
||||
pytest.param("$DDG_TEST_MAX_RESULTS", "3", {"max_results": 2}, 3, False, id="environment-variable"),
|
||||
pytest.param("3", "3", {"max_results": 2}, 3, False, id="numeric-string"),
|
||||
pytest.param(3, "3", {"max_results": 2}, 3, False, id="integer-config"),
|
||||
pytest.param(None, "3", {"max_results": 2}, 2, False, id="call-argument"),
|
||||
pytest.param(None, "3", {}, 5, False, id="default"),
|
||||
pytest.param("$DDG_TEST_MAX_RESULTS", "abc", {"max_results": 2}, 5, True, id="invalid-env"),
|
||||
pytest.param("$DDG_TEST_MAX_RESULTS", "", {"max_results": 2}, 5, True, id="empty-env"),
|
||||
pytest.param("$DDG_TEST_MAX_RESULTS", "3.5", {"max_results": 2}, 5, True, id="fractional-env"),
|
||||
pytest.param("$DDG_TEST_MAX_RESULTS", "0", {"max_results": 2}, 5, True, id="zero-env"),
|
||||
pytest.param("$DDG_TEST_MAX_RESULTS", "-2", {"max_results": 2}, 5, True, id="negative-env"),
|
||||
pytest.param(0, "3", {}, 5, True, id="zero-config"),
|
||||
pytest.param(-2, "3", {}, 5, True, id="negative-config"),
|
||||
pytest.param([], "3", {}, 5, True, id="invalid-config-type"),
|
||||
pytest.param(None, "3", {"max_results": 0}, 5, True, id="zero-call-argument"),
|
||||
pytest.param(None, "3", {"max_results": -2}, 5, True, id="negative-call-argument"),
|
||||
],
|
||||
)
|
||||
def test_web_search_tool_max_results_with_real_ddgs(monkeypatch, caplog, configured_limit, env_value, call_args, expected_count, warns) -> None:
|
||||
"""Exercise SDK count arithmetic and slicing without contacting search engines."""
|
||||
from ddgs.ddgs import DDGS
|
||||
from ddgs.results import TextResult
|
||||
|
||||
from deerflow.config.app_config import AppConfig
|
||||
from deerflow.config.tool_config import ToolConfig
|
||||
|
||||
monkeypatch.setenv("DDG_TEST_MAX_RESULTS", env_value)
|
||||
caplog.set_level(logging.WARNING, logger=tools.__name__)
|
||||
raw_config = {"name": "web_search", "group": "web", "use": "deerflow.community.ddg_search.tools:web_search_tool"}
|
||||
if configured_limit is not None:
|
||||
raw_config["max_results"] = configured_limit
|
||||
tool_config = ToolConfig.model_validate(AppConfig.resolve_env_variables(raw_config))
|
||||
monkeypatch.setattr(tools, "get_app_config", lambda: SimpleNamespace(get_tool_config=lambda name: tool_config))
|
||||
|
||||
rows = [TextResult(title=f"Result {i}", href=f"https://example.com/{i}", body=f"Snippet {i}") for i in range(10)]
|
||||
engine = SimpleNamespace(provider="offline", name="offline", search=MagicMock(return_value=rows))
|
||||
monkeypatch.setattr(DDGS, "_get_network_client", lambda self: None)
|
||||
monkeypatch.setattr(DDGS, "_get_engines", lambda self, category, backend: [engine])
|
||||
|
||||
parsed = json.loads(tools.web_search_tool.invoke({"query": "Result", **call_args}))
|
||||
|
||||
assert "error" not in parsed
|
||||
assert parsed["total_results"] == len(parsed["results"]) == expected_count
|
||||
assert all(result["url"].startswith("https://example.com/") for result in parsed["results"])
|
||||
engine.search.assert_called_once()
|
||||
warnings = [record for record in caplog.records if record.name == tools.__name__ and record.levelno == logging.WARNING]
|
||||
assert len(warnings) == int(warns)
|
||||
if warns:
|
||||
assert "max_results" in warnings[0].getMessage()
|
||||
assert "using default 5" in warnings[0].getMessage()
|
||||
|
||||
|
||||
def test_resolve_ddgs_region_maps_worldwide_chinese_query_for_wikipedia() -> None:
|
||||
assert tools._resolve_ddgs_region("\u4e16\u754c\u676f\u65b0\u95fb 2026", "wt-wt", "auto") == "cn-zh"
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user