mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat: final platform plugin parity — webhook delivery, platform hints, docs
Closes remaining functional gaps and adds documentation.
## Functional fixes
webhook.py: Cross-platform delivery now checks the plugin registry
for unknown platform names instead of hardcoding 15 names in a tuple.
Plugin platforms can receive webhook-routed deliveries.
prompt_builder: Platform hints (system prompt LLM guidance) now fall
back to the plugin registry's platform_hint field. Plugin platforms
can tell the LLM 'you're on IRC, no markdown.'
PlatformEntry: Added platform_hint field for LLM guidance injection.
IRC adapter: Added acquire_scoped_lock/release_scoped_lock in
connect/disconnect to prevent two profiles from using the same IRC
identity. Added platform_hint for IRC-specific LLM guidance.
Removed dead token-empty-warning extension for plugin platforms
(plugin adapters handle their own env vars via check_fn).
## Documentation
website/docs/developer-guide/adding-platform-adapters.md:
- Added 'Plugin Path (Recommended)' section with full code examples,
PLUGIN.yaml template, config.yaml examples, and a table showing all
18 integration points the plugin system handles automatically
- Renamed built-in checklist to clarify it's for core contributors
gateway/platforms/ADDING_A_PLATFORM.md:
- Added Plugin Path section pointing to the reference implementation
and full docs guide
- Clarified built-in path is for core contributors only
This commit is contained in:
parent
e7fc6450fc
commit
2a304e5de4
8 changed files with 235 additions and 38 deletions
|
|
@ -733,15 +733,6 @@ def load_gateway_config() -> GatewayConfig:
|
|||
if not pconfig.enabled:
|
||||
continue
|
||||
env_name = _token_env_names.get(platform)
|
||||
if not env_name:
|
||||
# Check plugin registry for required_env
|
||||
try:
|
||||
from gateway.platform_registry import platform_registry
|
||||
entry = platform_registry.get(platform.value)
|
||||
if entry and entry.required_env:
|
||||
env_name = entry.required_env[0] # primary env var
|
||||
except Exception:
|
||||
pass
|
||||
if env_name and pconfig.token is not None and not pconfig.token.strip():
|
||||
logger.warning(
|
||||
"%s is enabled but %s is empty. "
|
||||
|
|
|
|||
|
|
@ -89,6 +89,11 @@ class PlatformEntry:
|
|||
# (allows /update command from this platform).
|
||||
allow_update_command: bool = True
|
||||
|
||||
# ── LLM guidance ──
|
||||
# Platform hint injected into the system prompt (e.g. "You are on IRC.
|
||||
# Do not use markdown."). Empty string = no hint.
|
||||
platform_hint: str = ""
|
||||
|
||||
|
||||
class PlatformRegistry:
|
||||
"""Central registry of platform adapters.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,30 @@
|
|||
# Adding a New Messaging Platform
|
||||
|
||||
Checklist for integrating a new messaging platform into the Hermes gateway.
|
||||
Use this as a reference when building a new adapter — every item here is a
|
||||
real integration point that exists in the codebase. Missing any of them will
|
||||
cause broken functionality, missing features, or inconsistent behavior.
|
||||
There are two ways to add a platform to the Hermes gateway:
|
||||
|
||||
## Plugin Path (Recommended for Community/Third-Party)
|
||||
|
||||
Create a plugin directory in `~/.hermes/plugins/` with a `PLUGIN.yaml` and
|
||||
`adapter.py`. The adapter inherits from `BasePlatformAdapter` and registers
|
||||
via `ctx.register_platform()` in the `register(ctx)` entry point. This
|
||||
requires **zero changes to core Hermes code**.
|
||||
|
||||
The plugin system automatically handles: adapter creation, config parsing,
|
||||
user authorization, cron delivery, send_message routing, system prompt hints,
|
||||
status display, gateway setup, and more.
|
||||
|
||||
See `plugins/platforms/irc/` for a complete reference implementation, and
|
||||
`website/docs/developer-guide/adding-platform-adapters.md` for the full
|
||||
plugin guide with code examples.
|
||||
|
||||
---
|
||||
|
||||
## Built-in Path (Core Contributors Only)
|
||||
|
||||
Checklist for integrating a platform directly into the Hermes core.
|
||||
Use this as a reference when building a built-in adapter — every item here
|
||||
is a real integration point. Missing any of them will cause broken
|
||||
functionality, missing features, or inconsistent behavior.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -186,25 +186,21 @@ class WebhookAdapter(BasePlatformAdapter):
|
|||
if deliver_type == "github_comment":
|
||||
return await self._deliver_github_comment(content, delivery)
|
||||
|
||||
# Cross-platform delivery — any platform with a gateway adapter
|
||||
if self.gateway_runner and deliver_type in (
|
||||
"telegram",
|
||||
"discord",
|
||||
"slack",
|
||||
"signal",
|
||||
"sms",
|
||||
"whatsapp",
|
||||
"matrix",
|
||||
"mattermost",
|
||||
"homeassistant",
|
||||
"email",
|
||||
"dingtalk",
|
||||
"feishu",
|
||||
"wecom",
|
||||
"wecom_callback",
|
||||
"weixin",
|
||||
"bluebubbles",
|
||||
):
|
||||
# Cross-platform delivery — any platform with a gateway adapter.
|
||||
# Check both built-in names and plugin-registered platforms.
|
||||
_BUILTIN_DELIVER_PLATFORMS = {
|
||||
"telegram", "discord", "slack", "signal", "sms", "whatsapp",
|
||||
"matrix", "mattermost", "homeassistant", "email", "dingtalk",
|
||||
"feishu", "wecom", "wecom_callback", "weixin", "bluebubbles",
|
||||
}
|
||||
_is_known_platform = deliver_type in _BUILTIN_DELIVER_PLATFORMS
|
||||
if not _is_known_platform:
|
||||
try:
|
||||
from gateway.platform_registry import platform_registry
|
||||
_is_known_platform = platform_registry.is_registered(deliver_type)
|
||||
except Exception:
|
||||
pass
|
||||
if self.gateway_runner and _is_known_platform:
|
||||
return await self._deliver_cross_platform(
|
||||
deliver_type, content, delivery
|
||||
)
|
||||
|
|
|
|||
|
|
@ -151,6 +151,18 @@ class IRCAdapter(BasePlatformAdapter):
|
|||
)
|
||||
return False
|
||||
|
||||
# Prevent two profiles from using the same IRC identity
|
||||
try:
|
||||
from gateway.status import acquire_scoped_lock, release_scoped_lock
|
||||
lock_key = f"{self.server}:{self.nickname}"
|
||||
if not acquire_scoped_lock("irc", lock_key):
|
||||
logger.error("IRC: %s@%s already in use by another profile", self.nickname, self.server)
|
||||
self._set_fatal_error("lock_conflict", "IRC identity in use by another profile", retryable=False)
|
||||
return False
|
||||
self._lock_key = lock_key
|
||||
except ImportError:
|
||||
self._lock_key = None # status module not available (e.g. tests)
|
||||
|
||||
try:
|
||||
ssl_ctx = None
|
||||
if self.use_tls:
|
||||
|
|
@ -197,6 +209,13 @@ class IRCAdapter(BasePlatformAdapter):
|
|||
|
||||
async def disconnect(self) -> None:
|
||||
"""Quit and close the connection."""
|
||||
# Release the scoped lock so another profile can use this identity
|
||||
if getattr(self, "_lock_key", None):
|
||||
try:
|
||||
from gateway.status import release_scoped_lock
|
||||
release_scoped_lock("irc", self._lock_key)
|
||||
except Exception:
|
||||
pass
|
||||
self._mark_disconnected()
|
||||
if self._writer and not self._writer.is_closing():
|
||||
try:
|
||||
|
|
@ -500,4 +519,12 @@ def register(ctx):
|
|||
# IRC doesn't have phone numbers to redact
|
||||
pii_safe=False,
|
||||
allow_update_command=True,
|
||||
# LLM guidance
|
||||
platform_hint=(
|
||||
"You are chatting via IRC. IRC does not support markdown formatting "
|
||||
"— use plain text only. Messages are limited to ~450 characters per "
|
||||
"line (long messages are automatically split). In channels, users "
|
||||
"address you by prefixing your nick. Keep responses concise and "
|
||||
"conversational."
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3181,6 +3181,15 @@ class AIAgent:
|
|||
platform_key = (self.platform or "").lower().strip()
|
||||
if platform_key in PLATFORM_HINTS:
|
||||
prompt_parts.append(PLATFORM_HINTS[platform_key])
|
||||
elif platform_key:
|
||||
# Check plugin registry for platform-specific LLM guidance
|
||||
try:
|
||||
from gateway.platform_registry import platform_registry
|
||||
_entry = platform_registry.get(platform_key)
|
||||
if _entry and _entry.platform_hint:
|
||||
prompt_parts.append(_entry.platform_hint)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return "\n\n".join(p.strip() for p in prompt_parts if p.strip())
|
||||
|
||||
|
|
|
|||
|
|
@ -335,10 +335,11 @@ class TestChannelDirectory(unittest.TestCase):
|
|||
"""Verify email in channel directory session-based discovery."""
|
||||
|
||||
def test_email_in_session_discovery(self):
|
||||
import gateway.channel_directory
|
||||
import inspect
|
||||
source = inspect.getsource(gateway.channel_directory.build_channel_directory)
|
||||
self.assertIn('"email"', source)
|
||||
"""Email is discovered via Platform enum iteration in build_channel_directory."""
|
||||
from gateway.config import Platform
|
||||
# Email is a built-in Platform member, so it's included in
|
||||
# `for plat in Platform:` iteration inside build_channel_directory.
|
||||
self.assertIn(Platform.EMAIL, Platform.__members__.values())
|
||||
|
||||
|
||||
class TestGatewaySetup(unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ sidebar_position: 9
|
|||
This guide covers adding a new messaging platform to the Hermes gateway. A platform adapter connects Hermes to an external messaging service (Telegram, Discord, WeCom, etc.) so users can interact with the agent through that service.
|
||||
|
||||
:::tip
|
||||
Adding a platform adapter touches 20+ files across code, config, and docs. Use this guide as a checklist — the adapter file itself is typically only 40% of the work.
|
||||
There are two ways to add a platform:
|
||||
- **Plugin** (recommended for community/third-party): Drop a plugin directory into `~/.hermes/plugins/` — zero core code changes needed. See [Plugin Path](#plugin-path-recommended) below.
|
||||
- **Built-in**: Modify 20+ files across code, config, and docs. Use the [Built-in Checklist](#step-by-step-checklist) below.
|
||||
:::
|
||||
|
||||
## Architecture Overview
|
||||
|
|
@ -26,7 +28,152 @@ Every adapter extends `BasePlatformAdapter` from `gateway/platforms/base.py` and
|
|||
|
||||
Inbound messages are received by the adapter and forwarded via `self.handle_message(event)`, which the base class routes to the gateway runner.
|
||||
|
||||
## Step-by-Step Checklist
|
||||
## Plugin Path (Recommended)
|
||||
|
||||
The plugin system lets you add a platform adapter without modifying any core Hermes code. Your plugin is a directory with two files:
|
||||
|
||||
```
|
||||
~/.hermes/plugins/my-platform/
|
||||
PLUGIN.yaml # Plugin metadata
|
||||
adapter.py # Adapter class + register() entry point
|
||||
```
|
||||
|
||||
### PLUGIN.yaml
|
||||
|
||||
```yaml
|
||||
name: my-platform
|
||||
version: 1.0.0
|
||||
description: My custom messaging platform adapter
|
||||
requires_env:
|
||||
- MY_PLATFORM_TOKEN
|
||||
- MY_PLATFORM_CHANNEL
|
||||
```
|
||||
|
||||
### adapter.py
|
||||
|
||||
```python
|
||||
import os
|
||||
from gateway.platforms.base import (
|
||||
BasePlatformAdapter, SendResult, MessageEvent, MessageType,
|
||||
)
|
||||
from gateway.config import Platform, PlatformConfig
|
||||
|
||||
|
||||
class MyPlatformAdapter(BasePlatformAdapter):
|
||||
def __init__(self, config: PlatformConfig):
|
||||
super().__init__(config, Platform("my_platform"))
|
||||
extra = config.extra or {}
|
||||
self.token = os.getenv("MY_PLATFORM_TOKEN") or extra.get("token", "")
|
||||
|
||||
async def connect(self) -> bool:
|
||||
# Connect to the platform API, start listeners
|
||||
self._mark_connected()
|
||||
return True
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
self._mark_disconnected()
|
||||
|
||||
async def send(self, chat_id, content, reply_to=None, metadata=None):
|
||||
# Send message via platform API
|
||||
return SendResult(success=True, message_id="...")
|
||||
|
||||
async def get_chat_info(self, chat_id):
|
||||
return {"name": chat_id, "type": "dm"}
|
||||
|
||||
|
||||
def check_requirements() -> bool:
|
||||
return bool(os.getenv("MY_PLATFORM_TOKEN"))
|
||||
|
||||
|
||||
def validate_config(config) -> bool:
|
||||
extra = getattr(config, "extra", {}) or {}
|
||||
return bool(os.getenv("MY_PLATFORM_TOKEN") or extra.get("token"))
|
||||
|
||||
|
||||
def register(ctx):
|
||||
"""Plugin entry point — called by the Hermes plugin system."""
|
||||
ctx.register_platform(
|
||||
name="my_platform",
|
||||
label="My Platform",
|
||||
adapter_factory=lambda cfg: MyPlatformAdapter(cfg),
|
||||
check_fn=check_requirements,
|
||||
validate_config=validate_config,
|
||||
required_env=["MY_PLATFORM_TOKEN"],
|
||||
install_hint="pip install my-platform-sdk",
|
||||
# Per-platform user authorization env vars
|
||||
allowed_users_env="MY_PLATFORM_ALLOWED_USERS",
|
||||
allow_all_env="MY_PLATFORM_ALLOW_ALL_USERS",
|
||||
# Message length limit for smart chunking (0 = no limit)
|
||||
max_message_length=4000,
|
||||
# LLM guidance injected into system prompt
|
||||
platform_hint=(
|
||||
"You are chatting via My Platform. "
|
||||
"It supports markdown formatting."
|
||||
),
|
||||
# Display
|
||||
emoji="💬",
|
||||
)
|
||||
|
||||
# Optional: register platform-specific tools
|
||||
ctx.register_tool(
|
||||
name="my_platform_search",
|
||||
toolset="my_platform",
|
||||
schema={...},
|
||||
handler=my_search_handler,
|
||||
)
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Users configure the platform in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
gateway:
|
||||
platforms:
|
||||
my_platform:
|
||||
enabled: true
|
||||
extra:
|
||||
token: "..."
|
||||
channel: "#general"
|
||||
```
|
||||
|
||||
Or via environment variables (which the adapter reads in `__init__`).
|
||||
|
||||
### What the Plugin System Handles Automatically
|
||||
|
||||
When you call `ctx.register_platform()`, the following integration points are handled for you — no core code changes needed:
|
||||
|
||||
| Integration point | How it works |
|
||||
|---|---|
|
||||
| Gateway adapter creation | Registry checked before built-in if/elif chain |
|
||||
| Config parsing | `Platform._missing_()` accepts any platform name |
|
||||
| Connected platform validation | Registry `validate_config()` called |
|
||||
| User authorization | `allowed_users_env` / `allow_all_env` checked |
|
||||
| Cron delivery | `Platform()` resolves any registered name |
|
||||
| send_message tool | Routes through live gateway adapter |
|
||||
| Webhook cross-platform delivery | Registry checked for known platforms |
|
||||
| `/update` command access | `allow_update_command` flag |
|
||||
| Channel directory | Plugin platforms included in enumeration |
|
||||
| System prompt hints | `platform_hint` injected into LLM context |
|
||||
| Message chunking | `max_message_length` for smart splitting |
|
||||
| PII redaction | `pii_safe` flag |
|
||||
| `hermes status` | Shows plugin platforms with `(plugin)` tag |
|
||||
| `hermes gateway setup` | Plugin platforms appear in setup menu |
|
||||
| `hermes tools` / `hermes skills` | Plugin platforms in per-platform config |
|
||||
| Token lock (multi-profile) | Use `acquire_scoped_lock()` in your `connect()` |
|
||||
| Orphaned config warning | Descriptive log when plugin is missing |
|
||||
|
||||
### Reference Implementation
|
||||
|
||||
See `plugins/platforms/irc/` in the repo for a complete working example — a full async IRC adapter with zero external dependencies.
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Checklist (Built-in Path)
|
||||
|
||||
:::note
|
||||
This checklist is for adding a platform directly to the Hermes core codebase — typically done by core contributors for officially supported platforms. Community/third-party platforms should use the [Plugin Path](#plugin-path-recommended) above.
|
||||
:::
|
||||
|
||||
### 1. Platform Enum
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue