mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
Three GUI Capabilities-tab defects reported on Windows:
1. Browser rows stuck on 'Setup required' after a successful setup run.
Root causes, all in the readiness probe (not the installer):
- _has_agent_browser() never searched the Hermes-managed Node dir
(%LOCALAPPDATA%/hermes/node / $HERMES_HOME/node/bin) where the
Windows install lands, and probed node_modules/.bin/agent-browser
as the extensionless POSIX shim, which fails exec on Windows
(WinError 193) — now resolved via PATHEXT-aware shutil.which
against both rungs, mirroring _find_agent_browser().
- Cloud rows (Nous Subscription Browser Use, Browserbase, Browser
Use, Firecrawl) declared post_setup: agent_browser, whose
readiness gate requires a LOCAL Chromium build the cloud never
uses — switched to the cloud-scoped 'browserbase' hook (CLI-only).
- _agent_browser_installed() could read browser_tool's stale cached
'Chromium missing' result from before the install ran in the
spawned post-setup process — cache now dropped before probing so
the pill flips to Ready right after a successful run.
2. No way to tell which backend is active, and clicking a row to read
its details silently rewrote config. Row click now only
expands/collapses; activation is an explicit 'Use this backend'
button, the active row carries an 'Active' pill, and the expanded
active row says 'This is your active backend'.
3. OpenAI TTS showed one model and one voice. The options were always
defined but rendered through a native <datalist>, which filters by
the field's current value — a field already set to a valid option
suggested only itself. Replaced with a real combobox (Input +
dropdown) that lists every option, and voice suggestions now track
the selected model per the OpenAI TTS docs: tts-1/tts-1-hd = 9
voices, gpt-4o-mini-tts = 13 (adds ballad, verse, marin, cedar).
173 lines
5.6 KiB
Python
173 lines
5.6 KiB
Python
"""Firecrawl cloud browser provider — plugin form.
|
|
|
|
Subclasses :class:`agent.browser_provider.BrowserProvider` (the plugin-facing
|
|
ABC introduced in PR #25214). The legacy in-tree module
|
|
``tools.browser_providers.firecrawl`` was removed in the same PR; this file
|
|
is now the canonical implementation.
|
|
|
|
This is the cloud-browser path — distinct from the firecrawl WEB plugin at
|
|
``plugins/web/firecrawl/`` which handles search/extract/crawl on
|
|
``/v2/search`` / ``/v2/scrape`` / ``/v2/crawl``. The two plugins share the
|
|
``FIRECRAWL_API_KEY`` env var but talk to different endpoints (this one
|
|
hits ``/v2/browser``).
|
|
|
|
Config keys this provider responds to::
|
|
|
|
browser:
|
|
cloud_provider: "firecrawl" # explicit selection only — not in the
|
|
# legacy auto-detect walk
|
|
|
|
Auth env vars::
|
|
|
|
FIRECRAWL_API_KEY=... # https://firecrawl.dev
|
|
FIRECRAWL_API_URL=... # optional override (default https://api.firecrawl.dev)
|
|
FIRECRAWL_BROWSER_TTL=... # optional, default 300 seconds
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import uuid
|
|
from typing import Any, Dict
|
|
|
|
import requests
|
|
|
|
from agent.browser_provider import BrowserProvider
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_BASE_URL = "https://api.firecrawl.dev"
|
|
|
|
|
|
class FirecrawlBrowserProvider(BrowserProvider):
|
|
"""Firecrawl (https://firecrawl.dev) cloud browser backend.
|
|
|
|
Cloud-browser path only — search/extract/crawl live in the separate
|
|
``plugins/web/firecrawl/`` plugin.
|
|
"""
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return "firecrawl"
|
|
|
|
@property
|
|
def display_name(self) -> str:
|
|
return "Firecrawl"
|
|
|
|
def is_available(self) -> bool:
|
|
return bool(os.environ.get("FIRECRAWL_API_KEY"))
|
|
|
|
# ------------------------------------------------------------------
|
|
# Session lifecycle
|
|
# ------------------------------------------------------------------
|
|
|
|
def _api_url(self) -> str:
|
|
return os.environ.get("FIRECRAWL_API_URL", _BASE_URL)
|
|
|
|
def _headers(self) -> Dict[str, str]:
|
|
api_key = os.environ.get("FIRECRAWL_API_KEY")
|
|
if not api_key:
|
|
raise ValueError(
|
|
"FIRECRAWL_API_KEY environment variable is required. "
|
|
"Get your key at https://firecrawl.dev"
|
|
)
|
|
return {
|
|
"Content-Type": "application/json",
|
|
"Authorization": f"Bearer {api_key}",
|
|
}
|
|
|
|
def create_session(self, task_id: str) -> Dict[str, object]:
|
|
try:
|
|
ttl = int(os.environ.get("FIRECRAWL_BROWSER_TTL", "300"))
|
|
except (ValueError, TypeError):
|
|
ttl = 300
|
|
|
|
body: Dict[str, object] = {"ttl": ttl}
|
|
|
|
try:
|
|
response = requests.post(
|
|
f"{self._api_url()}/v2/browser",
|
|
headers=self._headers(),
|
|
json=body,
|
|
timeout=30,
|
|
)
|
|
except requests.RequestException as exc:
|
|
raise RuntimeError(
|
|
f"Firecrawl API connection failed: {exc}"
|
|
) from exc
|
|
|
|
if not response.ok:
|
|
raise RuntimeError(
|
|
f"Failed to create Firecrawl browser session: "
|
|
f"{response.status_code} {response.text}"
|
|
)
|
|
|
|
data = response.json()
|
|
session_name = f"hermes_{task_id}_{uuid.uuid4().hex[:8]}"
|
|
|
|
logger.info("Created Firecrawl browser session %s", session_name)
|
|
|
|
return {
|
|
"session_name": session_name,
|
|
"bb_session_id": data["id"],
|
|
"cdp_url": data["cdpUrl"],
|
|
"features": {"firecrawl": True},
|
|
}
|
|
|
|
def close_session(self, session_id: str) -> bool:
|
|
try:
|
|
response = requests.delete(
|
|
f"{self._api_url()}/v2/browser/{session_id}",
|
|
headers=self._headers(),
|
|
timeout=10,
|
|
)
|
|
if response.status_code in {200, 201, 204}:
|
|
logger.debug("Successfully closed Firecrawl session %s", session_id)
|
|
return True
|
|
else:
|
|
logger.warning(
|
|
"Failed to close Firecrawl session %s: HTTP %s - %s",
|
|
session_id,
|
|
response.status_code,
|
|
response.text[:200],
|
|
)
|
|
return False
|
|
except Exception as e:
|
|
logger.error("Exception closing Firecrawl session %s: %s", session_id, e)
|
|
return False
|
|
|
|
def emergency_cleanup(self, session_id: str) -> None:
|
|
if not self.is_available():
|
|
logger.warning(
|
|
"Cannot emergency-cleanup Firecrawl session %s — missing credentials",
|
|
session_id,
|
|
)
|
|
return
|
|
try:
|
|
requests.delete(
|
|
f"{self._api_url()}/v2/browser/{session_id}",
|
|
headers=self._headers(),
|
|
timeout=5,
|
|
)
|
|
except Exception as e:
|
|
logger.debug(
|
|
"Emergency cleanup failed for Firecrawl session %s: %s", session_id, e
|
|
)
|
|
|
|
def get_setup_schema(self) -> Dict[str, Any]:
|
|
return {
|
|
"name": "Firecrawl",
|
|
"badge": "paid",
|
|
"tag": "Cloud browser with remote execution",
|
|
"env_vars": [
|
|
{
|
|
"key": "FIRECRAWL_API_KEY",
|
|
"prompt": "Firecrawl API key",
|
|
"url": "https://firecrawl.dev",
|
|
},
|
|
],
|
|
# Cloud-scoped hook: installs the agent-browser CLI only (no
|
|
# local Chromium — Firecrawl hosts the browser).
|
|
"post_setup": "browserbase",
|
|
}
|