From c254c5f23ea8c46f2077df04ff6740213868664a Mon Sep 17 00:00:00 2001 From: Abarnaa Sree Date: Fri, 21 Aug 2026 14:31:02 +0530 Subject: [PATCH] fix(slack): trigger monitor on bot mentions --- skills/slack-channel-monitor/scripts/main.py | 49 ++++++++++++++-- tests/test_slack_channel_monitor.py | 59 ++++++++++++++++++++ 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/skills/slack-channel-monitor/scripts/main.py b/skills/slack-channel-monitor/scripts/main.py index 66ba555b..1a4181f1 100644 --- a/skills/slack-channel-monitor/scripts/main.py +++ b/skills/slack-channel-monitor/scripts/main.py @@ -55,6 +55,7 @@ def print(*args, **kwargs): # noqa: A001 – intentional override # ── Embedded configuration (filled in by the skill at creation time) ────────── TRIGGER_PHRASE = "@openhands" +BOT_MENTION_ID = "" CHANNEL_IDS: list[str] = [] # e.g. ["C0123456789", "C9876543210"] DEFAULT_OPENHANDS_URL = "http://localhost:8000" @@ -398,7 +399,7 @@ def thread_replies(token: str, channel: str, thread_ts: str, oldest: str) -> lis return [m for m in messages if m.get("ts") != thread_ts] -def _trigger_index(text: str) -> int: +def _phrase_index(text: str) -> int: """Return the start index of an exact trigger phrase match, or -1.""" lowered = text.lower() trigger = TRIGGER_PHRASE.lower() @@ -417,15 +418,50 @@ def _trigger_index(text: str) -> int: start = idx + 1 +def _mention_span(text: str) -> tuple[int, int]: + """Locate a Slack mention of the bot: `<@U123>` or `<@U123|name>`.""" + if not BOT_MENTION_ID: + return -1, -1 + + needle = f"<@{BOT_MENTION_ID}" + idx = text.find(needle) + + if idx < 0: + return -1, -1 + + end = text.find(">", idx + len(needle)) + + if end < 0: + return -1, -1 + + return idx, end + 1 + + +def _trigger_span(text: str) -> tuple[int, int]: + """Return the span of the earliest literal or Slack mention trigger.""" + spans = [] + + idx = _phrase_index(text) + if idx >= 0: + spans.append((idx, idx + len(TRIGGER_PHRASE))) + + mention = _mention_span(text) + if mention[0] >= 0: + spans.append(mention) + + return min(spans) if spans else (-1, -1) + + def _has_trigger(text: str) -> bool: - return _trigger_index(text) >= 0 + return _trigger_span(text)[0] >= 0 def _request_after_trigger(text: str) -> str: - idx = _trigger_index(text) - if idx < 0: + start, end = _trigger_span(text) + if start < 0: return text - return text[idx + len(TRIGGER_PHRASE):].strip(" :–—") + + return text[end:].strip(" :–—") def full_thread_history( @@ -1024,6 +1060,7 @@ def _check_conversation_completion( def main() -> str | None: """Run one polling cycle. Returns the last conversation ID created, if any.""" + global BOT_MENTION_ID state = load_state() agent_url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/") @@ -1036,8 +1073,8 @@ def main() -> str | None: # Raises RuntimeError immediately if the token is invalid - no point polling. bot_user_id_new, scopes = _slack_auth_test(slack_token) state["bot_user_id"] = bot_user_id_new + BOT_MENTION_ID = bot_user_id_new print(f"Bot user ID: {bot_user_id_new}") - can_react = _verify_token_scopes(scopes) bot_user_id: str = state.get("bot_user_id") or "" diff --git a/tests/test_slack_channel_monitor.py b/tests/test_slack_channel_monitor.py index cba179ff..168b6177 100644 --- a/tests/test_slack_channel_monitor.py +++ b/tests/test_slack_channel_monitor.py @@ -140,3 +140,62 @@ def fake_thread_replies( assert rec["next_reply_poll_at"] == 1006.0 assert rec["watch_until"] == 1301.0 +def test_literal_trigger_still_works(): + helpers = load_slack_monitor_helpers() + helpers["BOT_MENTION_ID"] = "U0B6US8S0DP" + + assert helpers["_has_trigger"]("@openhands please review") + assert ( + helpers["_request_after_trigger"]("@openhands please review") + == "please review" + ) + + +def test_slack_bot_mention_triggers(): + helpers = load_slack_monitor_helpers() + helpers["BOT_MENTION_ID"] = "U0B6US8S0DP" + + text = "<@U0B6US8S0DP> Are you awake?" + + assert helpers["_has_trigger"](text) + assert helpers["_request_after_trigger"](text) == "Are you awake?" + + +def test_slack_bot_mention_with_name_triggers(): + helpers = load_slack_monitor_helpers() + helpers["BOT_MENTION_ID"] = "U0B6US8S0DP" + + text = "<@U0B6US8S0DP|openhands>: fix the build" + + assert helpers["_has_trigger"](text) + assert helpers["_request_after_trigger"](text) == "fix the build" + + +def test_other_slack_user_mention_does_not_trigger(): + helpers = load_slack_monitor_helpers() + helpers["BOT_MENTION_ID"] = "U0B6US8S0DP" + + text = "<@UOTHERUSER> hi" + + assert not helpers["_has_trigger"](text) + + +def test_similar_literal_trigger_does_not_match(): + helpers = load_slack_monitor_helpers() + helpers["BOT_MENTION_ID"] = "U0B6US8S0DP" + + text = "@openhandsbot nope" + + assert not helpers["_has_trigger"](text) + + +def test_earliest_trigger_is_used(): + helpers = load_slack_monitor_helpers() + helpers["BOT_MENTION_ID"] = "U0B6US8S0DP" + + text = "@openhands first <@U0B6US8S0DP> second" + + assert helpers["_request_after_trigger"](text) == ( + "first <@U0B6US8S0DP> second" + ) +