Создание плагина провайдера веб-поиска

Плагины провайдеров веб-поиска регистрируют бэкенд, который обслуживает вызовы инструментов web_search, web_extract и (опционально) глубокого краулинга. Встроенные провайдеры — Firecrawl, SearXNG, Tavily, Exa, Parallel, Brave Search (бесплатный тариф), xAI и DDGS — поставляются как плагины в директории plugins/web/<name>/. Чтобы добавить новый или переопределить встроенный, достаточно положить рядом с ними свою директорию.

совет

Веб-поиск — один из нескольких бэкенд-плагинов, которые поддерживает Hermes. Остальные (со своими ABC) — это Плагины провайдеров генерации изображений, Плагины провайдеров генерации видео, Плагины провайдеров памяти, Плагины движка контекста и Плагины провайдеров модели. Обычные плагины инструментов, хуков и CLI описаны в Создание плагина для Hermes.

Как работает обнаружение плагинов

Hermes ищет бэкенды веб-поиска в трёх местах:

  1. Встроенные<repo>/plugins/web/<name>/ (загружаются автоматически при kind: backend, доступны всегда)
  2. Пользовательские~/.hermes/plugins/web/<name>/ (подключаются через plugins.enabled или командой hermes plugins enable <name>)
  3. Pip — пакеты, объявляющие точку входа hermes_agent.plugins

Функция register(ctx) каждого плагина вызывает ctx.register_web_search_provider(...) и кладёт экземпляр в реестр agent/web_search_registry.py. Какой провайдер активен для каждой возможности, решает конфигурация:

ВозможностьКлюч конфигурацииФолбэк
web_searchweb.search_backendweb.backend
web_extractweb.extract_backendweb.backend
Режимы глубокого краулинга внутри web_extractweb.extract_backendweb.backend

Если ни один ключ не задан, Hermes сам определяет бэкенд по тому, какой API-ключ или URL присутствует в переменных окружения. Команда hermes tools проведёт пользователя через выбор провайдера.

Структура директории

plugins/web/my-backend/
├── __init__.py     # точка входа register()
├── provider.py     # подкласс WebSearchProvider
└── plugin.yaml     # манифест с kind: backend и provides_web_providers

brave_free/ и ddgs/ — самые компактные эталонные реализации в репозитории: brave_free — провайдер только для поиска, доступ к которому открывает API-ключ, а ddgs — провайдер без ключа, лениво доставляющий свой SDK.

ABC WebSearchProvider

Создайте подкласс agent.web_search_provider.WebSearchProvider. Обязательны только name, is_available() и хотя бы один из методов search() / extract(). (Глубокий краулинг — это режим внутри extract(), а не отдельный метод.)

# plugins/web/my-backend/provider.py
from __future__ import annotations

import os
from typing import Any, Dict, List

from agent.web_search_provider import WebSearchProvider


class MyBackendWebSearchProvider(WebSearchProvider):
    """Минимальный провайдер только для поиска через HTTP API My Backend."""

    @property
    def name(self) -> str:
        # Стабильный идентификатор, используемый в ключах конфигурации
        # web.search_backend / web.extract_backend / web.backend.
        # Строчные буквы, без пробелов; дефисы допустимы.
        return "my-backend"

    @property
    def display_name(self) -> str:
        # Метка, отображаемая в `hermes tools`. По умолчанию равна `name`.
        return "My Backend"

    def is_available(self) -> bool:
        # Лёгкая проверка — наличие переменной окружения, импортируемость зависимости и т. п.
        # ЗАПРЕЩЕНЫ сетевые вызовы (метод вызывается при каждой отрисовке `hermes tools`).
        return bool(os.getenv("MY_BACKEND_API_KEY", "").strip())

    def supports_search(self) -> bool:
        return True

    def supports_extract(self) -> bool:
        return False

    def search(self, query: str, limit: int = 5) -> Dict[str, Any]:
        import httpx

        api_key = os.environ["MY_BACKEND_API_KEY"]
        try:
            resp = httpx.get(
                "https://api.example.com/search",
                params={"q": query, "count": max(1, min(int(limit), 20))},
                headers={"Authorization": f"Bearer {api_key}"},
                timeout=15,
            )
            resp.raise_for_status()
            data = resp.json()
        except httpx.HTTPError as exc:
            return {"success": False, "error": str(exc)}

        # Структура ответа фиксирована — см. раздел «Структура ответа» ниже.
        return {
            "success": True,
            "data": {
                "web": [
                    {
                        "title": item.get("title", ""),
                        "url": item.get("url", ""),
                        "description": item.get("snippet", ""),
                        "position": idx + 1,
                    }
                    for idx, item in enumerate(data.get("results", []))
                ],
            },
        }
# plugins/web/my-backend/__init__.py
from plugins.web.my_backend.provider import MyBackendWebSearchProvider


def register(ctx) -> None:
    """Точка входа плагина — вызывается один раз при загрузке."""
    ctx.register_web_search_provider(MyBackendWebSearchProvider())

plugin.yaml

name: web-my-backend
version: 1.0.0
description: "My Backend web search — Bearer-auth REST API"
author: Your Name
kind: backend
provides_web_providers:
  - my-backend
requires_env:
  - MY_BACKEND_API_KEY
КлючНазначение
kind: backendНаправляет плагин по пути загрузки бэкендов
provides_web_providersСписок name провайдеров, которые регистрирует плагин — по нему загрузчик анонсирует плагин в hermes tools ещё до вызова register()
requires_envИнтерактивный запрос учётных данных во время hermes plugins install (расширенный формат описан в Создание плагина для Hermes)

Справочник по ABC

Полный контракт — в agent/web_search_provider.py. Методы, доступные для переопределения:

ЧленОбязательныйЗначение по умолчаниюНазначение
nameСтабильный идентификатор для ключей конфигурации web.*_backend
display_namenameМетка в hermes tools
is_available()Лёгкая проверка доступности — переменные окружения, опциональные зависимости
supports_search()TrueФлаг возможности для маршрутизации web_search
supports_extract()FalseФлаг возможности для маршрутизации web_extract
search(query, limit)условноraisesТребуется, когда supports_search() возвращает True
extract(urls, **kwargs)условноraisesТребуется, когда supports_extract() возвращает True

Один класс может объявлять сразу несколько возможностей — Firecrawl, Tavily, Exa и Parallel реализуют и поиск, и извлечение. Brave Search и DDGS работают только на поиск; SearXNG — тоже только поиск, но с задокументированным сценарием «свяжите меня с провайдером извлечения».

Структура ответа

Обёртка инструмента ждёт конверт фиксированной формы, чтобы не переводить формат с одного бэкенда на другой.

Успешный поиск:

{
    "success": True,
    "data": {
        "web": [
            {"title": str, "url": str, "description": str, "position": int},
            ...
        ],
    },
}

Успешное извлечение:

{
    "success": True,
    "data": [
        {
            "url": str,
            "title": str,
            "content": str,
            "raw_content": str,
            "metadata": dict,    # опционально
            "error": str,        # опционально, только при ошибке на уровне URL
        },
        ...
    ],
}

Любая возможность, при ошибке:

{"success": False, "error": "human-readable message"}

Оба метода — search() и extract() — можно объявить через async def: диспетчер распознаёт корутины через inspect.iscoroutinefunction и вызывает их с await. Синхронные реализации с блокирующим I/O (HTTP, SDK-вызовы) вполне годятся для небольших бэкендов — потоки диспетчер берёт на себя.

Флаги возможностей

Hermes направляет вызовы к нужному провайдеру по флагам supports_*. Типичная конфигурация с несколькими провайдерами:

# ~/.hermes/config.yaml
web:
  search_backend: "brave-free"     # только поиск, быстро, 2к запросов/мес бесплатно
  extract_backend: "firecrawl"     # извлечение + краулинг, платная квота

Если web.search_backend или web.extract_backend не заданы, оба ключа откатываются на web.backend. Если не задан и он, Hermes выбирает первый доступный провайдер с нужной возможностью, ориентируясь на наличие переменных окружения.

Когда провайдер поддерживает только одну возможность, оставьте остальные флаги со значением по умолчанию (False) — для неподдерживаемого инструмента реестр просто пропустит его. Тогда пользователи не увидят сбивающих с толку ошибок «провайдер X недоступен» в ситуации, когда X задействован только для поиска, а агент просят что-то извлечь.

Как Hermes подключает провайдер к инструментам

Инструменты web_search и web_extract описаны в tools/web_tools.py. На каждом вызове они:

  1. Читают соответствующий ключ конфигурации (web.search_backend для web_search, web.extract_backend для web_extract)
  2. Запрашивают у реестра провайдера с нужным name
  3. Проверяют is_available() и соответствующий флаг supports_*()
  4. Вызывают search() / extract() (глубокий краулинг выполняется как режим внутри extract()), добавляя await, если метод — корутина
  5. JSON-сериализуют конверт ответа и передают его LLM

Ошибки приходят как результат инструмента — а уже LLM решает, как их объяснить. Если не зарегистрирован ни один провайдер (или все доступные не проходят проверку возможности), инструмент возвращает понятную ошибку со ссылкой на hermes tools.

Ленивая установка опциональных зависимостей

Если провайдер оборачивает сторонний SDK (как DDGS — пакет ddgs), не делайте import на верхнем уровне модуля. Вызывайте tools.lazy_deps.ensure(...) внутри is_available() или search() — Hermes установит пакет при первом использовании, если включён параметр security.allow_lazy_installs. Модель безопасности описана в Создание плагина для Hermes → Ленивая установка.

Эталонные реализации

  • plugins/web/brave_free/ — компактный HTTP-провайдер только для поиска, с доступом по API-ключу. Хорошая отправная точка.
  • plugins/web/ddgs/ — провайдер без ключа с ленивой установкой SDK. Полезный паттерн для бэкендов, которые оборачивают Python-пакет.
  • plugins/web/firecrawl/ — полноценный многофункциональный провайдер (поиск + извлечение + краулинг) с несколькими режимами форматирования.
  • plugins/web/searxng/ — самохостируемый бэкенд с настройкой через URL, без аутентификации.
  • plugins/web/xai/ — поиск на базе LLM через серверный инструмент web_search от Grok. Показывает, как переиспользовать готовую поверхность OAuth/env-var (tools/xai_http.py), не заводя новых переменных окружения, и как написать дешёвый is_available(), который соблюдает контракт отсутствия сетевых вызовов.

Распространение через pip

# pyproject.toml
[project.entry-points."hermes_agent.plugins"]
my-backend-web = "my_backend_web_package"

my_backend_web_package должен предоставлять функцию register на верхнем уровне. Полная инструкция по настройке — в разделе Распространение через pip общего руководства по плагинам.

Связанные страницы

ESC