From 7c2e23cfcbea1578a671f57898616157a6d13bc5 Mon Sep 17 00:00:00 2001 From: JCMarques15 Date: Tue, 15 Oct 2024 13:41:58 +0200 Subject: [PATCH 1/9] [Add] Implement Slack Socket Mode support - Add Slack Socket Mode handler in server.py - Create start_socket_mode function in slack/app.py - Add example script for testing Slack websocket handler Introduce Slack Socket Mode support alongside existing HTTP handler. This allows for real-time message processing using websockets when SLACK_WEBSOCKET_TOKEN is set. The HTTP handler remains active when only SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET are provided. --- backend/chainlit/server.py | 15 +++++++++++++-- backend/chainlit/slack/app.py | 11 +++++++++++ backend/chainlit/slack_websocket_test.py | 18 ++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 backend/chainlit/slack_websocket_test.py diff --git a/backend/chainlit/server.py b/backend/chainlit/server.py index cfbef30ee4..373700f41d 100644 --- a/backend/chainlit/server.py +++ b/backend/chainlit/server.py @@ -131,6 +131,13 @@ async def watch_files_for_changes(): discord_task = asyncio.create_task(client.start(discord_bot_token)) + slack_task = None + + # Slack Socket Handler if env variable SLACK_APP_TOKEN is set + if os.environ.get("SLACK_BOT_TOKEN") and os.environ.get("SLACK_WEBSOCKET_TOKEN"): + from chainlit.slack.app import start_socket_mode + slack_task = asyncio.create_task(start_socket_mode()) + try: yield finally: @@ -143,6 +150,10 @@ async def watch_files_for_changes(): if discord_task: discord_task.cancel() await discord_task + + if slack_task: + slack_task.cancel() + await slack_task except asyncio.exceptions.CancelledError: pass @@ -230,10 +241,10 @@ def get_build_dir(local_target: str, packaged_target: str) -> str: # ------------------------------------------------------------------------------- -# SLACK HANDLER +# SLACK HTTP HANDLER # ------------------------------------------------------------------------------- -if os.environ.get("SLACK_BOT_TOKEN") and os.environ.get("SLACK_SIGNING_SECRET"): +if os.environ.get("SLACK_BOT_TOKEN") and os.environ.get("SLACK_SIGNING_SECRET") and not os.environ.get("SLACK_WEBSOCKET_TOKEN"): from chainlit.slack.app import slack_app_handler @router.post("/slack/events") diff --git a/backend/chainlit/slack/app.py b/backend/chainlit/slack/app.py index 747d0af9dd..b727678dc5 100644 --- a/backend/chainlit/slack/app.py +++ b/backend/chainlit/slack/app.py @@ -19,6 +19,7 @@ from chainlit.user import PersistedUser, User from chainlit.user_session import user_session from slack_bolt.adapter.fastapi.async_handler import AsyncSlackRequestHandler +from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler from slack_bolt.async_app import AsyncApp @@ -126,6 +127,16 @@ async def update_step(self, step_dict: StepDict): ) +async def start_socket_mode(): + """ + Initializes and starts the Slack app in Socket Mode asynchronously. + + Uses the SLACK_APP_TOKEN from environment variables to authenticate. + """ + handler = AsyncSocketModeHandler(slack_app, os.environ.get("SLACK_WEBSOCKET_TOKEN")) + await handler.start_async() + + @trace def init_slack_context( session: HTTPSession, diff --git a/backend/chainlit/slack_websocket_test.py b/backend/chainlit/slack_websocket_test.py new file mode 100644 index 0000000000..1df6e1e2da --- /dev/null +++ b/backend/chainlit/slack_websocket_test.py @@ -0,0 +1,18 @@ +# This is a simple example to test the slack websocket handler. +# To initiate the websocket dont forget to set the variables: +# - SLACK_BOT_TOKEN +# - SLACK_SIGNING_SECRET +# - SLACK_WEBSOCKET_TOKEN <- this one dictates if websocket or http handler + +from chainlit import Message, on_message, user_session + + +@on_message +async def main(message: Message): + client_type = user_session.get("client_type") + if client_type == "slack": + user_email = user_session.get("user").metadata.get("email") + print(f"Received a message from: {user_email}") + await Message( + content=f"Hi {user_email}, I have received the following message:\n{message.content}", + ).send() From c58c1eb491ffab86d336d13b9530714384b702a0 Mon Sep 17 00:00:00 2001 From: JCMarques15 Date: Tue, 15 Oct 2024 13:52:56 +0200 Subject: [PATCH 2/9] [Fix] Correct comment for Slack socket handler env variable - Update comment to mention "SLACK_WEBSOCKET_TOKEN" instead of "SLACK_APP_TOKEN" Fix a typo in the comment describing the environment variable check for the Slack socket handler. This change aligns the comment with the actual code implementation, improving code readability and preventing potential confusion. --- backend/chainlit/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/chainlit/server.py b/backend/chainlit/server.py index 373700f41d..7e935039b7 100644 --- a/backend/chainlit/server.py +++ b/backend/chainlit/server.py @@ -133,7 +133,7 @@ async def watch_files_for_changes(): slack_task = None - # Slack Socket Handler if env variable SLACK_APP_TOKEN is set + # Slack Socket Handler if env variable SLACK_WEBSOCKET_TOKEN is set if os.environ.get("SLACK_BOT_TOKEN") and os.environ.get("SLACK_WEBSOCKET_TOKEN"): from chainlit.slack.app import start_socket_mode slack_task = asyncio.create_task(start_socket_mode()) From f1aa97841932e75eda1c5b3d955bbb4b9e6fd7cd Mon Sep 17 00:00:00 2001 From: JCMarques15 Date: Tue, 15 Oct 2024 13:55:51 +0200 Subject: [PATCH 3/9] [Fix] Correct Slack token reference in app.py docstring - Update docstring to mention "SLACK_WEBSOCKET_TOKEN" instead of "SLACK_APP_TOKEN" Correct the documentation in the `start_socket_mode` function to accurately reflect the environment variable used for Slack authentication. This change aligns the docstring with the actual code implementation, improving clarity and preventing potential confusion for developers. --- backend/chainlit/slack/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/chainlit/slack/app.py b/backend/chainlit/slack/app.py index b727678dc5..26b29c2cf8 100644 --- a/backend/chainlit/slack/app.py +++ b/backend/chainlit/slack/app.py @@ -131,7 +131,7 @@ async def start_socket_mode(): """ Initializes and starts the Slack app in Socket Mode asynchronously. - Uses the SLACK_APP_TOKEN from environment variables to authenticate. + Uses the SLACK_WEBSOCKET_TOKEN from environment variables to authenticate. """ handler = AsyncSocketModeHandler(slack_app, os.environ.get("SLACK_WEBSOCKET_TOKEN")) await handler.start_async() From affc956167b638e2f5bd96d96aa1f4689ec0be16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Carlos=20de=20Novais=20Marques?= Date: Wed, 30 Apr 2025 10:28:05 +0000 Subject: [PATCH 4/9] [Add] Update gitignore with node dependencies - Add `node_modules` to ignored files - Add `.pnpm-store` to ignored files Update .gitignore to exclude node package manager related directories to keep repository clean and prevent committing dependencies. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1b0f2b0171..051375d14a 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,5 @@ dist-ssr .coverage backend/README.md +node_modules +.pnpm-store From 5da9a80f17c8262fd5c6043b7cfc257e2c8dd3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Carlos=20de=20Novais=20Marques?= Date: Wed, 30 Apr 2025 10:38:57 +0000 Subject: [PATCH 5/9] [Cut] Move Slack websocket test to Cookbook repo - Remove `slack_websocket_test.py` from backend/chainlit directory - Relocate example to Cookbook repository with other test examples Consolidated example files by moving Slack websocket test to the Cookbook repository where other test examples are located for better organization and discoverability. --- backend/chainlit/slack_websocket_test.py | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 backend/chainlit/slack_websocket_test.py diff --git a/backend/chainlit/slack_websocket_test.py b/backend/chainlit/slack_websocket_test.py deleted file mode 100644 index 1df6e1e2da..0000000000 --- a/backend/chainlit/slack_websocket_test.py +++ /dev/null @@ -1,18 +0,0 @@ -# This is a simple example to test the slack websocket handler. -# To initiate the websocket dont forget to set the variables: -# - SLACK_BOT_TOKEN -# - SLACK_SIGNING_SECRET -# - SLACK_WEBSOCKET_TOKEN <- this one dictates if websocket or http handler - -from chainlit import Message, on_message, user_session - - -@on_message -async def main(message: Message): - client_type = user_session.get("client_type") - if client_type == "slack": - user_email = user_session.get("user").metadata.get("email") - print(f"Received a message from: {user_email}") - await Message( - content=f"Hi {user_email}, I have received the following message:\n{message.content}", - ).send() From 9e6a9aa9d797a3a42f22be9eb72dab439eab4054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Carlos=20de=20Novais=20Marques?= Date: Wed, 30 Apr 2025 11:23:42 +0000 Subject: [PATCH 6/9] [Add] Slack Socket Mode tests - Create test file `test_slack_socket_mode.py` with integration tests - Add test for socket mode handler initialization and startup - Add test for HTTP route registration with classic tokens Adds comprehensive tests for Slack integration modes, covering both WebSocket and HTTP endpoints. Tests verify proper handler initialization, token usage, and route registration. --- backend/tests/test_slack_socket_mode.py | 55 +++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 backend/tests/test_slack_socket_mode.py diff --git a/backend/tests/test_slack_socket_mode.py b/backend/tests/test_slack_socket_mode.py new file mode 100644 index 0000000000..fef6b91c00 --- /dev/null +++ b/backend/tests/test_slack_socket_mode.py @@ -0,0 +1,55 @@ +# tests/test_slack_socket_mode.py +import asyncio +import importlib +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.mark.asyncio +async def test_start_socket_mode_starts_handler(monkeypatch): + """ + The function should: + • build an AsyncSocketModeHandler with the global slack_app + • use the token found in SLACK_WEBSOCKET_TOKEN + • await the handler.start_async() coroutine exactly once + """ + token = "xapp-fake-token" + # minimal env required for the Slack module to initialise + monkeypatch.setenv("SLACK_BOT_TOKEN", "xoxb-fake-bot") + monkeypatch.setenv("SLACK_WEBSOCKET_TOKEN", token) + + # Patch BEFORE importing chainlit.slack.app so the module sees our fake class + with patch( + "chainlit.slack.app.AsyncSocketModeHandler", autospec=True + ) as handler_cls: + handler_instance = AsyncMock() + handler_cls.return_value = handler_instance + + # (Re-)import the module under the patched context + slack_app_mod = importlib.import_module("chainlit.slack.app") + + # Run: should build handler + await start_async + await slack_app_mod.start_socket_mode() + + handler_cls.assert_called_once_with(slack_app_mod.slack_app, token) + handler_instance.start_async.assert_awaited_once() + + +def test_slack_http_route_registered(monkeypatch): + """ + When only the classic HTTP tokens are set (no websocket token), + the FastAPI app should expose POST /slack/events. + """ + #  HTTP-only environment + monkeypatch.setenv("SLACK_BOT_TOKEN", "xoxb-fake-bot") + monkeypatch.setenv("SLACK_SIGNING_SECRET", "shhh-fake-secret") + monkeypatch.delenv("SLACK_WEBSOCKET_TOKEN", raising=False) + + # Re-import server with the fresh env so the route table is built correctly + server = importlib.reload(importlib.import_module("chainlit.server")) + + assert any( + route.path == "/slack/events" and "POST" in route.methods + for route in server.router.routes + ), "Slack HTTP handler route was not registered" From 5c1b321d6fe142ff02ed43e94c0f5586a677b77b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Marques?= Date: Mon, 21 Jul 2025 13:19:19 +0000 Subject: [PATCH 7/9] Clean up Slack socket mode tests: remove unused import and fix comment formatting Remove unused asyncio import and standardize comment spacing in test_slack_socket_mode.py --- backend/tests/test_slack_socket_mode.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/tests/test_slack_socket_mode.py b/backend/tests/test_slack_socket_mode.py index fef6b91c00..b8557e4a02 100644 --- a/backend/tests/test_slack_socket_mode.py +++ b/backend/tests/test_slack_socket_mode.py @@ -1,5 +1,4 @@ # tests/test_slack_socket_mode.py -import asyncio import importlib from unittest.mock import AsyncMock, patch @@ -41,7 +40,7 @@ def test_slack_http_route_registered(monkeypatch): When only the classic HTTP tokens are set (no websocket token), the FastAPI app should expose POST /slack/events. """ - #  HTTP-only environment + # HTTP-only environment monkeypatch.setenv("SLACK_BOT_TOKEN", "xoxb-fake-bot") monkeypatch.setenv("SLACK_SIGNING_SECRET", "shhh-fake-secret") monkeypatch.delenv("SLACK_WEBSOCKET_TOKEN", raising=False) From 6c14508a1abc71318a01387290376ceebb808ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Marques?= Date: Mon, 21 Jul 2025 13:41:51 +0000 Subject: [PATCH 8/9] Fix CI/CD test failure in slack socket mode tests Replace string-based patch with object-based patch to avoid lazy import registry issues. The test was failing in CI/CD with KeyError: 'slack' because the patch() function tried to access chainlit.slack through the lazy import registry, but slack is not registered there. This fix imports the module directly then patches the object, bypassing the registry entirely. Fixes the test while maintaining the same test behavior and assertions. --- backend/tests/test_slack_socket_mode.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/tests/test_slack_socket_mode.py b/backend/tests/test_slack_socket_mode.py index b8557e4a02..8e0e4368a4 100644 --- a/backend/tests/test_slack_socket_mode.py +++ b/backend/tests/test_slack_socket_mode.py @@ -18,16 +18,16 @@ async def test_start_socket_mode_starts_handler(monkeypatch): monkeypatch.setenv("SLACK_BOT_TOKEN", "xoxb-fake-bot") monkeypatch.setenv("SLACK_WEBSOCKET_TOKEN", token) - # Patch BEFORE importing chainlit.slack.app so the module sees our fake class - with patch( - "chainlit.slack.app.AsyncSocketModeHandler", autospec=True + # Import the module first to avoid lazy import registry issues + slack_app_mod = importlib.import_module("chainlit.slack.app") + + # Patch the object directly instead of using string path + with patch.object( + slack_app_mod, "AsyncSocketModeHandler", autospec=True ) as handler_cls: handler_instance = AsyncMock() handler_cls.return_value = handler_instance - # (Re-)import the module under the patched context - slack_app_mod = importlib.import_module("chainlit.slack.app") - # Run: should build handler + await start_async await slack_app_mod.start_socket_mode() From 5c0f433f36d7c7fb08b39f7d378fa3e00bbd8fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Marques?= Date: Tue, 22 Jul 2025 16:13:39 +0000 Subject: [PATCH 9/9] Improve code formatting in Slack handler sections Add blank line after import and break long conditional statement across multiple lines for better readability --- backend/chainlit/server.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/chainlit/server.py b/backend/chainlit/server.py index 0cc7d78c7a..59deb9774a 100644 --- a/backend/chainlit/server.py +++ b/backend/chainlit/server.py @@ -149,6 +149,7 @@ async def watch_files_for_changes(): # Slack Socket Handler if env variable SLACK_WEBSOCKET_TOKEN is set if os.environ.get("SLACK_BOT_TOKEN") and os.environ.get("SLACK_WEBSOCKET_TOKEN"): from chainlit.slack.app import start_socket_mode + slack_task = asyncio.create_task(start_socket_mode()) try: @@ -289,7 +290,11 @@ async def serve_copilot_file( # SLACK HTTP HANDLER # ------------------------------------------------------------------------------- -if os.environ.get("SLACK_BOT_TOKEN") and os.environ.get("SLACK_SIGNING_SECRET") and not os.environ.get("SLACK_WEBSOCKET_TOKEN"): +if ( + os.environ.get("SLACK_BOT_TOKEN") + and os.environ.get("SLACK_SIGNING_SECRET") + and not os.environ.get("SLACK_WEBSOCKET_TOKEN") +): from chainlit.slack.app import slack_app_handler @router.post("/slack/events")