Slack & ChatGPT Integration|Build Custom OpenAI API Slack App with Google Cloud Functions & Python
Develop a tailored Slack app using ChatGPT OpenAI API, Google Cloud Functions, and Python to automate workflows and enhance team communication efficiently.
点击这里查看本文章简体中文版本。
點擊這裡查看本文章正體中文版本。
This post was translated with AI assistance — let me know if anything sounds off!
Slack & ChatGPT Integration
Build Your Own ChatGPT OpenAI API for Slack App (Google Cloud Functions & Python)
Background
Recently, our team has been promoting the use of Generative AI to improve work efficiency. Initially, we aim to implement an AI Assistant (ChatGPT functionality) to reduce the time spent on daily data queries, organizing tedious data, and manual data processing, thereby enhancing productivity. We hope that engineers, designers, PMs, and marketers can all use it freely.
The simplest way is to directly purchase the ChatGPT Team plan, which costs $25 per seat per year. However, since the usage frequency and volume are uncertain, and there is a desire to integrate with more external collaboration and development workflows, we chose to use the OpenAI API instead, then package and integrate it through other services for team members.
OpenAI API Key can be generated from this page. The Key is not tied to a specific Model version; you need to specify the Model version when using it, which determines the corresponding token cost.
We need a service that can set the OpenAI API Key by itself and use that Key for ChatGPT-like interactions.
Whether it’s a Chrome Extension or a Slack App, it’s quite hard to find services that allow users to set their own OpenAI API Key. Most services sell their own subscription plans, so letting users customize the API Key means they can’t make money and are essentially doing charity.
[Chrome Extension] SidebarGPT
After installation, go to Settings -> General -> enter your OpenAI API Key.
You can directly open the chat interface from the browser toolbar or side icon and use it directly:
[Chrome Extension] OpenAI Translator
If you only need translation, you can use this. It allows you to customize the OpenAI API Key for translation.
Additionally, it is an open-source project and also offers desktop applications for macOS and Windows:
The advantage of Chrome Extensions is that they are fast, simple, and convenient—just install and use. The downside is that the API Key must be shared with all members, making it hard to control leaks. Additionally, using third-party services makes it difficult to ensure everyone’s data security.
[Self-hosted] LibreChat
The OpenAI API Chat wrapper service recommended by colleagues in the R&D department offers authentication and an open-source project that nearly replicates the ChatGPT interface, with features more powerful than ChatGPT.
Just the project, Docker installed, .env configured, and Docker service started are needed to access and use it directly through the website.
Tried it out and it’s flawless, basically a local version of ChatGPT; the only downside is that it requires server deployment; if there are no other concerns, you can directly use this open-source project.
Slack App
Actually, the LibreChat service works once it’s deployed on a server, but I thought it would be more convenient if it could be integrated into everyday tools. Plus, the company server has strict permission settings, so running the service freely is difficult.
At that time, I didn’t think much and assumed there would be many Slack apps integrating OpenAI API services, so I just needed to find one and set it up; I didn’t expect it to be that complicated.
Google search only found one official Slack x OpenAI press release from March 2023: “Why we built the ChatGPT app for Slack” and some beta images:
https://www.salesforce.com/news/stories/chatgpt-app-for-slack/
It seems very feature-rich and can greatly improve work efficiency. However, as of January 2024, there is no news of a release. The Beta registration link provided at the end of the article is also no longer valid, with no further updates for now. (Or is Microsoft planning to support Teams first?)
[2024/02/14 Update]:
- See Slack official news; it seems the integration with ChatGPT (OpenAI) has been abandoned or merged into Slack AI.
Slack Apps
Since there is no official app, I turned to search for third-party developer apps. I tried several but ran into dead ends; not only were there few suitable apps, but none offered a custom key feature. Each one was designed to sell services and make money.
Implementing ChatGPT OpenAI API for Slack App by Yourself
I had some experience developing Slack apps before, so I decided to give it a try myself.
⚠️Disclaimer⚠️
This article uses connecting to the OpenAI API as an example to demonstrate how to create a Slack App and quickly use Google Cloud Functions to meet the requirements. Slack Apps have many possible uses, so feel free to explore and customize.
⚠️⚠️ Google Cloud Functions, a Function as a Service (FaaS), offers advantages such as convenience, speed, free usage tiers, easy deployment after coding, and automatic scaling. However, the service environment is controlled by GCP. If the function is not called for a long time, it goes into a sleep state. When called again, it undergoes a Cold Start, which requires longer response time. It is also more difficult to have multiple services interact with each other.
For more comprehensive or high-demand usage, it is still recommended to set up your own VM (App Engine) to host the server and run the service.
Final Result Image
Complete Cloud Functions Python code and Slack App settings are provided at the end of the article. For those who don’t want to go through it step by step, feel free to check them out quickly.
Step 1. Create a Slack App
Go to Slack App:
Click “Create New App”
Choose “From scratch”
Enter the “App Name” and select the Workspace to add.
After creation, go to “OAuth & Permissions” to add the required permissions for the Bot.
Scroll down to the “Scopes” section, click “Add an OAuth Scope,” and search to add the following permissions:
chat:write
im:history
im:read
im:write
After adding Bot permissions, click “Install App” on the left -> “Install to Workspace”
If the Slack App adds new permissions in the future, you will need to click “Reinstall” again for them to take effect.
But rest assured, the Bot Token will not change due to reinstallation.
After setting the Slack Bot Token permissions, go to “App Home”:
Scroll down to the “Show Tabs” section, enable the “Messages Tab” and “Allow users to send Slash commands and messages from the messages tab” (if this is not checked, you still can’t send messages, and it will show “Sending messages to this app has been turned off.”).
Back in the Slack Workspace, press “Command+R” to refresh the screen and see the newly created Slack App and message input box:
At this point, sending messages to the app has no functionality.
Enable Event Subscriptions feature
Next, we need to enable the event subscription feature of the Slack App, which will send an API call to the specified URL when a designated event occurs.
Add Google Cloud Functions
For the Request URL part, Google Cloud Functions comes into play.
After setting up the project and billing information, click “Create Function”
Function name: Enter the project name. Authentication: Select “Allow unauthenticated invocations,” which means anyone with the URL can access it.
If you cannot create a Function or change Authentication, it means your GCP account lacks full Google Cloud Functions permissions. You need to ask your organization administrator to add the Cloud Functions Admin role to your existing identity to use these features.
Runtime: Python 3.8 or higher
main.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import functions_framework
@functions_framework.http
def hello_http(request):
request_json = request.get_json(silent=True)
request_args = request.args
request_headers = request.headers
# You can simply use print to log runtime information, which can be viewed in Logs
# For advanced logging levels, refer to: https://cloud.google.com/logging/docs/reference/libraries
print(request_json)
# Due to FAAS (Cloud Functions) limitations, if the service is not called for a long time, the next call triggers a cold start,
# which might not respond within Slack's 3-second requirement
# Plus, OpenAI API requests and responses take some time (depending on response length, it may take nearly 1 minute)
# If Slack does not receive a response within the time limit, it considers the request lost and retries
# This causes duplicate requests and responses, so we can set X-Slack-No-Retry: 1 in Response Headers to tell Slack not to retry even if no timely response is received
headers = {'X-Slack-No-Retry':1}
# Ignore Slack Retry requests...
if request_headers and 'X-Slack-Retry-Num' in request_headers:
return ('OK!', 200, headers)
# Slack App Event Subscriptions Verify
# https://api.slack.com/events/url_verification
if request_json and 'type' in request_json and request_json['type'] == 'url_verification':
challenge = ""
if 'challenge' in request_json:
challenge = request_json['challenge']
return (challenge, 200, headers)
return ("Access Denied!", 400, headers)
requirements.txt
Enter the following dependencies:
1
2
3
functions-framework==3.*
requests==2.31.0
openai==1.9.0
Currently, there are no features yet. It only allows the Slack App to enable verification through Event Subscriptions. You can directly click “Deploy” to complete the initial deployment.
⚠️If you are not familiar with the Cloud Functions editor, you can scroll down to the bottom of the article to see additional information.
After the deployment is complete (green checkmark), copy the Cloud Functions URL:
Paste the Request URL back into the Slack App Enable Events.
If there are no issues, “Verified” will appear to complete the verification.
This part handles receiving verification requests sent from Slack:
1
2
3
4
5
{
"token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
"challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
"type": "url_verification"
}
Respond to the content in the challenge
field to pass the verification.
After successful activation, scroll down to the “Subscribe to bot events” section and click “Add Bot User Event” to add the “message.im” permission.
After granting full permissions, click the “reinstall your app” link above to reinstall the Slack App to your Workspace. This completes the Slack App setup.
You can also go to “App Home” or “Basic Information” to customize the Slack App’s name and avatar.
Basic Information
Step 2. Improve OpenAI API and Slack App Integration (Direct Messages)
First, we need to obtain the required OPENAI API KEY
and Bot User OAuth Token
.
OPENAI API KEY
: OpenAI API key page .
Bot User OAuth Token
: OAuth Tokens for Your Workspace
Handling Direct Message (IM) Event & Integrating OpenAI API Response
When a user sends a message with the Slack App, the following Event JSON Payload is received:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{
"token": "XXX",
"team_id": "XXX",
"context_team_id": "XXX",
"context_enterprise_id": null,
"api_app_id": "XXX",
"event": {
"client_msg_id": "XXX",
"type": "message",
"text": "Hello",
"user": "XXX",
"ts": "1707920753.115429",
"blocks": [
{
"type": "rich_text",
"block_id": "orfng",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "Hello"
}
]
}
]
}
],
"team": "XXX",
"channel": "XXX",
"event_ts": "1707920753.115429",
"channel_type": "im"
},
"type": "event_callback",
"event_id": "XXX",
"event_time": 1707920753,
"authorizations": [
{
"enterprise_id": null,
"team_id": "XXX",
"user_id": "XXX",
"is_bot": true,
"is_enterprise_install": false
}
],
"is_ext_shared_channel": false,
"event_context": "4-XXX"
}
Based on the above Json Payload, we can complete the integration from Slack message to OpenAI API and then back to replying to the Slack message:
Cloud Functions main.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
import functions_framework
import requests
import asyncio
import json
import time
from openai import AsyncOpenAI
OPENAI_API_KEY = "OPENAI API KEY"
SLACK_BOT_TOKEN = "Bot User OAuth Token"
# The OPENAI API Model used
# https://platform.openai.com/docs/models
OPENAI_MODEL = "gpt-4-1106-preview"
@functions_framework.http
def hello_http(request):
request_json = request.get_json(silent=True)
request_args = request.args
request_headers = request.headers
# You can simply use print to record runtime logs, viewable in Logs
# For advanced logging levels, see: https://cloud.google.com/logging/docs/reference/libraries
print(request_json)
# Due to FAAS (Cloud Functions) constraints, if the service is idle for too long, it enters cold start,
# which may prevent responding within Slack's 3-second limit.
# Also, OpenAI API requests take time to complete (up to nearly 1 minute depending on response length).
# Slack considers the request lost if no response is received within the limit and retries.
# This causes duplicate requests and responses, so we set X-Slack-No-Retry: 1 in the response headers
# to tell Slack not to retry even if no timely response is received.
headers = {'X-Slack-No-Retry':1}
# Ignore Slack retry requests...
if request_headers and 'X-Slack-Retry-Num' in request_headers:
return ('OK!', 200, headers)
# Slack App Event Subscriptions Verify
# https://api.slack.com/events/url_verification
if request_json and 'type' in request_json and request_json['type'] == 'url_verification':
challenge = ""
if 'challenge' in request_json:
challenge = request_json['challenge']
return (challenge, 200, headers)
# Handle Event Subscriptions Events...
if request_json and 'event' in request_json and 'type' in request_json['event']:
# If the event source is the app itself and app ID matches Slack App ID,
# ignore to avoid infinite loops Slack App -> Cloud Functions -> Slack App -> Cloud Functions...
if 'api_app_id' in request_json and 'app_id' in request_json['event'] and request_json['api_app_id'] == request_json['event']['app_id']:
return ('OK!', 200, headers)
# Event name, e.g., message (related to messages), app_mention (being mentioned)...
eventType = request_json['event']['type']
# SubType, e.g., message_changed (edited message), message_deleted (deleted message)...
# New messages have no SubType
eventSubType = None
if 'subtype' in request_json['event']:
eventSubType = request_json['event']['subtype']
if eventType == 'message':
# SubTypes are message edits, deletions, replies...
# Ignore these
if eventSubType is not None:
return ("OK!", 200, headers)
# Sender of the event message
eventUser = request_json['event']['user']
# Channel of the event message
eventChannel = request_json['event']['channel']
# Content of the event message
eventText = request_json['event']['text']
# Timestamp (ID) of the event message
eventTS = request_json['event']['event_ts']
# Parent message timestamp (ID) of the event message's thread
# Only present for messages inside threads
eventThreadTS = None
if 'thread_ts' in request_json['event']:
eventThreadTS = request_json['event']['thread_ts']
openAIRequest(eventChannel, eventTS, eventThreadTS, eventText)
return ("OK!", 200, headers)
return ("Access Denied!", 400, headers)
def openAIRequest(eventChannel, eventTS, eventThreadTS, eventText):
# Set Custom instructions
# Thanks to colleague (https://twitter.com/je_suis_marku) for support
messages = [
{"role": "system", "content": "I only understand Traditional Chinese (Taiwan) and English."},
{"role": "system", "content": "I do not understand Simplified Chinese."},
{"role": "system", "content": "If I speak Chinese, I reply in Traditional Chinese (Taiwan) and use common Taiwanese phrases."},
{"role": "system", "content": "If I speak English, I reply in English."},
{"role": "system", "content": "Do not respond with greetings or small talk."},
{"role": "system", "content": "There should be a space between Chinese and English. There should also be a space between Chinese characters and any other language characters including numbers and emoji."},
{"role": "system", "content": "If you don't know the answer or your knowledge is outdated, please search online before replying."},
{"role": "system", "content": "I will tip you 200 USD, if you answer well."}
]
messages.append({
"role": "user", "content": eventText
})
replyMessageTS = slackRequestPostMessage(eventChannel, eventTS, "Generating response...")
asyncio.run(openAIRequestAsync(eventChannel, replyMessageTS, messages))
async def openAIRequestAsync(eventChannel, eventTS, messages):
client = AsyncOpenAI(
api_key=OPENAI_API_KEY,
)
# Stream Response (partial response)
stream = await client.chat.completions.create(
model=OPENAI_MODEL,
messages=messages,
stream=True,
)
result = ""
try:
debounceSlackUpdateTime = None
async for chunk in stream:
result += chunk.choices[0].delta.content or ""
# Update message every 0.8 seconds to avoid frequent Slack Update API calls that may fail or waste Cloud Functions calls
if debounceSlackUpdateTime is None or time.time() - debounceSlackUpdateTime >= 0.8:
response = slackUpdateMessage(eventChannel, eventTS, None, result+"...")
debounceSlackUpdateTime = time.time()
except Exception as e:
print(e)
result += "...*[Error occurred]*"
slackUpdateMessage(eventChannel, eventTS, None, result)
### Slack ###
def slackUpdateMessage(channel, ts, metadata, text):
endpoint = "/chat.update"
payload = {
"channel": channel,
"ts": ts
}
if metadata is not None:
payload['metadata'] = metadata
payload['text'] = text
response = slackRequest(endpoint, "POST", payload)
return response
def slackRequestPostMessage(channel, target_ts, text):
endpoint = "/chat.postMessage"
payload = {
"channel": channel,
"text": text,
}
if target_ts is not None:
payload['thread_ts'] = target_ts
response = slackRequest(endpoint, "POST", payload)
if response is not None and 'ts' in response:
return response['ts']
return None
def slackRequest(endpoint, method, payload):
url = "https://slack.com/api"+endpoint
headers = {
"Authorization": f"Bearer {SLACK_BOT_TOKEN}",
"Content-Type": "application/json",
}
response = None
if method == "POST":
response = requests.post(url, headers=headers, data=json.dumps(payload))
elif method == "GET":
response = requests.post(url, headers=headers)
if response and response.status_code == 200:
result = response.json()
return result
else:
return None
Back to Slack for testing:
You can now perform ChatGPT-like Q&A using the OpenAI API.
Add Interrupt Stream Response Feature to Save Tokens
There are many ways to implement this. For example, if the user sends a new message in the same thread before the previous response is finished, the previous response can be interrupted. Alternatively, clicking the message can trigger a shortcut to interrupt the response.
This article uses the addition of the “Message Interrupt” Shortcut as an example.
No matter which interruption method is used, the core principle is the same because we do not have a database to store generated messages or message status information. Therefore, the implementation relies on Slack message metadata fields (which can store custom information within specific messages).
When using the chat.update API Endpoint, a successful call returns the current message text and metadata. Therefore, in the above OpenAI API Stream -> Slack Update Message code, we added a check to see if the metadata in the update response contains a “stop” marker. If it does, the OpenAI Stream Response is interrupted.
First, you need to add a Slack App message Shortcut
Go to the Slack App management interface, find the “Interactivity & Shortcuts” section, click to enable it, and use the same Cloud Functions URL.
Click “Create New Shortcut” to add a new message Shortcut.
Select “On messages”.
Name Action Title:
Stop OpenAI API from Generating Response
Short Description:
Stop OpenAI API from generating responses
Callback ID:
abort_openai_api
(Program identifier, customizable)
After clicking “Create” to complete, remember to click “Save Changes” at the bottom right to save the settings.
Click “reinstall your app” above again to take effect.
Back in Slack, clicking the “…” at the top right of a message will show the “Stop OpenAI API Response” shortcut (clicking it at this time has no effect).
When the user taps the Shortcut on the message, an Event Json Payload will be sent:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
{
"type": "message_action",
"token": "XXXXXX",
"action_ts": "1706188005.387646",
"team": {
"id": "XXXXXX",
"domain": "XXXXXX-XXXXXX"
},
"user": {
"id": "XXXXXX",
"username": "zhgchgli",
"team_id": "XXXXXX",
"name": "zhgchgli"
},
"channel": {
"id": "XXXXXX",
"name": "directmessage"
},
"is_enterprise_install": false,
"enterprise": null,
"callback_id": "abort_openai_api",
"trigger_id": "XXXXXX",
"response_url": "https://hooks.slack.com/app/XXXXXX/XXXXXX/XXXXXX",
"message_ts": "1706178957.161109",
"message": {
"bot_id": "XXXXXX",
"type": "message",
"text": "The English translation of 高麗菜包 is \"cabbage wrap.\" If you use it as a dish name, sometimes the name specifies the contents, such as \"pork cabbage wrap\" or \"vegetable cabbage wrap.\"",
"user": "XXXXXX",
"ts": "1706178957.161109",
"app_id": "XXXXXX",
"blocks": [
{
"type": "rich_text",
"block_id": "eKgaG",
"elements": [
{
"type": "rich_text_section",
"elements": [
{
"type": "text",
"text": "The English translation of 高麗菜包 is \"cabbage wrap.\" If you use it as a dish name, sometimes the name specifies the contents, such as \"pork cabbage wrap\" or \"vegetable cabbage wrap.\""
}
]
}
]
}
],
"team": "XXXXXX",
"bot_profile": {
"id": "XXXXXX",
"deleted": false,
"name": "Rick C-137",
"updated": 1706001605,
"app_id": "XXXXXX",
"icons": {
"image_36": "https://avatars.slack-edge.com/2024-01-23/6517244582244_0c708dfa3f893c72d4c2_36.png",
"image_48": "https://avatars.slack-edge.com/2024-01-23/6517244582244_0c708dfa3f893c72d4c2_48.png",
"image_72": "https://avatars.slack-edge.com/2024-01-23/6517244582244_0c708dfa3f893c72d4c2_72.png"
},
"team_id": "XXXXXX"
},
"edited": {
"user": "XXXXXX",
"ts": "1706187989.000000"
},
"thread_ts": "1706178832.102439",
"parent_user_id": "XXXXXX"
}
}
Improve Cloud Functions main.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
import functions_framework
import requests
import asyncio
import json
import time
from openai import AsyncOpenAI
OPENAI_API_KEY = "OPENAI API KEY"
SLACK_BOT_TOKEN = "Bot User OAuth Token"
# The OPENAI API Model used
# https://platform.openai.com/docs/models
OPENAI_MODEL = "gpt-4-1106-preview"
@functions_framework.http
def hello_http(request):
request_json = request.get_json(silent=True)
request_args = request.args
request_headers = request.headers
# Shortcut events come from the post payload field
# https://api.slack.com/reference/interaction-payloads/shortcuts
payload = request.form.get('payload')
if payload is not None:
payload = json.loads(payload)
# You can simply use print to log runtime logs, viewable in Logs
# For advanced logging levels, refer to: https://cloud.google.com/logging/docs/reference/libraries
print(payload)
# Due to FAAS (Cloud Functions) limitations, if the service is idle for too long, the next call triggers a cold start,
# which may fail to respond within Slack's 3-second limit.
# Also, OpenAI API requests take time to complete (up to nearly 1 minute depending on response length).
# Slack considers requests lost if no response within the limit and retries the request.
# This causes duplicate requests and responses, so we set 'X-Slack-No-Retry: 1' in response headers to tell Slack not to retry even if no timely response.
headers = {'X-Slack-No-Retry':1}
# Ignore Slack retry requests...
if request_headers and 'X-Slack-Retry-Num' in request_headers:
return ('OK!', 200, headers)
# Slack App Event Subscriptions Verify
# https://api.slack.com/events/url_verification
if request_json and 'type' in request_json and request_json['type'] == 'url_verification':
challenge = ""
if 'challenge' in request_json:
challenge = request_json['challenge']
return (challenge, 200, headers)
# Handle Event Subscriptions Events...
if request_json and 'event' in request_json and 'type' in request_json['event']:
# If the event source is the app itself and app ID matches Slack App ID, ignore to avoid infinite loops Slack App -> Cloud Functions -> Slack App -> Cloud Functions...
if 'api_app_id' in request_json and 'app_id' in request_json['event'] and request_json['api_app_id'] == request_json['event']['app_id']:
return ('OK!', 200, headers)
# Event name, e.g., message (related to messages), app_mention (mentioned)...
eventType = request_json['event']['type']
# SubType, e.g., message_changed (edited message), message_deleted (deleted message)...
# New messages have no SubType
eventSubType = None
if 'subtype' in request_json['event']:
eventSubType = request_json['event']['subtype']
if eventType == 'message':
# SubTypes are message edits, deletions, replies...
# Ignore and do not process
if eventSubType is not None:
return ("OK!", 200, headers)
# Event message sender
eventUser = request_json['event']['user']
# Event message channel
eventChannel = request_json['event']['channel']
# Event message content
eventText = request_json['event']['text']
# Event message TS (message ID)
eventTS = request_json['event']['event_ts']
# Event thread parent message TS (message ID)
# Only new messages in threads have this data
eventThreadTS = None
if 'thread_ts' in request_json['event']:
eventThreadTS = request_json['event']['thread_ts']
openAIRequest(eventChannel, eventTS, eventThreadTS, eventText)
return ("OK!", 200, headers)
# Handle Shortcut
if payload and 'type' in payload:
payloadType = payload['type']
# If it's a message shortcut
if payloadType == 'message_action':
print(payloadType)
callbackID = None
channel = None
ts = None
text = None
triggerID = None
if 'callback_id' in payload:
callbackID = payload['callback_id']
if 'channel' in payload:
channel = payload['channel']['id']
if 'message' in payload:
ts = payload['message']['ts']
text = payload['message']['text']
if 'trigger_id' in payload:
triggerID = payload['trigger_id']
if channel is not None and ts is not None and text is not None:
# If it's the shortcut to stop OpenAI API response generation
if callbackID == "abort_openai_api":
slackUpdateMessage(channel, ts, {"event_type": "aborted", "event_payload": { }}, text)
if triggerID is not None:
slackOpenModal(triggerID, callbackID, "Successfully stopped OpenAI API response generation!")
return ("OK!", 200, headers)
return ("OK!", 200, headers)
return ("Access Denied!", 400, headers)
def openAIRequest(eventChannel, eventTS, eventThreadTS, eventText):
# Set Custom instructions
# Thanks to colleague (https://twitter.com/je_suis_marku) for support
messages = [
{"role": "system", "content": "I only understand Traditional Chinese (Taiwan) and English."},
{"role": "system", "content": "I do not understand Simplified Chinese."},
{"role": "system", "content": "If I speak Chinese, I will respond in Traditional Chinese used in Taiwan."},
{"role": "system", "content": "If I speak English, I will respond in English."},
{"role": "system", "content": "Do not respond with greetings."},
{"role": "system", "content": "There should be a space between Chinese and English. There should be a space between Chinese text and any other language characters including numbers and emoji."},
{"role": "system", "content": "If you don't know the answer or your knowledge is outdated, please search online before answering."},
{"role": "system", "content": "I will tip you 200 USD, if you answer well."}
]
messages.append({
"role": "user", "content": eventText
})
replyMessageTS = slackRequestPostMessage(eventChannel, eventTS, "Generating response...")
asyncio.run(openAIRequestAsync(eventChannel, replyMessageTS, messages))
async def openAIRequestAsync(eventChannel, eventTS, messages):
client = AsyncOpenAI(
api_key=OPENAI_API_KEY,
)
# Stream Response (chunked response)
stream = await client.chat.completions.create(
model=OPENAI_MODEL,
messages=messages,
stream=True,
)
result = ""
try:
debounceSlackUpdateTime = None
async for chunk in stream:
result += chunk.choices[0].delta.content or ""
# Update message every 0.8 seconds to avoid frequent Slack Update API calls causing failures or wasting Cloud Functions requests
if debounceSlackUpdateTime is None or time.time() - debounceSlackUpdateTime >= 0.8:
response = slackUpdateMessage(eventChannel, eventTS, None, result+"...")
debounceSlackUpdateTime = time.time()
# If message has metadata & metadata event_type == aborted, it means user marked this response as terminated
if response and 'ok' in response and response['ok'] == True and 'message' in response and 'metadata' in response['message'] and 'event_type' in response['message']['metadata'] and response['message']['metadata']['event_type'] == "aborted":
break
result += "...*[Terminated]*"
# Message was deleted
elif response and 'ok' in response and response['ok'] == False and 'error' in response and response['error'] == "message_not_found" :
break
await stream.close()
except Exception as e:
print(e)
result += "...*[Error occurred]*"
slackUpdateMessage(eventChannel, eventTS, None, result)
### Slack ###
def slackOpenModal(trigger_id, callback_id, text):
slackRequest("/views.open", "POST", {
"trigger_id": trigger_id,
"view": {
"type": "modal",
"callback_id": callback_id,
"title": {
"type": "plain_text",
"text": "Notice"
},
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": text
}
}
]
}
})
def slackUpdateMessage(channel, ts, metadata, text):
endpoint = "/chat.update"
payload = {
"channel": channel,
"ts": ts
}
if metadata is not None:
payload['metadata'] = metadata
payload['text'] = text
response = slackRequest(endpoint, "POST", payload)
return response
def slackRequestPostMessage(channel, target_ts, text):
endpoint = "/chat.postMessage"
payload = {
"channel": channel,
"text": text,
}
if target_ts is not None:
payload['thread_ts'] = target_ts
response = slackRequest(endpoint, "POST", payload)
if response is not None and 'ts' in response:
return response['ts']
return None
def slackRequest(endpoint, method, payload):
url = "https://slack.com/api"+endpoint
headers = {
"Authorization": f"Bearer {SLACK_BOT_TOKEN}",
"Content-Type": "application/json",
}
response = None
if method == "POST":
response = requests.post(url, headers=headers, data=json.dumps(payload))
elif method == "GET":
response = requests.post(url, headers=headers)
if response and response.status_code == 200:
result = response.json()
return result
else:
return None
Back to Slack to test:
Success! When we complete the Stop OpenAI API
Shortcut, the ongoing response will be terminated and respond with [Terminated].
Similarly, using the same principle, you can create a Shortcut to delete messages, implementing the deletion of messages sent by the Slack App.
Add Context Feature Within the Same Discussion Thread (Threads)
If you send a new message in the same thread, it can be treated as a follow-up question to the original issue. At this point, a feature can be added to append the previous conversation to the new prompt.
Add slackGetReplies
& Fill Content into OpenAI API Prompt:
Improve Cloud Functions main.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
import functions_framework
import requests
import asyncio
import json
import time
from openai import AsyncOpenAI
OPENAI_API_KEY = "OPENAI API KEY"
SLACK_BOT_TOKEN = "Bot User OAuth Token"
# OPENAI API Model used
# https://platform.openai.com/docs/models
OPENAI_MODEL = "gpt-4-1106-preview"
@functions_framework.http
def hello_http(request):
request_json = request.get_json(silent=True)
request_args = request.args
request_headers = request.headers
# Shortcut Event payload field from post
# https://api.slack.com/reference/interaction-payloads/shortcuts
payload = request.form.get('payload')
if payload is not None:
payload = json.loads(payload)
# Simple print to log runtime, viewable in Logs
# For advanced Logging Level reference: https://cloud.google.com/logging/docs/reference/libraries
print(payload)
# Due to FAAS (Cloud Functions) cold start, long idle time may cause delay exceeding Slack's 3-second response limit
# OpenAI API request and reply may take up to nearly 1 minute depending on response length
# Slack considers no response within limit as lost request and retries
# This causes duplicate requests and responses, so we set X-Slack-No-Retry: 1 in Response Headers to tell Slack no retry even if no timely response
headers = {'X-Slack-No-Retry':1}
# Ignore Slack Retry requests...
if request_headers and 'X-Slack-Retry-Num' in request_headers:
return ('OK!', 200, headers)
# Slack App Event Subscriptions Verify
# https://api.slack.com/events/url_verification
if request_json and 'type' in request_json and request_json['type'] == 'url_verification':
challenge = ""
if 'challenge' in request_json:
challenge = request_json['challenge']
return (challenge, 200, headers)
# Handle Event Subscriptions Events...
if request_json and 'event' in request_json and 'type' in request_json['event']:
apiAppID = None
if 'api_app_id' in request_json:
apiAppID = request_json['api_app_id']
# If event source is App and App ID == Slack App ID, it means event triggered by own Slack App
# Ignore to avoid infinite loop Slack App -> Cloud Functions -> Slack App -> Cloud Functions...
if 'app_id' in request_json['event'] and apiAppID == request_json['event']['app_id']:
return ('OK!', 200, headers)
# Event type, e.g., message, app_mention...
eventType = request_json['event']['type']
# SubType, e.g., message_changed, message_deleted...
# New messages have no SubType
eventSubType = None
if 'subtype' in request_json['event']:
eventSubType = request_json['event']['subtype']
if eventType == 'message':
# Messages with SubType are edits, deletions, replies...
# Ignore these
if eventSubType is not None:
return ("OK!", 200, headers)
# Message sender
eventUser = request_json['event']['user']
# Channel of the message
eventChannel = request_json['event']['channel']
# Message content
eventText = request_json['event']['text']
# Message timestamp (ID)
eventTS = request_json['event']['event_ts']
# Thread parent message timestamp (ID)
# Only present for thread replies
eventThreadTS = None
if 'thread_ts' in request_json['event']:
eventThreadTS = request_json['event']['thread_ts']
openAIRequest(apiAppID, eventChannel, eventTS, eventThreadTS, eventText)
return ("OK!", 200, headers)
# Handle Shortcut (message)
if payload and 'type' in payload:
payloadType = payload['type']
# If message Shortcut
if payloadType == 'message_action':
callbackID = None
channel = None
ts = None
text = None
triggerID = None
if 'callback_id' in payload:
callbackID = payload['callback_id']
if 'channel' in payload:
channel = payload['channel']['id']
if 'message' in payload:
ts = payload['message']['ts']
text = payload['message']['text']
if 'trigger_id' in payload:
triggerID = payload['trigger_id']
if channel is not None and ts is not None and text is not None:
# If Shortcut to abort OpenAI API response generation
if callbackID == "abort_openai_api":
slackUpdateMessage(channel, ts, {"event_type": "aborted", "event_payload": { }}, text)
if triggerID is not None:
slackOpenModal(triggerID, callbackID, "Successfully stopped OpenAI API response generation!")
return ("OK!", 200, headers)
return ("Access Denied!", 400, headers)
def openAIRequest(apiAppID, eventChannel, eventTS, eventThreadTS, eventText):
# Set Custom instructions
# Thanks to colleague (https://twitter.com/je_suis_marku) for support
messages = [
{"role": "system", "content": "I only understand Traditional Chinese (Taiwan) and English."},
{"role": "system", "content": "I do not understand Simplified Chinese."},
{"role": "system", "content": "If I speak Chinese, I respond in Traditional Chinese used in Taiwan."},
{"role": "system", "content": "If I speak English, I respond in English."},
{"role": "system", "content": "Do not respond with greetings."},
{"role": "system", "content": "There should be a space between Chinese and English. There should be a space between Chinese characters and any other language characters including numbers and emojis."},
{"role": "system", "content": "If you don't know the answer or your knowledge is outdated, please search online before answering."},
{"role": "system", "content": "I will tip you 200 USD, if you answer well."}
]
if eventThreadTS is not None:
threadMessages = slackGetReplies(eventTS, eventThreadTS)
if threadMessages is not None:
for threadMessage in threadMessages:
appID = None
if 'app_id' in threadMessage:
appID = threadMessage['app_id']
threadMessageText = threadMessage['text']
threadMessageTs = threadMessage['ts']
# If Slack App (OpenAI API Response), mark as assistant
if appID and appID == apiAppID:
messages.append({
"role": "assistant", "content": threadMessageText
})
else:
# User message marked as user
messages.append({
"role": "user", "content": threadMessageText
})
messages.append({
"role": "user", "content": eventText
})
replyMessageTS = slackRequestPostMessage(eventChannel, eventTS, "Generating response...")
asyncio.run(openAIRequestAsync(eventChannel, replyMessageTS, messages))
async def openAIRequestAsync(eventChannel, eventTS, messages):
client = AsyncOpenAI(
api_key=OPENAI_API_KEY,
)
# Stream Response (chunked)
stream = await client.chat.completions.create(
model=OPENAI_MODEL,
messages=messages,
stream=True,
)
result = ""
try:
debounceSlackUpdateTime = None
async for chunk in stream:
result += chunk.choices[0].delta.content or ""
# Update message every 0.8 seconds to avoid frequent Slack Update API calls causing failure or wasting Cloud Functions invocations
if debounceSlackUpdateTime is None or time.time() - debounceSlackUpdateTime >= 0.8:
response = slackUpdateMessage(eventChannel, eventTS, None, result+"...")
debounceSlackUpdateTime = time.time()
# If message has metadata & metadata event_type == aborted, user marked response as aborted
if response and 'ok' in response and response['ok'] == True and 'message' in response and 'metadata' in response['message'] and 'event_type' in response['message']['metadata'] and response['message']['metadata']['event_type'] == "aborted":
break
result += "...*[Aborted]*"
# Message deleted
elif response and 'ok' in response and response['ok'] == False and 'error' in response and response['error'] == "message_not_found" :
break
await stream.close()
except Exception as e:
print(e)
result += "...*[Error occurred]*"
slackUpdateMessage(eventChannel, eventTS, None, result)
### Slack ###
def slackGetReplies(channel, ts):
endpoint = "/conversations.replies?channel="+channel+"&ts="+ts
response = slackRequest(endpoint, "GET", None)
if response is not None and 'messages' in response:
return response['messages']
return None
def slackOpenModal(trigger_id, callback_id, text):
slackRequest("/views.open", "POST", {
"trigger_id": trigger_id,
"view": {
"type": "modal",
"callback_id": callback_id,
"title": {
"type": "plain_text",
"text": "Notice"
},
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": text
}
}
]
}
})
def slackUpdateMessage(channel, ts, metadata, text):
endpoint = "/chat.update"
payload = {
"channel": channel,
"ts": ts
}
if metadata is not None:
payload['metadata'] = metadata
payload['text'] = text
response = slackRequest(endpoint, "POST", payload)
return response
def slackRequestPostMessage(channel, target_ts, text):
endpoint = "/chat.postMessage"
payload = {
"channel": channel,
"text": text,
}
if target_ts is not None:
payload['thread_ts'] = target_ts
response = slackRequest(endpoint, "POST", payload)
if response is not None and 'ts' in response:
return response['ts']
return None
def slackRequest(endpoint, method, payload):
url = "https://slack.com/api"+endpoint
headers = {
"Authorization": f"Bearer {SLACK_BOT_TOKEN}",
"Content-Type": "application/json",
}
response = None
if method == "POST":
response = requests.post(url, headers=headers, data=json.dumps(payload))
elif method == "GET":
response = requests.post(url, headers=headers)
if response and response.status_code == 200:
result = response.json()
return result
else:
return None
Back to Slack for testing:
The left image shows that when you ask a follow-up question without adding the Context, it will be treated as a new conversation.
The image on the right shows that adding context helps understand the entire conversation and the new question.
Done!
So far, we have built our own ChatGPT (via OpenAI API) Slack App Bot.
You can also refer to Slack API and OpenAI API Custom instructions to integrate them in Cloud Functions Python programs according to your needs. For example, you can train one channel to answer team questions and find project documents, another channel for translations, and another for data analysis, and so on.
Supplement
Marking bot answers outside of 1:1 messages
- You can mention the bot in any channel (the bot must be added to the channel) to ask questions.
First, you need to add the app_mention
Event Subscription:
Add completion -> “Save Changes” saved -> “reinstall your app” completed.
In the above main.py
script #Handle Event Subscriptions Events…
Add new Event Type judgment inside the Code Block:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Mention type Event (@SlackApp hello)
if eventType == 'app_mention':
# Sender of the event message
eventUser = request_json['event']['user']
# Channel of the event message
eventChannel = request_json['event']['channel']
# Content of the event message, remove leading tag string <@SLACKAPPID>
eventText = re.sub(r"<@\w+>\W*", "", request_json['event']['text'])
# Timestamp (message ID) of the event message
eventTS = request_json['event']['event_ts']
# Timestamp (message ID) of the parent message in the thread of the event message
# Only present for new messages in a thread
eventThreadTS = None
if 'thread_ts' in request_json['event']:
eventThreadTS = request_json['event']['thread_ts']
openAIRequest(apiAppID, eventChannel, eventTS, eventThreadTS, eventText)
return ("OK!", 200, headers)
After deployment, it is complete.
Deleting Messages Sent in Slack App
You cannot directly delete messages sent by the Slack App on Slack. You can refer to the above “ Stop OpenAI API Response
“ Shortcut method to add a “Delete Message” Shortcut.
In the Cloud Functions main.py
program:
# Handling Shortcut Code Block
Add a callback_id check to see if it equals your defined “Delete Message” Shortcut Callback ID, then pass the parameters into the following method to complete the deletion:
1
2
3
4
5
6
7
8
9
def slackDeleteMessage(channel, ts):
endpoint = "/chat.delete"
payload = {
"channel": channel,
"ts": ts
}
response = slackRequest(endpoint, "POST", payload)
return response
Slack App Not Responding
Check if the Token is correct
Check Cloud Functions Logs for Errors
Has the Cloud Functions deployment been completed?
Is the Slack App in the channel where you asked the question? (If it’s not a 1:1 conversation with the Slack App, you need to add the bot to the channel for it to work.)
Log Slack API Response under the SlackRequest method
Cloud Functions Public URL Is Not Secure Enough
- If you worry that the Cloud Functions URL is not secure enough, you can add a query token for verification.
1
2
3
4
5
6
7
8
9
10
SAFE_ACCESS_TOKEN = "nF4JwxfG9abqPZCJnBerwwhtodC28BuC"
@functions_framework.http
def hello_http(request):
request_json = request.get_json(silent=True)
request_args = request.args
request_headers = request.headers
# Verify if the token parameter is valid
if not(request_args and 'token' in request_args and request_args['token'] == SAFE_ACCESS_TOKEN):
return ('', 400, headers)
Cloud Functions Related Issues
Billing Method
Different regions, CPU, RAM, storage, and traffic have different prices. Please refer to the official pricing table.
Free tier as follows: (2024/02/15)
1
2
3
4
5
6
7
8
9
10
11
Cloud Functions offers a permanent free tier for compute time resources,
including allocation based on GB-seconds and GHz-seconds. Besides 2 million invocations,
this free tier also provides 400,000 GB-seconds and 200,000 GHz-seconds of compute time,
as well as 5 GB of internet data transfer per month.
The usage limits of the free tier are calculated at the equivalent USD amount of Tier 1 prices mentioned above.
Regardless of whether the function runs in regions priced at Tier 1 and/or Tier 2,
the system allocates the equivalent USD amount to you.
However, when deducting from the free tier quota, the system uses the function's execution region tier (Tier 1 or Tier 2) as the basis.
Please note that even if you use the free tier, a valid billing account is required.
btw. The Slack App is free and does not require a Premium subscription to use.
Slack App Responds Too Slowly or Times Out Too Long
(Excluding the slower response issue during OpenAI API peak times), if the bottleneck is Cloud Function, you can expand the settings on the first page of the Cloud Function editor:
Adjust CPU, RAM, Timeout duration, and Concurrent count to improve request processing speed.
*But may require a fee
Development Phase Testing & Debug
Click “Test Function” to open the Cloud Shell window in the bottom toolbar. Wait about 3–5 minutes (longer for the first launch). After the build completes and you accept the following permissions:
Once you see “Function is ready to test,” you can click “Run Test” to execute the method debug test.
You can enter the JSON Body in the “Triggering event” box on the right to pass the request_json
parameter for testing, or directly modify the code to inject a test object for testing.
*Please note that Cloud Shell/Cloud Run may incur additional charges.
It is recommended to run a test before deployment to ensure the build succeeds.
Build failed, what to do if the code is missing?
If you accidentally write incorrect code causing the Cloud Function Deploy Build to fail, an error message will appear. Clicking “EDIT AND REDEPLOY” to return to the editor will show that the code you just changed is gone!!!
No need to worry. At this point, click “Source Code” on the left and select “Last Failed Deployment” to restore the code from the recent Build Failed:
View runtime print
Logs
*Please note that Cloud Logging and Querying Logs may incur additional charges.
Final Code (Python 3.8)
Cloud Functions
main.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
import functions_framework
import requests
import re
import asyncio
import json
import time
from openai import AsyncOpenAI
OPENAI_API_KEY = "OPENAI API KEY"
SLACK_BOT_TOKEN = "Bot User OAuth Token"
# Custom security verification token
# The URL must include ?token=SAFE_ACCESS_TOKEN parameter to accept requests
SAFE_ACCESS_TOKEN = "nF4JwxfG9abqPZCJnBerwwhtodC28BuC"
# OPENAI API Model used
# https://platform.openai.com/docs/models
OPENAI_MODEL = "gpt-4-1106-preview"
@functions_framework.http
def hello_http(request):
request_json = request.get_json(silent=True)
request_args = request.args
request_headers = request.headers
# Shortcut events come from the post payload field
# https://api.slack.com/reference/interaction-payloads/shortcuts
payload = request.form.get('payload')
if payload is not None:
payload = json.loads(payload)
# You can simply use print to log runtime info, viewable in Logs
# For advanced logging levels, see: https://cloud.google.com/logging/docs/reference/libraries
# print(payload)
# Due to FAAS (Cloud Functions) cold start, if the service is idle for too long,
# the next call may trigger cold start and fail to respond within Slack's 3-second limit.
# OpenAI API requests also take time (up to about 1 minute depending on response length).
# Slack treats no response within the timeout as a lost request and retries,
# causing duplicate requests and responses.
# We set X-Slack-No-Retry: 1 in response headers to tell Slack not to retry even if timeout occurs.
headers = {'X-Slack-No-Retry':1}
# Validate token parameter
if not(request_args and 'token' in request_args and request_args['token'] == SAFE_ACCESS_TOKEN):
return ('', 400, headers)
# Ignore Slack retry requests
if request_headers and 'X-Slack-Retry-Num' in request_headers:
return ('OK!', 200, headers)
# Slack App Event Subscriptions Verify
# https://api.slack.com/events/url_verification
if request_json and 'type' in request_json and request_json['type'] == 'url_verification':
challenge = ""
if 'challenge' in request_json:
challenge = request_json['challenge']
return (challenge, 200, headers)
# Handle Event Subscriptions Events...
if request_json and 'event' in request_json and 'type' in request_json['event']:
apiAppID = None
if 'api_app_id' in request_json:
apiAppID = request_json['api_app_id']
# If event source is App and App ID == Slack App ID, it means event triggered by own Slack App
# Ignore to avoid infinite loop Slack App -> Cloud Functions -> Slack App -> Cloud Functions...
if 'app_id' in request_json['event'] and apiAppID == request_json['event']['app_id']:
return ('OK!', 200, headers)
# Event name, e.g., message, app_mention...
eventType = request_json['event']['type']
# SubType, e.g., message_changed, message_deleted...
# New messages have no Sub Type
eventSubType = None
if 'subtype' in request_json['event']:
eventSubType = request_json['event']['subtype']
# Message type events
if eventType == 'message':
# Messages with Sub Type are edits, deletions, replies...
# Ignore these
if eventSubType is not None:
return ("OK!", 200, headers)
# Message sender
eventUser = request_json['event']['user']
# Message channel
eventChannel = request_json['event']['channel']
# Message content
eventText = request_json['event']['text']
# Message timestamp (ID)
eventTS = request_json['event']['event_ts']
# Thread parent message timestamp (ID)
# Only new messages in a thread have this
eventThreadTS = None
if 'thread_ts' in request_json['event']:
eventThreadTS = request_json['event']['thread_ts']
openAIRequest(apiAppID, eventChannel, eventTS, eventThreadTS, eventText)
return ("OK!", 200, headers)
# Mention events (@SlackApp hello)
if eventType == 'app_mention':
# Message sender
eventUser = request_json['event']['user']
# Message channel
eventChannel = request_json['event']['channel']
# Message content, remove leading mention string <@SLACKAPPID>
eventText = re.sub(r"<@\w+>\W*", "", request_json['event']['text'])
# Message timestamp (ID)
eventTS = request_json['event']['event_ts']
# Thread parent message timestamp (ID)
eventThreadTS = None
if 'thread_ts' in request_json['event']:
eventThreadTS = request_json['event']['thread_ts']
openAIRequest(apiAppID, eventChannel, eventTS, eventThreadTS, eventText)
return ("OK!", 200, headers)
# Handle Shortcut (message)
if payload and 'type' in payload:
payloadType = payload['type']
# If it's a message shortcut
if payloadType == 'message_action':
callbackID = None
channel = None
ts = None
text = None
triggerID = None
if 'callback_id' in payload:
callbackID = payload['callback_id']
if 'channel' in payload:
channel = payload['channel']['id']
if 'message' in payload:
ts = payload['message']['ts']
text = payload['message']['text']
if 'trigger_id' in payload:
triggerID = payload['trigger_id']
if channel is not None and ts is not None and text is not None:
# If it's the shortcut to stop OpenAI API response generation
if callbackID == "abort_openai_api":
slackUpdateMessage(channel, ts, {"event_type": "aborted", "event_payload": { }}, text)
if triggerID is not None:
slackOpenModal(triggerID, callbackID, "Successfully stopped OpenAI API response generation!")
return ("OK!", 200, headers)
# If it's delete message
if callbackID == "delete_message":
slackDeleteMessage(channel, ts)
if triggerID is not None:
slackOpenModal(triggerID, callbackID, "Successfully deleted Slack App message!")
return ("OK!", 200, headers)
return ("Access Denied!", 400, headers)
def openAIRequest(apiAppID, eventChannel, eventTS, eventThreadTS, eventText):
# Set Custom instructions
# Thanks to colleague (https://twitter.com/je_suis_marku) for support
messages = [
{"role": "system", "content": "I only understand Taiwanese Traditional Chinese and English."},
{"role": "system", "content": "I do not understand Simplified Chinese."},
{"role": "system", "content": "If I speak Chinese, I reply in Taiwanese Traditional Chinese using common Taiwanese expressions."},
{"role": "system", "content": "If I speak English, I reply in English."},
{"role": "system", "content": "Do not respond with greetings or small talk."},
{"role": "system", "content": "There must be a space between Chinese and English. There must be a space between Chinese characters and any other language characters including numbers and emojis."},
{"role": "system", "content": "If you don't know the answer or your knowledge is outdated, please search online before answering."},
{"role": "system", "content": "I will tip you 200 USD , if you answer well."}
]
if eventThreadTS is not None:
threadMessages = slackGetReplies(eventChannel, eventThreadTS)
if threadMessages is not None:
for threadMessage in threadMessages:
appID = None
if 'app_id' in threadMessage:
appID = threadMessage['app_id']
threadMessageText = threadMessage['text']
threadMessageTs = threadMessage['ts']
# If from Slack App (OpenAI API Response), mark as assistant
if appID and appID == apiAppID:
messages.append({
"role": "assistant", "content": threadMessageText
})
else:
# User message content marked as user
messages.append({
"role": "user", "content": threadMessageText
})
messages.append({
"role": "user", "content": eventText
})
replyMessageTS = slackRequestPostMessage(eventChannel, eventTS, "Generating response...")
asyncio.run(openAIRequestAsync(eventChannel, replyMessageTS, messages))
async def openAIRequestAsync(eventChannel, eventTS, messages):
client = AsyncOpenAI(
api_key=OPENAI_API_KEY,
)
# Stream Response (partial responses)
stream = await client.chat.completions.create(
model=OPENAI_MODEL,
messages=messages,
stream=True,
)
result = ""
try:
debounceSlackUpdateTime = None
async for chunk in stream:
result += chunk.choices[0].delta.content or ""
# Update message every 0.8 seconds to avoid frequent Slack update API calls causing failures or wasting Cloud Functions calls
if debounceSlackUpdateTime is None or time.time() - debounceSlackUpdateTime >= 0.8:
response = slackUpdateMessage(eventChannel, eventTS, None, result+"...")
debounceSlackUpdateTime = time.time()
# If message has metadata & metadata event_type == aborted, it means user marked this response as aborted
if response and 'ok' in response and response['ok'] == True and 'message' in response and 'metadata' in response['message'] and 'event_type' in response['message']['metadata'] and response['message']['metadata']['event_type'] == "aborted":
break
result += "...*[Aborted]*"
# Message was deleted
elif response and 'ok' in response and response['ok'] == False and 'error' in response and response['error'] == "message_not_found" :
break
await stream.close()
except Exception as e:
print(e)
result += "...*[Error occurred]*"
slackUpdateMessage(eventChannel, eventTS, None, result)
### Slack ###
def slackGetReplies(channel, ts):
endpoint = "/conversations.replies?channel="+channel+"&ts="+ts
response = slackRequest(endpoint, "GET", None)
if response is not None and 'messages' in response:
return response['messages']
return None
def slackOpenModal(trigger_id, callback_id, text):
slackRequest("/views.open", "POST", {
"trigger_id": trigger_id,
"view": {
"type": "modal",
"callback_id": callback_id,
"title": {
"type": "plain_text",
"text": "Notice"
},
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": text
}
}
]
}
})
def slackDeleteMessage(channel, ts):
endpoint = "/chat.delete"
payload = {
"channel": channel,
"ts": ts
}
response = slackRequest(endpoint, "POST", payload)
return response
def slackUpdateMessage(channel, ts, metadata, text):
endpoint = "/chat.update"
payload = {
"channel": channel,
"ts": ts
}
if metadata is not None:
payload['metadata'] = metadata
payload['text'] = text
response = slackRequest(endpoint, "POST", payload)
return response
def slackRequestPostMessage(channel, target_ts, text):
endpoint = "/chat.postMessage"
payload = {
"channel": channel,
"text": text,
}
if target_ts is not None:
payload['thread_ts'] = target_ts
response = slackRequest(endpoint, "POST", payload)
if response is not None and 'ts' in response:
return response['ts']
return None
def slackRequest(endpoint, method, payload):
url = "https://slack.com/api"+endpoint
headers = {
"Authorization": f"Bearer {SLACK_BOT_TOKEN}",
"Content-Type": "application/json",
}
response = None
if method == "POST":
response = requests.post(url, headers=headers, data=json.dumps(payload))
elif method == "GET":
response = requests.post(url, headers=headers)
if response and response.status_code == 200:
result = response.json()
return result
else:
return None
requirements.txt
:
1
2
3
functions-framework==3.*
requests==2.31.0
openai==1.9.0
Slack App Setup
OAuth & Permissions
- Items with disabled delete buttons are permissions automatically added by Slack after adding the Shortcut.
Interactivity & Shortcuts
Interactivity: Enable
Request URL:
https://us-central1-xxx-xxx.cloudfunctions.net/SlackBot-Rick-C-137?token=nF4JwxfG9abqPZCJnBerwwhtodC28BuC
Subscribe to bot events:
Interactivity & Shortcuts
Interactivity: Enable
Request URL:
https://us-central1-xxx-xxx.cloudfunctions.net/SlackBot-Rick-C-137?token=nF4JwxfG9abqPZCJnBerwwhtodC28BuC
Shortcuts:
App Home
Always Show My Bot as Online: Enable
Messages Tab: Enable
Allow users to send Slash commands and messages from the messages tab: ✅
Basic Information
Rick & Morty 🤘🤘🤘
Commercial Time
If you and your team need automation tools or process integrations, whether it’s Slack App development, Notion, Asana, Google Sheets, Google Forms, GA data, or any other integration needs, feel free to contact me for development.
If you have any questions or feedback, feel free to contact me.
This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.