mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 08:03:15 +00:00
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:
parent
b2b49ed944
commit
3c6077c418
10 changed files with 137 additions and 4 deletions
|
|
@ -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`).
|
||||
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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...",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
46
agent/tools/slack_add_reaction.py
Normal file
46
agent/tools/slack_add_reaction.py
Normal 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}
|
||||
|
|
@ -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)]
|
||||
|
||||
|
|
|
|||
78
tests/test_slack_add_reaction_tool.py
Normal file
78
tests/test_slack_add_reaction_tool.py
Normal 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"}
|
||||
Loading…
Add table
Reference in a new issue