mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
refactor(community): fold serper onto the shared max_results owner (#6966)
Serper keeps a private _coerce_max_results in the pre-#5852 shape: a bare int() silently turns max_results: true into 1 and 3.5 into 3, and an invalid value falls back without a warning (#5865). Delegate to deerflow.community.search_max_results.coerce_max_results, which owns what invalid means for the folded providers, and keep Serper's 10-result cap at the call site the way You.com does, because clamping policy is deliberately per-provider. Invalid configured counts now warn under the Serper label and fall back to the default instead of being silently coerced.
This commit is contained in:
parent
c3ac4b981f
commit
0f2c2f788a
@ -21,6 +21,7 @@ from urllib.parse import urlparse
|
||||
import httpx
|
||||
from langchain.tools import tool
|
||||
|
||||
from deerflow.community.search_max_results import coerce_max_results
|
||||
from deerflow.community.search_time_range import SearchTimeRange
|
||||
from deerflow.community.url_safety import safe_public_result_url as _safe_public_url
|
||||
from deerflow.config import get_app_config
|
||||
@ -53,15 +54,18 @@ def _get_api_key(tool_name: str, *, extras: Mapping[str, object] | None = None)
|
||||
return None
|
||||
|
||||
|
||||
def _coerce_max_results(value: object, default: int = 5, max_allowed: int = _SERPER_MAX_RESULTS) -> int:
|
||||
"""Coerce config/parameter input into a bounded positive result count."""
|
||||
try:
|
||||
count = int(value)
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return default
|
||||
if count <= 0:
|
||||
return default
|
||||
return min(count, max_allowed)
|
||||
def _count_for_request(value: object) -> int:
|
||||
"""Coerce ``max_results`` via the shared owner, then mirror Serper's clamp.
|
||||
|
||||
``deerflow.community.search_max_results.coerce_max_results`` owns what
|
||||
"invalid" means (booleans, non-integral floats and ``OverflowError`` all
|
||||
fall back to the default); the 10-result cap stays here because clamping
|
||||
policy is deliberately per-provider.
|
||||
"""
|
||||
return min(
|
||||
coerce_max_results(value, provider="Serper", logger=logger),
|
||||
_SERPER_MAX_RESULTS,
|
||||
)
|
||||
|
||||
|
||||
def _missing_key_error(query: str, tool_name: str) -> str:
|
||||
@ -392,7 +396,7 @@ def web_search_tool(query: str, max_results: int = 5, time_range: SearchTimeRang
|
||||
config = get_app_config().get_tool_config("web_search")
|
||||
if config is not None and "max_results" in config.model_extra:
|
||||
max_results = config.model_extra.get("max_results", max_results)
|
||||
max_results = _coerce_max_results(max_results)
|
||||
max_results = _count_for_request(max_results)
|
||||
query = _clean_query(query)
|
||||
|
||||
try:
|
||||
@ -457,7 +461,7 @@ def image_search_tool(query: str, max_results: int = 5) -> str:
|
||||
config = get_app_config().get_tool_config("image_search")
|
||||
if config is not None and "max_results" in config.model_extra:
|
||||
max_results = config.model_extra.get("max_results", max_results)
|
||||
max_results = _coerce_max_results(max_results)
|
||||
max_results = _count_for_request(max_results)
|
||||
query = _clean_query(query)
|
||||
|
||||
extra = config.model_extra if config is not None else {}
|
||||
|
||||
@ -31,6 +31,7 @@ from deerflow.community.fastcrw import tools as fastcrw_tools
|
||||
from deerflow.community.firecrawl import tools as firecrawl_tools
|
||||
from deerflow.community.image_search import tools as image_search_tools
|
||||
from deerflow.community.search_max_results import DEFAULT_MAX_RESULTS, coerce_max_results
|
||||
from deerflow.community.serper import tools as serper_tools
|
||||
from deerflow.community.youcom import tools as youcom_tools
|
||||
|
||||
COMMUNITY_ROOT = Path(ddg_search_tools.__file__).resolve().parent.parent
|
||||
@ -42,6 +43,7 @@ COMMUNITY_ROOT = Path(ddg_search_tools.__file__).resolve().parent.parent
|
||||
SHARED_OWNER_PROVIDERS = {
|
||||
"ddg_search": ("DDG Search", ddg_search_tools),
|
||||
"image_search": ("DDG image search", image_search_tools),
|
||||
"serper": ("Serper", serper_tools),
|
||||
"fastcrw": ("fastCRW", fastcrw_tools),
|
||||
"firecrawl": ("Firecrawl", firecrawl_tools),
|
||||
"youcom": ("You.com", youcom_tools),
|
||||
@ -58,7 +60,6 @@ DEFERRED_PROVIDERS = {
|
||||
"exa",
|
||||
"groundroute",
|
||||
"searxng",
|
||||
"serper",
|
||||
"serply",
|
||||
"sofya",
|
||||
"tencent_wsa",
|
||||
|
||||
@ -223,56 +223,56 @@ class TestGetApiKey:
|
||||
mock.return_value.get_tool_config.assert_called_with("image_search")
|
||||
|
||||
|
||||
class TestCoerceMaxResults:
|
||||
def test_returns_value_when_valid_positive_int(self):
|
||||
from deerflow.community.serper.tools import _coerce_max_results
|
||||
class TestCountForRequest:
|
||||
"""Serper delegates "invalid" to the shared owner, then applies its 10-result cap."""
|
||||
|
||||
assert _coerce_max_results(3) == 3
|
||||
def test_returns_value_when_valid_positive_int(self):
|
||||
from deerflow.community.serper.tools import _count_for_request
|
||||
|
||||
assert _count_for_request(3) == 3
|
||||
|
||||
def test_returns_value_for_numeric_string(self):
|
||||
from deerflow.community.serper.tools import _coerce_max_results
|
||||
from deerflow.community.serper.tools import _count_for_request
|
||||
|
||||
assert _coerce_max_results("7") == 7
|
||||
assert _count_for_request("7") == 7
|
||||
|
||||
def test_caps_value_at_default_maximum(self):
|
||||
from deerflow.community.serper.tools import _coerce_max_results
|
||||
def test_keeps_integral_float(self):
|
||||
from deerflow.community.serper.tools import _count_for_request
|
||||
|
||||
assert _coerce_max_results(999) == 10
|
||||
assert _count_for_request(4.0) == 4
|
||||
|
||||
def test_respects_custom_maximum(self):
|
||||
from deerflow.community.serper.tools import _coerce_max_results
|
||||
def test_caps_value_at_serper_maximum(self):
|
||||
from deerflow.community.serper.tools import _count_for_request
|
||||
|
||||
assert _coerce_max_results(999, max_allowed=3) == 3
|
||||
assert _count_for_request(999) == 10
|
||||
|
||||
def test_returns_default_for_non_numeric_string(self):
|
||||
from deerflow.community.serper.tools import _coerce_max_results
|
||||
@pytest.mark.parametrize("value", ["oops", None, object(), 0, -3, float("inf")])
|
||||
def test_returns_default_for_invalid_values(self, value):
|
||||
from deerflow.community.serper.tools import _count_for_request
|
||||
|
||||
assert _coerce_max_results("oops") == 5
|
||||
assert _count_for_request(value) == 5
|
||||
|
||||
def test_returns_default_for_none(self):
|
||||
from deerflow.community.serper.tools import _coerce_max_results
|
||||
@pytest.mark.parametrize("value", [True, False])
|
||||
def test_rejects_booleans(self, value):
|
||||
"""bool is an int subclass, so it must never be coerced to 1 (#5865)."""
|
||||
from deerflow.community.serper.tools import _count_for_request
|
||||
|
||||
assert _coerce_max_results(None) == 5
|
||||
assert _count_for_request(value) == 5
|
||||
|
||||
def test_returns_default_for_non_coercible_object(self):
|
||||
from deerflow.community.serper.tools import _coerce_max_results
|
||||
def test_rejects_non_integral_float(self):
|
||||
"""A fractional float is a miswritten config value, not a truncation candidate (#5865)."""
|
||||
from deerflow.community.serper.tools import _count_for_request
|
||||
|
||||
assert _coerce_max_results(object()) == 5
|
||||
assert _count_for_request(3.5) == 5
|
||||
|
||||
def test_returns_default_for_zero(self):
|
||||
from deerflow.community.serper.tools import _coerce_max_results
|
||||
def test_warns_under_the_serper_label_on_the_module_logger(self, caplog):
|
||||
from deerflow.community.serper.tools import _count_for_request
|
||||
|
||||
assert _coerce_max_results(0) == 5
|
||||
with caplog.at_level(logging.WARNING, logger="deerflow.community.serper.tools"):
|
||||
_count_for_request(True)
|
||||
|
||||
def test_returns_default_for_negative(self):
|
||||
from deerflow.community.serper.tools import _coerce_max_results
|
||||
|
||||
assert _coerce_max_results(-3) == 5
|
||||
|
||||
def test_respects_custom_default(self):
|
||||
from deerflow.community.serper.tools import _coerce_max_results
|
||||
|
||||
assert _coerce_max_results("bad", default=2) == 2
|
||||
assert "Invalid Serper max_results=True; using default 5" in caplog.text
|
||||
assert [record.name for record in caplog.records] == ["deerflow.community.serper.tools"]
|
||||
|
||||
|
||||
class TestMissingKeyError:
|
||||
@ -1346,13 +1346,6 @@ def test_package_exports_image_search_tool():
|
||||
assert image_search_tool is direct_image_search_tool
|
||||
|
||||
|
||||
def test_coerce_max_results_inf_falls_back_to_default():
|
||||
"""A YAML `.inf` max_results must fall back to the default, not crash."""
|
||||
import deerflow.community.serper.tools as serper_mod
|
||||
|
||||
assert serper_mod._coerce_max_results(float("inf")) == 5
|
||||
|
||||
|
||||
class TestSearchTimeRange:
|
||||
@pytest.mark.parametrize(("time_range", "expected_tbs"), [("day", "qdr:d"), ("week", "qdr:w"), ("month", "qdr:m"), ("year", "qdr:y")])
|
||||
def test_forwards_native_time_filter(self, mock_config_with_key, time_range, expected_tbs):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user