fix(serply): delegate max_results coercion to shared owner (#7869) (#7872)

* fix(serply): delegate max_results coercion to shared owner (#7869)

* refactor(serply): simplify count helper and consolidate tests

---------

Co-authored-by: AochenShen99 <sac1063467113@hotmail.com>
This commit is contained in:
xuilen 2026-11-10 14:09:40 +08:00 committed by GitHub
parent bfa2794c76
commit 10bbcc92de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 30 additions and 36 deletions

View File

@ -17,6 +17,7 @@ import re
import httpx
from langchain.tools import tool
from deerflow.community.search_max_results import coerce_max_results
from deerflow.community.url_safety import safe_public_result_url
from deerflow.config import get_app_config
@ -51,21 +52,18 @@ def _get_api_key(tool_name: str = "web_search") -> str | None:
return None
def _coerce_max_results(
value: object,
*,
default: int = _DEFAULT_MAX_RESULTS,
max_allowed: int = _SERPLY_MAX_RESULTS,
) -> int:
try:
coerced = int(value)
except (TypeError, ValueError, OverflowError):
logger.warning("Invalid Serply max_results=%r; using default %s", value, default)
coerced = default
if coerced < 1:
logger.warning("Invalid Serply max_results=%r; using default %s", value, default)
coerced = default
return min(coerced, max_allowed)
def _count_for_request(value: object) -> int:
"""Coerce ``max_results`` via the shared owner, then mirror Serply'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 1-100 cap stays here because clamping
policy is deliberately per-provider.
"""
return min(
coerce_max_results(value, provider="Serply", logger=logger, default=_DEFAULT_MAX_RESULTS),
_SERPLY_MAX_RESULTS,
)
def _coerce_vertical(value: object) -> str:
@ -197,7 +195,7 @@ def web_search_tool(query: str, max_results: int = 5) -> str:
extra = (config.model_extra or {}) if config is not None else {}
if "max_results" in extra:
max_results = extra["max_results"]
max_results = _coerce_max_results(max_results)
max_results = _count_for_request(max_results)
vertical = _coerce_vertical(extra.get("vertical"))
query = _clean_query(query)

View File

@ -32,6 +32,7 @@ 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.serply import tools as serply_tools
from deerflow.community.specstream import tools as specstream_tools
from deerflow.community.youcom import tools as youcom_tools
@ -45,6 +46,7 @@ SHARED_OWNER_PROVIDERS = {
"ddg_search": ("DDG Search", ddg_search_tools),
"image_search": ("DDG image search", image_search_tools),
"serper": ("Serper", serper_tools),
"serply": ("Serply", serply_tools),
"fastcrw": ("fastCRW", fastcrw_tools),
"firecrawl": ("Firecrawl", firecrawl_tools),
"youcom": ("You.com", youcom_tools),
@ -62,7 +64,6 @@ DEFERRED_PROVIDERS = {
"exa",
"groundroute",
"searxng",
"serply",
"sofya",
"tencent_wsa",
}

View File

@ -103,25 +103,27 @@ class TestGetApiKey:
patcher.stop()
class TestCoerceMaxResults:
class TestCountForRequest:
def test_returns_value_when_valid(self):
from deerflow.community.serply.tools import _coerce_max_results
from deerflow.community.serply.tools import _count_for_request
assert _coerce_max_results(3) == 3
assert _coerce_max_results("7") == 7
assert _count_for_request(3) == 3
assert _count_for_request("7") == 7
def test_caps_at_serply_maximum(self):
from deerflow.community.serply.tools import _coerce_max_results
from deerflow.community.serply.tools import _count_for_request
assert _coerce_max_results(999) == 100
assert _count_for_request(999) == 100
def test_invalid_values_fall_back_to_default(self):
from deerflow.community.serply.tools import _coerce_max_results
@pytest.mark.parametrize(
"raw",
["oops", None, 0, -3, True, False, 3.5, float("inf")],
ids=["invalid-string", "none", "zero", "negative", "true", "false", "non-integral-float", "yaml-inf"],
)
def test_invalid_values_fall_back_to_default(self, raw):
from deerflow.community.serply.tools import _count_for_request
assert _coerce_max_results("oops") == 5
assert _coerce_max_results(None) == 5
assert _coerce_max_results(0) == 5
assert _coerce_max_results(-3) == 5
assert _count_for_request(raw) == 5
class TestCoerceVertical:
@ -376,10 +378,3 @@ class TestWebSearchTool:
assert result["total_results"] == 1
assert mock_get.call_args.kwargs["headers"]["X-Api-Key"] == "env-key"
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.serply.tools as serply_mod
assert serply_mod._coerce_max_results(float("inf")) == serply_mod._DEFAULT_MAX_RESULTS