mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-04 19:32:12 +00:00
feat: add Slack Open in Web link (#1392)
* feat: add Slack Open in Web link Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * fix: skip web link for Slack reviewer runs Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * test: accept include_dashboard_link kwarg in Slack reviewer test double --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com> Co-authored-by: open-swe[bot] <johannes@langchain.dev>
This commit is contained in:
parent
1d4f1aed33
commit
f4b27c0fee
4 changed files with 77 additions and 11 deletions
|
|
@ -12,7 +12,7 @@ import re
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import quote, urlparse
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from langgraph_sdk.client import LangGraphClient
|
from langgraph_sdk.client import LangGraphClient
|
||||||
|
|
@ -715,20 +715,38 @@ TRACE_REPLY_TIPS: tuple[str, ...] = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _format_trace_reply(trace_url: str | None) -> str:
|
def _get_dashboard_thread_url(thread_id: str) -> str | None:
|
||||||
|
"""Build the dashboard thread URL for a given thread ID."""
|
||||||
|
base_url = (
|
||||||
|
os.environ.get("DASHBOARD_BASE_URL", "https://openswe.vercel.app").strip().rstrip("/")
|
||||||
|
)
|
||||||
|
if not base_url:
|
||||||
|
return None
|
||||||
|
return f"{base_url}/agents/{quote(thread_id, safe='')}"
|
||||||
|
|
||||||
|
|
||||||
|
def _format_trace_reply(trace_url: str | None, dashboard_url: str | None) -> str:
|
||||||
"""Format the initial trace reply with a randomly selected tip."""
|
"""Format the initial trace reply with a randomly selected tip."""
|
||||||
tip = random.choice(TRACE_REPLY_TIPS)
|
tip = random.choice(TRACE_REPLY_TIPS)
|
||||||
head = f"<{trace_url}|View trace>\n" if trace_url else ""
|
links = []
|
||||||
|
if trace_url:
|
||||||
|
links.append(f"<{trace_url}|View trace>")
|
||||||
|
if dashboard_url:
|
||||||
|
links.append(f"<{dashboard_url}|Open in Web>")
|
||||||
|
head = f"{' • '.join(links)}\n" if links else ""
|
||||||
return f"{head}_Tip: {tip}_"
|
return f"{head}_Tip: {tip}_"
|
||||||
|
|
||||||
|
|
||||||
async def post_slack_trace_reply(channel_id: str, thread_ts: str, thread_id: str) -> str | None:
|
async def post_slack_trace_reply(
|
||||||
|
channel_id: str, thread_ts: str, thread_id: str, *, include_dashboard_link: bool = True
|
||||||
|
) -> str | None:
|
||||||
"""Post a trace URL reply in a Slack thread and return its Slack timestamp."""
|
"""Post a trace URL reply in a Slack thread and return its Slack timestamp."""
|
||||||
trace_url = get_langsmith_trace_url(thread_id)
|
trace_url = get_langsmith_trace_url(thread_id)
|
||||||
|
dashboard_url = _get_dashboard_thread_url(thread_id) if include_dashboard_link else None
|
||||||
message_ts, _ = await post_slack_thread_reply_with_ts(
|
message_ts, _ = await post_slack_thread_reply_with_ts(
|
||||||
channel_id,
|
channel_id,
|
||||||
thread_ts,
|
thread_ts,
|
||||||
_format_trace_reply(trace_url),
|
_format_trace_reply(trace_url, dashboard_url),
|
||||||
unfurl_links=False,
|
unfurl_links=False,
|
||||||
unfurl_media=False,
|
unfurl_media=False,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1176,7 +1176,9 @@ async def process_slack_pr_review_request(
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
thread_id = result.get("thread_id")
|
thread_id = result.get("thread_id")
|
||||||
if isinstance(thread_id, str) and thread_id:
|
if isinstance(thread_id, str) and thread_id:
|
||||||
await post_slack_trace_reply(channel_id, thread_ts, thread_id)
|
await post_slack_trace_reply(
|
||||||
|
channel_id, thread_ts, thread_id, include_dashboard_link=False
|
||||||
|
)
|
||||||
await set_slack_assistant_status(channel_id, thread_ts)
|
await set_slack_assistant_status(channel_id, thread_ts)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -834,11 +834,14 @@ def test_process_slack_pr_review_request_posts_trace_reply(monkeypatch) -> None:
|
||||||
captured["slack_thread_ts"] = slack_thread_ts
|
captured["slack_thread_ts"] = slack_thread_ts
|
||||||
return {"success": True, "thread_id": "reviewer-thread-id", "pr_url": pr_ref.url}
|
return {"success": True, "thread_id": "reviewer-thread-id", "pr_url": pr_ref.url}
|
||||||
|
|
||||||
async def fake_post_slack_trace_reply(channel_id: str, thread_ts: str, thread_id: str) -> None:
|
async def fake_post_slack_trace_reply(
|
||||||
|
channel_id: str, thread_ts: str, thread_id: str, *, include_dashboard_link: bool = True
|
||||||
|
) -> None:
|
||||||
captured["trace_reply"] = {
|
captured["trace_reply"] = {
|
||||||
"channel_id": channel_id,
|
"channel_id": channel_id,
|
||||||
"thread_ts": thread_ts,
|
"thread_ts": thread_ts,
|
||||||
"thread_id": thread_id,
|
"thread_id": thread_id,
|
||||||
|
"include_dashboard_link": include_dashboard_link,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def fake_set_slack_assistant_status(channel_id: str, thread_ts: str) -> bool:
|
async def fake_set_slack_assistant_status(channel_id: str, thread_ts: str) -> bool:
|
||||||
|
|
@ -869,6 +872,7 @@ def test_process_slack_pr_review_request_posts_trace_reply(monkeypatch) -> None:
|
||||||
"channel_id": "C123",
|
"channel_id": "C123",
|
||||||
"thread_ts": "1700000000.000100",
|
"thread_ts": "1700000000.000100",
|
||||||
"thread_id": "reviewer-thread-id",
|
"thread_id": "reviewer-thread-id",
|
||||||
|
"include_dashboard_link": False,
|
||||||
}
|
}
|
||||||
assert captured["status_calls"] == [
|
assert captured["status_calls"] == [
|
||||||
("C123", "1700000000.000100"),
|
("C123", "1700000000.000100"),
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,7 @@ def test_format_slack_messages_for_prompt_replaces_bot_id_mention_in_text() -> N
|
||||||
assert formatted == "@alice(U123): @open-swe status update?"
|
assert formatted == "@alice(U123): @open-swe status update?"
|
||||||
|
|
||||||
|
|
||||||
def test_post_slack_trace_reply_emits_tip_only_when_no_trace_url(
|
def test_post_slack_trace_reply_includes_web_link_without_trace_url(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
posted: list[dict] = []
|
posted: list[dict] = []
|
||||||
|
|
@ -237,6 +237,7 @@ def test_post_slack_trace_reply_emits_tip_only_when_no_trace_url(
|
||||||
posted.append({"text": text, "unfurl_links": unfurl_links, "unfurl_media": unfurl_media})
|
posted.append({"text": text, "unfurl_links": unfurl_links, "unfurl_media": unfurl_media})
|
||||||
return "1.1", None
|
return "1.1", None
|
||||||
|
|
||||||
|
monkeypatch.setenv("DASHBOARD_BASE_URL", "https://app.example.com/")
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
slack_utils, "post_slack_thread_reply_with_ts", fake_post_slack_thread_reply_with_ts
|
slack_utils, "post_slack_thread_reply_with_ts", fake_post_slack_thread_reply_with_ts
|
||||||
)
|
)
|
||||||
|
|
@ -246,8 +247,10 @@ def test_post_slack_trace_reply_emits_tip_only_when_no_trace_url(
|
||||||
|
|
||||||
assert len(posted) == 1
|
assert len(posted) == 1
|
||||||
text = posted[0]["text"]
|
text = posted[0]["text"]
|
||||||
assert text.startswith("_Tip: ") and text.endswith("_")
|
head, _, tip_line = text.partition("\n")
|
||||||
assert any(tip in text for tip in TRACE_REPLY_TIPS)
|
assert head == "<https://app.example.com/agents/thread-id|Open in Web>"
|
||||||
|
assert tip_line.startswith("_Tip: ") and tip_line.endswith("_")
|
||||||
|
assert any(tip in tip_line for tip in TRACE_REPLY_TIPS)
|
||||||
assert posted[0]["unfurl_links"] is False
|
assert posted[0]["unfurl_links"] is False
|
||||||
assert posted[0]["unfurl_media"] is False
|
assert posted[0]["unfurl_media"] is False
|
||||||
|
|
||||||
|
|
@ -268,6 +271,7 @@ def test_post_slack_trace_reply_includes_trace_link_and_tip(
|
||||||
posted.append({"text": text, "unfurl_links": unfurl_links, "unfurl_media": unfurl_media})
|
posted.append({"text": text, "unfurl_links": unfurl_links, "unfurl_media": unfurl_media})
|
||||||
return "1.1", None
|
return "1.1", None
|
||||||
|
|
||||||
|
monkeypatch.setenv("DASHBOARD_BASE_URL", "https://app.example.com")
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
slack_utils, "post_slack_thread_reply_with_ts", fake_post_slack_thread_reply_with_ts
|
slack_utils, "post_slack_thread_reply_with_ts", fake_post_slack_thread_reply_with_ts
|
||||||
)
|
)
|
||||||
|
|
@ -278,13 +282,51 @@ def test_post_slack_trace_reply_includes_trace_link_and_tip(
|
||||||
assert len(posted) == 1
|
assert len(posted) == 1
|
||||||
text = posted[0]["text"]
|
text = posted[0]["text"]
|
||||||
head, _, tip_line = text.partition("\n")
|
head, _, tip_line = text.partition("\n")
|
||||||
assert head == "<https://smith/x|View trace>"
|
assert (
|
||||||
|
head
|
||||||
|
== "<https://smith/x|View trace> • <https://app.example.com/agents/thread-id|Open in Web>"
|
||||||
|
)
|
||||||
assert tip_line.startswith("_Tip: ") and tip_line.endswith("_")
|
assert tip_line.startswith("_Tip: ") and tip_line.endswith("_")
|
||||||
assert any(tip in tip_line for tip in TRACE_REPLY_TIPS)
|
assert any(tip in tip_line for tip in TRACE_REPLY_TIPS)
|
||||||
assert posted[0]["unfurl_links"] is False
|
assert posted[0]["unfurl_links"] is False
|
||||||
assert posted[0]["unfurl_media"] is False
|
assert posted[0]["unfurl_media"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_slack_trace_reply_can_skip_web_link(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
posted: list[dict] = []
|
||||||
|
|
||||||
|
async def fake_post_slack_thread_reply_with_ts(
|
||||||
|
channel_id: str,
|
||||||
|
thread_ts: str,
|
||||||
|
text: str,
|
||||||
|
*,
|
||||||
|
unfurl_links: bool = True,
|
||||||
|
unfurl_media: bool = True,
|
||||||
|
) -> tuple[str | None, str | None]:
|
||||||
|
posted.append({"text": text, "unfurl_links": unfurl_links, "unfurl_media": unfurl_media})
|
||||||
|
return "1.1", None
|
||||||
|
|
||||||
|
monkeypatch.setenv("DASHBOARD_BASE_URL", "https://app.example.com")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
slack_utils, "post_slack_thread_reply_with_ts", fake_post_slack_thread_reply_with_ts
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(slack_utils, "get_langsmith_trace_url", lambda thread_id: "https://smith/x")
|
||||||
|
|
||||||
|
asyncio.run(
|
||||||
|
post_slack_trace_reply("C123", "1.0", "reviewer-thread-id", include_dashboard_link=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(posted) == 1
|
||||||
|
head, _, tip_line = posted[0]["text"].partition("\n")
|
||||||
|
assert head == "<https://smith/x|View trace>"
|
||||||
|
assert "Open in Web" not in posted[0]["text"]
|
||||||
|
assert tip_line.startswith("_Tip: ") and tip_line.endswith("_")
|
||||||
|
assert posted[0]["unfurl_links"] is False
|
||||||
|
assert posted[0]["unfurl_media"] is False
|
||||||
|
|
||||||
|
|
||||||
def test_select_slack_context_messages_detects_username_mention() -> None:
|
def test_select_slack_context_messages_detects_username_mention() -> None:
|
||||||
selected, mode = select_slack_context_messages(
|
selected, mode = select_slack_context_messages(
|
||||||
[
|
[
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue