Skip to content

Commit 0e83c52

Browse files
committed
feat: ai app streaming and loading states
1 parent 42cf39f commit 0e83c52

File tree

6 files changed

+52
-71
lines changed

6 files changed

+52
-71
lines changed

.slack/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
apps.dev.json
2+
cache/

.slack/config.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"manifest": {
3+
"source": "remote"
4+
},
5+
"project_id": "22e2b5e7-ef8f-4fbf-8026-a62ae0623037"
6+
}

.slack/hooks.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"hooks": {
3+
"get-hooks": "python3 -m slack_cli_hooks.hooks.get_hooks"
4+
}
5+
}

listeners/assistant/assistant.py

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import logging
22
from typing import List, Dict
3-
from slack_bolt import Assistant, BoltContext, Say, SetSuggestedPrompts, SetStatus
3+
from slack_bolt import Assistant, BoltContext, Say, SetSuggestedPrompts
44
from slack_bolt.context.get_thread_context import GetThreadContext
55
from slack_sdk import WebClient
6-
from slack_sdk.errors import SlackApiError
76

87
from ..llm_caller import call_llm
98

@@ -57,39 +56,20 @@ def respond_in_assistant_thread(
5756
payload: dict,
5857
logger: logging.Logger,
5958
context: BoltContext,
60-
set_status: SetStatus,
61-
get_thread_context: GetThreadContext,
6259
client: WebClient,
6360
say: Say,
6461
):
6562
try:
66-
user_message = payload["text"]
67-
set_status("is typing...")
68-
69-
if user_message == "Can you generate a brief summary of the referred channel?":
70-
# the logic here requires the additional bot scopes:
71-
# channels:join, channels:history, groups:history
72-
thread_context = get_thread_context()
73-
referred_channel_id = thread_context.get("channel_id")
74-
try:
75-
channel_history = client.conversations_history(channel=referred_channel_id, limit=50)
76-
except SlackApiError as e:
77-
if e.response["error"] == "not_in_channel":
78-
# If this app's bot user is not in the public channel,
79-
# we'll try joining the channel and then calling the same API again
80-
client.conversations_join(channel=referred_channel_id)
81-
channel_history = client.conversations_history(channel=referred_channel_id, limit=50)
82-
else:
83-
raise e
63+
channel_id = payload["channel"]
64+
thread_ts = payload["thread_ts"]
8465

85-
prompt = f"Can you generate a brief summary of these messages in a Slack channel <#{referred_channel_id}>?\n\n"
86-
for message in reversed(channel_history.get("messages")):
87-
if message.get("user") is not None:
88-
prompt += f"\n<@{message['user']}> says: {message['text']}\n"
89-
messages_in_thread = [{"role": "user", "content": prompt}]
90-
returned_message = call_llm(messages_in_thread)
91-
say(returned_message)
92-
return
66+
loading_messages = [
67+
"Teaching the hamsters to type faster…",
68+
"Untangling the internet cables…",
69+
"Consulting the office goldfish…",
70+
"Polishing up the response just for you…",
71+
"Convincing the AI to stop overthinking…",
72+
]
9373

9474
replies = client.conversations_replies(
9575
channel=context.channel_id,
@@ -101,8 +81,28 @@ def respond_in_assistant_thread(
10181
for message in replies["messages"]:
10282
role = "user" if message.get("bot_id") is None else "assistant"
10383
messages_in_thread.append({"role": role, "content": message["text"]})
84+
10485
returned_message = call_llm(messages_in_thread)
105-
say(returned_message)
86+
client.assistant_threads_setStatus(
87+
channel_id=channel_id, thread_ts=thread_ts, status="Bolt is typing", loading_messages=loading_messages
88+
)
89+
stream_response = client.chat_startStream(
90+
channel=channel_id,
91+
thread_ts=thread_ts,
92+
)
93+
stream_ts = stream_response["ts"]
94+
# use of this for loop is specific to openai response method
95+
for event in returned_message:
96+
print(f"\n{event.type}")
97+
if event.type == "response.output_text.delta":
98+
client.chat_appendStream(channel=channel_id, ts=stream_ts, markdown_text=f"{event.delta}")
99+
else:
100+
continue
101+
102+
client.chat_stopStream(
103+
channel=channel_id,
104+
ts=stream_ts,
105+
)
106106

107107
except Exception as e:
108108
logger.exception(f"Failed to handle a user message event: {e}")

listeners/llm_caller.py

Lines changed: 6 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import os
2-
import re
32
from typing import List, Dict
43

54
import openai
5+
from openai import Stream
6+
from openai.types.responses import ResponseStreamEvent
7+
68

79
DEFAULT_SYSTEM_CONTENT = """
810
You're an assistant in a Slack workspace.
@@ -16,44 +18,9 @@
1618
def call_llm(
1719
messages_in_thread: List[Dict[str, str]],
1820
system_content: str = DEFAULT_SYSTEM_CONTENT,
19-
) -> str:
21+
) -> Stream[ResponseStreamEvent]:
2022
openai_client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
2123
messages = [{"role": "system", "content": system_content}]
2224
messages.extend(messages_in_thread)
23-
response = openai_client.chat.completions.create(
24-
model="gpt-4o-mini",
25-
n=1,
26-
messages=messages,
27-
max_tokens=16384,
28-
)
29-
return markdown_to_slack(response.choices[0].message.content)
30-
31-
32-
# Conversion from OpenAI markdown to Slack mrkdwn
33-
# See also: https://api.slack.com/reference/surfaces/formatting#basics
34-
def markdown_to_slack(content: str) -> str:
35-
# Split the input string into parts based on code blocks and inline code
36-
parts = re.split(r"(?s)(```.+?```|`[^`\n]+?`)", content)
37-
38-
# Apply the bold, italic, and strikethrough formatting to text not within code
39-
result = ""
40-
for part in parts:
41-
if part.startswith("```") or part.startswith("`"):
42-
result += part
43-
else:
44-
for o, n in [
45-
(
46-
r"\*\*\*(?!\s)([^\*\n]+?)(?<!\s)\*\*\*",
47-
r"_*\1*_",
48-
), # ***bold italic*** to *_bold italic_*
49-
(
50-
r"(?<![\*_])\*(?!\s)([^\*\n]+?)(?<!\s)\*(?![\*_])",
51-
r"_\1_",
52-
), # *italic* to _italic_
53-
(r"\*\*(?!\s)([^\*\n]+?)(?<!\s)\*\*", r"*\1*"), # **bold** to *bold*
54-
(r"__(?!\s)([^_\n]+?)(?<!\s)__", r"*\1*"), # __bold__ to *bold*
55-
(r"~~(?!\s)([^~\n]+?)(?<!\s)~~", r"~\1~"), # ~~strike~~ to ~strike~
56-
]:
57-
part = re.sub(o, n, part)
58-
result += part
59-
return result
25+
response = openai_client.responses.create(model="gpt-4o-mini", input=messages, stream=True)
26+
return response

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
slack-bolt>=1.21,<2
2-
slack-sdk>=3.33.1,<4
2+
slack-cli-hooks<1.0.0
3+
slack_sdk==3.36.0.dev0
34
# If you use a different LLM vendor, replace this dependency
45
openai
56

0 commit comments

Comments
 (0)