fix(web): guarantee the keyless web default registers even if discovery doesn't
web_search/web_extract are documented to work with zero setup via the bundled
keyless Parallel free-MCP backend, but that only holds when the bundled
plugins/web/* providers are registered. The dispatch relied entirely on the
general plugin sweep to do that; when the sweep finishes without registering
them (its exception swallowed as a warning, a packaged layout where it ran
before the bundled tree was importable, or a stale empty-discovery cache), the
registry is empty and BOTH tools dead-end on "No web {search,extract} provider
configured" — despite needing no setup at all.
_ensure_web_plugins_loaded now verifies the keyless default landed after the
sweep and, if not, registers the bundled web providers directly against the
registry. Idempotent, a no-op on the healthy path (one dict lookup), and honors
an explicit plugins.disabled entry.
This commit is contained in:
@@ -810,6 +810,17 @@ def _ensure_web_plugins_loaded() -> None:
|
||||
Mirrors :func:`tools.browser_tool._ensure_browser_plugins_loaded` exactly:
|
||||
the underlying discovery call is idempotent and cheap on subsequent
|
||||
invocations.
|
||||
|
||||
Triggering discovery is necessary but not *sufficient*: the sweep can
|
||||
finish without registering the bundled web providers (its exception
|
||||
swallowed below as a warning, a packaged layout where discovery ran before
|
||||
the bundled tree was importable, or a stale empty-discovery cache). When
|
||||
that happens the registry is empty and *both* web_search and web_extract
|
||||
dead-end on "No web {search,extract} provider configured" — even though the
|
||||
keyless Parallel default is supposed to work with zero setup. So after
|
||||
discovery we verify the keyless default landed and, if not, register the
|
||||
bundled providers directly (see
|
||||
:func:`_register_bundled_web_providers_directly`).
|
||||
"""
|
||||
try:
|
||||
from hermes_cli.plugins import _ensure_plugins_discovered
|
||||
@@ -822,6 +833,87 @@ def _ensure_web_plugins_loaded() -> None:
|
||||
# clue in normal logs about the real cause.
|
||||
logger.warning("Web plugin discovery failed (non-fatal): %s", exc)
|
||||
|
||||
# Belt-and-suspenders: guarantee the keyless Parallel default (the
|
||||
# documented zero-setup backend for both web_search and web_extract) is
|
||||
# actually registered. The lookup is a cheap dict hit on the healthy path
|
||||
# (discovery already registered it → no-op); only an empty registry pays
|
||||
# for the direct-registration sweep.
|
||||
try:
|
||||
from agent.web_search_registry import get_provider
|
||||
|
||||
if get_provider("parallel") is None:
|
||||
_register_bundled_web_providers_directly()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.debug("Bundled web provider fallback check failed: %s", exc)
|
||||
|
||||
|
||||
def _register_bundled_web_providers_directly() -> None:
|
||||
"""Register the repo's bundled web providers without the plugin manager.
|
||||
|
||||
The normal path is the general plugin sweep
|
||||
(:func:`hermes_cli.plugins._ensure_plugins_discovered`), which auto-loads
|
||||
every ``plugins/web/<name>`` backend (they are ``kind: backend``). This
|
||||
fallback exists for the runtimes where that sweep does not leave the web
|
||||
registry populated — so the keyless Parallel default (and any bundled
|
||||
backend the user explicitly configured) keeps working instead of
|
||||
surfacing a misleading "No web provider configured" error.
|
||||
|
||||
Imports each bundled ``plugins/web/<name>`` package and calls its
|
||||
``register()`` directly against :mod:`agent.web_search_registry`. Idempotent
|
||||
(re-register overwrites) and honors an explicit ``plugins.disabled`` entry
|
||||
so a backend the user turned off stays off.
|
||||
"""
|
||||
try:
|
||||
from hermes_cli.plugins import (
|
||||
_get_disabled_plugins,
|
||||
get_bundled_plugins_dir,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.debug("Bundled web provider fallback unavailable: %s", exc)
|
||||
return
|
||||
|
||||
web_dir = get_bundled_plugins_dir() / "web"
|
||||
if not web_dir.is_dir():
|
||||
return
|
||||
|
||||
disabled = _get_disabled_plugins()
|
||||
|
||||
from agent.web_search_provider import WebSearchProvider
|
||||
from agent.web_search_registry import register_provider
|
||||
|
||||
class _DirectRegistrationCtx:
|
||||
"""Minimal plugin ctx exposing only web-provider registration."""
|
||||
|
||||
def register_web_search_provider(self, provider) -> None:
|
||||
if isinstance(provider, WebSearchProvider):
|
||||
register_provider(provider)
|
||||
|
||||
ctx = _DirectRegistrationCtx()
|
||||
import importlib
|
||||
|
||||
for child in sorted(web_dir.iterdir()):
|
||||
if not child.is_dir():
|
||||
continue
|
||||
if not (child / "plugin.yaml").exists() and not (child / "plugin.yml").exists():
|
||||
continue
|
||||
# Respect an explicit disable — match discover_and_load's key/name
|
||||
# check (key ``web/<dir>``; manifest name ``web-<dir-with-dashes>``).
|
||||
if (
|
||||
f"web/{child.name}" in disabled
|
||||
or f"web-{child.name.replace('_', '-')}" in disabled
|
||||
):
|
||||
continue
|
||||
try:
|
||||
module = importlib.import_module(f"plugins.web.{child.name}")
|
||||
register_fn = getattr(module, "register", None)
|
||||
if callable(register_fn):
|
||||
register_fn(ctx)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.debug(
|
||||
"Direct registration of bundled web provider '%s' failed: %s",
|
||||
child.name, exc,
|
||||
)
|
||||
|
||||
|
||||
def web_search_tool(query: str, limit: int = 5) -> str:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user