fix(browser): tighten Lightpanda fallback edge cases

This commit is contained in:
Kshitij Kapoor 2026-05-06 16:08:20 +05:30 committed by kshitij
parent 68162eb18f
commit 629d8b843d
2 changed files with 129 additions and 12 deletions

View file

@ -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
# ---------------------------------------------------------------------------

View file

@ -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