From a00e5c1ff7d9b4f4592dabffae0a098b55795f66 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 18 Sep 2025 16:33:41 -0400 Subject: [PATCH 01/16] docs: clarify that listeners/events is educational only --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index c2019a5..3c999c9 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,10 @@ black . Every incoming request is routed to a "listener". Inside this directory, we group each listener based on the Slack Platform feature used, so `/listeners/events` handles incoming events, `/listeners/shortcuts` would handle incoming [Shortcuts](https://api.slack.com/interactivity/shortcuts) requests, and so on. +The main implementation for this assistant app is in `listeners/assistant.py`, which contains the actual thread and messaging logic using Slack's Assistant framework. + +**Note**: The `listeners/events` folder is purely educational and demonstrates alternative implementation approaches. These listeners are **not registered** and are not used in the actual application. They serve as examples to show how you could implement similar functionality using event listeners instead of the Assistant framework. For the working implementation, refer to `listeners/assistant.py`. + ## App Distribution / OAuth Only implement OAuth if you plan to distribute your application across multiple workspaces. A separate `app_oauth.py` file can be found with relevant OAuth settings. From df8b62090b1ef36468c0bb9a8cbf1359b5beea48 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 18 Sep 2025 16:43:31 -0400 Subject: [PATCH 02/16] docs: shorten the listeners/events clarification --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3c999c9..987d9b3 100644 --- a/README.md +++ b/README.md @@ -72,11 +72,15 @@ black . ### `/listeners` -Every incoming request is routed to a "listener". Inside this directory, we group each listener based on the Slack Platform feature used, so `/listeners/events` handles incoming events, `/listeners/shortcuts` would handle incoming [Shortcuts](https://api.slack.com/interactivity/shortcuts) requests, and so on. +Every incoming request is routed to a "listener". Inside this directory, we group each listener based on the Slack Platform feature used, so `/listeners/events` handles incoming events like messages sent to the app. -The main implementation for this assistant app is in `listeners/assistant.py`, which contains the actual thread and messaging logic using Slack's Assistant framework. +**Note**: The `listeners/events` folder is purely educational and demonstrates alternative implementation approaches. These listeners are **not registered** and are not used in the actual application. For the working implementation, refer to `listeners/assistant.py`. -**Note**: The `listeners/events` folder is purely educational and demonstrates alternative implementation approaches. These listeners are **not registered** and are not used in the actual application. They serve as examples to show how you could implement similar functionality using event listeners instead of the Assistant framework. For the working implementation, refer to `listeners/assistant.py`. +**`/listeners/assistant`** + +Configures the new Slack Assistant features, providing a dedicated side panel UI for users to interact with the AI chatbot. This includes: +* `@assistant.thread_started` - Manages when users start new assistant threads. +* `@assistant.user_message` - Processes user messages in assistant threads and app DMs. **Replaces traditional DM handling as seen in** `/listeners/events/user_message.py` ## App Distribution / OAuth From 475139f0e4f599a9c8b9c3d89616107ab142916a Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Fri, 19 Sep 2025 13:01:05 -0400 Subject: [PATCH 03/16] refactor: move assistant logic into dedicated folder --- README.md | 8 +++++++- listeners/__init__.py | 2 +- listeners/assistant/__init__.py | 3 +++ listeners/{ => assistant}/assistant.py | 0 listeners/{ => assistant}/llm_caller.py | 0 5 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 listeners/assistant/__init__.py rename listeners/{ => assistant}/assistant.py (100%) rename listeners/{ => assistant}/llm_caller.py (100%) diff --git a/README.md b/README.md index 987d9b3..384171b 100644 --- a/README.md +++ b/README.md @@ -78,10 +78,16 @@ Every incoming request is routed to a "listener". Inside this directory, we grou **`/listeners/assistant`** -Configures the new Slack Assistant features, providing a dedicated side panel UI for users to interact with the AI chatbot. This includes: +Configures the new Slack Assistant features, providing a dedicated side panel UI for users to interact with the AI chatbot. This module includes: + +`assistant.py` * `@assistant.thread_started` - Manages when users start new assistant threads. * `@assistant.user_message` - Processes user messages in assistant threads and app DMs. **Replaces traditional DM handling as seen in** `/listeners/events/user_message.py` +`llm_caller.py` +* Handles OpenAI API integration and message formatting. Includes the `call_llm()` function that sends conversation threads to OpenAI's models and converts markdown responses to Slack-compatible formatting. + + ## App Distribution / OAuth Only implement OAuth if you plan to distribute your application across multiple workspaces. A separate `app_oauth.py` file can be found with relevant OAuth settings. diff --git a/listeners/__init__.py b/listeners/__init__.py index 523ce09..bf9d7a2 100644 --- a/listeners/__init__.py +++ b/listeners/__init__.py @@ -1,4 +1,4 @@ -from .assistant import assistant +from listeners.assistant import assistant def register_listeners(app): diff --git a/listeners/assistant/__init__.py b/listeners/assistant/__init__.py new file mode 100644 index 0000000..1bbba68 --- /dev/null +++ b/listeners/assistant/__init__.py @@ -0,0 +1,3 @@ +from .assistant import assistant + +__all__ = ["assistant"] diff --git a/listeners/assistant.py b/listeners/assistant/assistant.py similarity index 100% rename from listeners/assistant.py rename to listeners/assistant/assistant.py diff --git a/listeners/llm_caller.py b/listeners/assistant/llm_caller.py similarity index 100% rename from listeners/llm_caller.py rename to listeners/assistant/llm_caller.py From e1bc98a775cceb21237fb0152796130d598988a6 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Fri, 19 Sep 2025 13:10:56 -0400 Subject: [PATCH 04/16] docs: align wording with docs.slack.dev --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 384171b..01eed67 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ black . ### `/listeners` -Every incoming request is routed to a "listener". Inside this directory, we group each listener based on the Slack Platform feature used, so `/listeners/events` handles incoming events like messages sent to the app. +Every incoming request is routed to a "listener". Inside this directory, we group each listener based on the Slack Platform feature used, so `/listeners/events` handles incoming events, `/listeners/shortcuts` would handle incoming [Shortcuts](https://docs.slack.dev/interactivity/implementing-shortcuts/) requests, and so on. **Note**: The `listeners/events` folder is purely educational and demonstrates alternative implementation approaches. These listeners are **not registered** and are not used in the actual application. For the working implementation, refer to `listeners/assistant.py`. @@ -81,8 +81,8 @@ Every incoming request is routed to a "listener". Inside this directory, we grou Configures the new Slack Assistant features, providing a dedicated side panel UI for users to interact with the AI chatbot. This module includes: `assistant.py` -* `@assistant.thread_started` - Manages when users start new assistant threads. -* `@assistant.user_message` - Processes user messages in assistant threads and app DMs. **Replaces traditional DM handling as seen in** `/listeners/events/user_message.py` +* `@assistant.thread_started` - Receives an event when users start new app thread. +* `@assistant.user_message` - Processes user messages in app threads or from the app **Chat** and **History** tab. `llm_caller.py` * Handles OpenAI API integration and message formatting. Includes the `call_llm()` function that sends conversation threads to OpenAI's models and converts markdown responses to Slack-compatible formatting. From 291ef2d451c921bb8f30e56fbb7790c798b3631d Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Fri, 19 Sep 2025 17:02:25 -0400 Subject: [PATCH 05/16] fix: update imports after file structure refactor --- listeners/assistant/assistant.py | 10 +++++++--- listeners/events/__init__.py | 13 ++++++++++--- listeners/events/thread_context_store.py | 9 +++++++-- listeners/{assistant => }/llm_caller.py | 0 4 files changed, 24 insertions(+), 8 deletions(-) rename listeners/{assistant => }/llm_caller.py (100%) diff --git a/listeners/assistant/assistant.py b/listeners/assistant/assistant.py index 817030f..6d7ec6a 100644 --- a/listeners/assistant/assistant.py +++ b/listeners/assistant/assistant.py @@ -5,7 +5,7 @@ from slack_sdk import WebClient from slack_sdk.errors import SlackApiError -from .llm_caller import call_llm +from ..llm_caller import call_llm # Refer to https://tools.slack.dev/bolt-python/concepts/assistant/ for more details assistant = Assistant() @@ -72,13 +72,17 @@ def respond_in_assistant_thread( thread_context = get_thread_context() referred_channel_id = thread_context.get("channel_id") try: - channel_history = client.conversations_history(channel=referred_channel_id, limit=50) + channel_history = client.conversations_history( + channel=referred_channel_id, limit=50 + ) except SlackApiError as e: if e.response["error"] == "not_in_channel": # If this app's bot user is not in the public channel, # we'll try joining the channel and then calling the same API again client.conversations_join(channel=referred_channel_id) - channel_history = client.conversations_history(channel=referred_channel_id, limit=50) + channel_history = client.conversations_history( + channel=referred_channel_id, limit=50 + ) else: raise e diff --git a/listeners/events/__init__.py b/listeners/events/__init__.py index a2029af..4c97cd6 100644 --- a/listeners/events/__init__.py +++ b/listeners/events/__init__.py @@ -15,18 +15,25 @@ def register(app: App): app.event("assistant_thread_started")(start_thread_with_suggested_prompts) app.event("assistant_thread_context_changed")(save_new_thread_context) - app.event("message", matchers=[is_user_message_event_in_assistant_thread])(respond_to_user_message) + app.event("message", matchers=[is_user_message_event_in_assistant_thread])( + respond_to_user_message + ) app.event("message", matchers=[is_message_event_in_assistant_thread])(just_ack) def is_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool: if is_event(body): - return body["event"]["type"] == "message" and body["event"].get("channel_type") == "im" + return ( + body["event"]["type"] == "message" + and body["event"].get("channel_type") == "im" + ) return False def is_user_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool: - return is_message_event_in_assistant_thread(body) and body["event"].get("subtype") in (None, "file_share") + return is_message_event_in_assistant_thread(body) and body["event"].get( + "subtype" + ) in (None, "file_share") def just_ack(): diff --git a/listeners/events/thread_context_store.py b/listeners/events/thread_context_store.py index 282cdfb..dca5c6d 100644 --- a/listeners/events/thread_context_store.py +++ b/listeners/events/thread_context_store.py @@ -23,7 +23,10 @@ def _find_parent_message( ) if response.get("messages"): for message in response.get("messages"): - if message.get("subtype") is None and message.get("user") == context.bot_user_id: + if ( + message.get("subtype") is None + and message.get("user") == context.bot_user_id + ): return message @@ -34,7 +37,9 @@ def get_thread_context( channel_id: str, thread_ts: str, ) -> Optional[dict]: - parent_message = _find_parent_message(context=context, client=client, channel_id=channel_id, thread_ts=thread_ts) + parent_message = _find_parent_message( + context=context, client=client, channel_id=channel_id, thread_ts=thread_ts + ) if parent_message is not None and parent_message.get("metadata") is not None: return parent_message["metadata"]["event_payload"] diff --git a/listeners/assistant/llm_caller.py b/listeners/llm_caller.py similarity index 100% rename from listeners/assistant/llm_caller.py rename to listeners/llm_caller.py From a8389b1505ffd225de284ea99b4c753f5f0999a0 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Fri, 19 Sep 2025 17:04:19 -0400 Subject: [PATCH 06/16] build: black reformat --- listeners/assistant/assistant.py | 8 ++------ listeners/events/__init__.py | 13 +++---------- listeners/events/thread_context_store.py | 9 ++------- 3 files changed, 7 insertions(+), 23 deletions(-) diff --git a/listeners/assistant/assistant.py b/listeners/assistant/assistant.py index 6d7ec6a..40ecaa8 100644 --- a/listeners/assistant/assistant.py +++ b/listeners/assistant/assistant.py @@ -72,17 +72,13 @@ def respond_in_assistant_thread( thread_context = get_thread_context() referred_channel_id = thread_context.get("channel_id") try: - channel_history = client.conversations_history( - channel=referred_channel_id, limit=50 - ) + channel_history = client.conversations_history(channel=referred_channel_id, limit=50) except SlackApiError as e: if e.response["error"] == "not_in_channel": # If this app's bot user is not in the public channel, # we'll try joining the channel and then calling the same API again client.conversations_join(channel=referred_channel_id) - channel_history = client.conversations_history( - channel=referred_channel_id, limit=50 - ) + channel_history = client.conversations_history(channel=referred_channel_id, limit=50) else: raise e diff --git a/listeners/events/__init__.py b/listeners/events/__init__.py index 4c97cd6..a2029af 100644 --- a/listeners/events/__init__.py +++ b/listeners/events/__init__.py @@ -15,25 +15,18 @@ def register(app: App): app.event("assistant_thread_started")(start_thread_with_suggested_prompts) app.event("assistant_thread_context_changed")(save_new_thread_context) - app.event("message", matchers=[is_user_message_event_in_assistant_thread])( - respond_to_user_message - ) + app.event("message", matchers=[is_user_message_event_in_assistant_thread])(respond_to_user_message) app.event("message", matchers=[is_message_event_in_assistant_thread])(just_ack) def is_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool: if is_event(body): - return ( - body["event"]["type"] == "message" - and body["event"].get("channel_type") == "im" - ) + return body["event"]["type"] == "message" and body["event"].get("channel_type") == "im" return False def is_user_message_event_in_assistant_thread(body: Dict[str, Any]) -> bool: - return is_message_event_in_assistant_thread(body) and body["event"].get( - "subtype" - ) in (None, "file_share") + return is_message_event_in_assistant_thread(body) and body["event"].get("subtype") in (None, "file_share") def just_ack(): diff --git a/listeners/events/thread_context_store.py b/listeners/events/thread_context_store.py index dca5c6d..282cdfb 100644 --- a/listeners/events/thread_context_store.py +++ b/listeners/events/thread_context_store.py @@ -23,10 +23,7 @@ def _find_parent_message( ) if response.get("messages"): for message in response.get("messages"): - if ( - message.get("subtype") is None - and message.get("user") == context.bot_user_id - ): + if message.get("subtype") is None and message.get("user") == context.bot_user_id: return message @@ -37,9 +34,7 @@ def get_thread_context( channel_id: str, thread_ts: str, ) -> Optional[dict]: - parent_message = _find_parent_message( - context=context, client=client, channel_id=channel_id, thread_ts=thread_ts - ) + parent_message = _find_parent_message(context=context, client=client, channel_id=channel_id, thread_ts=thread_ts) if parent_message is not None and parent_message.get("metadata") is not None: return parent_message["metadata"]["event_payload"] From 42cf39ffb270bb9177e933404d65845d5e4baffd Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Mon, 22 Sep 2025 11:14:52 -0400 Subject: [PATCH 07/16] docs: align wording with docs.slack.dev --- README.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 01eed67..212deff 100644 --- a/README.md +++ b/README.md @@ -72,21 +72,19 @@ black . ### `/listeners` -Every incoming request is routed to a "listener". Inside this directory, we group each listener based on the Slack Platform feature used, so `/listeners/events` handles incoming events, `/listeners/shortcuts` would handle incoming [Shortcuts](https://docs.slack.dev/interactivity/implementing-shortcuts/) requests, and so on. - -**Note**: The `listeners/events` folder is purely educational and demonstrates alternative implementation approaches. These listeners are **not registered** and are not used in the actual application. For the working implementation, refer to `listeners/assistant.py`. +Every incoming request is routed to a "listener". This directory groups each listener based on the Slack Platform feature used, so `/listeners/events` handles incoming events, `/listeners/shortcuts` would handle incoming [Shortcuts](https://docs.slack.dev/interactivity/implementing-shortcuts/) requests, and so on. +:::info[The `listeners/events` folder is purely educational and demonstrates alternative approaches to implementation] +These listeners are **not registered** and are not used in the actual application. For the working implementation, refer to `listeners/assistant.py`. **`/listeners/assistant`** Configures the new Slack Assistant features, providing a dedicated side panel UI for users to interact with the AI chatbot. This module includes: -`assistant.py` -* `@assistant.thread_started` - Receives an event when users start new app thread. -* `@assistant.user_message` - Processes user messages in app threads or from the app **Chat** and **History** tab. - -`llm_caller.py` -* Handles OpenAI API integration and message formatting. Includes the `call_llm()` function that sends conversation threads to OpenAI's models and converts markdown responses to Slack-compatible formatting. +`assistant.py`, which contains two listeners: +* The `@assistant.thread_started` listener receives an event when users start new app thread. +* The `@assistant.user_message` listener processes user messages in app threads or from the app **Chat** and **History** tab. +`llm_caller.py`, which handles OpenAI API integration and message formatting. It includes the `call_llm()` function that sends conversation threads to OpenAI's models and converts markdown responses to Slack-compatible formatting. ## App Distribution / OAuth From 2acdaf46bf81a975015acfe7d4d376e049f262cf Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Mon, 22 Sep 2025 18:46:12 -0400 Subject: [PATCH 08/16] feat: ai app streaming and loading states --- .slack/.gitignore | 2 ++ .slack/config.json | 6 ++++ .slack/hooks.json | 5 +++ listeners/assistant/assistant.py | 62 ++++++++++++++++---------------- listeners/llm_caller.py | 45 ++++------------------- requirements.txt | 3 +- 6 files changed, 52 insertions(+), 71 deletions(-) create mode 100644 .slack/.gitignore create mode 100644 .slack/config.json create mode 100644 .slack/hooks.json diff --git a/.slack/.gitignore b/.slack/.gitignore new file mode 100644 index 0000000..973ba60 --- /dev/null +++ b/.slack/.gitignore @@ -0,0 +1,2 @@ +apps.dev.json +cache/ diff --git a/.slack/config.json b/.slack/config.json new file mode 100644 index 0000000..67cab18 --- /dev/null +++ b/.slack/config.json @@ -0,0 +1,6 @@ +{ + "manifest": { + "source": "remote" + }, + "project_id": "22e2b5e7-ef8f-4fbf-8026-a62ae0623037" +} \ No newline at end of file diff --git a/.slack/hooks.json b/.slack/hooks.json new file mode 100644 index 0000000..ce474c9 --- /dev/null +++ b/.slack/hooks.json @@ -0,0 +1,5 @@ +{ + "hooks": { + "get-hooks": "python3 -m slack_cli_hooks.hooks.get_hooks" + } +} diff --git a/listeners/assistant/assistant.py b/listeners/assistant/assistant.py index 40ecaa8..b64359a 100644 --- a/listeners/assistant/assistant.py +++ b/listeners/assistant/assistant.py @@ -1,9 +1,8 @@ import logging from typing import List, Dict -from slack_bolt import Assistant, BoltContext, Say, SetSuggestedPrompts, SetStatus +from slack_bolt import Assistant, BoltContext, Say, SetSuggestedPrompts from slack_bolt.context.get_thread_context import GetThreadContext from slack_sdk import WebClient -from slack_sdk.errors import SlackApiError from ..llm_caller import call_llm @@ -57,39 +56,20 @@ def respond_in_assistant_thread( payload: dict, logger: logging.Logger, context: BoltContext, - set_status: SetStatus, - get_thread_context: GetThreadContext, client: WebClient, say: Say, ): try: - user_message = payload["text"] - set_status("is typing...") - - if user_message == "Can you generate a brief summary of the referred channel?": - # the logic here requires the additional bot scopes: - # channels:join, channels:history, groups:history - thread_context = get_thread_context() - referred_channel_id = thread_context.get("channel_id") - try: - channel_history = client.conversations_history(channel=referred_channel_id, limit=50) - except SlackApiError as e: - if e.response["error"] == "not_in_channel": - # If this app's bot user is not in the public channel, - # we'll try joining the channel and then calling the same API again - client.conversations_join(channel=referred_channel_id) - channel_history = client.conversations_history(channel=referred_channel_id, limit=50) - else: - raise e + channel_id = payload["channel"] + thread_ts = payload["thread_ts"] - prompt = f"Can you generate a brief summary of these messages in a Slack channel <#{referred_channel_id}>?\n\n" - for message in reversed(channel_history.get("messages")): - if message.get("user") is not None: - prompt += f"\n<@{message['user']}> says: {message['text']}\n" - messages_in_thread = [{"role": "user", "content": prompt}] - returned_message = call_llm(messages_in_thread) - say(returned_message) - return + loading_messages = [ + "Teaching the hamsters to type faster…", + "Untangling the internet cables…", + "Consulting the office goldfish…", + "Polishing up the response just for you…", + "Convincing the AI to stop overthinking…", + ] replies = client.conversations_replies( channel=context.channel_id, @@ -101,8 +81,28 @@ def respond_in_assistant_thread( for message in replies["messages"]: role = "user" if message.get("bot_id") is None else "assistant" messages_in_thread.append({"role": role, "content": message["text"]}) + returned_message = call_llm(messages_in_thread) - say(returned_message) + client.assistant_threads_setStatus( + channel_id=channel_id, thread_ts=thread_ts, status="Bolt is typing", loading_messages=loading_messages + ) + stream_response = client.chat_startStream( + channel=channel_id, + thread_ts=thread_ts, + ) + stream_ts = stream_response["ts"] + # use of this for loop is specific to openai response method + for event in returned_message: + print(f"\n{event.type}") + if event.type == "response.output_text.delta": + client.chat_appendStream(channel=channel_id, ts=stream_ts, markdown_text=f"{event.delta}") + else: + continue + + client.chat_stopStream( + channel=channel_id, + ts=stream_ts, + ) except Exception as e: logger.exception(f"Failed to handle a user message event: {e}") diff --git a/listeners/llm_caller.py b/listeners/llm_caller.py index 863c0eb..e808068 100644 --- a/listeners/llm_caller.py +++ b/listeners/llm_caller.py @@ -1,8 +1,10 @@ import os -import re from typing import List, Dict import openai +from openai import Stream +from openai.types.responses import ResponseStreamEvent + DEFAULT_SYSTEM_CONTENT = """ You're an assistant in a Slack workspace. @@ -16,44 +18,9 @@ def call_llm( messages_in_thread: List[Dict[str, str]], system_content: str = DEFAULT_SYSTEM_CONTENT, -) -> str: +) -> Stream[ResponseStreamEvent]: openai_client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY")) messages = [{"role": "system", "content": system_content}] messages.extend(messages_in_thread) - response = openai_client.chat.completions.create( - model="gpt-4o-mini", - n=1, - messages=messages, - max_tokens=16384, - ) - return markdown_to_slack(response.choices[0].message.content) - - -# Conversion from OpenAI markdown to Slack mrkdwn -# See also: https://api.slack.com/reference/surfaces/formatting#basics -def markdown_to_slack(content: str) -> str: - # Split the input string into parts based on code blocks and inline code - parts = re.split(r"(?s)(```.+?```|`[^`\n]+?`)", content) - - # Apply the bold, italic, and strikethrough formatting to text not within code - result = "" - for part in parts: - if part.startswith("```") or part.startswith("`"): - result += part - else: - for o, n in [ - ( - r"\*\*\*(?!\s)([^\*\n]+?)(?=1.21,<2 -slack-sdk>=3.33.1,<4 +slack-cli-hooks<1.0.0 +slack_sdk==3.36.0.dev0 # If you use a different LLM vendor, replace this dependency openai From 5bf3bf49e4e2c658fd040f088975a455c267057e Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Tue, 23 Sep 2025 10:56:39 -0400 Subject: [PATCH 09/16] build: add python sdk beta release to requirements.txt --- README.md | 2 +- listeners/assistant/assistant.py | 1 - requirements.txt | 4 +++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 212deff..2b89a2d 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Configures the new Slack Assistant features, providing a dedicated side panel UI * The `@assistant.thread_started` listener receives an event when users start new app thread. * The `@assistant.user_message` listener processes user messages in app threads or from the app **Chat** and **History** tab. -`llm_caller.py`, which handles OpenAI API integration and message formatting. It includes the `call_llm()` function that sends conversation threads to OpenAI's models and converts markdown responses to Slack-compatible formatting. +`llm_caller.py`, which handles OpenAI API integration and message formatting. It includes the `call_llm()` function that sends conversation threads to OpenAI's models. ## App Distribution / OAuth diff --git a/listeners/assistant/assistant.py b/listeners/assistant/assistant.py index b64359a..1e37bda 100644 --- a/listeners/assistant/assistant.py +++ b/listeners/assistant/assistant.py @@ -93,7 +93,6 @@ def respond_in_assistant_thread( stream_ts = stream_response["ts"] # use of this for loop is specific to openai response method for event in returned_message: - print(f"\n{event.type}") if event.type == "response.output_text.delta": client.chat_appendStream(channel=channel_id, ts=stream_ts, markdown_text=f"{event.delta}") else: diff --git a/requirements.txt b/requirements.txt index 98be8fb..aac8252 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ +--extra-index-url=https://test.pypi.org/simple/ +slack_sdk==3.36.0.dev0 slack-bolt>=1.21,<2 slack-cli-hooks<1.0.0 -slack_sdk==3.36.0.dev0 + # If you use a different LLM vendor, replace this dependency openai From bfd4649c9defb9daf5a2fbbf65cd98399a694a56 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Tue, 23 Sep 2025 17:34:55 -0400 Subject: [PATCH 10/16] fix: address pr feedback --- .gitignore | 1 + .slack/.gitignore | 2 -- .slack/config.json | 6 ------ .slack/hooks.json | 5 ----- README.md | 5 +++-- app.py | 9 ++++++++- requirements.txt | 1 - 7 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 .slack/.gitignore delete mode 100644 .slack/config.json delete mode 100644 .slack/hooks.json diff --git a/.gitignore b/.gitignore index abc3d97..ca0490a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ logs/ *.db .pytype/ .idea/ +.slack \ No newline at end of file diff --git a/.slack/.gitignore b/.slack/.gitignore deleted file mode 100644 index 973ba60..0000000 --- a/.slack/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -apps.dev.json -cache/ diff --git a/.slack/config.json b/.slack/config.json deleted file mode 100644 index 67cab18..0000000 --- a/.slack/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "manifest": { - "source": "remote" - }, - "project_id": "22e2b5e7-ef8f-4fbf-8026-a62ae0623037" -} \ No newline at end of file diff --git a/.slack/hooks.json b/.slack/hooks.json deleted file mode 100644 index ce474c9..0000000 --- a/.slack/hooks.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "hooks": { - "get-hooks": "python3 -m slack_cli_hooks.hooks.get_hooks" - } -} diff --git a/README.md b/README.md index 2b89a2d..4a86964 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,9 @@ black . Every incoming request is routed to a "listener". This directory groups each listener based on the Slack Platform feature used, so `/listeners/events` handles incoming events, `/listeners/shortcuts` would handle incoming [Shortcuts](https://docs.slack.dev/interactivity/implementing-shortcuts/) requests, and so on. -:::info[The `listeners/events` folder is purely educational and demonstrates alternative approaches to implementation] -These listeners are **not registered** and are not used in the actual application. For the working implementation, refer to `listeners/assistant.py`. +> [!NOTE] +> The `listeners/events` folder is purely educational and demonstrates alternative approaches to implementation. These listeners are **not registered** and are not used in the actual application. For the working implementation, refer to `listeners/assistant/assistant.py`. + **`/listeners/assistant`** Configures the new Slack Assistant features, providing a dedicated side panel UI for users to interact with the AI chatbot. This module includes: diff --git a/app.py b/app.py index 092471f..fb7bccf 100644 --- a/app.py +++ b/app.py @@ -3,13 +3,20 @@ from slack_bolt import App from slack_bolt.adapter.socket_mode import SocketModeHandler +from slack_sdk import WebClient from listeners import register_listeners # Initialization logging.basicConfig(level=logging.DEBUG) -app = App(token=os.environ.get("SLACK_BOT_TOKEN")) +app = App( + token=os.environ.get("SLACK_BOT_TOKEN"), + client=WebClient( + base_url=os.environ.get("SLACK_API_URL", "https://slack.com/api"), + token=os.environ.get("SLACK_BOT_TOKEN"), + ), +) # Register Listeners register_listeners(app) diff --git a/requirements.txt b/requirements.txt index aac8252..e1eda6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ --extra-index-url=https://test.pypi.org/simple/ slack_sdk==3.36.0.dev0 slack-bolt>=1.21,<2 -slack-cli-hooks<1.0.0 # If you use a different LLM vendor, replace this dependency openai From 530052f7f52e3229aa88ac61938bbdd7b1c9f720 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Tue, 23 Sep 2025 18:23:14 -0400 Subject: [PATCH 11/16] docs: update README title --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4a86964..bfc0e5f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# App Agent & Assistant Template (Bolt for Python) +# AI App Template (Bolt for Python) -This Bolt for Python template demonstrates how to build [Agents & Assistants](https://api.slack.com/docs/apps/ai) in Slack. +This Bolt for Python template demonstrates how to build [AI Apps](https://api.slack.com/docs/apps/ai) in Slack. ## Setup Before getting started, make sure you have a development workspace where you have permissions to install apps. If you don’t have one setup, go ahead and [create one](https://slack.com/create). @@ -74,8 +74,8 @@ black . Every incoming request is routed to a "listener". This directory groups each listener based on the Slack Platform feature used, so `/listeners/events` handles incoming events, `/listeners/shortcuts` would handle incoming [Shortcuts](https://docs.slack.dev/interactivity/implementing-shortcuts/) requests, and so on. -> [!NOTE] -> The `listeners/events` folder is purely educational and demonstrates alternative approaches to implementation. These listeners are **not registered** and are not used in the actual application. For the working implementation, refer to `listeners/assistant/assistant.py`. +:::info[The `listeners/events` folder is purely educational and demonstrates alternative approaches to implementation] +These listeners are **not registered** and are not used in the actual application. For the working implementation, refer to `listeners/assistant.py`. **`/listeners/assistant`** From 905f4fb1e4f429dde665817c8eb35339b6570588 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Tue, 23 Sep 2025 19:39:01 -0400 Subject: [PATCH 12/16] fix: address pr feedback --- .gitignore | 1 - README.md | 2 +- requirements.txt | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index ca0490a..abc3d97 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,3 @@ logs/ *.db .pytype/ .idea/ -.slack \ No newline at end of file diff --git a/README.md b/README.md index bfc0e5f..b03ebb5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # AI App Template (Bolt for Python) -This Bolt for Python template demonstrates how to build [AI Apps](https://api.slack.com/docs/apps/ai) in Slack. +This Bolt for Python template demonstrates how to build [AI Apps](https://docs.slack.dev/ai/) in Slack. ## Setup Before getting started, make sure you have a development workspace where you have permissions to install apps. If you don’t have one setup, go ahead and [create one](https://slack.com/create). diff --git a/requirements.txt b/requirements.txt index e1eda6c..6516993 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ --extra-index-url=https://test.pypi.org/simple/ -slack_sdk==3.36.0.dev0 +slack_sdk==3.36.0.dev1 slack-bolt>=1.21,<2 # If you use a different LLM vendor, replace this dependency From 9dda237df57693a2b9a58b308d00aa183bfef5f6 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 23 Sep 2025 23:04:46 -0700 Subject: [PATCH 13/16] refactor: replace assistant_threads_setStatus method with assistant class set_status --- listeners/assistant/assistant.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/listeners/assistant/assistant.py b/listeners/assistant/assistant.py index 1e37bda..c90eec6 100644 --- a/listeners/assistant/assistant.py +++ b/listeners/assistant/assistant.py @@ -1,6 +1,7 @@ import logging -from typing import List, Dict -from slack_bolt import Assistant, BoltContext, Say, SetSuggestedPrompts +from typing import Dict, List + +from slack_bolt import Assistant, BoltContext, Say, SetStatus, SetSuggestedPrompts from slack_bolt.context.get_thread_context import GetThreadContext from slack_sdk import WebClient @@ -53,11 +54,12 @@ def start_assistant_thread( # This listener is invoked when the human user sends a reply in the assistant thread @assistant.user_message def respond_in_assistant_thread( - payload: dict, - logger: logging.Logger, - context: BoltContext, client: WebClient, + context: BoltContext, + logger: logging.Logger, + payload: dict, say: Say, + set_status: SetStatus, ): try: channel_id = payload["channel"] @@ -83,8 +85,9 @@ def respond_in_assistant_thread( messages_in_thread.append({"role": role, "content": message["text"]}) returned_message = call_llm(messages_in_thread) - client.assistant_threads_setStatus( - channel_id=channel_id, thread_ts=thread_ts, status="Bolt is typing", loading_messages=loading_messages + set_status( + status="Bolt is typing", + loading_messages=loading_messages, ) stream_response = client.chat_startStream( channel=channel_id, From 4f16ad2e577d11a67029882f480d42a349154bce Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 25 Sep 2025 18:53:44 -0700 Subject: [PATCH 14/16] chore: lint --- listeners/assistant/assistant.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/listeners/assistant/assistant.py b/listeners/assistant/assistant.py index cf58b60..8dfd2db 100644 --- a/listeners/assistant/assistant.py +++ b/listeners/assistant/assistant.py @@ -1,13 +1,10 @@ import logging from typing import Dict, List -from slack_bolt import (Assistant, BoltContext, Say, SetStatus, - SetSuggestedPrompts) +from slack_bolt import Assistant, BoltContext, Say, SetStatus, SetSuggestedPrompts from slack_bolt.context.get_thread_context import GetThreadContext from slack_sdk import WebClient -from slack_sdk.models.blocks import (Block, ContextActionsBlock, - FeedbackButtonObject, - FeedbackButtonsElement) +from slack_sdk.models.blocks import Block, ContextActionsBlock, FeedbackButtonObject, FeedbackButtonsElement from ..llm_caller import call_llm From 19f236e44f2ec0bfd6545c26c1ffca13dde7f45a Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 25 Sep 2025 18:58:33 -0700 Subject: [PATCH 15/16] refactor: inline the loading messages --- listeners/assistant/assistant.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/listeners/assistant/assistant.py b/listeners/assistant/assistant.py index 8dfd2db..bf0ff93 100644 --- a/listeners/assistant/assistant.py +++ b/listeners/assistant/assistant.py @@ -95,17 +95,15 @@ def respond_in_assistant_thread( channel_id = payload["channel"] thread_ts = payload["thread_ts"] - loading_messages = [ - "Teaching the hamsters to type faster…", - "Untangling the internet cables…", - "Consulting the office goldfish…", - "Polishing up the response just for you…", - "Convincing the AI to stop overthinking…", - ] - set_status( status="Drafting...", - loading_messages=loading_messages, + loading_messages=[ + "Teaching the hamsters to type faster…", + "Untangling the internet cables…", + "Consulting the office goldfish…", + "Polishing up the response just for you…", + "Convincing the AI to stop overthinking…", + ], ) replies = client.conversations_replies( @@ -120,6 +118,7 @@ def respond_in_assistant_thread( messages_in_thread.append({"role": role, "content": message["text"]}) returned_message = call_llm(messages_in_thread) + stream_response = client.chat_startStream( channel=channel_id, thread_ts=thread_ts, From 87571fbb8a7c1164788116d240566cb15f18967b Mon Sep 17 00:00:00 2001 From: Michael Brooks Date: Tue, 30 Sep 2025 14:41:42 -0700 Subject: [PATCH 16/16] chore(deps): bump slack-sdk==3.36.0.dev5 and slack-bolt==1.26.0.dev2 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index f73759d..083826a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -slack-sdk==3.36.0.dev3 -slack-bolt>=1.21,<2 +slack-sdk==3.36.0.dev5 +slack-bolt==1.26.0.dev2 # If you use a different LLM vendor, replace this dependency openai