feat: add Slack reaction tool (#1650)

Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
(cherry picked from commit ee224d3e91576f771e93df7bad4523a7a7036324)
This commit is contained in:
Ramon Nogueira 2026-06-30 17:35:51 -04:00 • committed by Adam Moussa
parent b2b49ed944
commit 3c6077c418
No known key found for this signature in database
10 changed files with 137 additions and 4 deletions

View file

@ -85,7 +85,7 @@ There is intentionally no after-agent safety net that opens a PR for the agent.
All tools live in `agent/tools/` and are flat-imported via `agent/tools/__init__.py`. The set is intentionally small and curated — see README "Tools — Curated, Not Accumulated".
Wired into `get_agent`:
`http_request`, `fetch_url`, `web_search`, `linear_comment`, `linear_create_issue`, `linear_delete_issue`, `linear_get_issue`, `linear_get_issue_comments`, `linear_list_teams`, `linear_update_issue`, `request_pr_review`, `schedule_thread_wakeup`, `slack_read_thread_messages`, `slack_thread_reply`.
`http_request`, `fetch_url`, `web_search`, `linear_comment`, `linear_create_issue`, `linear_delete_issue`, `linear_get_issue`, `linear_get_issue_comments`, `linear_list_teams`, `linear_update_issue`, `request_pr_review`, `schedule_thread_wakeup`, `slack_add_reaction`, `slack_read_thread_messages`, `slack_thread_reply`.
Reviewer-only tools (in `agent/reviewer.py`): `add_finding`, `update_finding`, `list_findings`, `publish_review`. The review-style analyzer uses `save_review_style` (exported as `save_review_style_prompt`).

View file

@ -81,7 +81,7 @@ There is intentionally no after-agent safety net that opens a PR for the agent.
All tools live in `agent/tools/` and are flat-imported via `agent/tools/__init__.py`. The set is intentionally small and curated — see README "Tools — Curated, Not Accumulated".
Wired into `get_agent`:
`http_request`, `fetch_url`, `web_search`, `linear_comment`, `linear_create_issue`, `linear_delete_issue`, `linear_get_issue`, `linear_get_issue_comments`, `linear_list_teams`, `linear_update_issue`, `request_pr_review`, `schedule_thread_wakeup`, `slack_read_thread_messages`, `slack_thread_reply`.
`http_request`, `fetch_url`, `web_search`, `linear_comment`, `linear_create_issue`, `linear_delete_issue`, `linear_get_issue`, `linear_get_issue_comments`, `linear_list_teams`, `linear_update_issue`, `request_pr_review`, `schedule_thread_wakeup`, `slack_add_reaction`, `slack_read_thread_messages`, `slack_thread_reply`.
Reviewer-only tools (in `agent/reviewer.py`): `add_finding`, `update_finding`, `list_findings`, `publish_review`. The review-style analyzer uses `save_review_style` (exported as `save_review_style_prompt`).

View file

@ -72,6 +72,7 @@ Stripe's key insight: *tool curation matters more than tool quantity.* Open SWE
| `fetch_url` | Fetch web pages as markdown |
| `http_request` | API calls (GET, POST, etc.) |
| `linear_comment` | Post updates to Linear tickets |
| `slack_add_reaction` | React to Slack messages |
| `slack_thread_reply` | Reply in Slack threads |
GitHub operations are performed with `GH_TOKEN=dummy gh` inside the sandbox, backed by the LangSmith proxy. Plus the built-in Deep Agents tools: `read_file`, `write_file`, `edit_file`, `ls`, `glob`, `grep`, `write_todos`, and `task` (subagent spawning).

View file

@ -55,6 +55,7 @@ _TOOL_STATUS: dict[str, str] = {
"fetch_url": "fetching a URL...",
"http_request": "making an HTTP request...",
"request_pr_review": "requesting a PR review...",
"slack_add_reaction": "reacting in Slack...",
"slack_read_thread_messages": "reading Slack history...",
"slack_thread_reply": "drafting a Slack reply...",
"linear_comment": "commenting on Linear...",

View file

@ -84,6 +84,7 @@ OPEN_SWE_SHARED_BASE = """You are **Open SWE**, an open-source agent built on La
- Focus on the substance and keep summaries brief. Use light markdown (`###`/`####` headings, bold, code) — avoid `#`/`##` titles.
- In Slack, when a user asks to “break out,” “split out,” or “start a separate thread” for part of the work, summarize the requested aspect and relevant context into self-contained instructions, then call `slack_start_new_thread` instead of only replying in the current thread.
- In Slack, when acknowledging a user follow-up while you continue working, prefer `slack_add_reaction` with the default `eyes` reaction over posting a perfunctory “Updating…” / “I’ll check…” confirmation reply.
- When you post to Slack with `slack_thread_reply`, do not repeat that text in a later assistant message; the user can already see the Slack message.
- When delegated work to a subagent: the calling agent only sees your final message, so make it the complete answer.

View file

@ -85,6 +85,7 @@ from .tools import (
request_pr_review,
save_plan,
schedule_thread_wakeup,
slack_add_reaction,
slack_read_thread_messages,
slack_start_new_thread,
slack_thread_reply,
@ -959,6 +960,7 @@ async def get_agent(config: RunnableConfig) -> Pregel:
open_pull_request,
request_pr_review,
schedule_thread_wakeup,
slack_add_reaction,
slack_read_thread_messages,
slack_start_new_thread,
slack_thread_reply,

View file

@ -20,6 +20,7 @@ from .resolve_finding_thread import resolve_finding_thread
from .save_plan import save_plan
from .schedule_thread_wakeup import schedule_thread_wakeup
from .search_repo_code import search_repo_code
from .slack_add_reaction import slack_add_reaction
from .slack_read_thread_messages import slack_read_thread_messages
from .slack_start_new_thread import slack_start_new_thread
from .slack_thread_reply import slack_thread_reply
@ -49,6 +50,7 @@ __all__ = [
"save_plan",
"schedule_thread_wakeup",
"search_repo_code",
"slack_add_reaction",
"slack_read_thread_messages",
"slack_start_new_thread",
"slack_thread_reply",

View file

@ -0,0 +1,46 @@
from typing import Any
from langgraph.config import get_config
from ..utils.slack import add_slack_reaction
async def slack_add_reaction(
emoji: str = "eyes",
message_ts: str | None = None,
) -> dict[str, Any]:
"""Add a reaction to a Slack message in the current Slack thread.
Use this with the default `eyes` reaction to acknowledge Slack user follow-up
requests while you continue working, instead of posting a perfunctory
confirmation reply. If `message_ts` is omitted, this reacts to the latest
message that triggered the run. Pass emoji names without surrounding colons.
"""
config = get_config()
configurable = config.get("configurable", {})
slack_thread = configurable.get("slack_thread", {})
channel_id = slack_thread.get("channel_id")
if not channel_id:
return {"success": False, "error": "Missing slack_thread.channel_id in config"}
target_ts = (message_ts or slack_thread.get("triggering_event_ts") or "").strip()
if not target_ts:
return {
"success": False,
"error": "Missing message_ts and slack_thread.triggering_event_ts in config",
}
reaction = emoji.strip().strip(":")
if not reaction:
return {"success": False, "error": "emoji is required"}
if any(char.isspace() for char in reaction):
return {
"success": False,
"error": "emoji must be a Slack reaction name without whitespace",
}
success = await add_slack_reaction(channel_id, target_ts, reaction)
if not success:
return {"success": False, "error": "Could not add Slack reaction"}
return {"success": True}

View file

@ -142,8 +142,10 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st
f"## Latest Mention Request\n{clean_text}\n\n"
+ (f"{resolved_links_section}\n\n" if resolved_links_section else "")
+ "Use `slack_thread_reply` to communicate in this Slack thread for clarifications, "
"status updates, and final summaries. Use `slack_read_thread_messages` to read any "
"Slack messages by providing channel_id and message_ts."
"substantive updates, and final summaries. Use `slack_add_reaction` with :eyes: "
"instead of posting perfunctory confirmation replies to user follow-up requests. "
"Use `slack_read_thread_messages` to read any Slack messages by providing channel_id "
"and message_ts."
)
content_blocks: list[dict[str, Any]] = [create_text_block(prompt)]

View file

@ -0,0 +1,78 @@
from __future__ import annotations
import importlib
from typing import Any
import pytest
slack_reaction_tool = importlib.import_module("agent.tools.slack_add_reaction")
def _config() -> dict[str, Any]:
return {
"configurable": {
"slack_thread": {
"channel_id": "C1",
"thread_ts": "1.0",
"triggering_event_ts": "1.1",
}
}
}
async def test_slack_add_reaction_defaults_to_triggering_event(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, str] = {}
async def fake_add_slack_reaction(
channel_id: str, message_ts: str, emoji: str = "eyes"
) -> bool:
captured.update({"channel_id": channel_id, "message_ts": message_ts, "emoji": emoji})
return True
monkeypatch.setattr(slack_reaction_tool, "get_config", _config)
monkeypatch.setattr(slack_reaction_tool, "add_slack_reaction", fake_add_slack_reaction)
result = await slack_reaction_tool.slack_add_reaction()
assert result == {"success": True}
assert captured == {"channel_id": "C1", "message_ts": "1.1", "emoji": "eyes"}
async def test_slack_add_reaction_accepts_explicit_message_and_normalizes_emoji(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, str] = {}
async def fake_add_slack_reaction(
channel_id: str, message_ts: str, emoji: str = "eyes"
) -> bool:
captured.update({"channel_id": channel_id, "message_ts": message_ts, "emoji": emoji})
return True
monkeypatch.setattr(slack_reaction_tool, "get_config", _config)
monkeypatch.setattr(slack_reaction_tool, "add_slack_reaction", fake_add_slack_reaction)
result = await slack_reaction_tool.slack_add_reaction(
emoji=":white_check_mark:", message_ts="1.2"
)
assert result == {"success": True}
assert captured == {"channel_id": "C1", "message_ts": "1.2", "emoji": "white_check_mark"}
async def test_slack_add_reaction_requires_slack_channel(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(slack_reaction_tool, "get_config", lambda: {"configurable": {}})
result = await slack_reaction_tool.slack_add_reaction()
assert result == {"success": False, "error": "Missing slack_thread.channel_id in config"}
async def test_slack_add_reaction_rejects_empty_emoji(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(slack_reaction_tool, "get_config", _config)
result = await slack_reaction_tool.slack_add_reaction(emoji="::")
assert result == {"success": False, "error": "emoji is required"}