refactor(browser): table-driven registry.register loop for the 10 browser tools; bracket-hug compaction across browser_* modules (AST-identical)
This commit is contained in:
@@ -25,8 +25,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
LIGHTPANDA_INSTALL_URL = "https://lightpanda.io/docs/run-locally/installation/one-liner"
|
||||
LIGHTPANDA_INSTALL_HINT = (
|
||||
f"Install Lightpanda from {LIGHTPANDA_INSTALL_URL} and make sure "
|
||||
"`lightpanda` is on PATH"
|
||||
f"Install Lightpanda from {LIGHTPANDA_INSTALL_URL} and make sure " "`lightpanda` is on PATH"
|
||||
)
|
||||
|
||||
_READY_TIMEOUT_S = 10.0
|
||||
@@ -57,8 +56,7 @@ class LightpandaServer:
|
||||
def _home_candidates() -> list:
|
||||
home = Path.home()
|
||||
candidates = [
|
||||
home / ".lightpanda" / "lightpanda",
|
||||
home / ".local" / "bin" / "lightpanda",
|
||||
home / ".lightpanda" / "lightpanda", home / ".local" / "bin" / "lightpanda"
|
||||
]
|
||||
try:
|
||||
from hermes_constants import get_hermes_home
|
||||
@@ -285,8 +283,7 @@ def launch_lightpanda(
|
||||
with _servers_lock:
|
||||
_servers[session_name] = server
|
||||
logger.info(
|
||||
"Started lightpanda serve (pid %s, port %s) for session %s",
|
||||
proc.pid, port, session_name,
|
||||
"Started lightpanda serve (pid %s, port %s) for session %s", proc.pid, port, session_name
|
||||
)
|
||||
return server, None
|
||||
|
||||
|
||||
@@ -555,8 +555,7 @@ class CDPSupervisor(DialogSupervisionMixin, FrameTrackingMixin):
|
||||
# CDP event → handler(self, params, session_id). Async handlers return an
|
||||
# awaitable that ``_read_loop`` awaits; sync handlers return None.
|
||||
_EVENT_HANDLERS: Dict[str, Callable[..., Any]] = {
|
||||
**DialogSupervisionMixin.EVENT_HANDLERS,
|
||||
**FrameTrackingMixin.EVENT_HANDLERS,
|
||||
**DialogSupervisionMixin.EVENT_HANDLERS, **FrameTrackingMixin.EVENT_HANDLERS
|
||||
}
|
||||
|
||||
|
||||
|
||||
+81
-210
@@ -722,8 +722,7 @@ class _BrowserSessionBackend:
|
||||
if reason is None:
|
||||
return True
|
||||
logger.info(
|
||||
"Recycling suspect browser session %s before reuse (%s)",
|
||||
self._session_key, reason,
|
||||
"Recycling suspect browser session %s before reuse (%s)", self._session_key, reason
|
||||
)
|
||||
try:
|
||||
_cleanup_single_browser_session(self._session_key)
|
||||
@@ -852,8 +851,7 @@ BROWSER_TOOL_SCHEMAS = [
|
||||
"description": "The element reference from the snapshot (e.g., '@e3')"
|
||||
},
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "The text to type into the field"
|
||||
"type": "string", "description": "The text to type into the field"
|
||||
}
|
||||
},
|
||||
"required": ["ref", "text"]
|
||||
@@ -866,9 +864,7 @@ BROWSER_TOOL_SCHEMAS = [
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"direction": {
|
||||
"type": "string",
|
||||
"enum": ["up", "down"],
|
||||
"description": "Direction to scroll"
|
||||
"type": "string", "enum": ["up", "down"], "description": "Direction to scroll"
|
||||
}
|
||||
},
|
||||
"required": ["direction"]
|
||||
@@ -878,9 +874,7 @@ BROWSER_TOOL_SCHEMAS = [
|
||||
"name": "browser_back",
|
||||
"description": "Navigate back to the previous page in browser history. Requires browser_navigate to be called first.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
"type": "object", "properties": {}, "required": []
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -901,9 +895,7 @@ BROWSER_TOOL_SCHEMAS = [
|
||||
"name": "browser_get_images",
|
||||
"description": "Get a list of all images on the current page with their URLs and alt text. Useful for finding images to analyze with the vision tool. Requires browser_navigate to be called first.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
"type": "object", "properties": {}, "required": []
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1043,8 +1035,7 @@ def _post_redirect_block(nav_session_key: str, url: str, final_url: str, auto_lo
|
||||
if _is_always_blocked_url(final_url):
|
||||
_run_browser_command(nav_session_key, "open", ["about:blank"], timeout=10)
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": "Blocked: redirect landed on a cloud metadata endpoint",
|
||||
"success": False, "error": "Blocked: redirect landed on a cloud metadata endpoint"
|
||||
})
|
||||
if (
|
||||
not _is_local_backend()
|
||||
@@ -1054,8 +1045,7 @@ def _post_redirect_block(nav_session_key: str, url: str, final_url: str, auto_lo
|
||||
):
|
||||
_run_browser_command(nav_session_key, "open", ["about:blank"], timeout=10)
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": "Blocked: redirect landed on a private/internal address",
|
||||
"success": False, "error": "Blocked: redirect landed on a private/internal address"
|
||||
})
|
||||
return None
|
||||
|
||||
@@ -1125,16 +1115,12 @@ def browser_navigate(url: str, task_id: Optional[str] = None) -> str:
|
||||
_maybe_start_recording(nav_session_key)
|
||||
|
||||
result = _run_browser_command(
|
||||
nav_session_key,
|
||||
"open",
|
||||
[url],
|
||||
timeout=_get_open_command_timeout(first_open=is_first_nav),
|
||||
nav_session_key, "open", [url], timeout=_get_open_command_timeout(first_open=is_first_nav)
|
||||
)
|
||||
|
||||
if not result.get("success"):
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": result.get("error", "Navigation failed")
|
||||
"success": False, "error": result.get("error", "Navigation failed")
|
||||
}, ensure_ascii=False)
|
||||
|
||||
data = result.get("data", {})
|
||||
@@ -1146,9 +1132,7 @@ def browser_navigate(url: str, task_id: Optional[str] = None) -> str:
|
||||
return blocked
|
||||
|
||||
response = {
|
||||
"success": True,
|
||||
"url": final_url,
|
||||
"title": title
|
||||
"success": True, "url": final_url, "title": title
|
||||
}
|
||||
# Auditability: stamp navigations that ran on the user's real-profile
|
||||
# copy-browser so usage is visible in the tool result.
|
||||
@@ -1188,9 +1172,7 @@ def browser_navigate(url: str, task_id: Optional[str] = None) -> str:
|
||||
|
||||
|
||||
def browser_snapshot(
|
||||
full: bool = False,
|
||||
task_id: Optional[str] = None,
|
||||
user_task: Optional[str] = None
|
||||
full: bool = False, task_id: Optional[str] = None, user_task: Optional[str] = None
|
||||
) -> str:
|
||||
"""Text snapshot of the page's accessibility tree (compact unless ``full``).
|
||||
|
||||
@@ -1252,8 +1234,7 @@ def browser_snapshot(
|
||||
return json.dumps(response, ensure_ascii=False)
|
||||
else:
|
||||
response = {
|
||||
"success": False,
|
||||
"error": result.get("error", "Failed to get snapshot")
|
||||
"success": False, "error": result.get("error", "Failed to get snapshot")
|
||||
}
|
||||
return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
|
||||
|
||||
@@ -1306,8 +1287,7 @@ def browser_type(ref: str, text: str, task_id: Optional[str] = None) -> str:
|
||||
result = _run_browser_command(effective_task_id, "fill", [ref, text])
|
||||
|
||||
from agent.display import (
|
||||
redact_browser_typed_text_for_display,
|
||||
redact_tool_args_for_display,
|
||||
redact_browser_typed_text_for_display, redact_tool_args_for_display
|
||||
)
|
||||
|
||||
# Typed text goes through the secret-pattern redactor so API keys / tokens
|
||||
@@ -1327,8 +1307,7 @@ def browser_scroll(direction: str, task_id: Optional[str] = None) -> str:
|
||||
"""Scroll the page ``direction`` ("up"/"down") by about half a viewport."""
|
||||
if direction not in {"up", "down"}:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": f"Invalid direction '{direction}'. Use 'up' or 'down'."
|
||||
"success": False, "error": f"Invalid direction '{direction}'. Use 'up' or 'down'."
|
||||
}, ensure_ascii=False)
|
||||
|
||||
# Single scroll with a pixel amount (~half a viewport) instead of 5x subprocess calls.
|
||||
@@ -1472,8 +1451,7 @@ def browser_console(clear: bool = False, expression: Optional[str] = None, task_
|
||||
if errors_result.get("success"):
|
||||
for err in errors_result.get("data", {}).get("errors", []):
|
||||
errors.append({
|
||||
"message": _redact_browser_output(err.get("message", "")),
|
||||
"source": "exception",
|
||||
"message": _redact_browser_output(err.get("message", "")), "source": "exception"
|
||||
})
|
||||
|
||||
response = {
|
||||
@@ -1552,8 +1530,7 @@ def _eval_supervisor_fast_path(effective_task_id: str, expression: str) -> Optio
|
||||
if "supervisor" not in err.lower():
|
||||
return json.dumps({"success": False, "error": err}, ensure_ascii=False)
|
||||
logger.debug(
|
||||
"browser_eval: supervisor path unavailable (%s), falling back to subprocess",
|
||||
err,
|
||||
"browser_eval: supervisor path unavailable (%s), falling back to subprocess", err
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
@@ -1758,23 +1735,17 @@ def browser_get_images(task_id: Optional[str] = None) -> str:
|
||||
images = raw_result
|
||||
|
||||
response = {
|
||||
"success": True,
|
||||
"images": _redact_browser_output(images),
|
||||
"count": len(images)
|
||||
"success": True, "images": _redact_browser_output(images), "count": len(images)
|
||||
}
|
||||
return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
|
||||
except json.JSONDecodeError:
|
||||
response = {
|
||||
"success": True,
|
||||
"images": [],
|
||||
"count": 0,
|
||||
"warning": "Could not parse image data"
|
||||
"success": True, "images": [], "count": 0, "warning": "Could not parse image data"
|
||||
}
|
||||
return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
|
||||
else:
|
||||
response = {
|
||||
"success": False,
|
||||
"error": result.get("error", "Failed to get images")
|
||||
"success": False, "error": result.get("error", "Failed to get images")
|
||||
}
|
||||
return json.dumps(_copy_fallback_warning(response, result), ensure_ascii=False)
|
||||
|
||||
@@ -1976,8 +1947,7 @@ if __name__ == "__main__":
|
||||
# ---------------------------------------------------------------------------
|
||||
from tools.registry import registry, tool_error
|
||||
from tools.browser_extension_router import (
|
||||
extension_controller_available,
|
||||
routed_browser_handler,
|
||||
extension_controller_available, routed_browser_handler
|
||||
)
|
||||
|
||||
_BROWSER_SCHEMA_MAP = {s["name"]: s for s in BROWSER_TOOL_SCHEMAS}
|
||||
@@ -1986,8 +1956,7 @@ _BROWSER_SCHEMA_MAP = {s["name"]: s for s in BROWSER_TOOL_SCHEMAS}
|
||||
def _browser_router_kw(kw: dict) -> dict:
|
||||
"""Identity kwargs forwarded to the extension router wrapper."""
|
||||
return {
|
||||
"task_id": kw.get("task_id"),
|
||||
"session_id": kw.get("session_id"),
|
||||
"task_id": kw.get("task_id"), "session_id": kw.get("session_id")
|
||||
}
|
||||
|
||||
|
||||
@@ -1996,163 +1965,65 @@ def check_browser_routed_requirements(action: str = "browser_snapshot") -> bool:
|
||||
return check_browser_requirements() or extension_controller_available(action)
|
||||
|
||||
|
||||
def check_browser_navigate_requirements() -> bool:
|
||||
return check_browser_routed_requirements("browser_navigate")
|
||||
|
||||
|
||||
def check_browser_snapshot_requirements() -> bool:
|
||||
return check_browser_routed_requirements("browser_snapshot")
|
||||
|
||||
|
||||
def check_browser_click_requirements() -> bool:
|
||||
return check_browser_routed_requirements("browser_click")
|
||||
|
||||
|
||||
def check_browser_type_requirements() -> bool:
|
||||
return check_browser_routed_requirements("browser_type")
|
||||
|
||||
|
||||
def check_browser_scroll_requirements() -> bool:
|
||||
return check_browser_routed_requirements("browser_scroll")
|
||||
|
||||
|
||||
def check_browser_back_requirements() -> bool:
|
||||
return check_browser_routed_requirements("browser_back")
|
||||
|
||||
|
||||
def check_browser_press_requirements() -> bool:
|
||||
return check_browser_routed_requirements("browser_press")
|
||||
|
||||
|
||||
registry.register(
|
||||
name="browser_navigate",
|
||||
toolset="browser",
|
||||
schema=_BROWSER_SCHEMA_MAP["browser_navigate"],
|
||||
handler=lambda args, **kw: routed_browser_handler(
|
||||
"browser_navigate",
|
||||
args,
|
||||
fallback=lambda: browser_navigate(url=args.get("url", ""), task_id=kw.get("task_id")),
|
||||
**_browser_router_kw(kw),
|
||||
),
|
||||
check_fn=check_browser_navigate_requirements,
|
||||
emoji="🌐",
|
||||
)
|
||||
registry.register(
|
||||
name="browser_snapshot",
|
||||
toolset="browser",
|
||||
schema=_BROWSER_SCHEMA_MAP["browser_snapshot"],
|
||||
handler=lambda args, **kw: routed_browser_handler(
|
||||
"browser_snapshot",
|
||||
args,
|
||||
fallback=lambda: browser_snapshot(
|
||||
full=args.get("full", False), task_id=kw.get("task_id"), user_task=kw.get("user_task")),
|
||||
**_browser_router_kw(kw),
|
||||
),
|
||||
check_fn=check_browser_snapshot_requirements,
|
||||
emoji="📸",
|
||||
)
|
||||
registry.register(
|
||||
name="browser_click",
|
||||
toolset="browser",
|
||||
schema=_BROWSER_SCHEMA_MAP["browser_click"],
|
||||
handler=lambda args, **kw: routed_browser_handler(
|
||||
"browser_click",
|
||||
args,
|
||||
fallback=lambda: browser_click(ref=args.get("ref", ""), task_id=kw.get("task_id")),
|
||||
**_browser_router_kw(kw),
|
||||
),
|
||||
check_fn=check_browser_click_requirements,
|
||||
emoji="👆",
|
||||
)
|
||||
registry.register(
|
||||
name="browser_type",
|
||||
toolset="browser",
|
||||
schema=_BROWSER_SCHEMA_MAP["browser_type"],
|
||||
handler=lambda args, **kw: routed_browser_handler(
|
||||
"browser_type",
|
||||
args,
|
||||
fallback=lambda: browser_type(ref=args.get("ref", ""), text=args.get("text", ""), task_id=kw.get("task_id")),
|
||||
**_browser_router_kw(kw),
|
||||
),
|
||||
check_fn=check_browser_type_requirements,
|
||||
emoji="⌨️",
|
||||
)
|
||||
registry.register(
|
||||
name="browser_scroll",
|
||||
toolset="browser",
|
||||
schema=_BROWSER_SCHEMA_MAP["browser_scroll"],
|
||||
handler=lambda args, **kw: routed_browser_handler(
|
||||
"browser_scroll",
|
||||
args,
|
||||
fallback=lambda: browser_scroll(direction=args.get("direction", "down"), task_id=kw.get("task_id")),
|
||||
**_browser_router_kw(kw),
|
||||
),
|
||||
check_fn=check_browser_scroll_requirements,
|
||||
emoji="📜",
|
||||
)
|
||||
registry.register(
|
||||
name="browser_back",
|
||||
toolset="browser",
|
||||
schema=_BROWSER_SCHEMA_MAP["browser_back"],
|
||||
handler=lambda args, **kw: routed_browser_handler(
|
||||
"browser_back",
|
||||
args,
|
||||
fallback=lambda: browser_back(task_id=kw.get("task_id")),
|
||||
**_browser_router_kw(kw),
|
||||
),
|
||||
check_fn=check_browser_back_requirements,
|
||||
emoji="◀️",
|
||||
)
|
||||
registry.register(
|
||||
name="browser_press",
|
||||
toolset="browser",
|
||||
schema=_BROWSER_SCHEMA_MAP["browser_press"],
|
||||
handler=lambda args, **kw: routed_browser_handler(
|
||||
"browser_press",
|
||||
args,
|
||||
fallback=lambda: browser_press(key=args.get("key", ""), task_id=kw.get("task_id")),
|
||||
**_browser_router_kw(kw),
|
||||
),
|
||||
check_fn=check_browser_press_requirements,
|
||||
emoji="⌨️",
|
||||
# (tool name, emoji, availability gate, fallback call) — the routed-through-
|
||||
# extension tools use the per-action gate; get_images/console/vision keep the
|
||||
# plain requirement checks. ``_fallback`` receives (args, kw).
|
||||
_BROWSER_TOOL_TABLE = (
|
||||
("browser_navigate", "🌐", None,
|
||||
lambda args, kw: browser_navigate(url=args.get("url", ""), task_id=kw.get("task_id"))),
|
||||
("browser_snapshot", "📸", None,
|
||||
lambda args, kw: browser_snapshot(
|
||||
full=args.get("full", False), task_id=kw.get("task_id"), user_task=kw.get("user_task"))),
|
||||
("browser_click", "👆", None,
|
||||
lambda args, kw: browser_click(ref=args.get("ref", ""), task_id=kw.get("task_id"))),
|
||||
("browser_type", "⌨️", None,
|
||||
lambda args, kw: browser_type(ref=args.get("ref", ""), text=args.get("text", ""), task_id=kw.get("task_id"))),
|
||||
("browser_scroll", "📜", None,
|
||||
lambda args, kw: browser_scroll(direction=args.get("direction", "down"), task_id=kw.get("task_id"))),
|
||||
("browser_back", "◀️", None,
|
||||
lambda args, kw: browser_back(task_id=kw.get("task_id"))),
|
||||
("browser_press", "⌨️", None,
|
||||
lambda args, kw: browser_press(key=args.get("key", ""), task_id=kw.get("task_id"))),
|
||||
("browser_get_images", "🖼️", check_browser_requirements,
|
||||
lambda args, kw: browser_get_images(task_id=kw.get("task_id"))),
|
||||
("browser_vision", "👁️", check_browser_vision_requirements,
|
||||
lambda args, kw: browser_vision(
|
||||
question=args.get("question", ""), annotate=args.get("annotate", False), task_id=kw.get("task_id"))),
|
||||
("browser_console", "🖥️", check_browser_requirements,
|
||||
lambda args, kw: browser_console(
|
||||
clear=args.get("clear", False), expression=args.get("expression"), task_id=kw.get("task_id"))),
|
||||
)
|
||||
|
||||
registry.register(
|
||||
name="browser_get_images",
|
||||
toolset="browser",
|
||||
schema=_BROWSER_SCHEMA_MAP["browser_get_images"],
|
||||
handler=lambda args, **kw: routed_browser_handler(
|
||||
"browser_get_images",
|
||||
args,
|
||||
fallback=lambda: browser_get_images(task_id=kw.get("task_id")),
|
||||
**_browser_router_kw(kw),
|
||||
),
|
||||
check_fn=check_browser_requirements,
|
||||
emoji="🖼️",
|
||||
)
|
||||
registry.register(
|
||||
name="browser_vision",
|
||||
toolset="browser",
|
||||
schema=_BROWSER_SCHEMA_MAP["browser_vision"],
|
||||
handler=lambda args, **kw: routed_browser_handler(
|
||||
"browser_vision",
|
||||
args,
|
||||
fallback=lambda: browser_vision(question=args.get("question", ""), annotate=args.get("annotate", False), task_id=kw.get("task_id")),
|
||||
**_browser_router_kw(kw),
|
||||
),
|
||||
check_fn=check_browser_vision_requirements,
|
||||
emoji="👁️",
|
||||
)
|
||||
registry.register(
|
||||
name="browser_console",
|
||||
toolset="browser",
|
||||
schema=_BROWSER_SCHEMA_MAP["browser_console"],
|
||||
handler=lambda args, **kw: routed_browser_handler(
|
||||
"browser_console",
|
||||
args,
|
||||
fallback=lambda: browser_console(clear=args.get("clear", False), expression=args.get("expression"), task_id=kw.get("task_id")),
|
||||
**_browser_router_kw(kw),
|
||||
),
|
||||
check_fn=check_browser_requirements,
|
||||
emoji="🖥️",
|
||||
)
|
||||
|
||||
def _routed_check_fn(name: str):
|
||||
"""Per-action availability gate (a named function, as the registry expects)."""
|
||||
def check() -> bool:
|
||||
return check_browser_routed_requirements(name)
|
||||
check.__name__ = check.__qualname__ = f"check_{name}_requirements"
|
||||
return check
|
||||
|
||||
|
||||
def _routed_handler(name: str, fallback):
|
||||
def handler(args, **kw):
|
||||
return routed_browser_handler(name, args, fallback=lambda: fallback(args, kw), **_browser_router_kw(kw))
|
||||
return handler
|
||||
|
||||
|
||||
# Legacy per-tool gate names (tests + external callers).
|
||||
check_browser_navigate_requirements = _routed_check_fn("browser_navigate")
|
||||
check_browser_snapshot_requirements = _routed_check_fn("browser_snapshot")
|
||||
check_browser_click_requirements = _routed_check_fn("browser_click")
|
||||
check_browser_type_requirements = _routed_check_fn("browser_type")
|
||||
check_browser_scroll_requirements = _routed_check_fn("browser_scroll")
|
||||
check_browser_back_requirements = _routed_check_fn("browser_back")
|
||||
check_browser_press_requirements = _routed_check_fn("browser_press")
|
||||
|
||||
for _name, _emoji, _check_fn, _fallback in _BROWSER_TOOL_TABLE:
|
||||
registry.register(
|
||||
name=_name,
|
||||
toolset="browser",
|
||||
schema=_BROWSER_SCHEMA_MAP[_name],
|
||||
handler=_routed_handler(_name, _fallback),
|
||||
check_fn=_check_fn or globals()[f"check_{_name}_requirements"],
|
||||
emoji=_emoji,
|
||||
)
|
||||
|
||||
@@ -116,9 +116,7 @@ def _get_dialog_policy_config() -> Tuple[str, float]:
|
||||
# Defer imports so browser_tool can be imported in minimal environments.
|
||||
_bt = _origin()
|
||||
from tools.browser_supervisor import (
|
||||
DEFAULT_DIALOG_POLICY,
|
||||
DEFAULT_DIALOG_TIMEOUT_S,
|
||||
_VALID_POLICIES,
|
||||
DEFAULT_DIALOG_POLICY, DEFAULT_DIALOG_TIMEOUT_S, _VALID_POLICIES
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -171,16 +169,11 @@ def _ensure_cdp_supervisor(task_id: str) -> None:
|
||||
|
||||
policy, timeout_s = _bt._get_dialog_policy_config()
|
||||
SUPERVISOR_REGISTRY.get_or_start(
|
||||
task_id=task_id,
|
||||
cdp_url=cdp_url,
|
||||
dialog_policy=policy,
|
||||
dialog_timeout_s=timeout_s,
|
||||
task_id=task_id, cdp_url=cdp_url, dialog_policy=policy, dialog_timeout_s=timeout_s
|
||||
)
|
||||
except Exception as exc:
|
||||
_bt.logger.debug(
|
||||
"CDP supervisor attach for task=%s failed (non-fatal): %s",
|
||||
task_id,
|
||||
exc,
|
||||
"CDP supervisor attach for task=%s failed (non-fatal): %s", task_id, exc
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -65,13 +65,11 @@ def _current_page_private_url(effective_task_id: str) -> Optional[str]:
|
||||
_bt = _origin()
|
||||
try:
|
||||
url_result = _bt._run_browser_command(
|
||||
effective_task_id, "eval", ["window.location.href"],
|
||||
timeout=5, _engine_override="auto",
|
||||
effective_task_id, "eval", ["window.location.href"], timeout=5, _engine_override="auto"
|
||||
)
|
||||
if url_result.get("success"):
|
||||
current_url = (
|
||||
url_result.get("data", {}).get("result", "")
|
||||
.strip().strip('"').strip("'")
|
||||
url_result.get("data", {}).get("result", "") .strip().strip('"').strip("'")
|
||||
)
|
||||
if current_url and (
|
||||
_bt._is_always_blocked_url(current_url) or not _bt._is_safe_url(current_url)
|
||||
@@ -96,8 +94,7 @@ _RISKY_BROWSER_EVAL_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
|
||||
|
||||
|
||||
_JS_STRING_LITERAL_RE = re.compile(
|
||||
r"""'(?:\\.|[^'\\])*'|\"(?:\\.|[^\"\\])*\"|`(?:\\.|[^`\\])*`""",
|
||||
re.S,
|
||||
r"""'(?:\\.|[^'\\])*'|\"(?:\\.|[^\"\\])*\"|`(?:\\.|[^`\\])*`""", re.S
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -131,9 +131,7 @@ def _session_owner_scope(task_id: str):
|
||||
return
|
||||
|
||||
from agent.secret_scope import (
|
||||
build_profile_secret_scope,
|
||||
reset_secret_scope,
|
||||
set_secret_scope,
|
||||
build_profile_secret_scope, reset_secret_scope, set_secret_scope
|
||||
)
|
||||
from hermes_cli.env_loader import hydrate_profile_secret_sources
|
||||
|
||||
@@ -487,9 +485,7 @@ def _start_browser_cleanup_thread():
|
||||
if _bt._cleanup_thread is None or not _bt._cleanup_thread.is_alive():
|
||||
_bt._cleanup_running = True
|
||||
_bt._cleanup_thread = threading.Thread(
|
||||
target=_bt._browser_cleanup_thread_worker,
|
||||
daemon=True,
|
||||
name="browser-cleanup"
|
||||
target=_bt._browser_cleanup_thread_worker, daemon=True, name="browser-cleanup"
|
||||
)
|
||||
_bt._cleanup_thread.start()
|
||||
_bt.logger.info("Started inactivity cleanup thread (timeout: %ss)", _bt.BROWSER_SESSION_INACTIVITY_TIMEOUT)
|
||||
@@ -815,15 +811,13 @@ def _cleanup_single_browser_session(task_id: str) -> None:
|
||||
_bt.logger.warning("lightpanda stop failed for task %s: %s", task_id, e)
|
||||
elif _bt._session_has_expired(session_info):
|
||||
_bt.logger.debug(
|
||||
"Skipping agent-browser close for expired session %s",
|
||||
task_id,
|
||||
"Skipping agent-browser close for expired session %s", task_id
|
||||
)
|
||||
else:
|
||||
try:
|
||||
_bt._run_browser_command(task_id, "close", [], timeout=10)
|
||||
_bt.logger.debug(
|
||||
"agent-browser close command completed for task %s",
|
||||
task_id,
|
||||
"agent-browser close command completed for task %s", task_id
|
||||
)
|
||||
except Exception as e:
|
||||
_bt.logger.warning("agent-browser close failed for task %s: %s", task_id, e)
|
||||
|
||||
@@ -58,8 +58,7 @@ def lightpanda_engine_status() -> Tuple[bool, str]:
|
||||
if bu_mode:
|
||||
try:
|
||||
from tools.browser_use_cli import (
|
||||
_read_browser_cfg,
|
||||
is_legacy_browser_use_cloud_config,
|
||||
_read_browser_cfg, is_legacy_browser_use_cloud_config
|
||||
)
|
||||
|
||||
if is_legacy_browser_use_cloud_config(_read_browser_cfg()):
|
||||
@@ -131,16 +130,13 @@ def _needs_lightpanda_fallback(engine: str, command: str, result: Dict[str, Any]
|
||||
def _annotate_lightpanda_fallback(result: Dict[str, Any], reason: str) -> Dict[str, Any]:
|
||||
"""Add a user-visible Chrome fallback warning to a browser command result."""
|
||||
warning = (
|
||||
"⚠ Lightpanda fallback: Chrome was used for this browser action. "
|
||||
f"{reason}"
|
||||
"⚠ Lightpanda fallback: Chrome was used for this browser action. " f"{reason}"
|
||||
)
|
||||
annotated = dict(result)
|
||||
annotated["fallback_warning"] = warning
|
||||
annotated["browser_engine"] = "chrome"
|
||||
annotated["browser_engine_fallback"] = {
|
||||
"from": "lightpanda",
|
||||
"to": "chrome",
|
||||
"reason": reason,
|
||||
"from": "lightpanda", "to": "chrome", "reason": reason
|
||||
}
|
||||
data = annotated.get("data")
|
||||
if isinstance(data, dict):
|
||||
@@ -148,8 +144,7 @@ def _annotate_lightpanda_fallback(result: Dict[str, Any], reason: str) -> Dict[s
|
||||
data.setdefault("fallback_warning", warning)
|
||||
data.setdefault("browser_engine", "chrome")
|
||||
data.setdefault(
|
||||
"browser_engine_fallback",
|
||||
{"from": "lightpanda", "to": "chrome", "reason": reason},
|
||||
"browser_engine_fallback", {"from": "lightpanda", "to": "chrome", "reason": reason}
|
||||
)
|
||||
annotated["data"] = data
|
||||
return annotated
|
||||
@@ -165,10 +160,7 @@ def _copy_fallback_warning(target: Dict[str, Any], result: Dict[str, Any]) -> Di
|
||||
|
||||
|
||||
def _run_chrome_fallback_command(
|
||||
task_id: str,
|
||||
command: str,
|
||||
args: List[str],
|
||||
timeout: int,
|
||||
task_id: str, command: str, args: List[str], timeout: int
|
||||
) -> Dict[str, Any]:
|
||||
"""Run a browser command in a temporary Chrome session at the current URL.
|
||||
|
||||
@@ -259,9 +251,7 @@ def _run_chrome_fallback_command(
|
||||
|
||||
|
||||
def _chrome_fallback_screenshot(
|
||||
task_id: str,
|
||||
args: List[str],
|
||||
timeout: int,
|
||||
task_id: str, args: List[str], timeout: int
|
||||
) -> Dict[str, Any]:
|
||||
"""Take a screenshot using a temporary Chrome session."""
|
||||
_bt = _origin()
|
||||
|
||||
@@ -332,10 +332,7 @@ def _real_profile_cdp() -> tuple:
|
||||
)
|
||||
|
||||
from hermes_cli.browser_connect import (
|
||||
chromium_executable,
|
||||
detect_default_chromium,
|
||||
real_profile_copy_dir,
|
||||
snapshot_real_profile,
|
||||
chromium_executable, detect_default_chromium, real_profile_copy_dir, snapshot_real_profile
|
||||
)
|
||||
|
||||
with _bt._real_profile_cdp_lock:
|
||||
|
||||
@@ -75,10 +75,7 @@ def _unlink_command_output_files(*paths: str) -> None:
|
||||
|
||||
|
||||
def _format_browser_timeout_error(
|
||||
command: str,
|
||||
timeout: int,
|
||||
stdout: str,
|
||||
stderr: str,
|
||||
command: str, timeout: int, stdout: str, stderr: str
|
||||
) -> str:
|
||||
"""Build an actionable timeout message from captured daemon output."""
|
||||
_bt = _origin()
|
||||
@@ -260,8 +257,7 @@ def _create_lightpanda_session(task_id: str) -> Dict[str, Any]:
|
||||
if err:
|
||||
raise RuntimeError(err)
|
||||
_bt.logger.info(
|
||||
"Created Lightpanda session %s (port %s) for task %s",
|
||||
session_name, server.port, task_id,
|
||||
"Created Lightpanda session %s (port %s) for task %s", session_name, server.port, task_id
|
||||
)
|
||||
return {
|
||||
"session_name": session_name,
|
||||
@@ -402,8 +398,7 @@ def _get_session_info(task_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
return existing_session
|
||||
|
||||
_bt.logger.info(
|
||||
"Replacing expired or dead browser session for task %s",
|
||||
task_id,
|
||||
"Replacing expired or dead browser session for task %s", task_id
|
||||
)
|
||||
_bt._cleanup_single_browser_session(task_id)
|
||||
# Cleanup removes the activity entry. The replacement session must be
|
||||
@@ -454,9 +449,7 @@ def _get_session_info(task_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
|
||||
|
||||
def _discard_timed_out_browser_session(
|
||||
task_id: str,
|
||||
session_info: Dict[str, Any],
|
||||
task_socket_dir: str,
|
||||
task_id: str, session_info: Dict[str, Any], task_socket_dir: str
|
||||
) -> None:
|
||||
"""Drop a stuck client generation without losing cloud cleanup state."""
|
||||
_bt = _origin()
|
||||
@@ -540,9 +533,7 @@ def _browser_daemon_responsive(task_socket_dir: str, probe_timeout_s: float = 1.
|
||||
|
||||
|
||||
def _handle_browser_command_timeout(
|
||||
task_id: str,
|
||||
session_info: Dict[str, Any],
|
||||
task_socket_dir: str,
|
||||
task_id: str, session_info: Dict[str, Any], task_socket_dir: str
|
||||
) -> None:
|
||||
"""Recover session state after a browser command timeout.
|
||||
|
||||
@@ -631,8 +622,7 @@ def _interpret_browser_command_output(command: str, stdout: str, stderr: str, re
|
||||
recovered_path = _bt._extract_screenshot_path_from_text(combined_text)
|
||||
if recovered_path and Path(recovered_path).exists():
|
||||
_bt.logger.info(
|
||||
"browser 'screenshot' recovered file from non-JSON output: %s",
|
||||
recovered_path,
|
||||
"browser 'screenshot' recovered file from non-JSON output: %s", recovered_path
|
||||
)
|
||||
return {"success": True, "data": {"path": recovered_path, "raw": raw}}
|
||||
return {"success": False, "error": f"Non-JSON output from agent-browser for '{command}': {raw}"}
|
||||
@@ -776,9 +766,7 @@ def _run_browser_command(
|
||||
_bt._handle_browser_command_timeout(task_id, session_info, task_socket_dir)
|
||||
if stderr and stderr.strip():
|
||||
_bt.logger.warning(
|
||||
"browser '%s' stderr after timeout: %s",
|
||||
command,
|
||||
stderr.strip()[:500],
|
||||
"browser '%s' stderr after timeout: %s", command, stderr.strip()[:500]
|
||||
)
|
||||
_bt.logger.warning("browser '%s' timed out after %ds (task=%s, socket_dir=%s)",
|
||||
command, timeout, task_id, task_socket_dir)
|
||||
|
||||
@@ -95,8 +95,7 @@ def _native_vision_result(
|
||||
if annotate and result.get("data", {}).get("annotations"):
|
||||
meta["annotations"] = result["data"]["annotations"]
|
||||
native_result["text_summary"] = (
|
||||
f"{native_result.get('text_summary', '')} "
|
||||
f"Screenshot path: {screenshot_path}"
|
||||
f"{native_result.get('text_summary', '')} " f"Screenshot path: {screenshot_path}"
|
||||
).strip()
|
||||
return native_result
|
||||
|
||||
|
||||
Reference in New Issue
Block a user