--- sidebar_position: 9 --- # Adding a Platform Adapter 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. ::: ## Architecture Overview ``` User ↔ Messaging Platform ↔ Platform Adapter ↔ Gateway Runner ↔ AIAgent ``` Every adapter extends `BasePlatformAdapter` from `gateway/platforms/base.py` and implements: - **`connect()`** — Establish connection (WebSocket, long-poll, HTTP server, etc.) - **`disconnect()`** — Clean shutdown - **`send()`** — Send a text message to a chat - **`send_typing()`** — Show typing indicator (optional) - **`get_chat_info()`** — Return chat metadata 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 ### 1. Platform Enum Add your platform to the `Platform` enum in `gateway/config.py`: ```python class Platform(str, Enum): # ... existing platforms ... NEWPLAT = "newplat" ``` ### 2. Adapter File Create `gateway/platforms/newplat.py`: ```python from gateway.config import Platform, PlatformConfig from gateway.platforms.base import ( BasePlatformAdapter, MessageEvent, MessageType, SendResult, ) def check_newplat_requirements() -> bool: """Return True if dependencies are available.""" return SOME_SDK_AVAILABLE class NewPlatAdapter(BasePlatformAdapter): def __init__(self, config: PlatformConfig): super().__init__(config, Platform.NEWPLAT) # Read config from config.extra dict extra = config.extra or {} self._api_key = extra.get("api_key") or os.getenv("NEWPLAT_API_KEY", "") async def connect(self) -> bool: # Set up connection, start polling/webhook self._mark_connected() return True async def disconnect(self) -> None: self._running = False 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"} ``` For inbound messages, build a `MessageEvent` and call `self.handle_message(event)`: ```python source = self.build_source( chat_id=chat_id, chat_name=name, chat_type="dm", # or "group" user_id=user_id, user_name=user_name, ) event = MessageEvent( text=content, message_type=MessageType.TEXT, source=source, message_id=msg_id, ) await self.handle_message(event) ``` ### 3. Gateway Config (`gateway/config.py`) Three touchpoints: 1. **`get_connected_platforms()`** — Add a check for your platform's required credentials 2. **`load_gateway_config()`** — Add token env map entry: `Platform.NEWPLAT: "NEWPLAT_TOKEN"` 3. **`_apply_env_overrides()`** — Map all `NEWPLAT_*` env vars to config ### 4. Gateway Runner (`gateway/run.py`) Five touchpoints: 1. **`_create_adapter()`** — Add an `elif platform == Platform.NEWPLAT:` branch 2. **`_is_user_authorized()` allowed_users map** — `Platform.NEWPLAT: "NEWPLAT_ALLOWED_USERS"` 3. **`_is_user_authorized()` allow_all map** — `Platform.NEWPLAT: "NEWPLAT_ALLOW_ALL_USERS"` 4. **Early env check `_any_allowlist` tuple** — Add `"NEWPLAT_ALLOWED_USERS"` 5. **Early env check `_allow_all` tuple** — Add `"NEWPLAT_ALLOW_ALL_USERS"` 6. **`_UPDATE_ALLOWED_PLATFORMS` frozenset** — Add `Platform.NEWPLAT` ### 5. Cross-Platform Delivery 1. **`gateway/platforms/webhook.py`** — Add `"newplat"` to the delivery type tuple 2. **`cron/scheduler.py`** — Add to `_KNOWN_DELIVERY_PLATFORMS` frozenset and `_deliver_result()` platform map ### 6. CLI Integration 1. **`hermes_cli/config.py`** — Add all `NEWPLAT_*` vars to `_EXTRA_ENV_KEYS` 2. **`hermes_cli/gateway.py`** — Add entry to `_PLATFORMS` list with key, label, emoji, token_var, setup_instructions, and vars 3. **`hermes_cli/platforms.py`** — Add `PlatformInfo` entry with label and default_toolset (used by `skills_config` and `tools_config` TUIs) 4. **`hermes_cli/setup.py`** — Add `_setup_newplat()` function (can delegate to `gateway.py`) and add tuple to the messaging platforms list 5. **`hermes_cli/status.py`** — Add platform detection entry: `"NewPlat": ("NEWPLAT_TOKEN", "NEWPLAT_HOME_CHANNEL")` 6. **`hermes_cli/dump.py`** — Add `"newplat": "NEWPLAT_TOKEN"` to platform detection dict ### 7. Tools 1. **`tools/send_message_tool.py`** — Add `"newplat": Platform.NEWPLAT` to platform map 2. **`tools/cronjob_tools.py`** — Add `newplat` to the delivery target description string ### 8. Toolsets 1. **`toolsets.py`** — Add `"hermes-newplat"` toolset definition with `_HERMES_CORE_TOOLS` 2. **`toolsets.py`** — Add `"hermes-newplat"` to the `"hermes-gateway"` includes list ### 9. Optional: Platform Hints **`agent/prompt_builder.py`** — If your platform has specific rendering limitations (no markdown, message length limits, etc.), add an entry to the `_PLATFORM_HINTS` dict. This injects platform-specific guidance into the system prompt: ```python _PLATFORM_HINTS = { # ... "newplat": ( "You are chatting via NewPlat. It supports markdown formatting " "but has a 4000-character message limit." ), } ``` Not all platforms need hints — only add one if the agent's behavior should differ. ### 10. Tests Create `tests/gateway/test_newplat.py` covering: - Adapter construction from config - Message event building - Send method (mock the external API) - Platform-specific features (encryption, routing, etc.) ### 11. Documentation | File | What to add | |------|-------------| | `website/docs/user-guide/messaging/newplat.md` | Full platform setup page | | `website/docs/user-guide/messaging/index.md` | Platform comparison table, architecture diagram, toolsets table, security section, next-steps link | | `website/docs/reference/environment-variables.md` | All NEWPLAT_* env vars | | `website/docs/reference/toolsets-reference.md` | hermes-newplat toolset | | `website/docs/integrations/index.md` | Platform link | | `website/sidebars.ts` | Sidebar entry for the docs page | | `website/docs/developer-guide/architecture.md` | Adapter count + listing | | `website/docs/developer-guide/gateway-internals.md` | Adapter file listing | ## Parity Audit Before marking a new platform PR as complete, run a parity audit against an established platform: ```bash # Find every .py file mentioning the reference platform search_files "bluebubbles" output_mode="files_only" file_glob="*.py" # Find every .py file mentioning the new platform search_files "newplat" output_mode="files_only" file_glob="*.py" # Any file in the first set but not the second is a potential gap ``` Repeat for `.md` and `.ts` files. Investigate each gap — is it a platform enumeration (needs updating) or a platform-specific reference (skip)? ## Common Patterns ### Long-Poll Adapters If your adapter uses long-polling (like Telegram or Weixin), use a polling loop task: ```python async def connect(self): self._poll_task = asyncio.create_task(self._poll_loop()) self._mark_connected() async def _poll_loop(self): while self._running: messages = await self._fetch_updates() for msg in messages: await self.handle_message(self._build_event(msg)) ``` ### Callback/Webhook Adapters If the platform pushes messages to your endpoint (like WeCom Callback), run an HTTP server: ```python async def connect(self): self._app = web.Application() self._app.router.add_post("/callback", self._handle_callback) # ... start aiohttp server self._mark_connected() async def _handle_callback(self, request): event = self._build_event(await request.text()) await self._message_queue.put(event) return web.Response(text="success") # Acknowledge immediately ``` For platforms with tight response deadlines (e.g., WeCom's 5-second limit), always acknowledge immediately and deliver the agent's reply proactively via API later. Agent sessions run 3–30 minutes — inline replies within a callback response window are not feasible. ### Token Locks If the adapter holds a persistent connection with a unique credential, add a scoped lock to prevent two profiles from using the same credential: ```python from gateway.status import acquire_scoped_lock, release_scoped_lock async def connect(self): if not acquire_scoped_lock("newplat", self._token): logger.error("Token already in use by another profile") return False # ... connect async def disconnect(self): release_scoped_lock("newplat", self._token) ``` ## Reference Implementations | Adapter | Pattern | Complexity | Good reference for | |---------|---------|------------|-------------------| | `bluebubbles.py` | REST + webhook | Medium | Simple REST API integration | | `weixin.py` | Long-poll + CDN | High | Media handling, encryption | | `wecom_callback.py` | Callback/webhook | Medium | HTTP server, AES crypto, multi-app | | `telegram.py` | Long-poll + Bot API | High | Full-featured adapter with groups, threads |