Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 43 additions & 6 deletions skills/slack-channel-monitor/scripts/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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()
Expand All @@ -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(
Expand Down Expand Up @@ -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("/")
Expand All @@ -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 ""
Expand Down
59 changes: 59 additions & 0 deletions tests/test_slack_channel_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Loading