mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-24 16:54:43 +00:00
fix(slack): dispatch mentioned bang commands in threads
This commit is contained in:
parent
ef8936d599
commit
5f3f1948b7
2 changed files with 133 additions and 18 deletions
|
|
@ -181,6 +181,23 @@ def _slack_mention_detection_text(event: dict) -> str:
|
|||
return (flat.strip() + "\n" + " ".join(extra)).strip()
|
||||
|
||||
|
||||
def _rewrite_known_bang_command(text: str) -> str:
|
||||
"""Rewrite a known leading ``!cmd`` to the gateway ``/cmd`` form."""
|
||||
if not text.startswith("!"):
|
||||
return text
|
||||
|
||||
try:
|
||||
from hermes_cli.commands import is_gateway_known_command
|
||||
|
||||
first_token = text[1:].split(maxsplit=1)[0]
|
||||
cmd_name = first_token.split("@", 1)[0].lower()
|
||||
if cmd_name and "/" not in cmd_name and is_gateway_known_command(cmd_name):
|
||||
return "/" + text[1:]
|
||||
except Exception: # pragma: no cover - defensive
|
||||
pass
|
||||
return text
|
||||
|
||||
|
||||
def _extract_text_from_slack_blocks(blocks: list) -> str:
|
||||
"""Extract readable text from Slack Block Kit blocks, including quoted/forwarded content.
|
||||
|
||||
|
|
@ -3788,24 +3805,9 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
# gateway dispatcher) handles it like a normal slash command. Only
|
||||
# rewrite when the first token resolves to a known gateway command
|
||||
# so casual messages like "!nice work" pass through unchanged.
|
||||
command_probe_text = original_text.lstrip()
|
||||
if command_probe_text.startswith("!"):
|
||||
try:
|
||||
from hermes_cli.commands import is_gateway_known_command
|
||||
|
||||
first_token = command_probe_text[1:].split(maxsplit=1)[0]
|
||||
# Strip "@suffix" the same way get_command() does, so
|
||||
# forms like ``!stop@hermes`` still resolve.
|
||||
cmd_name = first_token.split("@", 1)[0].lower()
|
||||
if (
|
||||
cmd_name
|
||||
and "/" not in cmd_name
|
||||
and is_gateway_known_command(cmd_name)
|
||||
):
|
||||
original_text = "/" + command_probe_text[1:]
|
||||
command_probe_text = original_text
|
||||
except Exception: # pragma: no cover - defensive
|
||||
pass
|
||||
command_probe_text = _rewrite_known_bang_command(original_text.lstrip())
|
||||
if command_probe_text != original_text.lstrip():
|
||||
original_text = command_probe_text
|
||||
|
||||
is_command_text = command_probe_text.startswith("/")
|
||||
text = original_text
|
||||
|
|
@ -4026,6 +4028,18 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
if is_mentioned:
|
||||
# Strip the bot mention from the text
|
||||
text = text.replace(f"<@{bot_uid}>", "").strip()
|
||||
# Re-run command normalization against the canonical Slack text,
|
||||
# not the block-augmented agent text. Otherwise quoted/forwarded
|
||||
# rich-text payload can become accidental command arguments.
|
||||
mention_stripped = original_text.replace(f"<@{bot_uid}>", "").strip()
|
||||
command_text = _rewrite_known_bang_command(mention_stripped)
|
||||
if command_text != mention_stripped:
|
||||
original_text = command_text
|
||||
text = command_text
|
||||
# Refresh command classification: the bang was hidden behind
|
||||
# the leading mention when the first probe ran.
|
||||
command_probe_text = command_text
|
||||
is_command_text = True
|
||||
# Register this thread so all future messages auto-trigger the bot.
|
||||
# Skipped in strict mode: strict_mention=true bots must be
|
||||
# re-mentioned every turn, so remembering the thread would
|
||||
|
|
|
|||
|
|
@ -1559,6 +1559,107 @@ class TestBangPrefixCommands:
|
|||
assert msg_event.text.startswith("/queue")
|
||||
assert msg_event.message_type == MessageType.COMMAND
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mention_prefixed_bang_is_rewritten(self, adapter):
|
||||
evt = self._make_event(
|
||||
"<@U_BOT> !new",
|
||||
thread_ts="1111111111.000001",
|
||||
channel_type="channel",
|
||||
channel="C123",
|
||||
)
|
||||
await adapter._handle_slack_message(evt)
|
||||
|
||||
msg_event = adapter.handle_message.call_args[0][0]
|
||||
assert msg_event.text == "/new"
|
||||
assert msg_event.message_type == MessageType.COMMAND
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mention_prefixed_bang_no_space(self, adapter):
|
||||
evt = self._make_event(
|
||||
"<@U_BOT>!new",
|
||||
thread_ts="1111111111.000001",
|
||||
channel_type="channel",
|
||||
channel="C123",
|
||||
)
|
||||
await adapter._handle_slack_message(evt)
|
||||
|
||||
msg_event = adapter.handle_message.call_args[0][0]
|
||||
assert msg_event.text == "/new"
|
||||
assert msg_event.message_type == MessageType.COMMAND
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mention_prefixed_unknown_bang_passes_through(self, adapter):
|
||||
evt = self._make_event(
|
||||
"<@U_BOT> !nice work",
|
||||
channel_type="channel",
|
||||
channel="C123",
|
||||
)
|
||||
await adapter._handle_slack_message(evt)
|
||||
|
||||
msg_event = adapter.handle_message.call_args[0][0]
|
||||
assert msg_event.text == "!nice work"
|
||||
assert msg_event.message_type != MessageType.COMMAND
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_thread_command_skips_context_prefix(self, adapter):
|
||||
adapter._has_active_session_for_thread = MagicMock(return_value=False)
|
||||
adapter._fetch_thread_context = AsyncMock(
|
||||
side_effect=AssertionError(
|
||||
"_fetch_thread_context must not be called for slash commands"
|
||||
)
|
||||
)
|
||||
evt = self._make_event(
|
||||
"<@U_BOT> !new",
|
||||
thread_ts="1111111111.000001",
|
||||
channel_type="channel",
|
||||
channel="C123",
|
||||
)
|
||||
await adapter._handle_slack_message(evt)
|
||||
|
||||
msg_event = adapter.handle_message.call_args[0][0]
|
||||
assert msg_event.text == "/new"
|
||||
assert msg_event.message_type == MessageType.COMMAND
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mention_command_drops_rich_text_command_arguments(self, adapter):
|
||||
evt = self._make_event(
|
||||
"<@U_BOT> !model",
|
||||
thread_ts="1111111111.000001",
|
||||
channel_type="channel",
|
||||
channel="C123",
|
||||
)
|
||||
evt["blocks"] = [
|
||||
{
|
||||
"type": "rich_text",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_section",
|
||||
"elements": [
|
||||
{"type": "user", "user_id": "U_BOT"},
|
||||
{"type": "text", "text": " !model"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "rich_text_quote",
|
||||
"elements": [
|
||||
{
|
||||
"type": "rich_text_section",
|
||||
"elements": [
|
||||
{"type": "text", "text": "quoted context"}
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
await adapter._handle_slack_message(evt)
|
||||
|
||||
msg_event = adapter.handle_message.call_args[0][0]
|
||||
assert msg_event.text == "/model"
|
||||
assert "quoted context" not in msg_event.text
|
||||
assert msg_event.message_type == MessageType.COMMAND
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestIncomingDocumentHandling
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue