mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-25 22:16:19 +00:00
fix(browserless): read web_fetch wait timeouts like web_capture (#5702)
web_fetch_tool parsed `wait_for_timeout_ms` with a bare int(), so any value that is not already an int raised ValueError and aborted the whole tool: a quoted number in YAML, or a typo like `2s`, turned every fetch into "Error: invalid literal for int() with base 10". web_capture_tool reads the same documented key through the tolerant `_as_int` helper two functions above, so the two tools disagreed about the same config key. While aligning them, web_fetch_tool now also reads `wait_for_selector_timeout_ms` instead of hardcoding 5000, so a `wait_for_selector` on a slow page can be given more than five seconds -- web_capture_tool already honours the key and the client has always accepted it. Co-authored-by: RXQ6 <RXQ6@users.noreply.github.com>
This commit is contained in:
parent
53352287a7
commit
6102179c67
@ -253,9 +253,9 @@ async def web_fetch_tool(url: str) -> str:
|
|||||||
reject_request_pattern: list[str] | None = None
|
reject_request_pattern: list[str] | None = None
|
||||||
|
|
||||||
wait_for_event = cfg.get("wait_for_event", wait_for_event)
|
wait_for_event = cfg.get("wait_for_event", wait_for_event)
|
||||||
raw_wait = cfg.get("wait_for_timeout_ms", wait_for_timeout_ms)
|
wait_for_timeout_ms = _as_int(cfg.get("wait_for_timeout_ms"), wait_for_timeout_ms)
|
||||||
wait_for_timeout_ms = int(raw_wait) if not isinstance(raw_wait, int) else raw_wait
|
|
||||||
wait_for_selector = cfg.get("wait_for_selector", wait_for_selector)
|
wait_for_selector = cfg.get("wait_for_selector", wait_for_selector)
|
||||||
|
wait_for_selector_timeout_ms = _as_int(cfg.get("wait_for_selector_timeout_ms"), wait_for_selector_timeout_ms)
|
||||||
|
|
||||||
client = _get_browserless_client("web_fetch")
|
client = _get_browserless_client("web_fetch")
|
||||||
result = await client.fetch_html_with_status(
|
result = await client.fetch_html_with_status(
|
||||||
|
|||||||
@ -482,6 +482,70 @@ class TestBrowserlessTools:
|
|||||||
assert "Error:" not in result
|
assert "Error:" not in result
|
||||||
assert "warning:" not in result
|
assert "warning:" not in result
|
||||||
|
|
||||||
|
@patch("deerflow.community.browserless.tools._get_browserless_client")
|
||||||
|
async def test_web_fetch_tool_reads_wait_timeouts_like_web_capture(self, mock_get_client):
|
||||||
|
"""web_fetch_tool resolves both wait timeouts with the tolerant int coercion.
|
||||||
|
|
||||||
|
web_capture_tool reads ``wait_for_timeout_ms`` and
|
||||||
|
``wait_for_selector_timeout_ms`` through ``_as_int``, so a loosely typed
|
||||||
|
value falls back to the default instead of raising. web_fetch_tool used a
|
||||||
|
bare ``int()`` for the first key and never read the second, so a typo'd
|
||||||
|
value made every fetch return "invalid literal for int()" and a slow
|
||||||
|
selector could not be given more than the hardcoded 5000 ms.
|
||||||
|
"""
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.fetch_html_with_status = AsyncMock(
|
||||||
|
return_value=BrowserlessFetchResult(
|
||||||
|
html="<html><body><article><h1>Title</h1></article></body></html>",
|
||||||
|
target_status_code="200",
|
||||||
|
target_status="OK",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
mock_get_client.return_value = mock_client
|
||||||
|
|
||||||
|
with patch("deerflow.community.browserless.tools._get_tool_config") as mock_cfg:
|
||||||
|
mock_cfg.return_value = {
|
||||||
|
"wait_for_timeout_ms": "2500", # quoted number: parsed, not rejected
|
||||||
|
"wait_for_selector": "article",
|
||||||
|
"wait_for_selector_timeout_ms": 9000,
|
||||||
|
}
|
||||||
|
with patch(
|
||||||
|
"deerflow.community.browserless.tools._resolve_host_addresses",
|
||||||
|
return_value=[ipaddress.ip_address("93.184.216.34")],
|
||||||
|
):
|
||||||
|
result = await tools.web_fetch_tool.ainvoke("https://example.com/article")
|
||||||
|
|
||||||
|
assert "Error:" not in result
|
||||||
|
kwargs = mock_client.fetch_html_with_status.call_args.kwargs
|
||||||
|
assert kwargs["wait_for_timeout_ms"] == 2500
|
||||||
|
assert kwargs["wait_for_selector_timeout_ms"] == 9000
|
||||||
|
|
||||||
|
@patch("deerflow.community.browserless.tools._get_browserless_client")
|
||||||
|
async def test_web_fetch_tool_bad_wait_timeout_falls_back_to_default(self, mock_get_client):
|
||||||
|
"""A non-numeric wait timeout must not fail the whole fetch."""
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.fetch_html_with_status = AsyncMock(
|
||||||
|
return_value=BrowserlessFetchResult(
|
||||||
|
html="<html><body><article><h1>Title</h1></article></body></html>",
|
||||||
|
target_status_code="200",
|
||||||
|
target_status="OK",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
mock_get_client.return_value = mock_client
|
||||||
|
|
||||||
|
with patch("deerflow.community.browserless.tools._get_tool_config") as mock_cfg:
|
||||||
|
mock_cfg.return_value = {"wait_for_timeout_ms": "2s"}
|
||||||
|
with patch(
|
||||||
|
"deerflow.community.browserless.tools._resolve_host_addresses",
|
||||||
|
return_value=[ipaddress.ip_address("93.184.216.34")],
|
||||||
|
):
|
||||||
|
result = await tools.web_fetch_tool.ainvoke("https://example.com/article")
|
||||||
|
|
||||||
|
assert "Error:" not in result
|
||||||
|
kwargs = mock_client.fetch_html_with_status.call_args.kwargs
|
||||||
|
assert kwargs["wait_for_timeout_ms"] == 0
|
||||||
|
assert kwargs["wait_for_selector_timeout_ms"] == 5000
|
||||||
|
|
||||||
async def test_web_fetch_and_web_capture_tools_agree_on_target_error_warning(self, tmp_path):
|
async def test_web_fetch_and_web_capture_tools_agree_on_target_error_warning(self, tmp_path):
|
||||||
"""web_fetch_tool and web_capture_tool surface the identical warning for identical target-error headers.
|
"""web_fetch_tool and web_capture_tool surface the identical warning for identical target-error headers.
|
||||||
|
|
||||||
|
|||||||
@ -991,6 +991,7 @@ tools:
|
|||||||
# # wait_for_event: "networkidle" # Wait for a page event before returning (e.g. "load", "networkidle")
|
# # wait_for_event: "networkidle" # Wait for a page event before returning (e.g. "load", "networkidle")
|
||||||
# # wait_for_timeout_ms: 2000 # Extra wait after page load in milliseconds
|
# # wait_for_timeout_ms: 2000 # Extra wait after page load in milliseconds
|
||||||
# # wait_for_selector: "article" # CSS selector to wait for before returning
|
# # wait_for_selector: "article" # CSS selector to wait for before returning
|
||||||
|
# # wait_for_selector_timeout_ms: 5000 # How long to wait for that selector, in milliseconds
|
||||||
|
|
||||||
# Web fetch tool (uses Crawl4AI - self-hosted headless Chromium, no third-party API key)
|
# Web fetch tool (uses Crawl4AI - self-hosted headless Chromium, no third-party API key)
|
||||||
# Crawl4AI returns server-cleaned "fit" markdown directly (no readability step needed),
|
# Crawl4AI returns server-cleaned "fit" markdown directly (no readability step needed),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user