mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-07 16:19:09 +00:00
feat(plan): notify Slack on plan approval
Re-land upstream #1632 onto the modular structure. When a plan is approved via the dashboard approve endpoint, post a thread reply to the originating Slack thread noting the comment count and approver, after the follow-up run is dispatched. Slack post failures never break approval. Adapted to the fork's approve_plan (no plan_markdown read). Refs #80, upstream #1632
This commit is contained in:
parent
9e997dd8ee
commit
80c9545f24
2 changed files with 102 additions and 1 deletions
|
|
@ -22,6 +22,7 @@ from langgraph_sdk import get_client
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from ..dispatch import dispatch_agent_run
|
from ..dispatch import dispatch_agent_run
|
||||||
|
from ..utils.slack import post_slack_thread_reply
|
||||||
from .oauth import require_same_origin_for_mutations, require_session
|
from .oauth import require_same_origin_for_mutations, require_session
|
||||||
from .plan_store import (
|
from .plan_store import (
|
||||||
PLAN_STATUS_APPROVED,
|
PLAN_STATUS_APPROVED,
|
||||||
|
|
@ -139,7 +140,8 @@ async def approve_plan(thread_id: str, session: dict[str, Any] = _SESSION_DEP) -
|
||||||
raise HTTPException(403, "only the plan owner can approve")
|
raise HTTPException(403, "only the plan owner can approve")
|
||||||
# Read comments BEFORE mutating state: a store failure here aborts the
|
# Read comments BEFORE mutating state: a store failure here aborts the
|
||||||
# decision (500) rather than dispatching the run without the feedback.
|
# decision (500) rather than dispatching the run without the feedback.
|
||||||
feedback = _format_comments(await list_plan_comments(thread_id, raise_on_error=True))
|
comments = await list_plan_comments(thread_id, raise_on_error=True)
|
||||||
|
feedback = _format_comments(comments)
|
||||||
await set_plan_status(thread_id, PLAN_STATUS_APPROVED, plan_mode=False)
|
await set_plan_status(thread_id, PLAN_STATUS_APPROVED, plan_mode=False)
|
||||||
if feedback:
|
if feedback:
|
||||||
text = (
|
text = (
|
||||||
|
|
@ -149,6 +151,9 @@ async def approve_plan(thread_id: str, session: dict[str, Any] = _SESSION_DEP) -
|
||||||
else:
|
else:
|
||||||
text = "The plan has been approved. Implement it now as described in the plan."
|
text = "The plan has been approved. Implement it now as described in the plan."
|
||||||
await _dispatch_followup(thread_id, metadata, text, plan_mode=False)
|
await _dispatch_followup(thread_id, metadata, text, plan_mode=False)
|
||||||
|
await _maybe_post_plan_approved_to_slack(
|
||||||
|
metadata, comment_count=len(comments), actor=_approval_actor_name(session)
|
||||||
|
)
|
||||||
return {"status": PLAN_STATUS_APPROVED}
|
return {"status": PLAN_STATUS_APPROVED}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -181,6 +186,45 @@ def _format_comments(comments: list[dict[str, Any]]) -> str:
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _approval_actor_name(session: dict[str, Any]) -> str:
|
||||||
|
return str(session.get("name") or session.get("sub") or "User").strip() or "User"
|
||||||
|
|
||||||
|
|
||||||
|
def _slack_thread_from_metadata(metadata: dict[str, Any]) -> tuple[str, str] | None:
|
||||||
|
source_context = metadata.get("source_context")
|
||||||
|
if not isinstance(source_context, dict):
|
||||||
|
return None
|
||||||
|
slack_thread = source_context.get("slack_thread")
|
||||||
|
if not isinstance(slack_thread, dict):
|
||||||
|
return None
|
||||||
|
channel_id = slack_thread.get("channel_id")
|
||||||
|
thread_ts = slack_thread.get("thread_ts")
|
||||||
|
if isinstance(channel_id, str) and channel_id and isinstance(thread_ts, str) and thread_ts:
|
||||||
|
return channel_id, thread_ts
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _plan_approved_slack_text(comment_count: int, actor: str) -> str:
|
||||||
|
return f"Plan approved with {comment_count} comments by {actor}\nbeginning implementation"
|
||||||
|
|
||||||
|
|
||||||
|
async def _maybe_post_plan_approved_to_slack(
|
||||||
|
metadata: dict[str, Any], *, comment_count: int, actor: str
|
||||||
|
) -> None:
|
||||||
|
slack_thread = _slack_thread_from_metadata(metadata)
|
||||||
|
if slack_thread is None:
|
||||||
|
return
|
||||||
|
channel_id, thread_ts = slack_thread
|
||||||
|
text = _plan_approved_slack_text(comment_count, actor)
|
||||||
|
try:
|
||||||
|
posted = await post_slack_thread_reply(channel_id, thread_ts, text)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
logger.warning("Failed to post plan-approval notice to Slack", exc_info=True)
|
||||||
|
return
|
||||||
|
if not posted:
|
||||||
|
logger.warning("Plan-approval Slack notice was not posted (channel=%s)", channel_id)
|
||||||
|
|
||||||
|
|
||||||
async def _dispatch_followup(
|
async def _dispatch_followup(
|
||||||
thread_id: str, metadata: dict[str, Any], text: str, *, plan_mode: bool
|
thread_id: str, metadata: dict[str, Any], text: str, *, plan_mode: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
||||||
|
|
@ -182,3 +182,60 @@ def test_plan_mode_middleware_self_activation_via_state() -> None:
|
||||||
# After enter_plan_mode sets state: the next request is filtered.
|
# After enter_plan_mode sets state: the next request is filtered.
|
||||||
on = _FakeReq([{"name": "read_file"}, {"name": "write_file"}], {"plan_mode": True})
|
on = _FakeReq([{"name": "read_file"}, {"name": "write_file"}], {"plan_mode": True})
|
||||||
assert _names(mw._filter(on)) == {"read_file"}
|
assert _names(mw._filter(on)) == {"read_file"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_plan_approved_slack_text_mentions_comments_actor_and_start() -> None:
|
||||||
|
from agent.dashboard.plan_api import _plan_approved_slack_text
|
||||||
|
|
||||||
|
text = _plan_approved_slack_text(3, "Alice")
|
||||||
|
assert text == "Plan approved with 3 comments by Alice\nbeginning implementation"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_approve_plan_posts_slack_approval_notice(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
from agent.dashboard import plan_api
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"github_login": "alice",
|
||||||
|
"source_context": {"slack_thread": {"channel_id": "C1", "thread_ts": "1700000000.0001"}},
|
||||||
|
}
|
||||||
|
posted: dict[str, Any] = {}
|
||||||
|
|
||||||
|
async def fake_thread_metadata(thread_id: str) -> dict[str, Any]:
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
def fake_user_owns_thread(md: dict, sub: str, email: str | None) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def fake_list_plan_comments(
|
||||||
|
thread_id: str, *, raise_on_error: bool = False
|
||||||
|
) -> list[dict]:
|
||||||
|
return [{"author": "bob", "body": "tweak this"}]
|
||||||
|
|
||||||
|
async def fake_set_plan_status(thread_id: str, status: str, *, plan_mode: bool) -> None:
|
||||||
|
posted["status"] = {"status": status, "plan_mode": plan_mode}
|
||||||
|
|
||||||
|
async def fake_dispatch_followup(
|
||||||
|
thread_id: str, md: dict, text: str, *, plan_mode: bool
|
||||||
|
) -> None:
|
||||||
|
posted["dispatch"] = {"text": text, "plan_mode": plan_mode}
|
||||||
|
|
||||||
|
async def fake_post_slack_thread_reply(channel_id: str, thread_ts: str, text: str) -> bool:
|
||||||
|
posted["slack"] = {"channel_id": channel_id, "thread_ts": thread_ts, "text": text}
|
||||||
|
return True
|
||||||
|
|
||||||
|
monkeypatch.setattr(plan_api, "_thread_metadata", fake_thread_metadata)
|
||||||
|
monkeypatch.setattr(plan_api, "_user_owns_thread", fake_user_owns_thread)
|
||||||
|
monkeypatch.setattr(plan_api, "list_plan_comments", fake_list_plan_comments)
|
||||||
|
monkeypatch.setattr(plan_api, "set_plan_status", fake_set_plan_status)
|
||||||
|
monkeypatch.setattr(plan_api, "_dispatch_followup", fake_dispatch_followup)
|
||||||
|
monkeypatch.setattr(plan_api, "post_slack_thread_reply", fake_post_slack_thread_reply)
|
||||||
|
|
||||||
|
result = await plan_api.approve_plan("tid", session={"sub": "u1", "name": "Alice"})
|
||||||
|
|
||||||
|
assert result == {"status": plan_api.PLAN_STATUS_APPROVED}
|
||||||
|
assert posted["dispatch"]["plan_mode"] is False
|
||||||
|
assert posted["slack"] == {
|
||||||
|
"channel_id": "C1",
|
||||||
|
"thread_ts": "1700000000.0001",
|
||||||
|
"text": "Plan approved with 1 comments by Alice\nbeginning implementation",
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue