From da74342da404f383b6ac58e8b7075adaca4d40f3 Mon Sep 17 00:00:00 2001 From: Johannes du Plessis Date: Thu, 7 May 2026 10:19:53 -0700 Subject: [PATCH] add logging (#1248) --- agent/webapp.py | 19 ++++- tests/test_github_issue_webhook.py | 55 ++++++++++++ tests/test_slack_context.py | 132 +++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 2 deletions(-) diff --git a/agent/webapp.py b/agent/webapp.py index 00acc727..1cc2be87 100644 --- a/agent/webapp.py +++ b/agent/webapp.py @@ -319,6 +319,15 @@ def _is_not_found_error(exc: Exception) -> bool: return getattr(exc, "status_code", None) == 404 +def _run_id_for_logging(run: Any) -> str: + """Extract a run id from SDK response shapes for log messages.""" + if isinstance(run, dict): + run_id = run.get("run_id") + else: + run_id = getattr(run, "run_id", None) + return run_id if isinstance(run_id, str) and run_id else "" + + def _is_repo_org_allowed(repo_config: dict[str, str]) -> bool: """Check if the repo owner/org is in the allowlist. @@ -873,13 +882,19 @@ async def process_slack_mention(event_data: dict[str, Any], repo_config: dict[st logger.error("Failed to queue Slack message for thread %s", thread_id) return - await langgraph_client.runs.create( + logger.info("Creating Slack LangGraph run for thread %s", thread_id) + run = await langgraph_client.runs.create( thread_id, "agent", input={"messages": [{"role": "user", "content": content_blocks}]}, config={"configurable": configurable, "metadata": _AGENT_VERSION_METADATA}, if_not_exists="create", - multitask_strategy="interrupt", + multitask_strategy="enqueue", + ) + logger.info( + "Slack LangGraph run %s created for thread %s", + _run_id_for_logging(run), + thread_id, ) await post_slack_trace_reply(channel_id, thread_ts, thread_id) diff --git a/tests/test_github_issue_webhook.py b/tests/test_github_issue_webhook.py index 07718061..af133e08 100644 --- a/tests/test_github_issue_webhook.py +++ b/tests/test_github_issue_webhook.py @@ -530,6 +530,61 @@ def test_slack_webhook_non_pr_review_request_starts_agent(monkeypatch) -> None: assert event_data["text"] == "<@UBOT> review this branch" +def test_slack_webhook_threaded_followup_uses_parent_thread_ts(monkeypatch) -> None: + captured: dict[str, object] = {} + + async def fake_get_slack_repo_config( + text: str, channel_id: str, thread_ts: str + ) -> dict[str, str]: + captured["repo_config_request"] = { + "text": text, + "channel_id": channel_id, + "thread_ts": thread_ts, + } + return {"owner": "langchain-ai", "name": "open-swe"} + + async def fake_process_slack_mention( + event_data: dict[str, object], repo_config: dict[str, str] + ) -> None: + captured["event_data"] = event_data + captured["repo_config"] = repo_config + + monkeypatch.setattr(webapp, "SLACK_SIGNING_SECRET", _TEST_SLACK_SECRET) + monkeypatch.setattr(webapp, "SLACK_BOT_USER_ID", "UBOT") + monkeypatch.setattr(webapp, "SLACK_BOT_USERNAME", "open-swe") + monkeypatch.setattr(slack_utils.time, "time", lambda: 1700000000) + monkeypatch.setattr(webapp, "get_slack_repo_config", fake_get_slack_repo_config) + monkeypatch.setattr(webapp, "process_slack_mention", fake_process_slack_mention) + + client = TestClient(webapp.app) + response = _post_slack_webhook( + client, + { + "type": "event_callback", + "event": { + "type": "app_mention", + "channel": "C123", + "ts": "1700000000.000200", + "thread_ts": "1700000000.000100", + "user": "U123", + "text": "<@UBOT> continue on the branch", + }, + }, + ) + + assert response.status_code == 200 + assert response.json()["message"] == "Slack mention queued" + assert captured["repo_config_request"] == { + "text": "<@UBOT> continue on the branch", + "channel_id": "C123", + "thread_ts": "1700000000.000100", + } + event_data = captured["event_data"] + assert isinstance(event_data, dict) + assert event_data["thread_ts"] == "1700000000.000100" + assert event_data["event_ts"] == "1700000000.000200" + + def test_process_slack_pr_review_request_posts_trace_reply(monkeypatch) -> None: captured: dict[str, object] = {} diff --git a/tests/test_slack_context.py b/tests/test_slack_context.py index 30376c46..4b217467 100644 --- a/tests/test_slack_context.py +++ b/tests/test_slack_context.py @@ -414,3 +414,135 @@ def test_get_slack_repo_config_repo_name_only_space_syntax( repo = asyncio.run(webapp.get_slack_repo_config("fix bug in repo open-swe", "C123", "1.234")) assert repo == {"owner": "langchain-ai", "name": "open-swe"} + + +def test_process_slack_mention_creates_thread_followup_run_with_enqueue( + 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]: + captured["fetch_thread"] = {"channel_id": channel_id, "thread_ts": thread_ts} + return [ + {"ts": "1700000000.000100", "text": "<@UBOT> first request", "user": "U123"}, + {"ts": "1700000000.000150", "text": "context", "user": "U456"}, + { + "ts": "1700000000.000200", + "text": "<@UBOT> continue on the branch", + "user": "U123", + }, + ] + + async def fake_get_slack_user_names(user_ids: list[str]) -> dict[str, str]: + captured["user_ids"] = user_ids + return {"U123": "Mason", "U456": "Teammate"} + + 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 + captured["user_names_by_id"] = user_names_by_id + return "", [] + + async def fake_is_thread_active(thread_id: str) -> bool: + captured["active_thread_id"] = thread_id + return False + + async def fake_post_slack_trace_reply( + channel_id: str, thread_ts: str, thread_id: str, message: str = "Working on it!" + ) -> None: + captured["trace_reply"] = { + "channel_id": channel_id, + "thread_ts": thread_ts, + "thread_id": thread_id, + "message": message, + } + + class _FakeRunsClient: + async def create(self, thread_id: str, graph: str, **kwargs) -> dict[str, str]: + captured["run_create"] = { + "thread_id": thread_id, + "graph": graph, + "kwargs": kwargs, + } + return {"run_id": "run-123"} + + 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, "is_thread_active", fake_is_thread_active) + 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> continue on the branch", + "bot_user_id": "UBOT", + }, + {"owner": "langchain-ai", "name": "open-swe"}, + ) + ) + + assert captured["fetch_thread"] == {"channel_id": "C123", "thread_ts": thread_ts} + assert captured["active_thread_id"] == expected_thread_id + assert captured["metadata_update"] == { + "thread_id": expected_thread_id, + "metadata": {"repo": {"owner": "langchain-ai", "name": "open-swe"}}, + } + assert captured["trace_reply"] == { + "channel_id": "C123", + "thread_ts": thread_ts, + "thread_id": expected_thread_id, + "message": "Working on it!", + } + + run_create = captured["run_create"] + assert isinstance(run_create, dict) + assert run_create["thread_id"] == expected_thread_id + assert run_create["graph"] == "agent" + kwargs = run_create["kwargs"] + assert kwargs["if_not_exists"] == "create" + assert kwargs["multitask_strategy"] == "enqueue" + 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"]