mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 06:53:14 +00:00
feat: only post Slack 'Working on it!' on first thread mention (#1250)
* feat: only post Slack 'Working on it!' on first thread mention * feat: randomize Slack trace reply phrase Pick from a small list of friendly phrases instead of always saying 'Working on it!' so the bot feels less robotic. Explicit messages (e.g. 'Taking a look...' from PR review path) are unaffected. * adjust phrases --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
This commit is contained in:
parent
bb836b58bc
commit
5a01aff1c7
5 changed files with 128 additions and 9 deletions
|
|
@ -150,7 +150,7 @@ Do not use this tool for GitHub API calls. Use `GH_TOKEN=dummy gh` in the sandbo
|
|||
Posts a comment to a Linear ticket given a `ticket_id`. Call this after opening/updating the pull request to notify stakeholders and include the PR link. You can tag Linear users with `@username` (their Linear display name).
|
||||
|
||||
#### `slack_thread_reply`
|
||||
Posts a message to the active Slack thread. Use this for clarifying questions, status updates, and final summaries when the task was triggered from Slack.
|
||||
Posts a message to the active Slack thread. Use this for clarifying questions, mid-run progress updates, and final summaries when the task was triggered from Slack. You can call it multiple times during a run — if you're about to do something long-running (cloning a large repo, big refactors, running heavy test suites), post a short status update first so the user knows what's happening. Always end the run with a final reply that summarizes what you did or answers the question. Do not post a status reply before quick, single-tool answers — only when the user would otherwise be left waiting.
|
||||
Format messages using Slack's mrkdwn format, NOT standard Markdown.
|
||||
Key differences: *bold*, _italic_, ~strikethrough~, <url|link text>,
|
||||
bullet lists with "• ", ```code blocks```, > blockquotes.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ from ..utils.slack import convert_mentions_to_slack_format, post_slack_thread_re
|
|||
def slack_thread_reply(message: str) -> dict[str, Any]:
|
||||
"""Post a message to the current Slack thread.
|
||||
|
||||
Use this for clarifying questions, mid-run progress updates, and the final
|
||||
summary. You can call this multiple times during a run — if you're about to
|
||||
do long-running work (cloning, large refactors, big test runs) consider
|
||||
posting a brief status update first so the user knows what's happening.
|
||||
Always end the run with a final reply summarizing what you did.
|
||||
|
||||
Format messages using Slack's mrkdwn format, NOT standard Markdown.
|
||||
Key differences: *bold*, _italic_, ~strikethrough~, <url|link text>,
|
||||
bullet lists with "• ", ```code blocks```, > blockquotes.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import hashlib
|
|||
import hmac
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -596,10 +597,25 @@ async def resolve_slack_links_in_context(
|
|||
return resolved_links_section, image_urls
|
||||
|
||||
|
||||
TRACE_REPLY_PHRASES: tuple[str, ...] = (
|
||||
"Working on it!",
|
||||
"On it!",
|
||||
"Diving in!",
|
||||
"Powering up!",
|
||||
"Heads down.",
|
||||
"Cracking knuckles...",
|
||||
"Spinning up...",
|
||||
"Looking...",
|
||||
"Time to cook. 🧑🍳",
|
||||
)
|
||||
|
||||
|
||||
async def post_slack_trace_reply(
|
||||
channel_id: str, thread_ts: str, thread_id: str, message: str = "Working on it!"
|
||||
channel_id: str, thread_ts: str, thread_id: str, message: str | None = None
|
||||
) -> None:
|
||||
"""Post a trace URL reply in a Slack thread."""
|
||||
if message is None:
|
||||
message = random.choice(TRACE_REPLY_PHRASES)
|
||||
trace_url = get_langsmith_trace_url(thread_id)
|
||||
if trace_url:
|
||||
await post_slack_thread_reply(channel_id, thread_ts, f"{message} <{trace_url}|View trace>")
|
||||
|
|
|
|||
|
|
@ -863,6 +863,7 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st
|
|||
}
|
||||
|
||||
langgraph_client = get_client(url=LANGGRAPH_URL)
|
||||
is_first_mention = not await _thread_exists(thread_id)
|
||||
await _upsert_slack_thread_repo_metadata(thread_id, repo_config, langgraph_client)
|
||||
|
||||
thread_active = await is_thread_active(thread_id)
|
||||
|
|
@ -895,7 +896,13 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st
|
|||
_run_id_for_logging(run),
|
||||
thread_id,
|
||||
)
|
||||
await post_slack_trace_reply(channel_id, thread_ts, thread_id)
|
||||
if is_first_mention:
|
||||
await post_slack_trace_reply(channel_id, thread_ts, thread_id)
|
||||
else:
|
||||
logger.info(
|
||||
"Skipping Slack trace reply for thread %s — agent will reply when run completes",
|
||||
thread_id,
|
||||
)
|
||||
|
||||
|
||||
async def process_slack_pr_review_request(
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@ import asyncio
|
|||
import pytest
|
||||
|
||||
from agent import webapp
|
||||
from agent.utils import slack as slack_utils
|
||||
from agent.utils.slack import (
|
||||
TRACE_REPLY_PHRASES,
|
||||
convert_mentions_to_slack_format,
|
||||
format_slack_messages_for_prompt,
|
||||
looks_like_slack_pr_review_command,
|
||||
parse_github_pr_url,
|
||||
parse_slack_review_command,
|
||||
post_slack_trace_reply,
|
||||
replace_bot_mention_with_username,
|
||||
select_slack_context_messages,
|
||||
strip_bot_mention,
|
||||
|
|
@ -218,6 +221,39 @@ def test_format_slack_messages_for_prompt_replaces_bot_id_mention_in_text() -> N
|
|||
assert formatted == "@alice(U123): @open-swe status update?"
|
||||
|
||||
|
||||
def test_post_slack_trace_reply_picks_random_phrase_when_no_message(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
posted: list[str] = []
|
||||
|
||||
async def fake_post_slack_thread_reply(channel_id: str, thread_ts: str, text: str) -> None:
|
||||
posted.append(text)
|
||||
|
||||
monkeypatch.setattr(slack_utils, "post_slack_thread_reply", fake_post_slack_thread_reply)
|
||||
monkeypatch.setattr(slack_utils, "get_langsmith_trace_url", lambda thread_id: None)
|
||||
|
||||
asyncio.run(post_slack_trace_reply("C123", "1.0", "thread-id"))
|
||||
|
||||
assert len(posted) == 1
|
||||
assert posted[0] in TRACE_REPLY_PHRASES
|
||||
|
||||
|
||||
def test_post_slack_trace_reply_uses_explicit_message_when_provided(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
posted: list[str] = []
|
||||
|
||||
async def fake_post_slack_thread_reply(channel_id: str, thread_ts: str, text: str) -> None:
|
||||
posted.append(text)
|
||||
|
||||
monkeypatch.setattr(slack_utils, "post_slack_thread_reply", fake_post_slack_thread_reply)
|
||||
monkeypatch.setattr(slack_utils, "get_langsmith_trace_url", lambda thread_id: None)
|
||||
|
||||
asyncio.run(post_slack_trace_reply("C123", "1.0", "thread-id", message="Taking a look..."))
|
||||
|
||||
assert posted == ["Taking a look..."]
|
||||
|
||||
|
||||
def test_select_slack_context_messages_detects_username_mention() -> None:
|
||||
selected, mode = select_slack_context_messages(
|
||||
[
|
||||
|
|
@ -416,11 +452,9 @@ def test_get_slack_repo_config_repo_name_only_space_syntax(
|
|||
assert repo == {"owner": "langchain-ai", "name": "open-swe"}
|
||||
|
||||
|
||||
def test_process_slack_mention_creates_thread_followup_run(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
def _setup_slack_mention_fakes(
|
||||
monkeypatch: pytest.MonkeyPatch, captured: dict[str, object]
|
||||
) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_add_slack_reaction(channel_id: str, message_ts: str, emoji: str) -> bool:
|
||||
captured["reaction"] = {
|
||||
"channel_id": channel_id,
|
||||
|
|
@ -465,7 +499,7 @@ def test_process_slack_mention_creates_thread_followup_run(
|
|||
return False
|
||||
|
||||
async def fake_post_slack_trace_reply(
|
||||
channel_id: str, thread_ts: str, thread_id: str, message: str = "Working on it!"
|
||||
channel_id: str, thread_ts: str, thread_id: str, message: str | None = None
|
||||
) -> None:
|
||||
captured["trace_reply"] = {
|
||||
"channel_id": channel_id,
|
||||
|
|
@ -503,6 +537,19 @@ def test_process_slack_mention_creates_thread_followup_run(
|
|||
monkeypatch.setattr(webapp, "post_slack_trace_reply", fake_post_slack_trace_reply)
|
||||
monkeypatch.setattr(webapp, "get_client", lambda url: _FakeLangGraphClientForProcess())
|
||||
|
||||
|
||||
def test_process_slack_mention_creates_thread_first_run_with_trace_reply(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
_setup_slack_mention_fakes(monkeypatch, captured)
|
||||
|
||||
async def fake_thread_exists(thread_id: str) -> bool:
|
||||
captured["thread_exists_check"] = thread_id
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(webapp, "_thread_exists", fake_thread_exists)
|
||||
|
||||
thread_ts = "1700000000.000100"
|
||||
event_ts = "1700000000.000200"
|
||||
expected_thread_id = generate_thread_id_from_slack_thread("C123", thread_ts)
|
||||
|
|
@ -521,6 +568,7 @@ def test_process_slack_mention_creates_thread_followup_run(
|
|||
)
|
||||
)
|
||||
|
||||
assert captured["thread_exists_check"] == expected_thread_id
|
||||
assert captured["fetch_thread"] == {"channel_id": "C123", "thread_ts": thread_ts}
|
||||
assert captured["active_thread_id"] == expected_thread_id
|
||||
assert captured["metadata_update"] == {
|
||||
|
|
@ -531,7 +579,7 @@ def test_process_slack_mention_creates_thread_followup_run(
|
|||
"channel_id": "C123",
|
||||
"thread_ts": thread_ts,
|
||||
"thread_id": expected_thread_id,
|
||||
"message": "Working on it!",
|
||||
"message": None,
|
||||
}
|
||||
|
||||
run_create = captured["run_create"]
|
||||
|
|
@ -548,6 +596,44 @@ def test_process_slack_mention_creates_thread_followup_run(
|
|||
assert "## Latest Mention Request\ncontinue on the branch" in prompt_block["text"]
|
||||
|
||||
|
||||
def test_process_slack_mention_skips_trace_reply_on_followup_mention(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Subsequent mentions in a Slack thread should not post 'Working on it!'."""
|
||||
captured: dict[str, object] = {}
|
||||
_setup_slack_mention_fakes(monkeypatch, captured)
|
||||
|
||||
async def fake_thread_exists(thread_id: str) -> bool:
|
||||
captured["thread_exists_check"] = thread_id
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(webapp, "_thread_exists", fake_thread_exists)
|
||||
|
||||
thread_ts = "1700000000.000100"
|
||||
event_ts = "1700000000.000300"
|
||||
expected_thread_id = generate_thread_id_from_slack_thread("C123", thread_ts)
|
||||
|
||||
asyncio.run(
|
||||
webapp.process_slack_mention(
|
||||
{
|
||||
"channel_id": "C123",
|
||||
"thread_ts": thread_ts,
|
||||
"event_ts": event_ts,
|
||||
"user_id": "U123",
|
||||
"text": "<@UBOT> follow up question",
|
||||
"bot_user_id": "UBOT",
|
||||
},
|
||||
{"owner": "langchain-ai", "name": "open-swe"},
|
||||
)
|
||||
)
|
||||
|
||||
assert captured["thread_exists_check"] == expected_thread_id
|
||||
assert "trace_reply" not in captured
|
||||
run_create = captured["run_create"]
|
||||
assert isinstance(run_create, dict)
|
||||
assert run_create["thread_id"] == expected_thread_id
|
||||
|
||||
|
||||
def test_process_slack_mention_queues_active_thread_message(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
|
@ -604,6 +690,9 @@ def test_process_slack_mention_queues_active_thread_message(
|
|||
async def fake_post_slack_trace_reply(*args, **kwargs) -> None:
|
||||
raise AssertionError("trace reply should not be posted for queued mid-run Slack messages")
|
||||
|
||||
async def fake_thread_exists(thread_id: str) -> bool:
|
||||
return True
|
||||
|
||||
class _FakeRunsClient:
|
||||
async def create(self, *args, **kwargs) -> None:
|
||||
raise AssertionError("run should not be created for active Slack threads")
|
||||
|
|
@ -628,6 +717,7 @@ def test_process_slack_mention_queues_active_thread_message(
|
|||
monkeypatch.setattr(webapp, "is_thread_active", fake_is_thread_active)
|
||||
monkeypatch.setattr(webapp, "queue_message_for_thread", fake_queue_message_for_thread)
|
||||
monkeypatch.setattr(webapp, "post_slack_trace_reply", fake_post_slack_trace_reply)
|
||||
monkeypatch.setattr(webapp, "_thread_exists", fake_thread_exists)
|
||||
monkeypatch.setattr(webapp, "get_client", lambda url: _FakeLangGraphClientForProcess())
|
||||
|
||||
thread_ts = "1700000000.000100"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue