Chuyển tới nội dung chính

Xây dựng plugin Hermes

Hướng dẫn này hướng dẫn cách xây dựng một plugin Hermes hoàn chỉnh từ đầu. Cuối cùng, bạn sẽ có một plugin hoạt động với nhiều công cụ, móc nối vòng đời, tệp dữ liệu được vận chuyển và kỹ năng đi kèm — mọi thứ mà hệ thống plugin hỗ trợ.

Những gì bạn đang xây dựng

Một plugin máy tính có hai công cụ:

  • tính toán — đánh giá các biểu thức toán học (2**16, sqrt(144), pi * 5**2)
  • unit_convert — chuyển đổi giữa các đơn vị (100 F → 37,78 C, 5 km → 3,11 mi)

Cộng với một cái móc ghi lại mọi lệnh gọi công cụ và một tệp kỹ năng đi kèm.

Bước 1: Tạo thư mục plugin

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

Bước 2: Viết bảng kê khai

Tạo 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

Điều này nói với Hermes: "Tôi là một plugin có tên là máy tính, tôi cung cấp các công cụ và móc nối." Các trường provides_toolsprovides_hooks là danh sách những gì plugin đăng ký.

Các trường tùy chọn bạn có thể thêm:

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

Bước 3: Viết lược đồ công cụ

Tạo schemas.py — đây là những gì LLM đọc để quyết định thời điểm gọi các công cụ của bạn:

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

Tại sao lược đồ lại quan trọng: Trường description là cách LLM quyết định thời điểm sử dụng công cụ của bạn. Hãy cụ thể về những gì nó làm và khi nào sử dụng nó. tham số xác định những đối số mà LLM chuyển qua.

Bước 4: Viết các trình xử lý công cụ

Tạo tools.py — đây là mã thực sự thực thi khi LLM gọi các công cụ của bạn:

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

Các quy tắc chính dành cho người xử lý:

  1. Chữ ký: def my_handler(args: dict, **kwargs) -> str
  2. Trả về: Luôn là chuỗi JSON. Thành công và sai sót như nhau.
  3. Không bao giờ tăng: Bắt tất cả các ngoại lệ, thay vào đó hãy trả về lỗi JSON.
  4. Chấp nhận **kwargs: Hermes có thể chuyển ngữ cảnh bổ sung trong tương lai.

Bước 5: Viết thông tin đăng ký

Tạo __init__.py — điều này nối các lược đồ với các trình xử lý:

"""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()` làm gì:

  • Được gọi đúng 1 lần khi khởi động
  • ctx.register_tool() đưa công cụ của bạn vào sổ đăng ký — mô hình sẽ thấy nó ngay lập tức
  • ctx.register_hook() đăng ký các sự kiện vòng đời
  • ctx.register_cli_command() đăng ký một lệnh phụ CLI (ví dụ: hermes my-plugin <subcommand>)
  • Nếu chức năng này gặp sự cố, plugin bị vô hiệu hóa nhưng Hermes vẫn hoạt động bình thường

Bước 6: Kiểm tra

Bắt đầu Hermes:

hermes

Bạn sẽ thấy máy tính: tính toán, unit_convert trong danh sách công cụ của biểu ngữ.

Hãy thử những lời nhắc sau:

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?

Kiểm tra trạng thái plugin:

/plugins

Đầu ra:

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

Cấu trúc cuối cùng của plugin của bạn

~/.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)

Bốn tập tin, tách biệt rõ ràng:

  • Manifest khai báo plugin là gì
  • Lược đồ mô tả các công cụ dành cho LLM
  • Trình xử lý triển khai logic thực tế
  • Đăng ký kết nối mọi thứ

Plugin có thể làm gì khác?

Gửi tệp dữ liệu

Đặt bất kỳ tệp nào vào thư mục plugin của bạn và đọc chúng tại thời điểm nhập:

# 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)

Kết hợp một kỹ năng

Bao gồm tệp skill.md và cài đặt nó trong quá trình đăng ký:

import shutil
from pathlib import Path

def _install_skill():
"""Copy our skill to ~/.hermes/skills/ on first load."""
try:
from hermes_cli.config import get_hermes_home
dest = get_hermes_home() / "skills" / "my-plugin" / "SKILL.md"
except Exception:
dest = Path.home() / ".hermes" / "skills" / "my-plugin" / "SKILL.md"

if dest.exists():
return # don't overwrite user edits

source = Path(__file__).parent / "skill.md"
if source.exists():
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, dest)

def register(ctx):
ctx.register_tool(...)
_install_skill()

Cổng vào các biến môi trường

Nếu plugin của bạn cần khóa API:

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

Nếu WEATHER_API_KEY không được đặt, plugin sẽ bị tắt kèm theo thông báo rõ ràng. Không có sự cố, không có lỗi trong tác nhân — chỉ là "Plugin Weather bị vô hiệu hóa (thiếu: WEATHER_API_KEY)".

Khi người dùng chạy cài đặt plugin hermes, họ được nhắc tương tác về bất kỳ biến requires_env nào bị thiếu. Các giá trị được lưu vào .env một cách tự động.

Để có trải nghiệm cài đặt tốt hơn, hãy sử dụng định dạng phong phú có mô tả và URL đăng ký:

# plugin.yaml — rich format
requires_env:
- name: WEATHER_API_KEY
description: "API key for OpenWeather"
url: "https://openweathermap.org/api"
secret: true
Lĩnh vựcBắt buộcMô tả
tênTên biến môi trường
mô tảKhôngHiển thị cho người dùng trong khi nhắc cài đặt
urlKhôngLấy chứng chỉ ở đâu
bí mậtKhôngNếu true, dữ liệu nhập bị ẩn (như trường mật khẩu)

Cả hai định dạng có thể được trộn lẫn trong cùng một danh sách. Các biến đã được đặt sẽ được bỏ qua một cách âm thầm.

Tính khả dụng của công cụ có điều kiện

Đối với các công cụ phụ thuộc vào thư viện tùy chọn:

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

Đăng ký nhiều hook

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)

Tham khảo móc

Mỗi hook đều được ghi lại đầy đủ trên Event Hooks reference — chữ ký gọi lại, bảng tham số, chính xác thời điểm mỗi hook kích hoạt và ví dụ. Đây là bản tóm tắt:

MócKích hoạt khiChữ ký gọi lạiTrả về
pre_tool_callTrước khi bất kỳ công cụ nào thực thitool_name: str, args: dict, task_id: strbỏ qua
post_tool_callSau khi bất kỳ công cụ nào trả vềtool_name: str, args: dict, result: str, task_id: strbỏ qua
pre_llm_callMỗi lượt một lần, trước vòng lặp gọi công cụsession_id: str, user_message: str, Conversation_history: list, is_first_turn: bool, model: str, platform: strchèn ngữ cảnh
post_llm_callMột lần trong lượt, sau vòng gọi công cụ (chỉ những lượt thành công)session_id: str, user_message: str, Assistant_response: str, Conversation_history: list, model: str, platform: strbỏ qua
on_session_startĐã tạo phiên mới (chỉ lượt đầu tiên)session_id: str, model: str, platform: strbỏ qua
on_session_endKết thúc mỗi cuộc gọi run_conversation + thoát CLIsession_id: str, đã hoàn thành: bool, bị gián đoạn: bool, model: str, platform: strbỏ qua
pre_api_requestTrước mỗi yêu cầu HTTP tới nhà cung cấp LLMphương thức: str, url: str, tiêu đề: dict, body: dictbỏ qua
post_api_requestSau mỗi phản hồi HTTP từ nhà cung cấp LLMphương thức: str, url: str, status_code: int, phản hồi: dictbỏ qua

Hầu hết các hook đều có chức năng quan sát kiểu fire-and-forget — giá trị trả về của chúng bị bỏ qua. Ngoại lệ là pre_llm_call, có thể đưa ngữ cảnh vào cuộc trò chuyện.

Tất cả lệnh gọi lại phải chấp nhận **kwargs để tương thích về sau. Nếu một lệnh gọi lại hook gặp sự cố, nó sẽ được ghi lại và bị bỏ qua. Các hook khác và đại lý vẫn tiếp tục bình thường.

chèn ngữ cảnh pre_llm_call

Đây là hook duy nhất có giá trị trả về quan trọng. Khi lệnh gọi lại pre_llm_call trả về một lệnh có khóa "ngữ cảnh" (hoặc một chuỗi đơn giản), Hermes sẽ đưa văn bản đó vào thông báo người dùng của lượt hiện tại. Đây là cơ chế dành cho các plugin bộ nhớ, tích hợp RAG, lan can và bất kỳ plugin nào cần cung cấp ngữ cảnh bổ sung cho mô hình.

Định dạng trả về

# 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

Bất kỳ kết quả trả về không trống, không trống nào có khóa "ngữ cảnh" (hoặc một chuỗi đơn giản không trống) đều được thu thập và thêm vào thông báo người dùng cho lượt hiện tại.

Cách hoạt động của quá trình tiêm

Ngữ cảnh được chèn sẽ được thêm vào thông báo của người dùng chứ không phải lời nhắc của hệ thống. Đây là một sự lựa chọn thiết kế có chủ ý:

  • Bảo quản bộ nhớ đệm theo lời nhắc — lời nhắc của hệ thống vẫn giống nhau qua các lượt. Anthropic và OpenRouter lưu vào bộ nhớ đệm tiền tố lời nhắc của hệ thống, do đó, việc duy trì ổn định sẽ tiết kiệm hơn 75% mã thông báo đầu vào trong các cuộc hội thoại nhiều lượt. Nếu các plugin sửa đổi lời nhắc hệ thống thì mỗi lượt sẽ là một lỗi bộ đệm.
  • Tạm thời — việc tiêm chỉ diễn ra tại thời điểm gọi API. Tin nhắn ban đầu của người dùng trong lịch sử hội thoại không bao giờ bị thay đổi và không có gì được lưu lại trong cơ sở dữ liệu phiên.
  • Lời nhắc hệ thống là lãnh thổ của Hermes — nó chứa hướng dẫn dành riêng cho từng mô hình, quy tắc thực thi công cụ, hướng dẫn tính cách và nội dung kỹ năng được lưu trong bộ nhớ đệm. Các plugin đóng góp ngữ cảnh cùng với thông tin đầu vào của người dùng chứ không phải bằng cách thay đổi các hướng dẫn cốt lõi của tác nhân.

Ví dụ: Plugin thu hồi bộ nhớ

"""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)

Ví dụ: Plugin 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)

Ví dụ: Hook chỉ dành cho người quan sát (không tiêm)

"""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)

Nhiều plugin trả về ngữ cảnh

Khi nhiều plugin trả về ngữ cảnh từ pre_llm_call, kết quả đầu ra của chúng sẽ được nối với hai dòng mới và được thêm vào thông báo của người dùng. Thứ tự tuân theo thứ tự khám phá plugin (theo bảng chữ cái theo tên thư mục plugin).

Đăng ký lệnh CLI

Các plugin có thể thêm cây lệnh phụ hermes <plugin> của riêng chúng:

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,
)

Sau khi đăng ký, người dùng có thể chạy hermes my-plugin status, hermes my-plugin config, v.v.

Các plugin của nhà cung cấp bộ nhớ thay vào đó sử dụng cách tiếp cận dựa trên quy ước: thêm hàm register_cli(subparser) vào tệp cli.py của plugin. Hệ thống phát hiện plugin bộ nhớ tự động tìm thấy nó - không cần lệnh gọi ctx.register_cli_command(). Xem Hướng dẫn Plugin nhà cung cấp bộ nhớ để biết chi tiết.

Gating nhà cung cấp hoạt động: Các lệnh CLI của plugin bộ nhớ chỉ xuất hiện khi nhà cung cấp của họ là memory.provider đang hoạt động trong config. Nếu người dùng chưa thiết lập nhà cung cấp của bạn, các lệnh CLI của bạn sẽ không làm lộn xộn kết quả trợ giúp.

Phân phối qua pip

Để chia sẻ plugin công khai, hãy thêm điểm vào gói Python của bạn:

# 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

##Những lỗi thường gặp

Trình xử lý không trả về chuỗi 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})

Thiếu **kwargs trong chữ ký trình xử lý:

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

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

Trình xử lý đưa ra các ngoại lệ:

# 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)})

Mô tả lược đồ quá mơ hồ:

# 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."