mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
fix(browser): tighten Lightpanda fallback edge cases
This commit is contained in:
parent
68162eb18f
commit
629d8b843d
2 changed files with 129 additions and 12 deletions
|
|
@ -250,6 +250,36 @@ class TestConfigIntegration:
|
|||
assert entry["advanced"] is True
|
||||
|
||||
|
||||
|
||||
|
||||
class TestLightpandaRequirements:
|
||||
"""Lightpanda should expose browser tools without local Chromium."""
|
||||
|
||||
def test_lightpanda_local_mode_does_not_require_chromium(self):
|
||||
import tools.browser_tool as bt
|
||||
|
||||
with patch("tools.browser_tool._is_camofox_mode", return_value=False), \
|
||||
patch("tools.browser_tool._get_cdp_override", return_value=""), \
|
||||
patch("tools.browser_tool._find_agent_browser", return_value="/usr/bin/agent-browser"), \
|
||||
patch("tools.browser_tool._requires_real_termux_browser_install", return_value=False), \
|
||||
patch("tools.browser_tool._get_cloud_provider", return_value=None), \
|
||||
patch("tools.browser_tool._get_browser_engine", return_value="lightpanda"), \
|
||||
patch("tools.browser_tool._chromium_installed", return_value=False):
|
||||
assert bt.check_browser_requirements() is True
|
||||
|
||||
def test_chrome_local_mode_still_requires_chromium(self):
|
||||
import tools.browser_tool as bt
|
||||
|
||||
with patch("tools.browser_tool._is_camofox_mode", return_value=False), \
|
||||
patch("tools.browser_tool._get_cdp_override", return_value=""), \
|
||||
patch("tools.browser_tool._find_agent_browser", return_value="/usr/bin/agent-browser"), \
|
||||
patch("tools.browser_tool._requires_real_termux_browser_install", return_value=False), \
|
||||
patch("tools.browser_tool._get_cloud_provider", return_value=None), \
|
||||
patch("tools.browser_tool._get_browser_engine", return_value="auto"), \
|
||||
patch("tools.browser_tool._chromium_installed", return_value=False):
|
||||
assert bt.check_browser_requirements() is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# cleanup_all_browsers resets engine cache
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -407,6 +437,57 @@ class TestLightpandaFallbackWarning:
|
|||
assert "images" not in captured_kwargs
|
||||
assert captured_kwargs["task"] == "vision"
|
||||
|
||||
|
||||
def test_browser_get_images_preserves_fallback_warning(self):
|
||||
import json
|
||||
import tools.browser_tool as bt
|
||||
|
||||
result = bt._annotate_lightpanda_fallback(
|
||||
{"success": True, "data": {"result": "[]"}},
|
||||
"Lightpanda 'eval' failed (timeout); retried with Chrome.",
|
||||
)
|
||||
bt._last_active_session_key["warn-images"] = "warn-images"
|
||||
with patch("tools.browser_tool._run_browser_command", return_value=result):
|
||||
response = json.loads(bt.browser_get_images(task_id="warn-images"))
|
||||
|
||||
assert response["success"] is True
|
||||
assert response["browser_engine"] == "chrome"
|
||||
assert "Lightpanda fallback" in response["fallback_warning"]
|
||||
bt._last_active_session_key.pop("warn-images", None)
|
||||
|
||||
def test_browser_vision_lightpanda_response_has_structured_fallback(self, tmp_path):
|
||||
import json
|
||||
import tools.browser_tool as bt
|
||||
|
||||
chrome_shot = tmp_path / "chrome-structured.png"
|
||||
chrome_shot.write_bytes(b"\x89PNG" + b"0" * 128)
|
||||
|
||||
class _Msg:
|
||||
content = "Example Domain screenshot"
|
||||
|
||||
class _Choice:
|
||||
message = _Msg()
|
||||
|
||||
class _Response:
|
||||
choices = [_Choice()]
|
||||
|
||||
with patch("tools.browser_tool._get_browser_engine", return_value="lightpanda"), \
|
||||
patch("tools.browser_tool._should_inject_engine", return_value=True), \
|
||||
patch("tools.browser_tool._chrome_fallback_screenshot", return_value={
|
||||
"success": True, "data": {"path": str(chrome_shot)}
|
||||
}), \
|
||||
patch("hermes_constants.get_hermes_dir", return_value=tmp_path), \
|
||||
patch("tools.browser_tool.call_llm", return_value=_Response()):
|
||||
response = json.loads(bt.browser_vision("what is this?", task_id="vision-structured"))
|
||||
|
||||
assert response["success"] is True
|
||||
assert response["browser_engine"] == "chrome"
|
||||
assert response["browser_engine_fallback"] == {
|
||||
"from": "lightpanda",
|
||||
"to": "chrome",
|
||||
"reason": "Lightpanda has no graphical renderer for screenshots; used Chrome for vision capture.",
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _engine_override parameter
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -555,6 +555,11 @@ def _should_inject_engine(engine: str) -> bool:
|
|||
return _is_local_mode()
|
||||
|
||||
|
||||
def _using_lightpanda_engine() -> bool:
|
||||
"""Return True when local browser commands are configured for Lightpanda."""
|
||||
return _get_browser_engine() == "lightpanda"
|
||||
|
||||
|
||||
def _lightpanda_fallback_reason(engine: str, command: str, result: Dict[str, Any]) -> Optional[str]:
|
||||
"""Return the user-visible reason a Lightpanda result needs Chrome fallback.
|
||||
|
||||
|
|
@ -684,6 +689,21 @@ def _run_chrome_fallback_command(
|
|||
except FileNotFoundError as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
if not _chromium_installed():
|
||||
if _running_in_docker():
|
||||
hint = (
|
||||
"Chrome fallback requires Chromium, but it is missing. "
|
||||
"You're running in Docker — pull the latest image: "
|
||||
"docker pull ghcr.io/nousresearch/hermes-agent:latest"
|
||||
)
|
||||
else:
|
||||
hint = (
|
||||
"Chrome fallback requires Chromium, but it is missing. Install it with: "
|
||||
"npx agent-browser install --with-deps "
|
||||
"(or: npx playwright install --with-deps chromium)"
|
||||
)
|
||||
return {"success": False, "error": hint}
|
||||
|
||||
cmd_prefix = ["npx", "agent-browser"] if browser_cmd == "npx agent-browser" else [browser_cmd]
|
||||
base_args = cmd_prefix + ["--engine", "chrome", "--session", tmp_session, "--json"]
|
||||
|
||||
|
|
@ -2686,23 +2706,26 @@ def browser_get_images(task_id: Optional[str] = None) -> str:
|
|||
else:
|
||||
images = raw_result
|
||||
|
||||
return json.dumps({
|
||||
response = {
|
||||
"success": True,
|
||||
"images": images,
|
||||
"count": len(images)
|
||||
}, ensure_ascii=False)
|
||||
}
|
||||
return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
|
||||
except json.JSONDecodeError:
|
||||
return json.dumps({
|
||||
response = {
|
||||
"success": True,
|
||||
"images": [],
|
||||
"count": 0,
|
||||
"warning": "Could not parse image data"
|
||||
}, ensure_ascii=False)
|
||||
}
|
||||
return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
|
||||
else:
|
||||
return json.dumps({
|
||||
response = {
|
||||
"success": False,
|
||||
"error": result.get("error", "Failed to get images")
|
||||
}, ensure_ascii=False)
|
||||
}
|
||||
return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
|
||||
|
||||
|
||||
def browser_vision(question: str, annotate: bool = False, task_id: Optional[str] = None) -> str:
|
||||
|
|
@ -2768,9 +2791,9 @@ def browser_vision(question: str, annotate: bool = False, task_id: Optional[str]
|
|||
screenshot_path = persistent_path
|
||||
else:
|
||||
logger.warning("Lightpanda Chrome fallback vision screenshot failed: %s", fb_result.get("error"))
|
||||
# Fall through to normal path as last resort. Mark that we already
|
||||
# tried Chrome so _run_browser_command doesn't recursively fallback.
|
||||
_lp_prerouted = True
|
||||
# Fall through to the normal screenshot path so _run_browser_command
|
||||
# can still produce the standard fallback metadata/error.
|
||||
_lp_prerouted = False
|
||||
|
||||
try:
|
||||
screenshots_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
|
@ -2793,6 +2816,11 @@ def browser_vision(question: str, annotate: bool = False, task_id: Optional[str]
|
|||
},
|
||||
"fallback_warning": _lp_fallback_warning,
|
||||
"browser_engine": "chrome",
|
||||
"browser_engine_fallback": {
|
||||
"from": "lightpanda",
|
||||
"to": "chrome",
|
||||
"reason": "Lightpanda has no graphical renderer for screenshots; used Chrome for vision capture.",
|
||||
},
|
||||
}
|
||||
else:
|
||||
# Take screenshot using agent-browser
|
||||
|
|
@ -3248,7 +3276,9 @@ def check_browser_requirements() -> bool:
|
|||
Check if browser tool requirements are met.
|
||||
|
||||
In **local mode** (no cloud provider configured): the ``agent-browser``
|
||||
CLI must be findable *and* a Chromium build must be installed on disk.
|
||||
CLI must be findable. Chrome/Chromium is required for the default Chrome
|
||||
engine and for fallback/screenshot paths, but not for Lightpanda-only text
|
||||
navigation/snapshot workflows.
|
||||
|
||||
In **cloud mode** (Browserbase, Browser Use, or Firecrawl): the CLI
|
||||
and the provider's required credentials must be present. The cloud
|
||||
|
|
@ -3285,8 +3315,14 @@ def check_browser_requirements() -> bool:
|
|||
if provider is not None:
|
||||
return provider.is_configured()
|
||||
|
||||
# Local mode: agent-browser needs a Chromium build on disk. Without it
|
||||
# the CLI hangs on first use until the command timeout fires.
|
||||
# Local mode with Lightpanda can provide text/navigation tools without a
|
||||
# local Chromium install. Chrome fallback, screenshots, and browser_vision
|
||||
# will still return actionable Chromium install errors if invoked.
|
||||
if _using_lightpanda_engine():
|
||||
return True
|
||||
|
||||
# Local Chrome mode: agent-browser needs a Chromium build on disk. Without
|
||||
# it the CLI hangs on first use until the command timeout fires.
|
||||
if not _chromium_installed():
|
||||
return False
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue