sidebar_position: 9 sidebar_label: "Build a Plugin" title: "Build a Hermes Plugin" description: "Step-by-step guide to building a complete Hermes plugin with tools, hooks, data files, and skills" lang: ru


Создайте плагин Hermes

В этом руководстве описывается создание полноценного плагина Hermes с нуля. К концу у вас будет работающий плагин с множеством инструментов, крючками жизненного цикла, отправленными файлами данных и набором навыков — всем, что поддерживает система плагинов.

Что вы строите

Плагин калькулятора с двумя инструментами: - calculate — оценивать математические выражения (2**16, sqrt(144), pi * 5**2) - unit_convert — конвертировать единицы измерения (100 F → 37.78 C, 5 km → 3.11 mi)

Плюс крючок, который регистрирует каждый вызов инструмента, и прилагаемый файл навыков.

Шаг 1: Создайте каталог плагина

mkdir -p ~/.hermes/plugins/calculator
cd ~/.hermes/plugins/calculator

Шаг 2. Напишите манифест

Создайте plugin.yaml:

name: calculator
version: 1.0.0
description: Math calculator — evaluate expressions and convert units
provides_tools:
  - calculate
  - unit_convert
provides_hooks:
  - post_tool_call

Это говорит Гермесу: «Я плагин под названием калькулятор, я предоставляю инструменты и крючки». Поля provides_tools и provides_hooks представляют собой списки того, что регистрирует плагин.

Необязательные поля, которые вы можете добавить:

author: Your Name
requires_env:          # gate loading on env vars; prompted during install
  - SOME_API_KEY       # simple format — plugin disabled if missing
  - name: OTHER_KEY    # rich format — shows description/url during install
    description: "Key for the Other service"
    url: "https://other.com/keys"
    secret: true

Шаг 3: Напишите схемы инструментов

Создайте schemas.py — это то, что читает LLM, чтобы решить, когда вызывать ваши инструменты:

"""Tool schemas — what the LLM sees."""

CALCULATE = {
    "name": "calculate",
    "description": (
        "Evaluate a mathematical expression and return the result. "
        "Supports arithmetic (+, -, *, /, **), functions (sqrt, sin, cos, "
        "log, abs, round, floor, ceil), and constants (pi, e). "
        "Use this for any math the user asks about."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "expression": {
                "type": "string",
                "description": "Math expression to evaluate (e.g., '2**10', 'sqrt(144)')",
            },
        },
        "required": ["expression"],
    },
}

UNIT_CONVERT = {
    "name": "unit_convert",
    "description": (
        "Convert a value between units. Supports length (m, km, mi, ft, in), "
        "weight (kg, lb, oz, g), temperature (C, F, K), data (B, KB, MB, GB, TB), "
        "and time (s, min, hr, day)."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "value": {
                "type": "number",
                "description": "The numeric value to convert",
            },
            "from_unit": {
                "type": "string",
                "description": "Source unit (e.g., 'km', 'lb', 'F', 'GB')",
            },
            "to_unit": {
                "type": "string",
                "description": "Target unit (e.g., 'mi', 'kg', 'C', 'MB')",
            },
        },
        "required": ["value", "from_unit", "to_unit"],
    },
}

Почему схемы важны. Поле description определяет, когда LLM будет использовать ваш инструмент. Будьте конкретны в том, что он делает и когда его использовать. parameters определяет, какие аргументы передает LLM.

Шаг 4. Напишите обработчики инструментов

Создайте tools.py — это код, который фактически выполняется, когда LLM вызывает ваши инструменты:

"""Tool handlers — the code that runs when the LLM calls each tool."""

import json
import math

# Safe globals for expression evaluation — no file/network access
_SAFE_MATH = {
    "abs": abs, "round": round, "min": min, "max": max,
    "pow": pow, "sqrt": math.sqrt, "sin": math.sin, "cos": math.cos,
    "tan": math.tan, "log": math.log, "log2": math.log2, "log10": math.log10,
    "floor": math.floor, "ceil": math.ceil,
    "pi": math.pi, "e": math.e,
    "factorial": math.factorial,
}


def calculate(args: dict, **kwargs) -> str:
    """Evaluate a math expression safely.

    Rules for handlers:
    1. Receive args (dict) — the parameters the LLM passed
    2. Do the work
    3. Return a JSON string — ALWAYS, even on error
    4. Accept **kwargs for forward compatibility
    """
    expression = args.get("expression", "").strip()
    if not expression:
        return json.dumps({"error": "No expression provided"})

    try:
        result = eval(expression, {"__builtins__": {}}, _SAFE_MATH)
        return json.dumps({"expression": expression, "result": result})
    except ZeroDivisionError:
        return json.dumps({"expression": expression, "error": "Division by zero"})
    except Exception as e:
        return json.dumps({"expression": expression, "error": f"Invalid: {e}"})


# Conversion tables — values are in base units
_LENGTH = {"m": 1, "km": 1000, "mi": 1609.34, "ft": 0.3048, "in": 0.0254, "cm": 0.01}
_WEIGHT = {"kg": 1, "g": 0.001, "lb": 0.453592, "oz": 0.0283495}
_DATA = {"B": 1, "KB": 1024, "MB": 1024**2, "GB": 1024**3, "TB": 1024**4}
_TIME = {"s": 1, "ms": 0.001, "min": 60, "hr": 3600, "day": 86400}


def _convert_temp(value, from_u, to_u):
    # Normalize to Celsius
    c = {"F": (value - 32) * 5/9, "K": value - 273.15}.get(from_u, value)
    # Convert to target
    return {"F": c * 9/5 + 32, "K": c + 273.15}.get(to_u, c)


def unit_convert(args: dict, **kwargs) -> str:
    """Convert between units."""
    value = args.get("value")
    from_unit = args.get("from_unit", "").strip()
    to_unit = args.get("to_unit", "").strip()

    if value is None or not from_unit or not to_unit:
        return json.dumps({"error": "Need value, from_unit, and to_unit"})

    try:
        # Temperature
        if from_unit.upper() in {"C","F","K"} and to_unit.upper() in {"C","F","K"}:
            result = _convert_temp(float(value), from_unit.upper(), to_unit.upper())
            return json.dumps({"input": f"{value} {from_unit}", "result": round(result, 4),
                             "output": f"{round(result, 4)} {to_unit}"})

        # Ratio-based conversions
        for table in (_LENGTH, _WEIGHT, _DATA, _TIME):
            lc = {k.lower(): v for k, v in table.items()}
            if from_unit.lower() in lc and to_unit.lower() in lc:
                result = float(value) * lc[from_unit.lower()] / lc[to_unit.lower()]
                return json.dumps({"input": f"{value} {from_unit}",
                                 "result": round(result, 6),
                                 "output": f"{round(result, 6)} {to_unit}"})

        return json.dumps({"error": f"Cannot convert {from_unit}{to_unit}"})
    except Exception as e:
        return json.dumps({"error": f"Conversion failed: {e}"})

Основные правила для кураторов: 1. Подпись: def my_handler(args: dict, **kwargs) -> str 2. Возврат: Всегда строка JSON. И успех, и ошибка. 3. Никогда не поднимайте: Перехватывайте все исключения, вместо этого возвращайте ошибку в формате JSON. 4. Принять **kwargs: Гермес может передать дополнительный контекст в будущем.

Шаг 5: Напишите регистрацию

Создайте __init__.py — это подключит схемы к обработчикам:

"""Calculator plugin — registration."""

import logging

from . import schemas, tools

logger = logging.getLogger(__name__)

# Track tool usage via hooks
_call_log = []

def _on_post_tool_call(tool_name, args, result, task_id, **kwargs):
    """Hook: runs after every tool call (not just ours)."""
    _call_log.append({"tool": tool_name, "session": task_id})
    if len(_call_log) > 100:
        _call_log.pop(0)
    logger.debug("Tool called: %s (session %s)", tool_name, task_id)


def register(ctx):
    """Wire schemas to handlers and register hooks."""
    ctx.register_tool(name="calculate",    toolset="calculator",
                      schema=schemas.CALCULATE,    handler=tools.calculate)
    ctx.register_tool(name="unit_convert", toolset="calculator",
                      schema=schemas.UNIT_CONVERT, handler=tools.unit_convert)

    # This hook fires for ALL tool calls, not just ours
    ctx.register_hook("post_tool_call", _on_post_tool_call)

Что делает register(): - Вызывается ровно один раз при запуске - ctx.register_tool() помещает ваш инструмент в реестр — модель его сразу видит - ctx.register_hook() подписывается на события жизненного цикла - ctx.register_cli_command() регистрирует подкоманду CLI (например, hermes my-plugin <subcommand>) - ctx.register_command() регистрирует команду косой черты в сеансе (например, /myplugin <args> внутри чата CLI/шлюза) — см. Регистрация команд косой черты ниже - ctx.dispatch_tool(name, arguments) — вызвать любой другой инструмент (встроенный или из другого плагина) с контекстом родительского агента (одобрения, учетные данные, Task_id), подключенным автоматически. Полезно для обработчиков косой черты, которым необходимо вызвать terminal, read_file или любой другой инструмент, как если бы модель вызывала его напрямую. - Если эта функция выходит из строя, плагин отключается, но Hermes продолжает работать нормально

dispatch_tool пример — косая черта, запускающая инструмент:

def handle_scan(ctx, argstr):
    """Implement /scan by invoking the terminal tool through the registry."""
    result = ctx.dispatch_tool("terminal", {"command": f"find . -name '{argstr}'"})
    return result  # returned to the caller's chat UI

def register(ctx):
    ctx.register_command("scan", handle_scan, help="Find files matching a glob")

Отправленный инструмент проходит через обычные конвейеры утверждения, редактирования и составления бюджета — это настоящий вызов инструмента, а не обходной путь к ним.

Шаг 6: Проверьте это

Запускаем Гермес:

hermes

В списке инструментов баннера вы должны увидеть calculator: calculate, unit_convert.

Попробуйте эти подсказки:

What's 2 to the power of 16?
Convert 100 fahrenheit to celsius
What's the square root of 2 times pi?
How many gigabytes is 1.5 terabytes?

Проверьте статус плагина:

/plugins

Выход:

Plugins (1):
   calculator v1.0.0 (2 tools, 1 hooks)

Окончательная структура вашего плагина

~/.hermes/plugins/calculator/
├── plugin.yaml      # "I'm calculator, I provide tools and hooks"
├── __init__.py      # Wiring: schemas → handlers, register hooks
├── schemas.py       # What the LLM reads (descriptions + parameter specs)
└── tools.py         # What runs (calculate, unit_convert functions)

Четыре файла, четкое разделение: - Манифест объявляет, что представляет собой плагин. - Схемы описывают инструменты для LLM. - Обработчики реализуют реальную логику. - Регистрация объединяет все

Что еще могут плагины?

Файлы данных корабля

Поместите любые файлы в каталог вашего плагина и прочитайте их во время импорта:

# In tools.py or __init__.py
from pathlib import Path

_PLUGIN_DIR = Path(__file__).parent
_DATA_FILE = _PLUGIN_DIR / "data" / "languages.yaml"

with open(_DATA_FILE) as f:
    _DATA = yaml.safe_load(f)

Набор навыков

Плагины могут отправлять файлы навыков, которые агент загружает через skill_view("plugin:skill"). Зарегистрируйте их в своем __init__.py:

~/.hermes/plugins/my-plugin/
├── __init__.py
├── plugin.yaml
└── skills/
    ├── my-workflow/
    │   └── SKILL.md
    └── my-checklist/
        └── SKILL.md
from pathlib import Path

def register(ctx):
    skills_dir = Path(__file__).parent / "skills"
    for child in sorted(skills_dir.iterdir()):
        skill_md = child / "SKILL.md"
        if child.is_dir() and skill_md.exists():
            ctx.register_skill(child.name, skill_md)

Теперь агент может загружать ваши навыки, используя их имена в пространстве имен:

skill_view("my-plugin:my-workflow")   # → plugin's version
skill_view("my-workflow")              # → built-in version (unchanged)

Основные свойства: - Навыки плагина доступны только для чтения — они не вводятся ~/.hermes/skills/ и не могут быть отредактированы через skill_manage. - Навыки плагинов не перечислены в индексе <available_skills> системной подсказки — они являются явной загрузкой по согласию. - Названия простых навыков не затрагиваются — пространство имен предотвращает конфликты со встроенными навыками. - Когда агент загружает навык плагина, перед ним добавляется контекстный баннер пакета со списком родственных навыков из того же плагина.

:::совет Устаревший шаблон Старый шаблон shutil.copy2 (копирование навыка в ~/.hermes/skills/) все еще работает, но создает риск конфликта имен со встроенными навыками. Предпочитайте ctx.register_skill() для новых плагинов.

Вход в переменные среды

Если вашему плагину нужен ключ API:

# plugin.yaml — simple format (backwards-compatible)
requires_env:
  - WEATHER_API_KEY

Если WEATHER_API_KEY не установлен, плагин отключается с четким сообщением. Ни сбоев, ни ошибок в агенте — просто «Плагин погоды отключен (отсутствует: WEATHER_API_KEY)».

Когда пользователи запускают hermes plugins install, им интерактивно предлагается указать недостающие переменные requires_env. Значения сохраняются в .env автоматически.

Для удобства установки используйте расширенный формат с описаниями и URL-адресами регистрации:

# plugin.yaml — rich format
requires_env:
  - name: WEATHER_API_KEY
    description: "API key for OpenWeather"
    url: "https://openweathermap.org/api"
    secret: true
Поле Требуется Описание
name Да Имя переменной среды
description Нет Показывается пользователю во время установки
url Нет Где получить удостоверение
secret Нет Если true, ввод скрыт (как поле пароля)

Оба формата могут быть смешаны в одном списке. Уже установленные переменные пропускаются автоматически.

Условная доступность инструмента

Для инструментов, которые зависят от дополнительных библиотек:

ctx.register_tool(
    name="my_tool",
    schema={...},
    handler=my_handler,
    check_fn=lambda: _has_optional_lib(),  # False = tool hidden from model
)

Регистрация нескольких хуков

def register(ctx):
    ctx.register_hook("pre_tool_call", before_any_tool)
    ctx.register_hook("post_tool_call", after_any_tool)
    ctx.register_hook("pre_llm_call", inject_memory)
    ctx.register_hook("on_session_start", on_new_session)
    ctx.register_hook("on_session_end", on_session_end)

Ссылка на крючок

Каждый крючок полностью описан в Справочнике по хукам событий — сигнатуры обратного вызова, таблицы параметров, когда именно срабатывает каждый из них, и примеры. Вот резюме:

Крюк Срабатывает, когда Подпись обратного вызова Возврат
pre_tool_call Перед выполнением любого инструмента tool_name: str, args: dict, task_id: str игнорируется
post_tool_call После возврата любого инструмента tool_name: str, args: dict, result: str, task_id: str, duration_ms: int игнорируется
pre_llm_call Один раз за ход, перед циклом вызова инструмента session_id: str, user_message: str, conversation_history: list, is_first_turn: bool, model: str, platform: str внедрение контекста
post_llm_call Один раз за ход, после цикла вызова инструмента (только успешные ходы) session_id: str, user_message: str, assistant_response: str, conversation_history: list, model: str, platform: str игнорируется
on_session_start Создана новая сессия (только первая очередь) session_id: str, model: str, platform: str игнорируется
on_session_end Конец каждого вызова run_conversation + выход из CLI session_id: str, completed: bool, interrupted: bool, model: str, platform: str игнорируется
on_session_finalize CLI/шлюз разрывает активный сеанс session_id: str \| None, platform: str игнорируется
on_session_reset Шлюз заменяет новый сеансовый ключ (/new, /reset) session_id: str, platform: str игнорируется

Большинство перехватчиков являются наблюдателями по принципу «выстрелил и забыл» — их возвращаемые значения игнорируются. Исключением является pre_llm_call, который может добавить контекст в разговор.

Все обратные вызовы должны принимать **kwargs для обеспечения совместимости. Если обратный вызов перехватчика дает сбой, он регистрируется и пропускается. Другие перехватчики и агент продолжают работать нормально.

pre_llm_call внедрение контекста

Это единственный хук, возвращаемое значение которого имеет значение. Когда обратный вызов pre_llm_call возвращает dict с ключом "context" (или простую строку), Hermes вставляет этот текст в сообщение пользователя текущего хода. Это механизм для плагинов памяти, интеграции RAG, ограждений и любого плагина, который должен предоставить модели дополнительный контекст.

Формат возврата

# Dict with context key
return {"context": "Recalled memories:\n- User prefers dark mode\n- Last project: hermes-agent"}

# Plain string (equivalent to the dict form above)
return "Recalled memories:\n- User prefers dark mode"

# Return None or don't return → no injection (observer-only)
return None

Любой непустой возврат, отличный от None, с ключом "context" (или простой непустой строкой) собирается и добавляется к сообщению пользователя для текущего хода.

Как работает инъекция

Внедренный контекст добавляется к сообщению пользователя, а не к системному приглашению. Это осознанный выбор дизайна:

Пример: плагин вызова памяти

"""Memory plugin — recalls relevant context from a vector store."""

import httpx

MEMORY_API = "https://your-memory-api.example.com"

def recall_context(session_id, user_message, is_first_turn, **kwargs):
    """Called before each LLM turn. Returns recalled memories."""
    try:
        resp = httpx.post(f"{MEMORY_API}/recall", json={
            "session_id": session_id,
            "query": user_message,
        }, timeout=3)
        memories = resp.json().get("results", [])
        if not memories:
            return None  # nothing to inject

        text = "Recalled context from previous sessions:\n"
        text += "\n".join(f"- {m['text']}" for m in memories)
        return {"context": text}
    except Exception:
        return None  # fail silently, don't break the agent

def register(ctx):
    ctx.register_hook("pre_llm_call", recall_context)

Пример: плагин Guardrails

"""Guardrails plugin — enforces content policies."""

POLICY = """You MUST follow these content policies for this session:
- Never generate code that accesses the filesystem outside the working directory
- Always warn before executing destructive operations
- Refuse requests involving personal data extraction"""

def inject_guardrails(**kwargs):
    """Injects policy text into every turn."""
    return {"context": POLICY}

def register(ctx):
    ctx.register_hook("pre_llm_call", inject_guardrails)

Пример: перехват только для наблюдателя (без внедрения)

"""Analytics plugin — tracks turn metadata without injecting context."""

import logging
logger = logging.getLogger(__name__)

def log_turn(session_id, user_message, model, is_first_turn, **kwargs):
    """Fires before each LLM call. Returns None — no context injected."""
    logger.info("Turn: session=%s model=%s first=%s msg_len=%d",
                session_id, model, is_first_turn, len(user_message or ""))
    # No return → no injection

def register(ctx):
    ctx.register_hook("pre_llm_call", log_turn)

Несколько плагинов, возвращающих контекст

Когда несколько плагинов возвращают контекст из pre_llm_call, их выходные данные объединяются двойными символами новой строки и вместе добавляются к сообщению пользователя. Порядок соответствует порядку обнаружения плагинов (в алфавитном порядке по имени каталога плагинов).

Регистрация команд CLI

Плагины могут добавлять собственное дерево подкоманд hermes <plugin>:

def _my_command(args):
    """Handler for hermes my-plugin <subcommand>."""
    sub = getattr(args, "my_command", None)
    if sub == "status":
        print("All good!")
    elif sub == "config":
        print("Current config: ...")
    else:
        print("Usage: hermes my-plugin <status|config>")

def _setup_argparse(subparser):
    """Build the argparse tree for hermes my-plugin."""
    subs = subparser.add_subparsers(dest="my_command")
    subs.add_parser("status", help="Show plugin status")
    subs.add_parser("config", help="Show plugin config")
    subparser.set_defaults(func=_my_command)

def register(ctx):
    ctx.register_tool(...)
    ctx.register_cli_command(
        name="my-plugin",
        help="Manage my plugin",
        setup_fn=_setup_argparse,
        handler_fn=_my_command,
    )

После регистрации пользователи могут запускать hermes my-plugin status, hermes my-plugin config и т. д.

Плагины поставщика памяти вместо этого используют подход, основанный на соглашениях: добавьте функцию register_cli(subparser) в файл cli.py вашего плагина. Система обнаружения плагинов памяти находит его автоматически — вызов ctx.register_cli_command() не требуется. Подробности см. в Руководстве по подключаемым модулям Memory Provider.

Ограничение активного поставщика: Команды CLI подключаемого модуля памяти появляются только в том случае, если их поставщиком является активный memory.provider в конфигурации. Если пользователь не настроил вашего провайдера, ваши команды CLI не будут загромождать вывод справки.

Регистрация косых команд

Плагины могут регистрировать внутрисессионные слэш-команды — команды, которые пользователи вводят во время разговора (например, /lcm status или /ping). Они работают как в CLI, так и в шлюзе (Telegram, Discord и т. д.).

def _handle_status(raw_args: str) -> str:
    """Handler for /mystatus — called with everything after the command name."""
    if raw_args.strip() == "help":
        return "Usage: /mystatus [help|check]"
    return "Plugin status: all systems nominal"

def register(ctx):
    ctx.register_command(
        "mystatus",
        handler=_handle_status,
        description="Show plugin status",
    )

После регистрации пользователи могут набирать /mystatus в любом сеансе. Команда появляется в автозаполнении, выводе /help и меню бота Telegram.

Подпись: ctx.register_command(name: str, handler: Callable, description: str = "")

Параметр Тип Описание
name str Имя команды без косой черты (например, "lcm", "mystatus")
handler Callable[[str], str \| None] Вызывается с необработанной строкой аргумента. Также может быть async.
description str Отображается в /help, автозаполнении и меню бота Telegram

Ключевые отличия от register_cli_command():

register_command() register_cli_command()
Вызывается как /name в сеансе hermes name в терминале
Где это работает Сеансы CLI, Telegram, Discord и т. д. Только терминал
Обработчик получает Необработанная строка аргументов argparse Namespace
Вариант использования Диагностика, статус, быстрые действия Сложные деревья подкоманд, мастера настройки

Защита от конфликтов: если плагин пытается зарегистрировать имя, которое конфликтует со встроенной командой (help, model, new и т. д.), регистрация автоматически отклоняется с предупреждением в журнале. Встроенные команды всегда имеют приоритет.

Асинхронные обработчики. Диспетчер шлюза автоматически обнаруживает и ожидает асинхронные обработчики, поэтому вы можете использовать как синхронизирующие, так и асинхронные функции:

async def _handle_check(raw_args: str) -> str:
    result = await some_async_operation()
    return f"Check result: {result}"

def register(ctx):
    ctx.register_command("check", handler=_handle_check, description="Run async check")

:::совет В этом руководстве рассматриваются общие плагины (инструменты, хуки, косая черта, команды CLI). Информацию о специализированных типах плагинов см.: - Плагины поставщика памяти — межсеансовые базы данных. - Плагины контекстного движка — альтернативные стратегии управления контекстом.

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

Чтобы предоставить общий доступ к плагинам, добавьте точку входа в свой пакет Python:

# pyproject.toml
[project.entry-points."hermes_agent.plugins"]
my-plugin = "my_plugin_package"
pip install hermes-plugin-calculator
# Plugin auto-discovered on next hermes startup

Дистрибутив для NixOS

Пользователи NixOS могут установить ваш плагин декларативно, если вы предоставите pyproject.toml с точками входа:

Плагины точки входа (рекомендуются к распространению):

# User's configuration.nix
services.hermes-agent.extraPythonPackages = [
  (pkgs.python312Packages.buildPythonPackage {
    pname = "my-plugin";
    version = "1.0.0";
    src = pkgs.fetchFromGitHub {
      owner = "you";
      repo = "hermes-my-plugin";
      rev = "v1.0.0";
      hash = "sha256-...";  # nix-prefetch-url --unpack
    };
    format = "pyproject";
    build-system = [ pkgs.python312Packages.setuptools ];
  })
];

Плагины каталогов (pyproject.toml не требуется):

services.hermes-agent.extraPlugins = [
  (pkgs.fetchFromGitHub {
    owner = "you";
    repo = "hermes-my-plugin";
    rev = "v1.0.0";
    hash = "sha256-...";
  })
];

См. Руководство по настройке Nix для получения полной документации, включая использование наложения и проверку коллизий.

Распространенные ошибки

Обработчик не возвращает строку JSON:

# Wrong — returns a dict
def handler(args, **kwargs):
    return {"result": 42}

# Right — returns a JSON string
def handler(args, **kwargs):
    return json.dumps({"result": 42})

Отсутствует **kwargs в подписи обработчика:

# Wrong — will break if Hermes passes extra context
def handler(args):
    ...

# Right
def handler(args, **kwargs):
    ...

Обработчик вызывает исключения:

# Wrong — exception propagates, tool call fails
def handler(args, **kwargs):
    result = 1 / int(args["value"])  # ZeroDivisionError!
    return json.dumps({"result": result})

# Right — catch and return error JSON
def handler(args, **kwargs):
    try:
        result = 1 / int(args.get("value", 0))
        return json.dumps({"result": result})
    except Exception as e:
        return json.dumps({"error": str(e)})

Описание схемы слишком расплывчато:

# Bad — model doesn't know when to use it
"description": "Does stuff"

# Good — model knows exactly when and how
"description": "Evaluate a mathematical expression. Use for arithmetic, trig, logarithms. Supports: +, -, *, /, **, sqrt, sin, cos, log, pi, e."