mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-01 00:11:39 +08:00
Compare commits
1 Commits
skill/gith
...
hermes/her
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
417eccc054 |
@@ -603,9 +603,19 @@ class MattermostAdapter(BasePlatformAdapter):
|
|||||||
# For DMs, user_id is sufficient. For channels, check for @mention.
|
# For DMs, user_id is sufficient. For channels, check for @mention.
|
||||||
message_text = post.get("message", "")
|
message_text = post.get("message", "")
|
||||||
|
|
||||||
# Mention-only mode: skip channel messages that don't @mention the bot.
|
# Mention-gating for non-DM channels.
|
||||||
# DMs (type "D") are always processed.
|
# Config (env vars):
|
||||||
|
# MATTERMOST_REQUIRE_MENTION: Require @mention in channels (default: true)
|
||||||
|
# MATTERMOST_FREE_RESPONSE_CHANNELS: Channel IDs where bot responds without mention
|
||||||
if channel_type_raw != "D":
|
if channel_type_raw != "D":
|
||||||
|
require_mention = os.getenv(
|
||||||
|
"MATTERMOST_REQUIRE_MENTION", "true"
|
||||||
|
).lower() not in ("false", "0", "no")
|
||||||
|
|
||||||
|
free_channels_raw = os.getenv("MATTERMOST_FREE_RESPONSE_CHANNELS", "")
|
||||||
|
free_channels = {ch.strip() for ch in free_channels_raw.split(",") if ch.strip()}
|
||||||
|
is_free_channel = channel_id in free_channels
|
||||||
|
|
||||||
mention_patterns = [
|
mention_patterns = [
|
||||||
f"@{self._bot_username}",
|
f"@{self._bot_username}",
|
||||||
f"@{self._bot_user_id}",
|
f"@{self._bot_user_id}",
|
||||||
@@ -614,13 +624,21 @@ class MattermostAdapter(BasePlatformAdapter):
|
|||||||
pattern.lower() in message_text.lower()
|
pattern.lower() in message_text.lower()
|
||||||
for pattern in mention_patterns
|
for pattern in mention_patterns
|
||||||
)
|
)
|
||||||
if not has_mention:
|
|
||||||
|
if require_mention and not is_free_channel and not has_mention:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Mattermost: skipping non-DM message without @mention (channel=%s)",
|
"Mattermost: skipping non-DM message without @mention (channel=%s)",
|
||||||
channel_id,
|
channel_id,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Strip @mention from the message text so the agent sees clean input.
|
||||||
|
if has_mention:
|
||||||
|
for pattern in mention_patterns:
|
||||||
|
message_text = re.sub(
|
||||||
|
re.escape(pattern), "", message_text, flags=re.IGNORECASE
|
||||||
|
).strip()
|
||||||
|
|
||||||
# Resolve sender info.
|
# Resolve sender info.
|
||||||
sender_id = post.get("user_id", "")
|
sender_id = post.get("user_id", "")
|
||||||
sender_name = data.get("sender_name", "").lstrip("@") or sender_id
|
sender_name = data.get("sender_name", "").lstrip("@") or sender_id
|
||||||
|
|||||||
@@ -817,6 +817,20 @@ OPTIONAL_ENV_VARS = {
|
|||||||
"password": False,
|
"password": False,
|
||||||
"category": "messaging",
|
"category": "messaging",
|
||||||
},
|
},
|
||||||
|
"MATTERMOST_REQUIRE_MENTION": {
|
||||||
|
"description": "Require @mention in Mattermost channels (default: true). Set to false to respond to all messages.",
|
||||||
|
"prompt": "Require @mention in channels",
|
||||||
|
"url": None,
|
||||||
|
"password": False,
|
||||||
|
"category": "messaging",
|
||||||
|
},
|
||||||
|
"MATTERMOST_FREE_RESPONSE_CHANNELS": {
|
||||||
|
"description": "Comma-separated Mattermost channel IDs where bot responds without @mention",
|
||||||
|
"prompt": "Free-response channel IDs (comma-separated)",
|
||||||
|
"url": None,
|
||||||
|
"password": False,
|
||||||
|
"category": "messaging",
|
||||||
|
},
|
||||||
"MATRIX_HOMESERVER": {
|
"MATRIX_HOMESERVER": {
|
||||||
"description": "Matrix homeserver URL (e.g. https://matrix.example.org)",
|
"description": "Matrix homeserver URL (e.g. https://matrix.example.org)",
|
||||||
"prompt": "Matrix homeserver URL",
|
"prompt": "Matrix homeserver URL",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Tests for Mattermost platform adapter."""
|
"""Tests for Mattermost platform adapter."""
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import MagicMock, patch, AsyncMock
|
from unittest.mock import MagicMock, patch, AsyncMock
|
||||||
@@ -269,6 +270,7 @@ class TestMattermostWebSocketParsing:
|
|||||||
def setup_method(self):
|
def setup_method(self):
|
||||||
self.adapter = _make_adapter()
|
self.adapter = _make_adapter()
|
||||||
self.adapter._bot_user_id = "bot_user_id"
|
self.adapter._bot_user_id = "bot_user_id"
|
||||||
|
self.adapter._bot_username = "hermes-bot"
|
||||||
# Mock handle_message to capture the MessageEvent without processing
|
# Mock handle_message to capture the MessageEvent without processing
|
||||||
self.adapter.handle_message = AsyncMock()
|
self.adapter.handle_message = AsyncMock()
|
||||||
|
|
||||||
@@ -293,7 +295,8 @@ class TestMattermostWebSocketParsing:
|
|||||||
await self.adapter._handle_ws_event(event)
|
await self.adapter._handle_ws_event(event)
|
||||||
assert self.adapter.handle_message.called
|
assert self.adapter.handle_message.called
|
||||||
msg_event = self.adapter.handle_message.call_args[0][0]
|
msg_event = self.adapter.handle_message.call_args[0][0]
|
||||||
assert msg_event.text == "@bot_user_id Hello from Matrix!"
|
# @mention is stripped from the message text
|
||||||
|
assert msg_event.text == "Hello from Matrix!"
|
||||||
assert msg_event.message_id == "post_abc"
|
assert msg_event.message_id == "post_abc"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -410,6 +413,87 @@ class TestMattermostWebSocketParsing:
|
|||||||
assert not self.adapter.handle_message.called
|
assert not self.adapter.handle_message.called
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Mention behavior (require_mention + free_response_channels)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestMattermostMentionBehavior:
|
||||||
|
def setup_method(self):
|
||||||
|
self.adapter = _make_adapter()
|
||||||
|
self.adapter._bot_user_id = "bot_user_id"
|
||||||
|
self.adapter._bot_username = "hermes-bot"
|
||||||
|
self.adapter.handle_message = AsyncMock()
|
||||||
|
|
||||||
|
def _make_event(self, message, channel_type="O", channel_id="chan_456"):
|
||||||
|
post_data = {
|
||||||
|
"id": "post_mention",
|
||||||
|
"user_id": "user_123",
|
||||||
|
"channel_id": channel_id,
|
||||||
|
"message": message,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"event": "posted",
|
||||||
|
"data": {
|
||||||
|
"post": json.dumps(post_data),
|
||||||
|
"channel_type": channel_type,
|
||||||
|
"sender_name": "@alice",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_require_mention_true_skips_without_mention(self):
|
||||||
|
"""Default: messages without @mention in channels are skipped."""
|
||||||
|
with patch.dict(os.environ, {}, clear=False):
|
||||||
|
os.environ.pop("MATTERMOST_REQUIRE_MENTION", None)
|
||||||
|
os.environ.pop("MATTERMOST_FREE_RESPONSE_CHANNELS", None)
|
||||||
|
await self.adapter._handle_ws_event(self._make_event("hello"))
|
||||||
|
assert not self.adapter.handle_message.called
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_require_mention_false_responds_to_all(self):
|
||||||
|
"""MATTERMOST_REQUIRE_MENTION=false: respond to all channel messages."""
|
||||||
|
with patch.dict(os.environ, {"MATTERMOST_REQUIRE_MENTION": "false"}):
|
||||||
|
await self.adapter._handle_ws_event(self._make_event("hello"))
|
||||||
|
assert self.adapter.handle_message.called
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_free_response_channel_responds_without_mention(self):
|
||||||
|
"""Messages in free-response channels don't need @mention."""
|
||||||
|
with patch.dict(os.environ, {"MATTERMOST_FREE_RESPONSE_CHANNELS": "chan_456,chan_789"}):
|
||||||
|
os.environ.pop("MATTERMOST_REQUIRE_MENTION", None)
|
||||||
|
await self.adapter._handle_ws_event(self._make_event("hello", channel_id="chan_456"))
|
||||||
|
assert self.adapter.handle_message.called
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_non_free_channel_still_requires_mention(self):
|
||||||
|
"""Channels NOT in free-response list still require @mention."""
|
||||||
|
with patch.dict(os.environ, {"MATTERMOST_FREE_RESPONSE_CHANNELS": "chan_789"}):
|
||||||
|
os.environ.pop("MATTERMOST_REQUIRE_MENTION", None)
|
||||||
|
await self.adapter._handle_ws_event(self._make_event("hello", channel_id="chan_456"))
|
||||||
|
assert not self.adapter.handle_message.called
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dm_always_responds(self):
|
||||||
|
"""DMs (channel_type=D) always respond regardless of mention settings."""
|
||||||
|
with patch.dict(os.environ, {}, clear=False):
|
||||||
|
os.environ.pop("MATTERMOST_REQUIRE_MENTION", None)
|
||||||
|
await self.adapter._handle_ws_event(self._make_event("hello", channel_type="D"))
|
||||||
|
assert self.adapter.handle_message.called
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_mention_stripped_from_text(self):
|
||||||
|
"""@mention is stripped from message text."""
|
||||||
|
with patch.dict(os.environ, {}, clear=False):
|
||||||
|
os.environ.pop("MATTERMOST_REQUIRE_MENTION", None)
|
||||||
|
await self.adapter._handle_ws_event(
|
||||||
|
self._make_event("@hermes-bot what is 2+2")
|
||||||
|
)
|
||||||
|
assert self.adapter.handle_message.called
|
||||||
|
msg = self.adapter.handle_message.call_args[0][0]
|
||||||
|
assert "@hermes-bot" not in msg.text
|
||||||
|
assert "2+2" in msg.text
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# File upload (send_image)
|
# File upload (send_image)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -200,6 +200,8 @@ For native Anthropic auth, Hermes prefers Claude Code's own credential files whe
|
|||||||
| `MATTERMOST_TOKEN` | Bot token or personal access token for Mattermost |
|
| `MATTERMOST_TOKEN` | Bot token or personal access token for Mattermost |
|
||||||
| `MATTERMOST_ALLOWED_USERS` | Comma-separated Mattermost user IDs allowed to message the bot |
|
| `MATTERMOST_ALLOWED_USERS` | Comma-separated Mattermost user IDs allowed to message the bot |
|
||||||
| `MATTERMOST_HOME_CHANNEL` | Channel ID for proactive message delivery (cron, notifications) |
|
| `MATTERMOST_HOME_CHANNEL` | Channel ID for proactive message delivery (cron, notifications) |
|
||||||
|
| `MATTERMOST_REQUIRE_MENTION` | Require `@mention` in channels (default: `true`). Set to `false` to respond to all messages. |
|
||||||
|
| `MATTERMOST_FREE_RESPONSE_CHANNELS` | Comma-separated channel IDs where bot responds without `@mention` |
|
||||||
| `MATTERMOST_REPLY_MODE` | Reply style: `thread` (threaded replies) or `off` (flat messages, default) |
|
| `MATTERMOST_REPLY_MODE` | Reply style: `thread` (threaded replies) or `off` (flat messages, default) |
|
||||||
| `MATRIX_HOMESERVER` | Matrix homeserver URL (e.g. `https://matrix.org`) |
|
| `MATRIX_HOMESERVER` | Matrix homeserver URL (e.g. `https://matrix.org`) |
|
||||||
| `MATRIX_ACCESS_TOKEN` | Matrix access token for bot authentication |
|
| `MATRIX_ACCESS_TOKEN` | Matrix access token for bot authentication |
|
||||||
|
|||||||
@@ -149,6 +149,12 @@ MATTERMOST_ALLOWED_USERS=3uo8dkh1p7g1mfk49ear5fzs5c
|
|||||||
|
|
||||||
# Optional: reply mode (thread or off, default: off)
|
# Optional: reply mode (thread or off, default: off)
|
||||||
# MATTERMOST_REPLY_MODE=thread
|
# MATTERMOST_REPLY_MODE=thread
|
||||||
|
|
||||||
|
# Optional: respond without @mention (default: true = require mention)
|
||||||
|
# MATTERMOST_REQUIRE_MENTION=false
|
||||||
|
|
||||||
|
# Optional: channels where bot responds without @mention (comma-separated channel IDs)
|
||||||
|
# MATTERMOST_FREE_RESPONSE_CHANNELS=channel_id_1,channel_id_2
|
||||||
```
|
```
|
||||||
|
|
||||||
Optional behavior settings in `~/.hermes/config.yaml`:
|
Optional behavior settings in `~/.hermes/config.yaml`:
|
||||||
@@ -206,6 +212,19 @@ Set it in your `~/.hermes/.env`:
|
|||||||
MATTERMOST_REPLY_MODE=thread
|
MATTERMOST_REPLY_MODE=thread
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Mention Behavior
|
||||||
|
|
||||||
|
By default, the bot only responds in channels when `@mentioned`. You can change this:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `MATTERMOST_REQUIRE_MENTION` | `true` | Set to `false` to respond to all messages in channels (DMs always work). |
|
||||||
|
| `MATTERMOST_FREE_RESPONSE_CHANNELS` | _(none)_ | Comma-separated channel IDs where the bot responds without `@mention`, even when require_mention is true. |
|
||||||
|
|
||||||
|
To find a channel ID in Mattermost: open the channel, click the channel name header, and look for the ID in the URL or channel details.
|
||||||
|
|
||||||
|
When the bot is `@mentioned`, the mention is automatically stripped from the message before processing.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Bot is not responding to messages
|
### Bot is not responding to messages
|
||||||
|
|||||||
Reference in New Issue
Block a user