diff --git a/agent/webapp.py b/agent/webapp.py index 1cc2be87..d89abffa 100644 --- a/agent/webapp.py +++ b/agent/webapp.py @@ -733,7 +733,7 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915 async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[str, str]) -> None: - """Process a Slack app mention by creating or interrupting a thread run.""" + """Process a Slack app mention by creating a run or queuing a mid-run message.""" channel_id = event_data.get("channel_id", "") thread_ts = event_data.get("thread_ts", "") event_ts = event_data.get("event_ts", "") @@ -871,7 +871,7 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st "Thread %s is active, queuing Slack message for middleware pickup", thread_id, ) - queued_payload = {"text": prompt, "image_urls": []} + queued_payload = {"text": prompt, "image_urls": image_urls} queued = await queue_message_for_thread( thread_id=thread_id, message_content=queued_payload, @@ -889,7 +889,6 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st input={"messages": [{"role": "user", "content": content_blocks}]}, config={"configurable": configurable, "metadata": _AGENT_VERSION_METADATA}, if_not_exists="create", - multitask_strategy="enqueue", ) logger.info( "Slack LangGraph run %s created for thread %s", diff --git a/tests/test_slack_context.py b/tests/test_slack_context.py index 4b217467..ba2e7bcb 100644 --- a/tests/test_slack_context.py +++ b/tests/test_slack_context.py @@ -416,7 +416,7 @@ 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_with_enqueue( +def test_process_slack_mention_creates_thread_followup_run( monkeypatch: pytest.MonkeyPatch, ) -> None: captured: dict[str, object] = {} @@ -540,9 +540,116 @@ def test_process_slack_mention_creates_thread_followup_run_with_enqueue( assert run_create["graph"] == "agent" kwargs = run_create["kwargs"] assert kwargs["if_not_exists"] == "create" - assert kwargs["multitask_strategy"] == "enqueue" + assert "multitask_strategy" not in kwargs assert kwargs["config"]["configurable"]["slack_thread"]["thread_ts"] == thread_ts prompt_block = kwargs["input"]["messages"][0]["content"][0] assert prompt_block["text"].count("## Slack Thread") == 1 assert f"Thread TS: {thread_ts}" in prompt_block["text"] assert "## Latest Mention Request\ncontinue on the branch" in prompt_block["text"] + + +def test_process_slack_mention_queues_active_thread_message( + monkeypatch: pytest.MonkeyPatch, +) -> 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, + "message_ts": message_ts, + "emoji": emoji, + } + return True + + async def fake_get_slack_user_info(user_id: str) -> dict: + return { + "profile": { + "email": "mason@example.com", + "display_name": "Mason", + } + } + + async def fake_fetch_slack_thread_messages(channel_id: str, thread_ts: str) -> list[dict]: + return [ + {"ts": "1700000000.000100", "text": "<@UBOT> first request", "user": "U123"}, + { + "ts": "1700000000.000200", + "text": "<@UBOT> include this screenshot https://example.com/image.png", + "user": "U123", + }, + ] + + async def fake_get_slack_user_names(user_ids: list[str]) -> dict[str, str]: + captured["user_ids"] = user_ids + return {"U123": "Mason"} + + async def fake_resolve_slack_links_in_context( + context_messages: list[dict], user_names_by_id: dict[str, str] + ) -> tuple[str, list[str]]: + captured["context_messages"] = context_messages + return "", [] + + async def fake_fetch_image_block(image_url: str, http_client: object) -> None: + captured["image_url"] = image_url + return None + + async def fake_is_thread_active(thread_id: str) -> bool: + captured["active_thread_id"] = thread_id + return True + + async def fake_queue_message_for_thread(thread_id: str, message_content: object) -> bool: + captured["queued"] = {"thread_id": thread_id, "message_content": message_content} + return True + + async def fake_post_slack_trace_reply(*args, **kwargs) -> None: + raise AssertionError("trace reply should not be posted for queued mid-run Slack messages") + + class _FakeRunsClient: + async def create(self, *args, **kwargs) -> None: + raise AssertionError("run should not be created for active Slack threads") + + class _FakeThreadsClientForProcess: + async def update(self, *, thread_id: str, metadata: dict) -> None: + captured["metadata_update"] = {"thread_id": thread_id, "metadata": metadata} + + class _FakeLangGraphClientForProcess: + runs = _FakeRunsClient() + threads = _FakeThreadsClientForProcess() + + monkeypatch.setattr(webapp, "SLACK_BOT_USERNAME", "open-swe") + monkeypatch.setattr(webapp, "add_slack_reaction", fake_add_slack_reaction) + monkeypatch.setattr(webapp, "get_slack_user_info", fake_get_slack_user_info) + monkeypatch.setattr(webapp, "fetch_slack_thread_messages", fake_fetch_slack_thread_messages) + monkeypatch.setattr(webapp, "get_slack_user_names", fake_get_slack_user_names) + monkeypatch.setattr( + webapp, "resolve_slack_links_in_context", fake_resolve_slack_links_in_context + ) + monkeypatch.setattr(webapp, "fetch_image_block", fake_fetch_image_block) + 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, "get_client", lambda url: _FakeLangGraphClientForProcess()) + + thread_ts = "1700000000.000100" + event_ts = "1700000000.000200" + 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> include this screenshot https://example.com/image.png", + "bot_user_id": "UBOT", + }, + {"owner": "langchain-ai", "name": "open-swe"}, + ) + ) + + assert captured["active_thread_id"] == expected_thread_id + assert captured["queued"]["thread_id"] == expected_thread_id + queued_payload = captured["queued"]["message_content"] + assert queued_payload["image_urls"] == ["https://example.com/image.png"] + assert "## Latest Mention Request\ninclude this screenshot" in queued_payload["text"]