mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat(gateway): add require_mention_channels for per-channel mention overrides
Adds a new `require_mention_channels` config key (and corresponding env vars) across all 7 gateway platforms that have mention-gating. This is the inverse of `free_response_channels` — channels listed here always require @mention even when the global `require_mention` setting is false. Use case: a user runs multiple channels, most with a single agent where no mention is needed, but a few 'agent group' channels where mentions make sense to avoid noise. Previously this required require_mention=true globally and listing every non-group channel in free_response_channels. Priority logic (highest wins): 1. DMs → always respond 2. Channel in free_response_channels → never require mention 3. Channel in require_mention_channels → always require mention 4. Global require_mention setting → fallback Also refactors: - Mattermost: inline os.getenv() → proper helper methods matching Discord/Slack pattern, adds config.yaml support via config.extra - Matrix: cached __init__ vars → helper methods with config.extra support - Mattermost config bridging: adds missing YAML→env bridging in gateway/config.py (was completely absent) New env vars: - DISCORD_REQUIRE_MENTION_CHANNELS - SLACK_REQUIRE_MENTION_CHANNELS - TELEGRAM_REQUIRE_MENTION_CHATS - WHATSAPP_REQUIRE_MENTION_CHATS - DINGTALK_REQUIRE_MENTION_CHATS - MATTERMOST_REQUIRE_MENTION_CHANNELS - MATRIX_REQUIRE_MENTION_ROOMS Naming follows each platform's convention (channels/chats/rooms). Fully backward compatible — empty by default, no behavior change. Requested by community member neeldhara on PR #3664.
This commit is contained in:
parent
ff9752410a
commit
d4178e0977
19 changed files with 1000 additions and 23 deletions
639
docs/plans/2026-04-22-require-mention-channels.md
Normal file
639
docs/plans/2026-04-22-require-mention-channels.md
Normal file
|
|
@ -0,0 +1,639 @@
|
|||
# Cross-Platform `require_mention_channels` Implementation Plan
|
||||
|
||||
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Add `require_mention_channels` config key across all 7 gateway platforms that have mention-gating, enabling per-channel mention overrides when `require_mention=false`. Also fix Mattermost and Matrix to use the `config.extra` pattern instead of raw `os.getenv()`.
|
||||
|
||||
**Origin:** Community request from neeldhara on PR #3664 — they want most channels to respond freely but a few "agent group" channels to require @mention.
|
||||
|
||||
**Architecture:** Each platform gets a new `_<platform>_require_mention_channels()` helper that returns a set of channel/chat/room IDs. The mention gate logic adds one new check: if `require_mention` is false but the channel is in `require_mention_channels`, treat it as requiring mention for that channel. Mattermost and Matrix are additionally refactored to use helper methods matching the Discord/Slack/Telegram pattern.
|
||||
|
||||
**Priority order of logic (highest wins):**
|
||||
1. DMs → always respond
|
||||
2. Channel in `free_response_channels` → never require mention
|
||||
3. Channel in `require_mention_channels` → always require mention
|
||||
4. Global `require_mention` setting → fallback
|
||||
|
||||
---
|
||||
|
||||
## Phase 0: Mattermost + Matrix config.extra alignment
|
||||
|
||||
Before adding the new feature, bring Mattermost and Matrix up to par with the other platforms by extracting their inline `os.getenv()` calls into proper helper methods that check `config.extra` first.
|
||||
|
||||
### Task 0A: Extract Mattermost mention helpers
|
||||
|
||||
**Objective:** Replace inline `os.getenv()` in Mattermost's `_handle_ws_event` with proper helper methods matching the Discord/Slack pattern.
|
||||
|
||||
**Files:**
|
||||
- Modify: `gateway/platforms/mattermost.py` (lines 619-626)
|
||||
|
||||
**Implementation:**
|
||||
|
||||
Add three helper methods to `MattermostAdapter`:
|
||||
|
||||
```python
|
||||
def _mattermost_require_mention(self) -> bool:
|
||||
"""Return whether Mattermost channel messages require a bot mention."""
|
||||
configured = self.config.extra.get("require_mention")
|
||||
if configured is not None:
|
||||
if isinstance(configured, str):
|
||||
return configured.lower() not in ("false", "0", "no", "off")
|
||||
return bool(configured)
|
||||
return os.getenv("MATTERMOST_REQUIRE_MENTION", "true").lower() not in ("false", "0", "no", "off")
|
||||
|
||||
def _mattermost_free_response_channels(self) -> set:
|
||||
"""Return Mattermost channel IDs where no bot mention is required."""
|
||||
raw = self.config.extra.get("free_response_channels")
|
||||
if raw is None:
|
||||
raw = os.getenv("MATTERMOST_FREE_RESPONSE_CHANNELS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
```
|
||||
|
||||
Then replace the inline code in `_handle_ws_event` (lines 620-626):
|
||||
```python
|
||||
# Before:
|
||||
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
|
||||
|
||||
# After:
|
||||
require_mention = self._mattermost_require_mention()
|
||||
free_channels = self._mattermost_free_response_channels()
|
||||
is_free_channel = channel_id in free_channels
|
||||
```
|
||||
|
||||
**Tests:**
|
||||
- Modify: `tests/gateway/test_mattermost.py`
|
||||
- Add unit tests for `_mattermost_require_mention()` and `_mattermost_free_response_channels()` that test both `config.extra` and env var fallback, matching the pattern in `tests/gateway/test_slack_mention.py`.
|
||||
- Update existing tests in `TestMattermostMentionBehavior` to also test `config.extra` path.
|
||||
|
||||
### Task 0B: Extract Matrix mention helpers
|
||||
|
||||
**Objective:** Replace inline `os.getenv()` in Matrix's `__init__` with proper helper methods.
|
||||
|
||||
**Files:**
|
||||
- Modify: `gateway/platforms/matrix.py`
|
||||
|
||||
**Implementation:**
|
||||
|
||||
Matrix currently reads mention config in `__init__` and stores as instance vars:
|
||||
```python
|
||||
self._require_mention = os.getenv("MATRIX_REQUIRE_MENTION", "true").lower() in ("true", "1", "yes")
|
||||
self._free_rooms = ...
|
||||
```
|
||||
|
||||
Refactor to use helper methods (called from the gate logic, not __init__):
|
||||
```python
|
||||
def _matrix_require_mention(self) -> bool:
|
||||
"""Return whether Matrix room messages require a bot mention."""
|
||||
configured = self.config.extra.get("require_mention")
|
||||
if configured is not None:
|
||||
if isinstance(configured, str):
|
||||
return configured.lower() not in ("false", "0", "no", "off")
|
||||
return bool(configured)
|
||||
return os.getenv("MATRIX_REQUIRE_MENTION", "true").lower() not in ("false", "0", "no", "off")
|
||||
|
||||
def _matrix_free_response_rooms(self) -> set:
|
||||
"""Return Matrix room IDs where no bot mention is required."""
|
||||
raw = self.config.extra.get("free_response_rooms")
|
||||
if raw is None:
|
||||
raw = os.getenv("MATRIX_FREE_RESPONSE_ROOMS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
```
|
||||
|
||||
Then replace `self._require_mention` and `self._free_rooms` usage in the gate logic with calls to these methods.
|
||||
|
||||
**Note:** Keep the `__init__` assignments for backward compat if anything else references `self._require_mention` — search the codebase first. If nothing else reads them, remove them entirely.
|
||||
|
||||
**Tests:**
|
||||
- Modify: `tests/gateway/test_matrix_mention.py`
|
||||
- Add unit tests for the new helper methods.
|
||||
- Update existing tests to verify config.extra path.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Add `require_mention_channels` to all 7 platforms
|
||||
|
||||
Each platform needs: (1) a new helper method, (2) updated gate logic, (3) tests, (4) docs.
|
||||
|
||||
### Task 1A: Discord `require_mention_channels`
|
||||
|
||||
**Objective:** Add `_discord_require_mention_channels()` helper and update gate logic.
|
||||
|
||||
**Files:**
|
||||
- Modify: `gateway/platforms/discord.py`
|
||||
- Add `_discord_require_mention_channels()` method (near line 2489, after `_discord_free_response_channels`)
|
||||
- Update gate logic at line 3010
|
||||
|
||||
**Helper method:**
|
||||
```python
|
||||
def _discord_require_mention_channels(self) -> set:
|
||||
"""Return Discord channel IDs where bot mention is always required."""
|
||||
raw = self.config.extra.get("require_mention_channels")
|
||||
if raw is None:
|
||||
raw = os.getenv("DISCORD_REQUIRE_MENTION_CHANNELS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
```
|
||||
|
||||
**Gate logic change** (discord.py around line 2994-3012):
|
||||
```python
|
||||
# Current:
|
||||
if require_mention and not is_free_channel and not in_bot_thread:
|
||||
if self._client.user not in message.mentions and not mention_prefix:
|
||||
return
|
||||
|
||||
# New:
|
||||
require_mention_chs = self._discord_require_mention_channels()
|
||||
is_force_mention_channel = bool(channel_ids & require_mention_chs)
|
||||
|
||||
if is_free_channel:
|
||||
pass # Free-response always wins
|
||||
elif is_force_mention_channel or require_mention:
|
||||
if not in_bot_thread:
|
||||
if self._client.user not in message.mentions and not mention_prefix:
|
||||
return
|
||||
```
|
||||
|
||||
**Tests:**
|
||||
- Modify: `tests/gateway/test_discord_free_response.py`
|
||||
- Add:
|
||||
- `test_discord_require_mention_channels_forces_mention_when_global_disabled`
|
||||
- `test_discord_require_mention_channels_from_config_extra`
|
||||
- `test_discord_require_mention_channels_from_env_var`
|
||||
- `test_discord_free_response_overrides_require_mention_channels`
|
||||
|
||||
### Task 1B: Slack `require_mention_channels`
|
||||
|
||||
**Objective:** Add `_slack_require_mention_channels()` and update gate logic.
|
||||
|
||||
**Files:**
|
||||
- Modify: `gateway/platforms/slack.py` (near line 1685, after `_slack_free_response_channels`)
|
||||
- Modify gate logic at line 1043-1065
|
||||
|
||||
**Helper method:**
|
||||
```python
|
||||
def _slack_require_mention_channels(self) -> set:
|
||||
"""Return Slack channel IDs where bot mention is always required."""
|
||||
raw = self.config.extra.get("require_mention_channels")
|
||||
if raw is None:
|
||||
raw = os.getenv("SLACK_REQUIRE_MENTION_CHANNELS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
```
|
||||
|
||||
**Gate logic change** (slack.py around line 1043-1065):
|
||||
```python
|
||||
# Current:
|
||||
if not is_dm and bot_uid:
|
||||
if channel_id in self._slack_free_response_channels():
|
||||
pass # Free-response channel
|
||||
elif not self._slack_require_mention():
|
||||
pass # Mention requirement disabled
|
||||
elif not is_mentioned:
|
||||
...check threads...
|
||||
if not any_thread_bypass:
|
||||
return
|
||||
|
||||
# New:
|
||||
if not is_dm and bot_uid:
|
||||
if channel_id in self._slack_free_response_channels():
|
||||
pass # Free-response channel — always process
|
||||
elif channel_id in self._slack_require_mention_channels():
|
||||
# Force-mention channel — require mention even if global is off
|
||||
if not is_mentioned:
|
||||
...check threads...
|
||||
if not any_thread_bypass:
|
||||
return
|
||||
elif not self._slack_require_mention():
|
||||
pass # Mention requirement disabled globally
|
||||
elif not is_mentioned:
|
||||
...check threads...
|
||||
if not any_thread_bypass:
|
||||
return
|
||||
```
|
||||
|
||||
**Tests:**
|
||||
- Modify: `tests/gateway/test_slack_mention.py`
|
||||
- Add:
|
||||
- `test_require_mention_channels_forces_mention_when_global_disabled`
|
||||
- `test_require_mention_channels_from_config_extra`
|
||||
- `test_require_mention_channels_from_env_var`
|
||||
- `test_free_response_overrides_require_mention_channels`
|
||||
|
||||
### Task 1C: Telegram `require_mention_channels`
|
||||
|
||||
**Objective:** Add `_telegram_require_mention_chats()` and update `_should_process_message`.
|
||||
|
||||
**Files:**
|
||||
- Modify: `gateway/platforms/telegram.py`
|
||||
|
||||
**Helper method:**
|
||||
```python
|
||||
def _telegram_require_mention_chats(self) -> set[str]:
|
||||
"""Return Telegram chat IDs where bot mention is always required."""
|
||||
raw = self.config.extra.get("require_mention_chats")
|
||||
if raw is None:
|
||||
raw = os.getenv("TELEGRAM_REQUIRE_MENTION_CHATS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
```
|
||||
|
||||
**Gate logic change** in `_should_process_message`:
|
||||
```python
|
||||
# Current order:
|
||||
# free_response_chats -> bypass
|
||||
# require_mention=False -> bypass
|
||||
# reply-to-bot, mentions, patterns -> accept
|
||||
|
||||
# New order:
|
||||
# free_response_chats -> bypass
|
||||
# require_mention_chats -> force mention (even when global=False)
|
||||
# require_mention=False -> bypass
|
||||
# reply-to-bot, mentions, patterns -> accept
|
||||
```
|
||||
|
||||
Insert after the `free_response_chats` check and before the `require_mention` check:
|
||||
```python
|
||||
if str(chat.id) in self._telegram_require_mention_chats():
|
||||
# Force mention required in this chat even when global require_mention is off
|
||||
if self._is_reply_to_bot(message):
|
||||
return True
|
||||
if self._message_mentions_bot(message):
|
||||
return True
|
||||
return self._message_matches_mention_patterns(message)
|
||||
```
|
||||
|
||||
**Tests:**
|
||||
- Modify: `tests/gateway/test_telegram_group_gating.py`
|
||||
- Add:
|
||||
- `test_require_mention_chats_forces_mention_when_global_disabled`
|
||||
- `test_free_response_overrides_require_mention_chats`
|
||||
|
||||
### Task 1D: WhatsApp `require_mention_channels`
|
||||
|
||||
**Objective:** Add `_whatsapp_require_mention_chats()` and update `_should_process_message`.
|
||||
|
||||
**Files:**
|
||||
- Modify: `gateway/platforms/whatsapp.py`
|
||||
|
||||
**Helper method:**
|
||||
```python
|
||||
def _whatsapp_require_mention_chats(self) -> set[str]:
|
||||
"""Return WhatsApp chat IDs where bot mention is always required."""
|
||||
raw = self.config.extra.get("require_mention_chats")
|
||||
if raw is None:
|
||||
raw = os.getenv("WHATSAPP_REQUIRE_MENTION_CHATS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
```
|
||||
|
||||
**Gate logic change** — same pattern as Telegram: insert force-mention check after `free_response_chats` and before the global `require_mention` check.
|
||||
|
||||
**Tests:**
|
||||
- Modify: `tests/gateway/test_whatsapp_group_gating.py`
|
||||
|
||||
### Task 1E: DingTalk `require_mention_channels`
|
||||
|
||||
**Objective:** Add `_dingtalk_require_mention_chats()` and update `_should_process_message`.
|
||||
|
||||
**Files:**
|
||||
- Modify: `gateway/platforms/dingtalk.py`
|
||||
|
||||
**Helper method:** Same pattern as WhatsApp/Telegram.
|
||||
|
||||
**Gate logic change:** Same insertion point — after `free_response_chats`, before global `require_mention`.
|
||||
|
||||
**Tests:**
|
||||
- Modify: `tests/gateway/test_dingtalk.py`
|
||||
|
||||
### Task 1F: Mattermost `require_mention_channels`
|
||||
|
||||
**Objective:** Add `_mattermost_require_mention_channels()` and update gate logic (uses helpers from Task 0A).
|
||||
|
||||
**Files:**
|
||||
- Modify: `gateway/platforms/mattermost.py`
|
||||
|
||||
**Helper method:**
|
||||
```python
|
||||
def _mattermost_require_mention_channels(self) -> set:
|
||||
"""Return Mattermost channel IDs where bot mention is always required."""
|
||||
raw = self.config.extra.get("require_mention_channels")
|
||||
if raw is None:
|
||||
raw = os.getenv("MATTERMOST_REQUIRE_MENTION_CHANNELS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
```
|
||||
|
||||
**Gate logic change** in `_handle_ws_event`:
|
||||
```python
|
||||
# After Task 0A, the logic already uses helper methods. Now add:
|
||||
require_mention_chs = self._mattermost_require_mention_channels()
|
||||
is_force_mention = channel_id in require_mention_chs
|
||||
|
||||
if is_free_channel:
|
||||
pass # Free-response always wins
|
||||
elif is_force_mention or require_mention:
|
||||
if not has_mention:
|
||||
logger.debug(...)
|
||||
return
|
||||
```
|
||||
|
||||
**Tests:**
|
||||
- Modify: `tests/gateway/test_mattermost.py`
|
||||
- Add:
|
||||
- `test_require_mention_channels_forces_mention_when_global_disabled`
|
||||
- `test_require_mention_channels_from_config_extra`
|
||||
- `test_require_mention_channels_from_env_var`
|
||||
- `test_free_response_overrides_require_mention_channels`
|
||||
|
||||
### Task 1G: Matrix `require_mention_channels`
|
||||
|
||||
**Objective:** Add `_matrix_require_mention_rooms()` and update gate logic (uses helpers from Task 0B).
|
||||
|
||||
**Files:**
|
||||
- Modify: `gateway/platforms/matrix.py`
|
||||
|
||||
**Helper method:**
|
||||
```python
|
||||
def _matrix_require_mention_rooms(self) -> set:
|
||||
"""Return Matrix room IDs where bot mention is always required."""
|
||||
raw = self.config.extra.get("require_mention_rooms")
|
||||
if raw is None:
|
||||
raw = os.getenv("MATRIX_REQUIRE_MENTION_ROOMS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
```
|
||||
|
||||
**Gate logic change:**
|
||||
```python
|
||||
if not is_dm:
|
||||
is_free_room = room_id in self._matrix_free_response_rooms()
|
||||
is_force_mention = room_id in self._matrix_require_mention_rooms()
|
||||
in_bot_thread = bool(thread_id and thread_id in self._threads)
|
||||
if is_free_room:
|
||||
pass # Free room — always respond
|
||||
elif (is_force_mention or self._matrix_require_mention()) and not in_bot_thread:
|
||||
if not is_mentioned:
|
||||
return None
|
||||
```
|
||||
|
||||
**Tests:**
|
||||
- Modify: `tests/gateway/test_matrix_mention.py`
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Config bridging in `gateway/config.py`
|
||||
|
||||
### Task 2-pre: Add config.yaml → env var bridging for `require_mention_channels`
|
||||
|
||||
**Objective:** Bridge the new config.yaml keys to env vars in `gateway/config.py`'s `load_gateway_config()`, following the exact pattern used for `free_response_channels`.
|
||||
|
||||
**Files:**
|
||||
- Modify: `gateway/config.py`
|
||||
|
||||
**Implementation:** For EACH platform section, add the bridging block right after the existing `free_response_channels` bridge:
|
||||
|
||||
```python
|
||||
# Slack (after line 618):
|
||||
rmc = slack_cfg.get("require_mention_channels")
|
||||
if rmc is not None and not os.getenv("SLACK_REQUIRE_MENTION_CHANNELS"):
|
||||
if isinstance(rmc, list):
|
||||
rmc = ",".join(str(v) for v in rmc)
|
||||
os.environ["SLACK_REQUIRE_MENTION_CHANNELS"] = str(rmc)
|
||||
|
||||
# Discord (after line 629):
|
||||
rmc = discord_cfg.get("require_mention_channels")
|
||||
if rmc is not None and not os.getenv("DISCORD_REQUIRE_MENTION_CHANNELS"):
|
||||
if isinstance(rmc, list):
|
||||
rmc = ",".join(str(v) for v in rmc)
|
||||
os.environ["DISCORD_REQUIRE_MENTION_CHANNELS"] = str(rmc)
|
||||
|
||||
# Telegram (after line 678):
|
||||
rmc = telegram_cfg.get("require_mention_chats")
|
||||
if rmc is not None and not os.getenv("TELEGRAM_REQUIRE_MENTION_CHATS"):
|
||||
if isinstance(rmc, list):
|
||||
rmc = ",".join(str(v) for v in rmc)
|
||||
os.environ["TELEGRAM_REQUIRE_MENTION_CHATS"] = str(rmc)
|
||||
|
||||
# WhatsApp (after line 709):
|
||||
rmc = whatsapp_cfg.get("require_mention_chats")
|
||||
if rmc is not None and not os.getenv("WHATSAPP_REQUIRE_MENTION_CHATS"):
|
||||
if isinstance(rmc, list):
|
||||
rmc = ",".join(str(v) for v in rmc)
|
||||
os.environ["WHATSAPP_REQUIRE_MENTION_CHATS"] = str(rmc)
|
||||
|
||||
# DingTalk (after line 736):
|
||||
rmc = dingtalk_cfg.get("require_mention_chats")
|
||||
if rmc is not None and not os.getenv("DINGTALK_REQUIRE_MENTION_CHATS"):
|
||||
if isinstance(rmc, list):
|
||||
rmc = ",".join(str(v) for v in rmc)
|
||||
os.environ["DINGTALK_REQUIRE_MENTION_CHATS"] = str(rmc)
|
||||
|
||||
# Mattermost (new section — mattermost currently has NO bridging at all):
|
||||
mattermost_cfg = yaml_cfg.get("mattermost", {})
|
||||
if isinstance(mattermost_cfg, dict):
|
||||
if "require_mention" in mattermost_cfg and not os.getenv("MATTERMOST_REQUIRE_MENTION"):
|
||||
os.environ["MATTERMOST_REQUIRE_MENTION"] = str(mattermost_cfg["require_mention"]).lower()
|
||||
frc = mattermost_cfg.get("free_response_channels")
|
||||
if frc is not None and not os.getenv("MATTERMOST_FREE_RESPONSE_CHANNELS"):
|
||||
if isinstance(frc, list):
|
||||
frc = ",".join(str(v) for v in frc)
|
||||
os.environ["MATTERMOST_FREE_RESPONSE_CHANNELS"] = str(frc)
|
||||
rmc = mattermost_cfg.get("require_mention_channels")
|
||||
if rmc is not None and not os.getenv("MATTERMOST_REQUIRE_MENTION_CHANNELS"):
|
||||
if isinstance(rmc, list):
|
||||
rmc = ",".join(str(v) for v in rmc)
|
||||
os.environ["MATTERMOST_REQUIRE_MENTION_CHANNELS"] = str(rmc)
|
||||
|
||||
# Matrix (after line 752):
|
||||
rmc = matrix_cfg.get("require_mention_rooms")
|
||||
if rmc is not None and not os.getenv("MATRIX_REQUIRE_MENTION_ROOMS"):
|
||||
if isinstance(rmc, list):
|
||||
rmc = ",".join(str(v) for v in rmc)
|
||||
os.environ["MATRIX_REQUIRE_MENTION_ROOMS"] = str(rmc)
|
||||
```
|
||||
|
||||
**Note:** Mattermost currently has ZERO config bridging in `gateway/config.py` — this is an existing gap. We add a full bridging block for Mattermost while we're here (require_mention, free_response_channels, AND the new require_mention_channels).
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Config, env vars, and docs
|
||||
|
||||
### Task 3A: Add env vars to `hermes_cli/config.py`
|
||||
|
||||
**Objective:** Register the new env vars in `OPTIONAL_ENV_VARS` for platforms that already have entries there (Mattermost, Matrix). Add DEFAULT_CONFIG entries where appropriate.
|
||||
|
||||
**Files:**
|
||||
- Modify: `hermes_cli/config.py`
|
||||
|
||||
**Add to OPTIONAL_ENV_VARS:**
|
||||
```python
|
||||
"MATTERMOST_REQUIRE_MENTION_CHANNELS": {
|
||||
"description": "Comma-separated Mattermost channel IDs where bot always requires @mention (even when require_mention is false)",
|
||||
"prompt": "Require-mention channel IDs (comma-separated)",
|
||||
"url": None,
|
||||
"password": False,
|
||||
"category": "messaging",
|
||||
},
|
||||
"MATRIX_REQUIRE_MENTION_ROOMS": {
|
||||
"description": "Comma-separated Matrix room IDs where bot always requires @mention (even when require_mention is false)",
|
||||
"prompt": "Require-mention room IDs (comma-separated)",
|
||||
"url": None,
|
||||
"password": False,
|
||||
"category": "messaging",
|
||||
"advanced": True,
|
||||
},
|
||||
```
|
||||
|
||||
**Add to DEFAULT_CONFIG discord section (if it already has require_mention/free_response_channels):**
|
||||
```python
|
||||
"discord": {
|
||||
"require_mention": True,
|
||||
"free_response_channels": "",
|
||||
"require_mention_channels": "", # <-- new
|
||||
}
|
||||
```
|
||||
|
||||
### Task 3B: Update docs — Mattermost
|
||||
|
||||
**Objective:** Update Mattermost docs with new config key and env var.
|
||||
|
||||
**Files:**
|
||||
- Modify: `website/docs/user-guide/messaging/mattermost.md`
|
||||
|
||||
**Changes:**
|
||||
1. Add `MATTERMOST_REQUIRE_MENTION_CHANNELS` to the Mention Behavior table
|
||||
2. Add config.yaml example showing the new key
|
||||
3. Add a use case example matching neeldhara's scenario
|
||||
|
||||
### Task 3C: Update docs — Discord
|
||||
|
||||
**Files:**
|
||||
- Modify: `website/docs/user-guide/messaging/discord.md`
|
||||
|
||||
Add `require_mention_channels` to the config.yaml example and env var table.
|
||||
|
||||
### Task 3D: Update docs — Slack
|
||||
|
||||
**Files:**
|
||||
- Modify: `website/docs/user-guide/messaging/slack.md`
|
||||
|
||||
### Task 3E: Update docs — Telegram
|
||||
|
||||
**Files:**
|
||||
- Modify: `website/docs/user-guide/messaging/telegram.md`
|
||||
|
||||
### Task 3F: Update docs — Matrix
|
||||
|
||||
**Files:**
|
||||
- Modify: `website/docs/user-guide/messaging/matrix.md`
|
||||
|
||||
### Task 3G: Update docs — environment-variables.md
|
||||
|
||||
**Files:**
|
||||
- Modify: `website/docs/reference/environment-variables.md`
|
||||
|
||||
Add entries for all new env vars:
|
||||
- `DISCORD_REQUIRE_MENTION_CHANNELS`
|
||||
- `SLACK_REQUIRE_MENTION_CHANNELS`
|
||||
- `TELEGRAM_REQUIRE_MENTION_CHATS`
|
||||
- `WHATSAPP_REQUIRE_MENTION_CHATS`
|
||||
- `DINGTALK_REQUIRE_MENTION_CHATS`
|
||||
- `MATTERMOST_REQUIRE_MENTION_CHANNELS`
|
||||
- `MATRIX_REQUIRE_MENTION_ROOMS`
|
||||
|
||||
### Task 3H: Update docs — configuration.md
|
||||
|
||||
**Files:**
|
||||
- Modify: `website/docs/user-guide/configuration.md`
|
||||
|
||||
Add `require_mention_channels` to the generic config example near lines 1234-1240.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Config bridging tests
|
||||
|
||||
### Task 4A: Config bridging tests for platforms that have them
|
||||
|
||||
**Objective:** Ensure config.yaml `require_mention_channels` key is bridged to the env var correctly for platforms that use the config bridging pattern.
|
||||
|
||||
**Files:**
|
||||
- Modify tests that already have `test_config_bridges_*` functions
|
||||
|
||||
For platforms using config.yaml bridging (Telegram, WhatsApp, Slack, DingTalk):
|
||||
```python
|
||||
def test_config_bridges_require_mention_channels(monkeypatch, tmp_path):
|
||||
"""config.yaml require_mention_chats bridges to env var."""
|
||||
# Write config with require_mention_chats list
|
||||
# Load config
|
||||
# Assert the env var is set correctly
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Naming Convention Summary
|
||||
|
||||
Each platform uses its own terminology for channels/chats/rooms. The new key follows the same convention:
|
||||
|
||||
| Platform | free_response key | NEW require_mention key | Env var |
|
||||
|------------|------------------------------|-------------------------------------|--------------------------------------|
|
||||
| Discord | `free_response_channels` | `require_mention_channels` | `DISCORD_REQUIRE_MENTION_CHANNELS` |
|
||||
| Slack | `free_response_channels` | `require_mention_channels` | `SLACK_REQUIRE_MENTION_CHANNELS` |
|
||||
| Telegram | `free_response_chats` | `require_mention_chats` | `TELEGRAM_REQUIRE_MENTION_CHATS` |
|
||||
| WhatsApp | `free_response_chats` | `require_mention_chats` | `WHATSAPP_REQUIRE_MENTION_CHATS` |
|
||||
| DingTalk | `free_response_chats` | `require_mention_chats` | `DINGTALK_REQUIRE_MENTION_CHATS` |
|
||||
| Mattermost | `free_response_channels` | `require_mention_channels` | `MATTERMOST_REQUIRE_MENTION_CHANNELS`|
|
||||
| Matrix | `free_response_rooms` | `require_mention_rooms` | `MATRIX_REQUIRE_MENTION_ROOMS` |
|
||||
|
||||
---
|
||||
|
||||
## Priority Logic (all platforms, documented for consistency)
|
||||
|
||||
```
|
||||
1. Is DM? → ALWAYS respond (no mention check)
|
||||
2. Channel in free_response_*? → RESPOND (no mention needed)
|
||||
3. Channel in require_mention_*? → REQUIRE MENTION (even if global require_mention=false)
|
||||
4. Global require_mention=true? → REQUIRE MENTION
|
||||
5. Global require_mention=false? → RESPOND (no mention needed)
|
||||
```
|
||||
|
||||
`free_response` takes priority over `require_mention_channels` because explicitly marking a channel as "free response" is a stronger signal than the channel appearing in both lists (which would be a config error, but we fail-open for free_response).
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
After all tasks, run:
|
||||
```bash
|
||||
bash scripts/run_tests.sh tests/gateway/
|
||||
```
|
||||
|
||||
All existing tests must pass (backward compat), plus the new tests for `require_mention_channels`.
|
||||
|
|
@ -616,6 +616,11 @@ def load_gateway_config() -> GatewayConfig:
|
|||
if isinstance(frc, list):
|
||||
frc = ",".join(str(v) for v in frc)
|
||||
os.environ["SLACK_FREE_RESPONSE_CHANNELS"] = str(frc)
|
||||
rmc = slack_cfg.get("require_mention_channels")
|
||||
if rmc is not None and not os.getenv("SLACK_REQUIRE_MENTION_CHANNELS"):
|
||||
if isinstance(rmc, list):
|
||||
rmc = ",".join(str(v) for v in rmc)
|
||||
os.environ["SLACK_REQUIRE_MENTION_CHANNELS"] = str(rmc)
|
||||
|
||||
# Discord settings → env vars (env vars take precedence)
|
||||
discord_cfg = yaml_cfg.get("discord", {})
|
||||
|
|
@ -627,6 +632,11 @@ def load_gateway_config() -> GatewayConfig:
|
|||
if isinstance(frc, list):
|
||||
frc = ",".join(str(v) for v in frc)
|
||||
os.environ["DISCORD_FREE_RESPONSE_CHANNELS"] = str(frc)
|
||||
rmc = discord_cfg.get("require_mention_channels")
|
||||
if rmc is not None and not os.getenv("DISCORD_REQUIRE_MENTION_CHANNELS"):
|
||||
if isinstance(rmc, list):
|
||||
rmc = ",".join(str(v) for v in rmc)
|
||||
os.environ["DISCORD_REQUIRE_MENTION_CHANNELS"] = str(rmc)
|
||||
if "auto_thread" in discord_cfg and not os.getenv("DISCORD_AUTO_THREAD"):
|
||||
os.environ["DISCORD_AUTO_THREAD"] = str(discord_cfg["auto_thread"]).lower()
|
||||
if "reactions" in discord_cfg and not os.getenv("DISCORD_REACTIONS"):
|
||||
|
|
@ -676,6 +686,11 @@ def load_gateway_config() -> GatewayConfig:
|
|||
if isinstance(frc, list):
|
||||
frc = ",".join(str(v) for v in frc)
|
||||
os.environ["TELEGRAM_FREE_RESPONSE_CHATS"] = str(frc)
|
||||
rmc = telegram_cfg.get("require_mention_chats")
|
||||
if rmc is not None and not os.getenv("TELEGRAM_REQUIRE_MENTION_CHATS"):
|
||||
if isinstance(rmc, list):
|
||||
rmc = ",".join(str(v) for v in rmc)
|
||||
os.environ["TELEGRAM_REQUIRE_MENTION_CHATS"] = str(rmc)
|
||||
ignored_threads = telegram_cfg.get("ignored_threads")
|
||||
if ignored_threads is not None and not os.getenv("TELEGRAM_IGNORED_THREADS"):
|
||||
if isinstance(ignored_threads, list):
|
||||
|
|
@ -707,6 +722,11 @@ def load_gateway_config() -> GatewayConfig:
|
|||
if isinstance(frc, list):
|
||||
frc = ",".join(str(v) for v in frc)
|
||||
os.environ["WHATSAPP_FREE_RESPONSE_CHATS"] = str(frc)
|
||||
rmc = whatsapp_cfg.get("require_mention_chats")
|
||||
if rmc is not None and not os.getenv("WHATSAPP_REQUIRE_MENTION_CHATS"):
|
||||
if isinstance(rmc, list):
|
||||
rmc = ",".join(str(v) for v in rmc)
|
||||
os.environ["WHATSAPP_REQUIRE_MENTION_CHATS"] = str(rmc)
|
||||
if "dm_policy" in whatsapp_cfg and not os.getenv("WHATSAPP_DM_POLICY"):
|
||||
os.environ["WHATSAPP_DM_POLICY"] = str(whatsapp_cfg["dm_policy"]).lower()
|
||||
af = whatsapp_cfg.get("allow_from")
|
||||
|
|
@ -734,12 +754,33 @@ def load_gateway_config() -> GatewayConfig:
|
|||
if isinstance(frc, list):
|
||||
frc = ",".join(str(v) for v in frc)
|
||||
os.environ["DINGTALK_FREE_RESPONSE_CHATS"] = str(frc)
|
||||
rmc = dingtalk_cfg.get("require_mention_chats")
|
||||
if rmc is not None and not os.getenv("DINGTALK_REQUIRE_MENTION_CHATS"):
|
||||
if isinstance(rmc, list):
|
||||
rmc = ",".join(str(v) for v in rmc)
|
||||
os.environ["DINGTALK_REQUIRE_MENTION_CHATS"] = str(rmc)
|
||||
allowed = dingtalk_cfg.get("allowed_users")
|
||||
if allowed is not None and not os.getenv("DINGTALK_ALLOWED_USERS"):
|
||||
if isinstance(allowed, list):
|
||||
allowed = ",".join(str(v) for v in allowed)
|
||||
os.environ["DINGTALK_ALLOWED_USERS"] = str(allowed)
|
||||
|
||||
# Mattermost settings → env vars (env vars take precedence)
|
||||
mattermost_cfg = yaml_cfg.get("mattermost", {})
|
||||
if isinstance(mattermost_cfg, dict):
|
||||
if "require_mention" in mattermost_cfg and not os.getenv("MATTERMOST_REQUIRE_MENTION"):
|
||||
os.environ["MATTERMOST_REQUIRE_MENTION"] = str(mattermost_cfg["require_mention"]).lower()
|
||||
frc = mattermost_cfg.get("free_response_channels")
|
||||
if frc is not None and not os.getenv("MATTERMOST_FREE_RESPONSE_CHANNELS"):
|
||||
if isinstance(frc, list):
|
||||
frc = ",".join(str(v) for v in frc)
|
||||
os.environ["MATTERMOST_FREE_RESPONSE_CHANNELS"] = str(frc)
|
||||
rmc = mattermost_cfg.get("require_mention_channels")
|
||||
if rmc is not None and not os.getenv("MATTERMOST_REQUIRE_MENTION_CHANNELS"):
|
||||
if isinstance(rmc, list):
|
||||
rmc = ",".join(str(v) for v in rmc)
|
||||
os.environ["MATTERMOST_REQUIRE_MENTION_CHANNELS"] = str(rmc)
|
||||
|
||||
# Matrix settings → env vars (env vars take precedence)
|
||||
matrix_cfg = yaml_cfg.get("matrix", {})
|
||||
if isinstance(matrix_cfg, dict):
|
||||
|
|
@ -750,6 +791,11 @@ def load_gateway_config() -> GatewayConfig:
|
|||
if isinstance(frc, list):
|
||||
frc = ",".join(str(v) for v in frc)
|
||||
os.environ["MATRIX_FREE_RESPONSE_ROOMS"] = str(frc)
|
||||
rmc = matrix_cfg.get("require_mention_rooms")
|
||||
if rmc is not None and not os.getenv("MATRIX_REQUIRE_MENTION_ROOMS"):
|
||||
if isinstance(rmc, list):
|
||||
rmc = ",".join(str(v) for v in rmc)
|
||||
os.environ["MATRIX_REQUIRE_MENTION_ROOMS"] = str(rmc)
|
||||
if "auto_thread" in matrix_cfg and not os.getenv("MATRIX_AUTO_THREAD"):
|
||||
os.environ["MATRIX_AUTO_THREAD"] = str(matrix_cfg["auto_thread"]).lower()
|
||||
if "dm_mention_threads" in matrix_cfg and not os.getenv("MATRIX_DM_MENTION_THREADS"):
|
||||
|
|
|
|||
|
|
@ -361,6 +361,15 @@ class DingTalkAdapter(BasePlatformAdapter):
|
|||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
return {part.strip() for part in str(raw).split(",") if part.strip()}
|
||||
|
||||
def _dingtalk_require_mention_chats(self) -> Set[str]:
|
||||
"""Return DingTalk chat IDs where bot mention is always required."""
|
||||
raw = self.config.extra.get("require_mention_chats")
|
||||
if raw is None:
|
||||
raw = os.getenv("DINGTALK_REQUIRE_MENTION_CHATS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
return {part.strip() for part in str(raw).split(",") if part.strip()}
|
||||
|
||||
def _compile_mention_patterns(self) -> List[re.Pattern]:
|
||||
"""Compile optional regex wake-word patterns for group triggers."""
|
||||
patterns = self.config.extra.get("mention_patterns") if self.config.extra else None
|
||||
|
|
@ -448,6 +457,11 @@ class DingTalkAdapter(BasePlatformAdapter):
|
|||
return True
|
||||
if chat_id and chat_id in self._dingtalk_free_response_chats():
|
||||
return True
|
||||
if chat_id and chat_id in self._dingtalk_require_mention_chats():
|
||||
# Force mention in this chat even when global require_mention is off
|
||||
if self._message_mentions_bot(message):
|
||||
return True
|
||||
return self._message_matches_mention_patterns(text)
|
||||
if not self._dingtalk_require_mention():
|
||||
return True
|
||||
if self._message_mentions_bot(message):
|
||||
|
|
|
|||
|
|
@ -2497,6 +2497,17 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
|
||||
def _discord_require_mention_channels(self) -> set:
|
||||
"""Return Discord channel IDs where bot mention is always required."""
|
||||
raw = self.config.extra.get("require_mention_channels")
|
||||
if raw is None:
|
||||
raw = os.getenv("DISCORD_REQUIRE_MENTION_CHANNELS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
|
||||
def _thread_parent_channel(self, channel: Any) -> Any:
|
||||
"""Return the parent text channel when invoked from a thread."""
|
||||
return getattr(channel, "parent", None) or channel
|
||||
|
|
@ -2996,18 +3007,22 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
channel_ids.add(parent_channel_id)
|
||||
|
||||
require_mention = self._discord_require_mention()
|
||||
require_mention_chs = self._discord_require_mention_channels()
|
||||
# Voice-linked text channels act as free-response while voice is active.
|
||||
# Only the exact bound channel gets the exemption, not sibling threads.
|
||||
voice_linked_ids = {str(ch_id) for ch_id in self._voice_text_channels.values()}
|
||||
current_channel_id = str(message.channel.id)
|
||||
is_voice_linked_channel = current_channel_id in voice_linked_ids
|
||||
is_free_channel = bool(channel_ids & free_channels) or is_voice_linked_channel
|
||||
is_force_mention = bool(channel_ids & require_mention_chs)
|
||||
|
||||
# Skip the mention check if the message is in a thread where
|
||||
# the bot has previously participated (auto-created or replied in).
|
||||
in_bot_thread = is_thread and thread_id in self._threads
|
||||
|
||||
if require_mention and not is_free_channel and not in_bot_thread:
|
||||
if is_free_channel:
|
||||
pass # Free-response channel — always respond
|
||||
elif (is_force_mention or require_mention) and not in_bot_thread:
|
||||
if self._client.user not in message.mentions and not mention_prefix:
|
||||
return
|
||||
# Auto-thread: when enabled, automatically create a thread for every
|
||||
|
|
|
|||
|
|
@ -247,14 +247,9 @@ class MatrixAdapter(BasePlatformAdapter):
|
|||
# Thread participation tracking (for require_mention bypass)
|
||||
self._threads = ThreadParticipationTracker("matrix")
|
||||
|
||||
# Mention/thread gating — parsed once from env vars.
|
||||
self._require_mention: bool = os.getenv(
|
||||
"MATRIX_REQUIRE_MENTION", "true"
|
||||
).lower() not in ("false", "0", "no")
|
||||
free_rooms_raw = os.getenv("MATRIX_FREE_RESPONSE_ROOMS", "")
|
||||
self._free_rooms: Set[str] = {
|
||||
r.strip() for r in free_rooms_raw.split(",") if r.strip()
|
||||
}
|
||||
# Mention/thread gating — legacy init-time parsing kept for backward
|
||||
# compat; runtime code uses the helper methods below which also
|
||||
# check config.extra (config.yaml) first.
|
||||
self._auto_thread: bool = os.getenv("MATRIX_AUTO_THREAD", "true").lower() in (
|
||||
"true",
|
||||
"1",
|
||||
|
|
@ -1262,9 +1257,13 @@ class MatrixAdapter(BasePlatformAdapter):
|
|||
|
||||
# Require-mention gating.
|
||||
if not is_dm:
|
||||
is_free_room = room_id in self._free_rooms
|
||||
is_free_room = room_id in self._matrix_free_response_rooms()
|
||||
is_force_mention = room_id in self._matrix_require_mention_rooms()
|
||||
in_bot_thread = bool(thread_id and thread_id in self._threads)
|
||||
if self._require_mention and not is_free_room and not in_bot_thread:
|
||||
require_mention = self._matrix_require_mention()
|
||||
if is_free_room:
|
||||
pass # Free-response room — always respond
|
||||
elif (is_force_mention or require_mention) and not in_bot_thread:
|
||||
if not is_mentioned:
|
||||
return None
|
||||
|
||||
|
|
@ -1274,7 +1273,7 @@ class MatrixAdapter(BasePlatformAdapter):
|
|||
self._threads.mark(thread_id)
|
||||
|
||||
# Strip mention from body (only when mention-gating is active).
|
||||
if is_mentioned and self._require_mention:
|
||||
if is_mentioned and self._matrix_require_mention():
|
||||
body = self._strip_mention(body)
|
||||
|
||||
# Auto-thread.
|
||||
|
|
@ -1958,6 +1957,41 @@ class MatrixAdapter(BasePlatformAdapter):
|
|||
|
||||
self._dm_rooms = {rid: (rid in dm_room_ids) for rid in self._joined_rooms}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Mention-gating helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _matrix_require_mention(self) -> bool:
|
||||
"""Return whether Matrix room messages require a bot mention."""
|
||||
configured = self.config.extra.get("require_mention")
|
||||
if configured is not None:
|
||||
if isinstance(configured, str):
|
||||
return configured.lower() not in ("false", "0", "no", "off")
|
||||
return bool(configured)
|
||||
return os.getenv("MATRIX_REQUIRE_MENTION", "true").lower() not in ("false", "0", "no", "off")
|
||||
|
||||
def _matrix_free_response_rooms(self) -> set:
|
||||
"""Return Matrix room IDs where no bot mention is required."""
|
||||
raw = self.config.extra.get("free_response_rooms")
|
||||
if raw is None:
|
||||
raw = os.getenv("MATRIX_FREE_RESPONSE_ROOMS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
|
||||
def _matrix_require_mention_rooms(self) -> set:
|
||||
"""Return Matrix room IDs where bot mention is always required."""
|
||||
raw = self.config.extra.get("require_mention_rooms")
|
||||
if raw is None:
|
||||
raw = os.getenv("MATRIX_REQUIRE_MENTION_ROOMS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Mention detection helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -497,6 +497,41 @@ class MattermostAdapter(BasePlatformAdapter):
|
|||
return SendResult(success=False, error="Failed to post with file")
|
||||
return SendResult(success=True, message_id=data["id"])
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Mention-gating helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _mattermost_require_mention(self) -> bool:
|
||||
"""Return whether Mattermost channel messages require a bot mention."""
|
||||
configured = self.config.extra.get("require_mention")
|
||||
if configured is not None:
|
||||
if isinstance(configured, str):
|
||||
return configured.lower() not in ("false", "0", "no", "off")
|
||||
return bool(configured)
|
||||
return os.getenv("MATTERMOST_REQUIRE_MENTION", "true").lower() not in ("false", "0", "no", "off")
|
||||
|
||||
def _mattermost_free_response_channels(self) -> set:
|
||||
"""Return Mattermost channel IDs where no bot mention is required."""
|
||||
raw = self.config.extra.get("free_response_channels")
|
||||
if raw is None:
|
||||
raw = os.getenv("MATTERMOST_FREE_RESPONSE_CHANNELS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
|
||||
def _mattermost_require_mention_channels(self) -> set:
|
||||
"""Return Mattermost channel IDs where bot mention is always required."""
|
||||
raw = self.config.extra.get("require_mention_channels")
|
||||
if raw is None:
|
||||
raw = os.getenv("MATTERMOST_REQUIRE_MENTION_CHANNELS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# WebSocket
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -613,17 +648,16 @@ class MattermostAdapter(BasePlatformAdapter):
|
|||
message_text = post.get("message", "")
|
||||
|
||||
# Mention-gating for non-DM channels.
|
||||
# Config (env vars):
|
||||
# MATTERMOST_REQUIRE_MENTION: Require @mention in channels (default: true)
|
||||
# MATTERMOST_FREE_RESPONSE_CHANNELS: Channel IDs where bot responds without mention
|
||||
# Config (config.yaml mattermost.* keys or env vars):
|
||||
# require_mention / MATTERMOST_REQUIRE_MENTION: Require @mention in channels (default: true)
|
||||
# free_response_channels / MATTERMOST_FREE_RESPONSE_CHANNELS: Channel IDs where bot responds without mention
|
||||
# require_mention_channels / MATTERMOST_REQUIRE_MENTION_CHANNELS: Channel IDs that always require mention
|
||||
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()}
|
||||
require_mention = self._mattermost_require_mention()
|
||||
free_channels = self._mattermost_free_response_channels()
|
||||
require_mention_chs = self._mattermost_require_mention_channels()
|
||||
is_free_channel = channel_id in free_channels
|
||||
is_force_mention = channel_id in require_mention_chs
|
||||
|
||||
mention_patterns = [
|
||||
f"@{self._bot_username}",
|
||||
|
|
@ -634,7 +668,9 @@ class MattermostAdapter(BasePlatformAdapter):
|
|||
for pattern in mention_patterns
|
||||
)
|
||||
|
||||
if require_mention and not is_free_channel and not has_mention:
|
||||
if is_free_channel:
|
||||
pass # Free-response channel — always respond
|
||||
elif (is_force_mention or require_mention) and not has_mention:
|
||||
logger.debug(
|
||||
"Mattermost: skipping non-DM message without @mention (channel=%s)",
|
||||
channel_id,
|
||||
|
|
|
|||
|
|
@ -1043,6 +1043,26 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
if not is_dm and bot_uid:
|
||||
if channel_id in self._slack_free_response_channels():
|
||||
pass # Free-response channel — always process
|
||||
elif channel_id in self._slack_require_mention_channels():
|
||||
# Force-mention channel — require mention even if global is off
|
||||
if not is_mentioned:
|
||||
reply_to_bot_thread = (
|
||||
is_thread_reply and event_thread_ts in self._bot_message_ts
|
||||
)
|
||||
in_mentioned_thread = (
|
||||
event_thread_ts is not None
|
||||
and event_thread_ts in self._mentioned_threads
|
||||
)
|
||||
has_session = (
|
||||
is_thread_reply
|
||||
and self._has_active_session_for_thread(
|
||||
channel_id=channel_id,
|
||||
thread_ts=event_thread_ts,
|
||||
user_id=user_id,
|
||||
)
|
||||
)
|
||||
if not reply_to_bot_thread and not in_mentioned_thread and not has_session:
|
||||
return
|
||||
elif not self._slack_require_mention():
|
||||
pass # Mention requirement disabled globally for Slack
|
||||
elif not is_mentioned:
|
||||
|
|
@ -1692,3 +1712,14 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
if isinstance(raw, str) and raw.strip():
|
||||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
|
||||
def _slack_require_mention_channels(self) -> set:
|
||||
"""Return channel IDs where bot mention is always required."""
|
||||
raw = self.config.extra.get("require_mention_channels")
|
||||
if raw is None:
|
||||
raw = os.getenv("SLACK_REQUIRE_MENTION_CHANNELS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
return {part.strip() for part in raw.split(",") if part.strip()}
|
||||
return set()
|
||||
|
|
|
|||
|
|
@ -2223,6 +2223,15 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
return {part.strip() for part in str(raw).split(",") if part.strip()}
|
||||
|
||||
def _telegram_require_mention_chats(self) -> set[str]:
|
||||
"""Return Telegram chat IDs where bot mention is always required."""
|
||||
raw = self.config.extra.get("require_mention_chats")
|
||||
if raw is None:
|
||||
raw = os.getenv("TELEGRAM_REQUIRE_MENTION_CHATS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
return {part.strip() for part in str(raw).split(",") if part.strip()}
|
||||
|
||||
def _telegram_ignored_threads(self) -> set[int]:
|
||||
raw = self.config.extra.get("ignored_threads")
|
||||
if raw is None:
|
||||
|
|
@ -2375,6 +2384,13 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
logger.warning("[%s] Ignoring non-numeric Telegram message_thread_id: %r", self.name, thread_id)
|
||||
if str(getattr(getattr(message, "chat", None), "id", "")) in self._telegram_free_response_chats():
|
||||
return True
|
||||
if str(getattr(getattr(message, "chat", None), "id", "")) in self._telegram_require_mention_chats():
|
||||
# Force mention in this chat even when global require_mention is off
|
||||
if self._is_reply_to_bot(message):
|
||||
return True
|
||||
if self._message_mentions_bot(message):
|
||||
return True
|
||||
return self._message_matches_mention_patterns(message)
|
||||
if not self._telegram_require_mention():
|
||||
return True
|
||||
if self._is_reply_to_bot(message):
|
||||
|
|
|
|||
|
|
@ -202,6 +202,15 @@ class WhatsAppAdapter(BasePlatformAdapter):
|
|||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
return {part.strip() for part in str(raw).split(",") if part.strip()}
|
||||
|
||||
def _whatsapp_require_mention_chats(self) -> set[str]:
|
||||
"""Return WhatsApp chat IDs where bot mention is always required."""
|
||||
raw = self.config.extra.get("require_mention_chats")
|
||||
if raw is None:
|
||||
raw = os.getenv("WHATSAPP_REQUIRE_MENTION_CHATS", "")
|
||||
if isinstance(raw, list):
|
||||
return {str(part).strip() for part in raw if str(part).strip()}
|
||||
return {part.strip() for part in str(raw).split(",") if part.strip()}
|
||||
|
||||
@staticmethod
|
||||
def _coerce_allow_list(raw) -> set[str]:
|
||||
"""Parse allow_from / group_allow_from from config or env var."""
|
||||
|
|
@ -336,6 +345,16 @@ class WhatsAppAdapter(BasePlatformAdapter):
|
|||
chat_id = str(data.get("chatId") or "")
|
||||
if chat_id in self._whatsapp_free_response_chats():
|
||||
return True
|
||||
if chat_id in self._whatsapp_require_mention_chats():
|
||||
# Force mention in this chat even when global require_mention is off
|
||||
body = str(data.get("body") or "").strip()
|
||||
if body.startswith("/"):
|
||||
return True
|
||||
if self._message_is_reply_to_bot(data):
|
||||
return True
|
||||
if self._message_mentions_bot(data):
|
||||
return True
|
||||
return self._message_matches_mention_patterns(data)
|
||||
if not self._whatsapp_require_mention():
|
||||
return True
|
||||
body = str(data.get("body") or "").strip()
|
||||
|
|
|
|||
|
|
@ -763,6 +763,7 @@ DEFAULT_CONFIG = {
|
|||
"discord": {
|
||||
"require_mention": True, # Require @mention to respond in server channels
|
||||
"free_response_channels": "", # Comma-separated channel IDs where bot responds without mention
|
||||
"require_mention_channels": "", # Comma-separated channel IDs where bot requires @mention (overrides free_response)
|
||||
"allowed_channels": "", # If set, bot ONLY responds in these channel IDs (whitelist)
|
||||
"auto_thread": True, # Auto-create threads on @mention in channels (like Slack)
|
||||
"reactions": True, # Add 👀/✅/❌ reactions to messages during processing
|
||||
|
|
@ -1530,6 +1531,13 @@ OPTIONAL_ENV_VARS = {
|
|||
"password": False,
|
||||
"category": "messaging",
|
||||
},
|
||||
"MATTERMOST_REQUIRE_MENTION_CHANNELS": {
|
||||
"description": "Comma-separated Mattermost channel IDs where bot always requires @mention (even when require_mention is false)",
|
||||
"prompt": "Require-mention channel IDs (comma-separated)",
|
||||
"url": None,
|
||||
"password": False,
|
||||
"category": "messaging",
|
||||
},
|
||||
"MATRIX_HOMESERVER": {
|
||||
"description": "Matrix homeserver URL (e.g. https://matrix.example.org)",
|
||||
"prompt": "Matrix homeserver URL",
|
||||
|
|
@ -1574,6 +1582,14 @@ OPTIONAL_ENV_VARS = {
|
|||
"category": "messaging",
|
||||
"advanced": True,
|
||||
},
|
||||
"MATRIX_REQUIRE_MENTION_ROOMS": {
|
||||
"description": "Comma-separated Matrix room IDs where bot always requires @mention (even when require_mention is false)",
|
||||
"prompt": "Require-mention room IDs (comma-separated)",
|
||||
"url": None,
|
||||
"password": False,
|
||||
"category": "messaging",
|
||||
"advanced": True,
|
||||
},
|
||||
"MATRIX_AUTO_THREAD": {
|
||||
"description": "Auto-create threads for messages in Matrix rooms (default: true)",
|
||||
"prompt": "Auto-create threads in rooms (true/false)",
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ For cloud sandbox backends, persistence is filesystem-oriented. `TERMINAL_LIFETI
|
|||
| `TELEGRAM_REACTIONS` | Enable emoji reactions on messages during processing (default: `false`) |
|
||||
| `TELEGRAM_REPLY_TO_MODE` | Reply-reference behavior: `off`, `first` (default), or `all`. Matches the Discord pattern. |
|
||||
| `TELEGRAM_IGNORED_THREADS` | Comma-separated Telegram forum topic/thread IDs where the bot never responds |
|
||||
| `TELEGRAM_REQUIRE_MENTION_CHATS` | Comma-separated chat IDs where bot always requires `@mention`, even when the global `require_mention` is `false`. Useful for noisy group chats. |
|
||||
| `TELEGRAM_PROXY` | Proxy URL for Telegram connections — overrides `HTTPS_PROXY`. Supports `http://`, `https://`, `socks5://` |
|
||||
| `DISCORD_BOT_TOKEN` | Discord bot token |
|
||||
| `DISCORD_ALLOWED_USERS` | Comma-separated Discord user IDs allowed to use the bot |
|
||||
|
|
@ -207,6 +208,7 @@ For cloud sandbox backends, persistence is filesystem-oriented. `TERMINAL_LIFETI
|
|||
| `DISCORD_HOME_CHANNEL` | Default Discord channel for cron delivery |
|
||||
| `DISCORD_HOME_CHANNEL_NAME` | Display name for the Discord home channel |
|
||||
| `DISCORD_REQUIRE_MENTION` | Require an @mention before responding in server channels |
|
||||
| `DISCORD_REQUIRE_MENTION_CHANNELS` | Comma-separated channel IDs where bot always requires `@mention`, even when `DISCORD_REQUIRE_MENTION` is `false`. Useful for noisy channels where you want mention-only behavior without changing the server-wide default. |
|
||||
| `DISCORD_FREE_RESPONSE_CHANNELS` | Comma-separated channel IDs where mention is not required |
|
||||
| `DISCORD_AUTO_THREAD` | Auto-thread long replies when supported |
|
||||
| `DISCORD_REACTIONS` | Enable emoji reactions on messages during processing (default: `true`) |
|
||||
|
|
@ -222,10 +224,12 @@ For cloud sandbox backends, persistence is filesystem-oriented. `TERMINAL_LIFETI
|
|||
| `SLACK_ALLOWED_USERS` | Comma-separated Slack user IDs |
|
||||
| `SLACK_HOME_CHANNEL` | Default Slack channel for cron delivery |
|
||||
| `SLACK_HOME_CHANNEL_NAME` | Display name for the Slack home channel |
|
||||
| `SLACK_REQUIRE_MENTION_CHANNELS` | Comma-separated channel IDs where bot always requires `@mention`, even when the global `require_mention` is `false`. Useful for high-traffic channels. |
|
||||
| `WHATSAPP_ENABLED` | Enable the WhatsApp bridge (`true`/`false`) |
|
||||
| `WHATSAPP_MODE` | `bot` (separate number) or `self-chat` (message yourself) |
|
||||
| `WHATSAPP_ALLOWED_USERS` | Comma-separated phone numbers (with country code, no `+`), or `*` to allow all senders |
|
||||
| `WHATSAPP_ALLOW_ALL_USERS` | Allow all WhatsApp senders without an allowlist (`true`/`false`) |
|
||||
| `WHATSAPP_REQUIRE_MENTION_CHATS` | Comma-separated chat IDs where bot always requires `@mention`, even when the global `require_mention` is `false`. Useful for busy group chats. |
|
||||
| `WHATSAPP_DEBUG` | Log raw message events in the bridge for troubleshooting (`true`/`false`) |
|
||||
| `SIGNAL_HTTP_URL` | signal-cli daemon HTTP endpoint (for example `http://127.0.0.1:8080`) |
|
||||
| `SIGNAL_ACCOUNT` | Bot phone number in E.164 format |
|
||||
|
|
@ -259,6 +263,7 @@ For cloud sandbox backends, persistence is filesystem-oriented. `TERMINAL_LIFETI
|
|||
| `DINGTALK_CLIENT_ID` | DingTalk bot AppKey from developer portal ([open.dingtalk.com](https://open.dingtalk.com)) |
|
||||
| `DINGTALK_CLIENT_SECRET` | DingTalk bot AppSecret from developer portal |
|
||||
| `DINGTALK_ALLOWED_USERS` | Comma-separated DingTalk user IDs allowed to message the bot |
|
||||
| `DINGTALK_REQUIRE_MENTION_CHATS` | Comma-separated chat IDs where bot always requires `@mention`, even when the global `require_mention` is `false`. Useful for busy group chats. |
|
||||
| `FEISHU_APP_ID` | Feishu/Lark bot App ID from [open.feishu.cn](https://open.feishu.cn/) |
|
||||
| `FEISHU_APP_SECRET` | Feishu/Lark bot App Secret |
|
||||
| `FEISHU_DOMAIN` | `feishu` (China) or `lark` (international). Default: `feishu` |
|
||||
|
|
@ -315,6 +320,7 @@ For cloud sandbox backends, persistence is filesystem-oriented. `TERMINAL_LIFETI
|
|||
| `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_REQUIRE_MENTION` | Require `@mention` in channels (default: `true`). Set to `false` to respond to all messages. |
|
||||
| `MATTERMOST_REQUIRE_MENTION_CHANNELS` | Comma-separated channel IDs where bot always requires `@mention`, even when `MATTERMOST_REQUIRE_MENTION` is `false`. Useful for noisy channels where you want mention-only behavior without changing the server-wide default. |
|
||||
| `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) |
|
||||
| `MATRIX_HOMESERVER` | Matrix homeserver URL (e.g. `https://matrix.org`) |
|
||||
|
|
@ -327,6 +333,7 @@ For cloud sandbox backends, persistence is filesystem-oriented. `TERMINAL_LIFETI
|
|||
| `MATRIX_DEVICE_ID` | Stable Matrix device ID for E2EE persistence across restarts (e.g. `HERMES_BOT`). Without this, E2EE keys rotate every startup and historic-room decrypt breaks. |
|
||||
| `MATRIX_REACTIONS` | Enable processing-lifecycle emoji reactions on inbound messages (default: `true`). Set to `false` to disable. |
|
||||
| `MATRIX_REQUIRE_MENTION` | Require `@mention` in rooms (default: `true`). Set to `false` to respond to all messages. |
|
||||
| `MATRIX_REQUIRE_MENTION_ROOMS` | Comma-separated room IDs where bot always requires `@mention`, even when `MATRIX_REQUIRE_MENTION` is `false`. Useful for noisy rooms where you want mention-only behavior without changing the server-wide default. |
|
||||
| `MATRIX_FREE_RESPONSE_ROOMS` | Comma-separated room IDs where bot responds without `@mention` |
|
||||
| `MATRIX_AUTO_THREAD` | Auto-create threads for room messages (default: `true`) |
|
||||
| `MATRIX_DM_MENTION_THREADS` | Create a thread when bot is `@mentioned` in a DM (default: `false`) |
|
||||
|
|
|
|||
|
|
@ -1232,11 +1232,13 @@ Configure Discord-specific behavior for the messaging gateway:
|
|||
```yaml
|
||||
discord:
|
||||
require_mention: true # Require @mention to respond in server channels
|
||||
require_mention_channels: "" # Comma-separated channel IDs where @mention is always required (even when require_mention is false)
|
||||
free_response_channels: "" # Comma-separated channel IDs where bot responds without @mention
|
||||
auto_thread: true # Auto-create threads on @mention in channels
|
||||
```
|
||||
|
||||
- `require_mention` — when `true` (default), the bot only responds in server channels when mentioned with `@BotName`. DMs always work without mention.
|
||||
- `require_mention_channels` — comma-separated list of channel IDs where the bot always requires a mention, even when `require_mention` is `false`. This is the inverse of `free_response_channels` — use it to enforce mention-only behavior in specific noisy channels without changing the server-wide default. Available on all platforms: `require_mention_channels` (Discord, Slack, Mattermost), `require_mention_chats` (Telegram, WhatsApp, DingTalk), `require_mention_rooms` (Matrix).
|
||||
- `free_response_channels` — comma-separated list of channel IDs where the bot responds to every message without requiring a mention.
|
||||
- `auto_thread` — when `true` (default), mentions in channels automatically create a thread for the conversation, keeping channels clean (similar to Slack threading).
|
||||
|
||||
|
|
|
|||
|
|
@ -198,6 +198,27 @@ display:
|
|||
interim_assistant_messages: false
|
||||
```
|
||||
|
||||
## Mention Behavior
|
||||
|
||||
By default, the bot only responds in group chats when `@mentioned`. You can change this:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `DINGTALK_REQUIRE_MENTION` | `true` | Set to `false` to respond to all messages in group chats (DMs always work). |
|
||||
| `DINGTALK_FREE_RESPONSE_CHATS` | _(none)_ | Comma-separated chat IDs where the bot responds without `@mention`, even when require_mention is true. |
|
||||
| `DINGTALK_REQUIRE_MENTION_CHATS` | _(none)_ | Comma-separated chat IDs where the bot **always** requires `@mention`, even when require_mention is false. The inverse of free_response_chats. |
|
||||
|
||||
### config.yaml
|
||||
|
||||
You can also configure mention behavior in `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
dingtalk:
|
||||
require_mention: true
|
||||
free_response_chats: ""
|
||||
require_mention_chats: "chat_id_1,chat_id_2"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bot is not responding to messages
|
||||
|
|
|
|||
|
|
@ -277,6 +277,7 @@ Discord behavior is controlled through two files: **`~/.hermes/.env`** for crede
|
|||
| `DISCORD_HOME_CHANNEL_NAME` | No | `"Home"` | Display name for the home channel in logs and status output. |
|
||||
| `DISCORD_REQUIRE_MENTION` | No | `true` | When `true`, the bot only responds in server channels when `@mentioned`. Set to `false` to respond to all messages in every channel. |
|
||||
| `DISCORD_FREE_RESPONSE_CHANNELS` | No | — | Comma-separated channel IDs where the bot responds without requiring an `@mention`, even when `DISCORD_REQUIRE_MENTION` is `true`. |
|
||||
| `DISCORD_REQUIRE_MENTION_CHANNELS` | No | — | Comma-separated channel IDs where the bot **always** requires an `@mention`, even when `DISCORD_REQUIRE_MENTION` is `false`. The inverse of `DISCORD_FREE_RESPONSE_CHANNELS`. |
|
||||
| `DISCORD_IGNORE_NO_MENTION` | No | `true` | When `true`, the bot stays silent if a message `@mentions` other users but does **not** mention the bot. Prevents the bot from jumping into conversations directed at other people. Only applies in server channels, not DMs. |
|
||||
| `DISCORD_AUTO_THREAD` | No | `true` | When `true`, automatically creates a new thread for every `@mention` in a text channel, so each conversation is isolated (similar to Slack behavior). Messages already inside threads or DMs are unaffected. |
|
||||
| `DISCORD_ALLOW_BOTS` | No | `"none"` | Controls how the bot handles messages from other Discord bots. `"none"` — ignore all other bots. `"mentions"` — only accept bot messages that `@mention` Hermes. `"all"` — accept all bot messages. |
|
||||
|
|
@ -302,6 +303,7 @@ The `discord` section in `~/.hermes/config.yaml` mirrors the env vars above. Con
|
|||
discord:
|
||||
require_mention: true # Require @mention in server channels
|
||||
free_response_channels: "" # Comma-separated channel IDs (or YAML list)
|
||||
require_mention_channels: "" # Channel IDs that always require @mention (or YAML list)
|
||||
auto_thread: true # Auto-create threads on @mention
|
||||
reactions: true # Add emoji reactions during processing
|
||||
ignored_channels: [] # Channel IDs where bot never responds
|
||||
|
|
@ -345,6 +347,26 @@ If a thread's parent channel is in this list, the thread also becomes mention-fr
|
|||
|
||||
Free-response channels also **skip auto-threading** — the bot replies inline rather than spinning off a new thread per message. This keeps the channel usable as a lightweight chat surface. If you want threading behavior, don't list the channel as free-response (use normal `@mention` flow instead).
|
||||
|
||||
#### `discord.require_mention_channels`
|
||||
|
||||
**Type:** string or list — **Default:** `""`
|
||||
|
||||
Channel IDs where the bot **always** requires an `@mention` to respond, even when `require_mention` is globally set to `false`. This is the inverse of `free_response_channels` — use it to lock down specific channels while keeping the rest of the server mention-free.
|
||||
|
||||
```yaml
|
||||
# String format
|
||||
discord:
|
||||
require_mention_channels: "1234567890,9876543210"
|
||||
|
||||
# List format
|
||||
discord:
|
||||
require_mention_channels:
|
||||
- 1234567890
|
||||
- 9876543210
|
||||
```
|
||||
|
||||
If a thread's parent channel is in this list, the thread also requires a mention.
|
||||
|
||||
#### `discord.auto_thread`
|
||||
|
||||
**Type:** boolean — **Default:** `true`
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ matrix:
|
|||
require_mention: true # Require @mention in rooms (default: true)
|
||||
free_response_rooms: # Rooms exempt from mention requirement
|
||||
- "!abc123:matrix.org"
|
||||
require_mention_rooms: # Rooms that always require @mention
|
||||
- "!locked789:matrix.org"
|
||||
auto_thread: true # Auto-create threads for responses (default: true)
|
||||
dm_mention_threads: false # Create thread when @mentioned in DM (default: false)
|
||||
```
|
||||
|
|
@ -70,6 +72,7 @@ Or via environment variables:
|
|||
```bash
|
||||
MATRIX_REQUIRE_MENTION=true
|
||||
MATRIX_FREE_RESPONSE_ROOMS=!abc123:matrix.org,!def456:matrix.org
|
||||
MATRIX_REQUIRE_MENTION_ROOMS=!locked789:matrix.org,!secure012:matrix.org
|
||||
MATRIX_AUTO_THREAD=true
|
||||
MATRIX_DM_MENTION_THREADS=false
|
||||
MATRIX_REACTIONS=true # default: true — emoji reactions during processing
|
||||
|
|
|
|||
|
|
@ -155,6 +155,9 @@ MATTERMOST_ALLOWED_USERS=3uo8dkh1p7g1mfk49ear5fzs5c
|
|||
|
||||
# Optional: channels where bot responds without @mention (comma-separated channel IDs)
|
||||
# MATTERMOST_FREE_RESPONSE_CHANNELS=channel_id_1,channel_id_2
|
||||
|
||||
# Optional: channels where bot always requires @mention (comma-separated channel IDs)
|
||||
# MATTERMOST_REQUIRE_MENTION_CHANNELS=channel_id_3,channel_id_4
|
||||
```
|
||||
|
||||
Optional behavior settings in `~/.hermes/config.yaml`:
|
||||
|
|
@ -220,11 +223,23 @@ By default, the bot only responds in channels when `@mentioned`. You can change
|
|||
|----------|---------|-------------|
|
||||
| `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. |
|
||||
| `MATTERMOST_REQUIRE_MENTION_CHANNELS` | _(none)_ | Comma-separated channel IDs where the bot **always** requires `@mention`, even when require_mention is false. The inverse of free_response_channels. |
|
||||
|
||||
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.
|
||||
|
||||
### config.yaml
|
||||
|
||||
You can also configure mention behavior in `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
mattermost:
|
||||
require_mention: true
|
||||
free_response_channels: "channel_id_1,channel_id_2"
|
||||
require_mention_channels: "channel_id_3,channel_id_4"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bot is not responding to messages
|
||||
|
|
|
|||
|
|
@ -283,9 +283,25 @@ slack:
|
|||
```
|
||||
|
||||
:::info
|
||||
Slack supports both patterns: `@mention` required to start a conversation by default, but you can opt specific channels out via `SLACK_FREE_RESPONSE_CHANNELS` (comma-separated channel IDs) or `slack.free_response_channels` in `config.yaml`. Once the bot has an active session in a thread, subsequent thread replies do not require a mention. In DMs the bot always responds without needing a mention.
|
||||
Slack supports both patterns: `@mention` required to start a conversation by default, but you can opt specific channels out via `SLACK_FREE_RESPONSE_CHANNELS` (comma-separated channel IDs) or `slack.free_response_channels` in `config.yaml`. Conversely, you can force specific channels to always require `@mention` — even when `require_mention` is globally `false` — with `SLACK_REQUIRE_MENTION_CHANNELS` or `slack.require_mention_channels`. Once the bot has an active session in a thread, subsequent thread replies do not require a mention. In DMs the bot always responds without needing a mention.
|
||||
:::
|
||||
|
||||
#### Channel-Level Mention Overrides
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `SLACK_FREE_RESPONSE_CHANNELS` | _(none)_ | Comma-separated channel IDs where the bot responds without `@mention`, even when `require_mention` is `true`. |
|
||||
| `SLACK_REQUIRE_MENTION_CHANNELS` | _(none)_ | Comma-separated channel IDs where the bot **always** requires `@mention`, even when `require_mention` is `false`. The inverse of free_response_channels. |
|
||||
|
||||
In `config.yaml`:
|
||||
|
||||
```yaml
|
||||
slack:
|
||||
require_mention: true
|
||||
free_response_channels: "C01CHANNEL1,C02CHANNEL2"
|
||||
require_mention_channels: "C03CHANNEL3,C04CHANNEL4"
|
||||
```
|
||||
|
||||
### Unauthorized User Handling
|
||||
|
||||
```yaml
|
||||
|
|
@ -324,6 +340,7 @@ stt_enabled: true
|
|||
# Slack-specific settings
|
||||
slack:
|
||||
require_mention: true
|
||||
require_mention_channels: [] # Channel IDs that always require @mention
|
||||
unauthorized_dm_behavior: "pair"
|
||||
|
||||
# Platform config
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ Add this to `~/.hermes/config.yaml`:
|
|||
```yaml
|
||||
telegram:
|
||||
require_mention: true
|
||||
require_mention_chats: [] # Chat IDs that always require @mention, even when require_mention is false
|
||||
mention_patterns:
|
||||
- "^\\s*chompy\\b"
|
||||
ignored_threads:
|
||||
|
|
|
|||
|
|
@ -201,6 +201,29 @@ When the agent calls tools (web search, file operations, etc.), WhatsApp display
|
|||
|
||||
---
|
||||
|
||||
## Mention Behavior
|
||||
|
||||
By default, the bot responds to all messages in WhatsApp chats (both 1:1 and group). You can restrict specific group chats to require an `@mention` before the bot responds:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `WHATSAPP_REQUIRE_MENTION` | `true` | When `true`, the bot only responds in group chats when `@mentioned`. DMs always get a response. |
|
||||
| `WHATSAPP_FREE_RESPONSE_CHATS` | _(none)_ | Comma-separated chat IDs where the bot responds without `@mention`, even when require_mention is true. |
|
||||
| `WHATSAPP_REQUIRE_MENTION_CHATS` | _(none)_ | Comma-separated chat IDs where the bot **always** requires `@mention`, even when require_mention is false. The inverse of free_response_chats. |
|
||||
|
||||
### config.yaml
|
||||
|
||||
You can also configure mention behavior in `~/.hermes/config.yaml`:
|
||||
|
||||
```yaml
|
||||
whatsapp:
|
||||
require_mention: true
|
||||
free_response_chats: ""
|
||||
require_mention_chats: "chat_id_1,chat_id_2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue