mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat(discord): add channel_prompts config
Add native Discord channel_prompts support with parent forum fallback, ephemeral runtime injection, config migration updates, docs, and tests.
This commit is contained in:
parent
2918328009
commit
2fbdc2c8fa
10 changed files with 355 additions and 6 deletions
|
|
@ -554,6 +554,12 @@ def load_gateway_config() -> GatewayConfig:
|
|||
bridged["mention_patterns"] = platform_cfg["mention_patterns"]
|
||||
if plat == Platform.DISCORD and "channel_skill_bindings" in platform_cfg:
|
||||
bridged["channel_skill_bindings"] = platform_cfg["channel_skill_bindings"]
|
||||
if plat == Platform.DISCORD and "channel_prompts" in platform_cfg:
|
||||
channel_prompts = platform_cfg["channel_prompts"]
|
||||
if isinstance(channel_prompts, dict):
|
||||
bridged["channel_prompts"] = {str(k): v for k, v in channel_prompts.items()}
|
||||
else:
|
||||
bridged["channel_prompts"] = channel_prompts
|
||||
if not bridged:
|
||||
continue
|
||||
plat_data = platforms_data.setdefault(plat.value, {})
|
||||
|
|
|
|||
|
|
@ -682,6 +682,10 @@ class MessageEvent:
|
|||
# Auto-loaded skill(s) for topic/channel bindings (e.g., Telegram DM Topics,
|
||||
# Discord channel_skill_bindings). A single name or ordered list.
|
||||
auto_skill: Optional[str | list[str]] = None
|
||||
|
||||
# Per-channel ephemeral system prompt (e.g. Discord channel_prompts).
|
||||
# Applied at API call time and never persisted to transcript history.
|
||||
channel_prompt: Optional[str] = None
|
||||
|
||||
# Internal flag — set for synthetic events (e.g. background process
|
||||
# completion notifications) that must bypass user authorization checks.
|
||||
|
|
|
|||
|
|
@ -1992,11 +1992,14 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
)
|
||||
|
||||
msg_type = MessageType.COMMAND if text.startswith("/") else MessageType.TEXT
|
||||
channel_id = str(interaction.channel_id)
|
||||
parent_id = str(getattr(getattr(interaction, "channel", None), "parent_id", "") or "")
|
||||
return MessageEvent(
|
||||
text=text,
|
||||
message_type=msg_type,
|
||||
source=source,
|
||||
raw_message=interaction,
|
||||
channel_prompt=self._resolve_channel_prompt(channel_id, parent_id or None),
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -2067,14 +2070,17 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
chat_topic=chat_topic,
|
||||
)
|
||||
|
||||
_parent_id = str(getattr(getattr(interaction, "channel", None), "parent_id", "") or "")
|
||||
_parent_channel = self._thread_parent_channel(getattr(interaction, "channel", None))
|
||||
_parent_id = str(getattr(_parent_channel, "id", "") or "")
|
||||
_skills = self._resolve_channel_skills(thread_id, _parent_id or None)
|
||||
_channel_prompt = self._resolve_channel_prompt(thread_id, _parent_id or None)
|
||||
event = MessageEvent(
|
||||
text=text,
|
||||
message_type=MessageType.TEXT,
|
||||
source=source,
|
||||
raw_message=interaction,
|
||||
auto_skill=_skills,
|
||||
channel_prompt=_channel_prompt,
|
||||
)
|
||||
await self.handle_message(event)
|
||||
|
||||
|
|
@ -2103,6 +2109,34 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
return list(dict.fromkeys(skills)) # dedup, preserve order
|
||||
return None
|
||||
|
||||
def _resolve_channel_prompt(self, channel_id: str, parent_id: str | None = None) -> str | None:
|
||||
"""Resolve a Discord per-channel prompt, preferring the exact channel over its parent.
|
||||
|
||||
Config format (in platform extra):
|
||||
channel_prompts:
|
||||
"123456": "Prompt text"
|
||||
|
||||
Forum/thread messages inherit the parent forum/channel prompt when the
|
||||
thread itself has no explicit override.
|
||||
"""
|
||||
prompts = self.config.extra.get("channel_prompts") or {}
|
||||
if not isinstance(prompts, dict):
|
||||
return None
|
||||
prompts = {str(k): v for k, v in prompts.items()}
|
||||
|
||||
for key in (channel_id, parent_id):
|
||||
if not key:
|
||||
continue
|
||||
prompt = prompts.get(key)
|
||||
if prompt is None:
|
||||
prompt = prompts.get(str(key))
|
||||
if prompt is None:
|
||||
continue
|
||||
prompt = str(prompt).strip()
|
||||
if prompt:
|
||||
return prompt
|
||||
return None
|
||||
|
||||
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
|
||||
|
|
@ -2654,6 +2688,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
_parent_id = str(getattr(_chan, "parent_id", "") or "")
|
||||
_chan_id = str(getattr(_chan, "id", ""))
|
||||
_skills = self._resolve_channel_skills(_chan_id, _parent_id or None)
|
||||
_channel_prompt = self._resolve_channel_prompt(_chan_id, _parent_id or None)
|
||||
|
||||
reply_to_id = None
|
||||
reply_to_text = None
|
||||
|
|
@ -2674,6 +2709,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
reply_to_text=reply_to_text,
|
||||
timestamp=message.created_at,
|
||||
auto_skill=_skills,
|
||||
channel_prompt=_channel_prompt,
|
||||
)
|
||||
|
||||
# Track thread participation so the bot won't require @mention for
|
||||
|
|
|
|||
|
|
@ -2891,6 +2891,7 @@ class GatewayRunner:
|
|||
message_type=_MT.TEXT,
|
||||
source=event.source,
|
||||
message_id=event.message_id,
|
||||
channel_prompt=getattr(event, "channel_prompt", None),
|
||||
)
|
||||
adapter._pending_messages[_quick_key] = queued_event
|
||||
return "Queued for the next turn."
|
||||
|
|
@ -3868,6 +3869,7 @@ class GatewayRunner:
|
|||
session_id=session_entry.session_id,
|
||||
session_key=session_key,
|
||||
event_message_id=event.message_id,
|
||||
channel_prompt=getattr(event, "channel_prompt", None),
|
||||
)
|
||||
|
||||
# Stop persistent typing indicator now that the agent is done
|
||||
|
|
@ -5089,6 +5091,7 @@ class GatewayRunner:
|
|||
message_type=MessageType.TEXT,
|
||||
source=source,
|
||||
raw_message=event.raw_message,
|
||||
channel_prompt=getattr(event, "channel_prompt", None),
|
||||
)
|
||||
|
||||
# Let the normal message handler process it
|
||||
|
|
@ -8069,6 +8072,7 @@ class GatewayRunner:
|
|||
session_key: str = None,
|
||||
_interrupt_depth: int = 0,
|
||||
event_message_id: Optional[str] = None,
|
||||
channel_prompt: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Run the agent with the given message and context.
|
||||
|
|
@ -8423,8 +8427,12 @@ class GatewayRunner:
|
|||
# Platform.LOCAL ("local") maps to "cli"; others pass through as-is.
|
||||
platform_key = "cli" if source.platform == Platform.LOCAL else source.platform.value
|
||||
|
||||
# Combine platform context with user-configured ephemeral system prompt
|
||||
# Combine platform context, per-channel context, and the user-configured
|
||||
# ephemeral system prompt.
|
||||
combined_ephemeral = context_prompt or ""
|
||||
event_channel_prompt = (channel_prompt or "").strip()
|
||||
if event_channel_prompt:
|
||||
combined_ephemeral = (combined_ephemeral + "\n\n" + event_channel_prompt).strip()
|
||||
if self._ephemeral_system_prompt:
|
||||
combined_ephemeral = (combined_ephemeral + "\n\n" + self._ephemeral_system_prompt).strip()
|
||||
|
||||
|
|
@ -9376,6 +9384,7 @@ class GatewayRunner:
|
|||
session_key=session_key,
|
||||
_interrupt_depth=_interrupt_depth + 1,
|
||||
event_message_id=next_message_id,
|
||||
channel_prompt=getattr(pending_event, "channel_prompt", None),
|
||||
)
|
||||
finally:
|
||||
# Stop progress sender, interrupt monitor, and notification task
|
||||
|
|
|
|||
|
|
@ -659,6 +659,7 @@ DEFAULT_CONFIG = {
|
|||
"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
|
||||
"channel_prompts": {}, # Per-channel ephemeral system prompts (forum parents apply to child threads)
|
||||
},
|
||||
|
||||
# WhatsApp platform settings (gateway mode)
|
||||
|
|
@ -724,7 +725,7 @@ DEFAULT_CONFIG = {
|
|||
},
|
||||
|
||||
# Config schema version - bump this when adding new required fields
|
||||
"_config_version": 17,
|
||||
"_config_version": 18,
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
|
|
|
|||
|
|
@ -193,6 +193,27 @@ class TestLoadGatewayConfig:
|
|||
|
||||
assert config.thread_sessions_per_user is False
|
||||
|
||||
def test_bridges_discord_channel_prompts_from_config_yaml(self, tmp_path, monkeypatch):
|
||||
hermes_home = tmp_path / ".hermes"
|
||||
hermes_home.mkdir()
|
||||
config_path = hermes_home / "config.yaml"
|
||||
config_path.write_text(
|
||||
"discord:\n"
|
||||
" channel_prompts:\n"
|
||||
" \"123\": Research mode\n"
|
||||
" 456: Therapist mode\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
|
||||
config = load_gateway_config()
|
||||
|
||||
assert config.platforms[Platform.DISCORD].extra["channel_prompts"] == {
|
||||
"123": "Research mode",
|
||||
"456": "Therapist mode",
|
||||
}
|
||||
|
||||
def test_invalid_quick_commands_in_config_yaml_are_ignored(self, tmp_path, monkeypatch):
|
||||
hermes_home = tmp_path / ".hermes"
|
||||
hermes_home.mkdir()
|
||||
|
|
|
|||
229
tests/gateway/test_discord_channel_prompts.py
Normal file
229
tests/gateway/test_discord_channel_prompts.py
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
"""Tests for Discord channel_prompts resolution and injection."""
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import types
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
import gateway.run as gateway_run
|
||||
from gateway.config import Platform
|
||||
from gateway.platforms.base import MessageEvent
|
||||
from gateway.session import SessionSource
|
||||
|
||||
|
||||
class _CapturingAgent:
|
||||
last_init = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
type(self).last_init = dict(kwargs)
|
||||
self.tools = []
|
||||
|
||||
def run_conversation(self, user_message, conversation_history=None, task_id=None, persist_user_message=None):
|
||||
return {
|
||||
"final_response": "ok",
|
||||
"messages": [],
|
||||
"api_calls": 1,
|
||||
"completed": True,
|
||||
}
|
||||
|
||||
|
||||
def _install_fake_agent(monkeypatch):
|
||||
fake_run_agent = types.ModuleType("run_agent")
|
||||
fake_run_agent.AIAgent = _CapturingAgent
|
||||
monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent)
|
||||
|
||||
|
||||
def _make_adapter():
|
||||
from gateway.platforms.discord import DiscordAdapter
|
||||
|
||||
adapter = object.__new__(DiscordAdapter)
|
||||
adapter.config = MagicMock()
|
||||
adapter.config.extra = {}
|
||||
return adapter
|
||||
|
||||
|
||||
def _make_runner():
|
||||
runner = object.__new__(gateway_run.GatewayRunner)
|
||||
runner.adapters = {}
|
||||
runner._ephemeral_system_prompt = "Global prompt"
|
||||
runner._prefill_messages = []
|
||||
runner._reasoning_config = None
|
||||
runner._service_tier = None
|
||||
runner._provider_routing = {}
|
||||
runner._fallback_model = None
|
||||
runner._smart_model_routing = {}
|
||||
runner._running_agents = {}
|
||||
runner._pending_model_notes = {}
|
||||
runner._session_db = None
|
||||
runner._agent_cache = {}
|
||||
runner._agent_cache_lock = threading.Lock()
|
||||
runner._session_model_overrides = {}
|
||||
runner.hooks = SimpleNamespace(loaded_hooks=False)
|
||||
runner.config = SimpleNamespace(streaming=None)
|
||||
runner.session_store = SimpleNamespace(
|
||||
get_or_create_session=lambda source: SimpleNamespace(session_id="session-1"),
|
||||
load_transcript=lambda session_id: [],
|
||||
)
|
||||
runner._get_or_create_gateway_honcho = lambda session_key: (None, None)
|
||||
runner._enrich_message_with_vision = AsyncMock(return_value="ENRICHED")
|
||||
return runner
|
||||
|
||||
|
||||
def _make_source() -> SessionSource:
|
||||
return SessionSource(
|
||||
platform=Platform.DISCORD,
|
||||
chat_id="12345",
|
||||
chat_type="thread",
|
||||
user_id="user-1",
|
||||
)
|
||||
|
||||
|
||||
class TestResolveChannelPrompts:
|
||||
def test_no_prompt_returns_none(self):
|
||||
adapter = _make_adapter()
|
||||
assert adapter._resolve_channel_prompt("123") is None
|
||||
|
||||
def test_match_by_channel_id(self):
|
||||
adapter = _make_adapter()
|
||||
adapter.config.extra = {"channel_prompts": {"100": "Research mode"}}
|
||||
assert adapter._resolve_channel_prompt("100") == "Research mode"
|
||||
|
||||
def test_match_by_numeric_channel_id_key(self):
|
||||
adapter = _make_adapter()
|
||||
adapter.config.extra = {"channel_prompts": {100: "Research mode"}}
|
||||
assert adapter._resolve_channel_prompt("100") == "Research mode"
|
||||
|
||||
def test_match_by_parent_id(self):
|
||||
adapter = _make_adapter()
|
||||
adapter.config.extra = {"channel_prompts": {"200": "Forum prompt"}}
|
||||
assert adapter._resolve_channel_prompt("999", parent_id="200") == "Forum prompt"
|
||||
|
||||
def test_exact_channel_overrides_parent(self):
|
||||
adapter = _make_adapter()
|
||||
adapter.config.extra = {
|
||||
"channel_prompts": {
|
||||
"999": "Thread override",
|
||||
"200": "Forum prompt",
|
||||
}
|
||||
}
|
||||
assert adapter._resolve_channel_prompt("999", parent_id="200") == "Thread override"
|
||||
|
||||
def test_build_message_event_sets_channel_prompt(self):
|
||||
adapter = _make_adapter()
|
||||
adapter.config.extra = {"channel_prompts": {"321": "Command prompt"}}
|
||||
adapter.build_source = MagicMock(return_value=SimpleNamespace())
|
||||
|
||||
interaction = SimpleNamespace(
|
||||
channel_id=321,
|
||||
channel=SimpleNamespace(name="general", guild=None, parent_id=None),
|
||||
user=SimpleNamespace(id=1, display_name="Brenner"),
|
||||
)
|
||||
adapter._get_effective_topic = MagicMock(return_value=None)
|
||||
|
||||
event = adapter._build_slash_event(interaction, "/retry")
|
||||
|
||||
assert event.channel_prompt == "Command prompt"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispatch_thread_session_inherits_parent_channel_prompt(self):
|
||||
adapter = _make_adapter()
|
||||
adapter.config.extra = {"channel_prompts": {"200": "Parent prompt"}}
|
||||
adapter.build_source = MagicMock(return_value=SimpleNamespace())
|
||||
adapter._get_effective_topic = MagicMock(return_value=None)
|
||||
adapter.handle_message = AsyncMock()
|
||||
|
||||
interaction = SimpleNamespace(
|
||||
guild=SimpleNamespace(name="Wetlands"),
|
||||
channel=SimpleNamespace(id=200, parent=None),
|
||||
user=SimpleNamespace(id=1, display_name="Brenner"),
|
||||
)
|
||||
|
||||
await adapter._dispatch_thread_session(interaction, "999", "new-thread", "hello")
|
||||
|
||||
dispatched_event = adapter.handle_message.await_args.args[0]
|
||||
assert dispatched_event.channel_prompt == "Parent prompt"
|
||||
|
||||
def test_blank_prompts_are_ignored(self):
|
||||
adapter = _make_adapter()
|
||||
adapter.config.extra = {"channel_prompts": {"100": " "}}
|
||||
assert adapter._resolve_channel_prompt("100") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_retry_preserves_channel_prompt(monkeypatch):
|
||||
runner = _make_runner()
|
||||
runner.session_store = SimpleNamespace(
|
||||
get_or_create_session=lambda source: SimpleNamespace(session_id="session-1", last_prompt_tokens=10),
|
||||
load_transcript=lambda session_id: [
|
||||
{"role": "user", "content": "original message"},
|
||||
{"role": "assistant", "content": "old reply"},
|
||||
],
|
||||
rewrite_transcript=MagicMock(),
|
||||
)
|
||||
runner._handle_message = AsyncMock(return_value="ok")
|
||||
|
||||
event = MessageEvent(
|
||||
text="/retry",
|
||||
message_type=gateway_run.MessageType.COMMAND,
|
||||
source=_make_source(),
|
||||
raw_message=SimpleNamespace(),
|
||||
channel_prompt="Channel prompt",
|
||||
)
|
||||
|
||||
result = await runner._handle_retry_command(event)
|
||||
|
||||
assert result == "ok"
|
||||
retried_event = runner._handle_message.await_args.args[0]
|
||||
assert retried_event.channel_prompt == "Channel prompt"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_agent_appends_channel_prompt_to_ephemeral_system_prompt(monkeypatch, tmp_path):
|
||||
_install_fake_agent(monkeypatch)
|
||||
runner = _make_runner()
|
||||
|
||||
(tmp_path / "config.yaml").write_text("agent:\n system_prompt: Global prompt\n", encoding="utf-8")
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
|
||||
monkeypatch.setattr(gateway_run, "_env_path", tmp_path / ".env")
|
||||
monkeypatch.setattr(gateway_run, "load_dotenv", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(gateway_run, "_load_gateway_config", lambda: {})
|
||||
monkeypatch.setattr(gateway_run, "_resolve_gateway_model", lambda config=None: "gpt-5.4")
|
||||
monkeypatch.setattr(
|
||||
gateway_run,
|
||||
"_resolve_runtime_agent_kwargs",
|
||||
lambda: {
|
||||
"provider": "openrouter",
|
||||
"api_mode": "chat_completions",
|
||||
"base_url": "https://openrouter.ai/api/v1",
|
||||
"api_key": "***",
|
||||
},
|
||||
)
|
||||
|
||||
import hermes_cli.tools_config as tools_config
|
||||
|
||||
monkeypatch.setattr(tools_config, "_get_platform_tools", lambda user_config, platform_key: {"core"})
|
||||
|
||||
_CapturingAgent.last_init = None
|
||||
event = MessageEvent(
|
||||
text="hi",
|
||||
source=_make_source(),
|
||||
message_id="m1",
|
||||
channel_prompt="Channel prompt",
|
||||
)
|
||||
result = await runner._run_agent(
|
||||
message="hi",
|
||||
context_prompt="Context prompt",
|
||||
history=[],
|
||||
source=_make_source(),
|
||||
session_id="session-1",
|
||||
session_key="agent:main:discord:thread:12345",
|
||||
channel_prompt=event.channel_prompt,
|
||||
)
|
||||
|
||||
assert result["final_response"] == "ok"
|
||||
assert _CapturingAgent.last_init["ephemeral_system_prompt"] == (
|
||||
"Context prompt\n\nChannel prompt\n\nGlobal prompt"
|
||||
)
|
||||
|
|
@ -459,7 +459,7 @@ class TestCustomProviderCompatibility:
|
|||
migrate_config(interactive=False, quiet=True)
|
||||
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
|
||||
assert raw["_config_version"] == 17
|
||||
assert raw["_config_version"] == 18
|
||||
assert raw["providers"]["openai-direct"] == {
|
||||
"api": "https://api.openai.com/v1",
|
||||
"api_key": "test-key",
|
||||
|
|
@ -606,6 +606,26 @@ class TestInterimAssistantMessageConfig:
|
|||
migrate_config(interactive=False, quiet=True)
|
||||
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
|
||||
assert raw["_config_version"] == 17
|
||||
assert raw["_config_version"] == 18
|
||||
assert raw["display"]["tool_progress"] == "off"
|
||||
assert raw["display"]["interim_assistant_messages"] is True
|
||||
|
||||
|
||||
class TestDiscordChannelPromptsConfig:
|
||||
def test_default_config_includes_discord_channel_prompts(self):
|
||||
assert DEFAULT_CONFIG["discord"]["channel_prompts"] == {}
|
||||
|
||||
def test_migrate_adds_discord_channel_prompts_default(self, tmp_path):
|
||||
config_path = tmp_path / "config.yaml"
|
||||
config_path.write_text(
|
||||
yaml.safe_dump({"_config_version": 17, "discord": {"auto_thread": True}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}):
|
||||
migrate_config(interactive=False, quiet=True)
|
||||
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
|
||||
assert raw["_config_version"] == 18
|
||||
assert raw["discord"]["auto_thread"] is True
|
||||
assert raw["discord"]["channel_prompts"] == {}
|
||||
|
|
|
|||
|
|
@ -64,4 +64,4 @@ class TestCamofoxConfigDefaults:
|
|||
|
||||
# The current schema version is tracked globally; unrelated default
|
||||
# options may bump it after browser defaults are added.
|
||||
assert DEFAULT_CONFIG["_config_version"] == 17
|
||||
assert DEFAULT_CONFIG["_config_version"] == 18
|
||||
|
|
|
|||
|
|
@ -297,6 +297,7 @@ discord:
|
|||
reactions: true # Add emoji reactions during processing
|
||||
ignored_channels: [] # Channel IDs where bot never responds
|
||||
no_thread_channels: [] # Channel IDs where bot responds without threading
|
||||
channel_prompts: {} # Per-channel ephemeral system prompts
|
||||
|
||||
# Session isolation (applies to all gateway platforms, not just Discord)
|
||||
group_sessions_per_user: true # Isolate sessions per user in shared channels
|
||||
|
|
@ -381,6 +382,28 @@ discord:
|
|||
|
||||
Useful for channels dedicated to bot interaction where threads would add unnecessary noise.
|
||||
|
||||
#### `discord.channel_prompts`
|
||||
|
||||
**Type:** mapping — **Default:** `{}`
|
||||
|
||||
Per-channel ephemeral system prompts that are injected on every turn in the matching Discord channel or thread without being persisted to transcript history.
|
||||
|
||||
```yaml
|
||||
discord:
|
||||
channel_prompts:
|
||||
"1234567890": |
|
||||
This channel is for research tasks. Prefer deep comparisons,
|
||||
citations, and concise synthesis.
|
||||
"9876543210": |
|
||||
This forum is for therapy-style support. Be warm, grounded,
|
||||
and non-judgmental.
|
||||
```
|
||||
|
||||
Behavior:
|
||||
- Exact thread/channel ID matches win.
|
||||
- If a message arrives inside a thread or forum post and that thread has no explicit entry, Hermes falls back to the parent channel/forum ID.
|
||||
- Prompts are applied ephemerally at runtime, so changing them affects future turns immediately without rewriting past session history.
|
||||
|
||||
#### `group_sessions_per_user`
|
||||
|
||||
**Type:** boolean — **Default:** `true`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue