mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-06 19:22:13 +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".
|
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`:
|
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`).
|
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".
|
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`:
|
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`).
|
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 |
|
| `fetch_url` | Fetch web pages as markdown |
|
||||||
| `http_request` | API calls (GET, POST, etc.) |
|
| `http_request` | API calls (GET, POST, etc.) |
|
||||||
| `linear_comment` | Post updates to Linear tickets |
|
| `linear_comment` | Post updates to Linear tickets |
|
||||||
|
| `slack_add_reaction` | React to Slack messages |
|
||||||
| `slack_thread_reply` | Reply in Slack threads |
|
| `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).
|
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...",
|
"fetch_url": "fetching a URL...",
|
||||||
"http_request": "making an HTTP request...",
|
"http_request": "making an HTTP request...",
|
||||||
"request_pr_review": "requesting a PR review...",
|
"request_pr_review": "requesting a PR review...",
|
||||||
|
"slack_add_reaction": "reacting in Slack...",
|
||||||
"slack_read_thread_messages": "reading Slack history...",
|
"slack_read_thread_messages": "reading Slack history...",
|
||||||
"slack_thread_reply": "drafting a Slack reply...",
|
"slack_thread_reply": "drafting a Slack reply...",
|
||||||
"linear_comment": "commenting on Linear...",
|
"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.
|
- 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 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 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.
|
- 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,
|
request_pr_review,
|
||||||
save_plan,
|
save_plan,
|
||||||
schedule_thread_wakeup,
|
schedule_thread_wakeup,
|
||||||
|
slack_add_reaction,
|
||||||
slack_read_thread_messages,
|
slack_read_thread_messages,
|
||||||
slack_start_new_thread,
|
slack_start_new_thread,
|
||||||
slack_thread_reply,
|
slack_thread_reply,
|
||||||
|
|
@ -959,6 +960,7 @@ async def get_agent(config: RunnableConfig) -> Pregel:
|
||||||
open_pull_request,
|
open_pull_request,
|
||||||
request_pr_review,
|
request_pr_review,
|
||||||
schedule_thread_wakeup,
|
schedule_thread_wakeup,
|
||||||
|
slack_add_reaction,
|
||||||
slack_read_thread_messages,
|
slack_read_thread_messages,
|
||||||
slack_start_new_thread,
|
slack_start_new_thread,
|
||||||
slack_thread_reply,
|
slack_thread_reply,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ from .resolve_finding_thread import resolve_finding_thread
|
||||||
from .save_plan import save_plan
|
from .save_plan import save_plan
|
||||||
from .schedule_thread_wakeup import schedule_thread_wakeup
|
from .schedule_thread_wakeup import schedule_thread_wakeup
|
||||||
from .search_repo_code import search_repo_code
|
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_read_thread_messages import slack_read_thread_messages
|
||||||
from .slack_start_new_thread import slack_start_new_thread
|
from .slack_start_new_thread import slack_start_new_thread
|
||||||
from .slack_thread_reply import slack_thread_reply
|
from .slack_thread_reply import slack_thread_reply
|
||||||
|
|
@ -49,6 +50,7 @@ __all__ = [
|
||||||
"save_plan",
|
"save_plan",
|
||||||
"schedule_thread_wakeup",
|
"schedule_thread_wakeup",
|
||||||
"search_repo_code",
|
"search_repo_code",
|
||||||
|
"slack_add_reaction",
|
||||||
"slack_read_thread_messages",
|
"slack_read_thread_messages",
|
||||||
"slack_start_new_thread",
|
"slack_start_new_thread",
|
||||||
"slack_thread_reply",
|
"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"## Latest Mention Request\n{clean_text}\n\n"
|
||||||
+ (f"{resolved_links_section}\n\n" if resolved_links_section else "")
|
+ (f"{resolved_links_section}\n\n" if resolved_links_section else "")
|
||||||
+ "Use `slack_thread_reply` to communicate in this Slack thread for clarifications, "
|
+ "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 "
|
"substantive updates, and final summaries. Use `slack_add_reaction` with :eyes: "
|
||||||
"Slack messages by providing channel_id and message_ts."
|
"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)]
|
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