hermes-agent/plugins/browser/firecrawl/provider.py
Teknium 2319dbb014 fix(desktop): honest browser-backend readiness + explicit backend activation + full OpenAI TTS voice/model options
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).
2026-07-28 23:52:49 -07:00

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",
}