Home Slack & ChatGPT Integration
Post
Cancel

Slack & ChatGPT Integration

Slack & ChatGPT Integration

Build your own ChatGPT OpenAI API for Slack App (Google Cloud Functions & Python)

Background

Recently, I have been promoting the use of Generative AI within the team to improve work efficiency. Initially, we only aim to achieve an AI Assistant (ChatGPT functionality) to reduce the time spent on daily data queries, organizing cumbersome data, and manual data processing, thereby improving work efficiency. We hope that engineers, designers, PMs, marketers, etc., can all use it freely.

The simplest method is to directly purchase the ChatGPT Team plan, which costs $25 per seat per year. However, since we are not yet sure about everyone’s usage frequency (volume) and hope to integrate with more collaboration and development processes in the future, we decided to use the OpenAI API method and then integrate it with other services for team members to use.

The OpenAI API Key can be generated from this page. The Key does not correspond to a specific Model version; you need to specify the Model version you want to use and generate the corresponding Token cost when using it.

We need a service that can set the OpenAI API Key by ourselves and use that Key for ChatGPT-like usage.

Whether it’s a Chrome Extension or a Slack App, it’s hard to find a service that allows you to set the OpenAI API Key by yourself. Most services sell their own subscriptions, and allowing users to set their own API Key means they can’t make money and are purely doing charity.

[Chrome Extension] SidebarGPT

After installation, go to Settings -> General -> Enter the OpenAI API Key.

You can call out the chat interface directly from the browser toolbar or side icon and use it directly:

[Chrome Extension] OpenAI Translator

If you only need translation, you can use this, which allows you to set the OpenAI API Key for translation.

Additionally, it is an open-source project and also provides macOS/Windows desktop programs:

Chrome Extension’s advantage is its speed, simplicity, and convenience—just install and use directly. The downside is that you need to provide the API Key to all members, making it difficult to control leakage issues. Additionally, using third-party services makes it hard to ensure data security.

[Self-hosted] LibreChat

A colleague from the R&D department recommended this OpenAI API Chat encapsulation service. It provides authentication and almost replicates the ChatGPT interface, with more powerful features than ChatGPT, as an open-source project.

You only need the project, install Docker, set up the .env file, and start the Docker service to use it directly through the website.

Tried it out, and it’s practically flawless, just like a local version of ChatGPT service. The only downside is that it requires server deployment. If there are no other considerations, you can directly use this open-source project.

Slack App

Actually, setting up the LibreChat service on a server already achieves the desired effect. However, I had a sudden thought: wouldn’t it be more convenient if it could be integrated into daily tools? Additionally, the company’s server has strict permission settings, making it difficult to start services arbitrarily.

At the time, I didn’t think much about it and assumed there would be many OpenAI API integration services for Slack App. I thought I could just find one and set it up. Unexpectedly, it wasn’t that simple.

A Google search only found an official Slack x OpenAI 2023/03 press release, “ Why we built the ChatGPT app for Slack,” and some Beta images:

[https://www.salesforce.com/news/stories/chatgpt-app-for-slack/](https://www.salesforce.com/news/stories/chatgpt-app-for-slack/){:target="_blank"}

https://www.salesforce.com/news/stories/chatgpt-app-for-slack/

It looks very comprehensive and could greatly improve work efficiency. However, as of 2024/01, there has been no release news. The Beta registration link provided at the end of the article is also invalid, with no further updates. (Is Microsoft trying to support Teams first?)

[2024/02/14 Update]:

  • According to Slack official news, it seems that the integration with ChatGPT (OpenAI) has either been abandoned or integrated into Slack AI.

Slack Apps

Due to the lack of an official app, I turned to search for third-party developer apps. I searched and tried several but hit a wall. There were not many suitable apps, and none provided a custom Key feature. Each one was designed to sell services and make money.

Implementing ChatGPT OpenAI API for Slack App Yourself

Previously had some experience developing Slack Apps, decided to do it myself.

⚠️Disclaimer⚠️

This article demonstrates how to create a Slack App and quickly use Google Cloud Functions to meet the requirements by integrating the OpenAI API. There are many applications for Slack Apps, feel free to explore.

⚠️⚠️ The advantage of Google Cloud Functions, Function as a Service (FaaS), is that it is convenient and fast, with a free quota. Once the program is written, it can be deployed and executed directly, and it scales automatically. The downside is that the service environment is controlled by GCP. If the service is not called for a long time, it will go into hibernation, and calling it again will enter Cold Start, requiring a longer response time. Additionally, it is more challenging to have multiple services interact with each other.

For more complete or high-demand usage, it is recommended to set up a VM (App Engine) to run the service.

Final Result

The complete Cloud Functions Python code and Slack App settings are attached at the end of the article. Those who are too lazy to follow step by step can quickly refer to it.

Step 1. Create a Slack App

Go to Slack App:

Click “Create New App”

Select “From scratch”

Enter “App Name” and choose the Workspace to join.

After creation, go to “OAuth & Permissions” to add the permissions needed for the Bot.

Scroll down to find the “Scopes” section, click “Add an OAuth Scope” and 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 other permissions later, you need to click “Reinstall” again for them to take effect.

But rest assured, the Bot Token will not change due to reinstallation.

After setting up the Slack Bot Token permissions, go to “App Home”:

Scroll down to find the “Show Tabs” section, enable “Messages Tab” and “Allow users to send Slash commands and messages from the messages tab” (if this is not checked, you cannot send messages, and it will display “Sending messages to this app has been turned off.”).

Return to the Slack Workspace, press “Command+R” to refresh the screen, and you will see the newly created Slack App and message input box:

At this point, sending a message to the App has no functionality.

Enable Event Subscriptions

Next, we need to enable the event subscription feature of the Slack App, which will call the API to the specified URL when a specified event occurs.

Add Google Cloud Functions

For the Request URL part, Google Cloud Functions will come into play.

After setting up the project and billing information, click “Create Function”.

Enter the project name for Function name, and select “Allow unauthenticated invocations” for Authentication, which means that knowing the URL allows access.

If you cannot create a Function or change Authentication, it means your GCP account does not have full Google Cloud Functions permissions. You need to ask the organization administrator to add the Cloud Functions Admin permission in addition to your original role to use it.

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
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 record runtime logs, which can be viewed in Logs
    # For advanced Logging Level usage, refer to: https://cloud.google.com/logging/docs/reference/libraries
    print(request_json)

    # Due to the FAAS (Cloud Functions) limitation, if the service is not called for a long time, it will enter a cold start when called again, which may not respond within the 3-second limit set by Slack
    # Additionally, the OpenAI API request takes a certain amount of time to respond (depending on the response length, it may take nearly 1 minute to complete)
    # If Slack does not receive a response within the time limit, it will consider the request lost and will call again
    # This can cause duplicate requests and responses, so we can set X-Slack-No-Retry: 1 in the Response Headers to inform Slack not to retry even if it does not receive a response within the time limit
    headers = {'X-Slack-No-Retry':1}

    # If it is a Slack Retry request...ignore it
    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)

Enter the following dependencies in requirements.txt:

1
2
3
functions-framework==3.*
requests==2.31.0
openai==1.9.0

Currently, there is no functionality, it just allows the Slack App to pass the Event Subscriptions verification. You can directly click “Deploy” to complete the first deployment.

⚠️If you are not familiar with the Cloud Functions editor, you can scroll down to the bottom of the article to see the supplementary content.

After the deployment is complete (green checkmark), copy the Cloud Functions URL:

Paste the Request URL back into the Slack App Enable Events.

If everything is correct, “Verified” will appear, completing the verification.

What happens here is that when a verification request is received from Slack:

1
2
3
4
5
{
    "token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
    "challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
    "type": "url_verification"
}

Respond with the content of the challenge field to pass the verification.

After enabling successfully, scroll down to find the “Subscribe to bot events” section, click “Add Bot User Event” to add the “message.im” permission.

After adding the full permissions, click the “reinstall your app” link at the top to reinstall the Slack App to the Workspace, and the Slack App setup is complete.

You can also go to “App Home” or “Basic Information” to customize the Slack App’s name and avatar.

Basic Information

Basic Information

Step 2. Integrate OpenAI API with Slack App (Direct Messages)

First, we need to obtain the essential OPENAI API KEY and Bot User OAuth Token.

Handling Direct Message (IM) Event & Integrating OpenAI API Response

When a user sends a message to 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": "你好",
    "user": "XXX",
    "ts": "1707920753.115429",
    "blocks": [
      {
        "type": "rich_text",
        "block_id": "orfng",
        "elements": [
          {
            "type": "rich_text_section",
            "elements": [
              {
                "type": "text",
                "text": "你好"
              }
            ]
          }
        ]
      }
    ],
    "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 messages to the OpenAI API and then back to replying to Slack messages:

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
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, which can be viewed in Logs
    # For advanced Logging Level usage, refer to: https://cloud.google.com/logging/docs/reference/libraries
    print(request_json)

    # Due to the nature of FAAS (Cloud Functions), if the service is not called for a long time, it will enter a cold start when called again, which may not respond within the 3-second limit set by Slack
    # Additionally, the OpenAI API request to response takes a certain amount of time (depending on the response length, it may take close to 1 minute to complete)
    # If Slack does not receive a response within the time limit, it will consider the request lost and will call again
    # This can cause duplicate requests and responses, so we can set X-Slack-No-Retry: 1 in the Response Headers to inform Slack not to retry even if it does not receive a response within the time limit
    headers = {'X-Slack-No-Retry':1}

    # If it is a Slack Retry request...ignore it
    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 and the App ID == Slack App ID, it means the event was triggered by the Slack App itself
        # Ignore and do not process, otherwise it will fall into an infinite loop 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, for example: message (related to messages), app_mention (mentioned)....
        eventType = request_json['event']['type']

        # SubType, for example: message_changed (edited message), message_deleted (deleted message)...
        # New messages do not have a Sub Type
        eventSubType = None
        if 'subtype' in request_json['event']:
            eventSubType = request_json['event']['subtype']
        
        if eventType == 'message':
            # Messages with Sub Type are edited, deleted, replied to...
            # Ignore and do not process
            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']
            # TS (message ID) of the event message
            eventTS = request_json['event']['event_ts']
                
            # TS (message ID) of the parent message in the thread of the event message
            # Only new messages in the thread will 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)


    return ("Access Denied!", 400, headers)

def openAIRequest(eventChannel, eventTS, eventThreadTS, eventText):
    
    # Set Custom instructions
    # Thanks to my colleague (https://twitter.com/je_suis_marku) for the support
    messages = [
        {"role": "system", "content": "I can only understand Traditional Chinese from Taiwan and English"},
        {"role": "system", "content": "I cannot understand Simplified Chinese"},
        {"role": "system", "content": "If I speak Chinese, I will respond in Traditional Chinese from Taiwan, and it must conform to common Taiwanese usage."},
        {"role": "system", "content": "If I speak English, I will respond in English."},
        {"role": "system", "content": "Do not respond with pleasantries."},
        {"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 if 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
    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 the message every 0.8 seconds to avoid frequent calls to the Slack Update Message API, which may fail or waste Cloud Functions request counts
            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 to test:

Now you can perform Q&A similar to ChatGPT and OpenAI API.

Add Stream Response Interruption Feature to Save Tokens

There are many ways to implement this. For example, if a user inputs a new message in the same thread before the previous response is complete, it interrupts the previous response, or by clicking a message to add an interruption shortcut.

This article uses the example of adding a “Message Interruption” shortcut.

Regardless of the interruption method, the core principle is the same. Since we do not have a database to store generated messages and message status information, the implementation relies on the metadata field of Slack messages (which can store custom information within specified messages).

When using the chat.update API Endpoint, if the call is successful, it will return the text content and metadata of the current message. Therefore, in the above OpenAI API Stream -> Slack Update Message code, we add a judgment to check if the metadata in the response of the modification request has an “interruption” mark. If it does, it interrupts the OpenAI Stream Response.

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: Stop OpenAI API Response
  • Short Description: Stop OpenAI API Response
  • Callback ID: abort_openai_api (for program identification, can be customized)

Click “Create” to complete the creation, and finally remember to click “Save Changes” at the bottom right to save the settings.

Click “reinstall your app” at the top again to take effect.

Back in Slack, click the “…” at the top right of the message, and the “Stop OpenAI API Response” shortcut will appear (clicking it at this time has no effect).

When the user presses 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 are using it as a dish name, it may sometimes be named specifically according to the contents of the dish, 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 are using it as a dish name, it may sometimes be named specifically according to the contents of the dish, 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"
  }
}

Complete 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 Event will be given from 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 record runtime logs, which can be viewed in Logs
    # For advanced Logging Level usage, refer to: https://cloud.google.com/logging/docs/reference/libraries
    print(payload)

    # Due to the nature of FAAS (Cloud Functions), if the service is not called for a long time, it will enter a cold start when called again, which may not respond within the 3-second limit set by Slack
    # Additionally, the OpenAI API request takes a certain amount of time to respond (depending on the response length, it may take nearly 1 minute to complete)
    # If Slack does not receive a response within the time limit, it will consider the request lost and will call again
    # This will cause repeated requests and responses, so we can set X-Slack-No-Retry: 1 in the Response Headers to inform Slack not to retry even if it does not receive a response within the time limit
    headers = {'X-Slack-No-Retry': 1}

    # If it is a Slack Retry request...ignore it
    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 and App ID == Slack App ID, it means the event was triggered by the Slack App itself
        # Ignore and do not process, otherwise it will fall into an infinite loop 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, for example: message (related to messages), app_mention (mentioned)...
        eventType = request_json['event']['type']

        # SubType, for example: message_changed (edited message), message_deleted (deleted message)...
        # New messages do not have Sub Type
        eventSubType = None
        if 'subtype' in request_json['event']:
            eventSubType = request_json['event']['subtype']
        
        if eventType == 'message':
            # Messages with Sub Type are edited, deleted, replied to...
            # Ignore and do not process
            if eventSubType is not None:
                return ("OK!", 200, headers)
               
            # Message sender of the Event
            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']
            # TS (message ID) of the Event message
            eventTS = request_json['event']['event_ts']
                
            # TS (message ID) of the parent message in the thread of the Event message
            # Only new messages in the thread will 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 is 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 is the Stop OpenAI API Response Generation Shortcut
                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 can only understand Traditional Chinese from Taiwan and English"},
        {"role": "system", "content": "I cannot understand Simplified Chinese"},
        {"role": "system", "content": "If I speak Chinese, I will respond in Traditional Chinese from Taiwan, and it must conform to common Taiwanese usage."},
        {"role": "system", "content": "If I speak English, I will respond in English."},
        {"role": "system", "content": "Do not respond with pleasantries."},
        {"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."}
    ]

    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
    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 the message every 0.8 seconds to avoid frequent calls to the Slack Update Message API, which may fail or waste Cloud Functions request counts
            if debounceSlackUpdateTime is None or time.time() - debounceSlackUpdateTime >= 0.8:
                response = slackUpdateMessage(eventChannel, eventTS, None, result+"...")
                debounceSlackUpdateTime = time.time()

                # If the message has metadata & metadata event_type == aborted, it means the response has been marked as terminated by the user
                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]*"
                # The message has been 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": "Prompt"
            },
            "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 it will respond with [Terminated].

Similarly, you can also create a Shortcut to delete messages, implementing the deletion of messages sent by the Slack App.

Adding Context Functionality in the Same Thread

If you send a new message in the same thread, it can be considered a follow-up question to the same issue. At this point, you can add a feature to supplement the new prompt with the previous conversation content.

Add slackGetReplies & Fill Content into OpenAI API Prompt:

Complete 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"

# 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

    # Event from Shortcut will be given in 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 record runtime logs, which can be viewed in Logs
    # For advanced Logging Level usage, refer to: https://cloud.google.com/logging/docs/reference/libraries
    print(payload)

    # Due to the nature of FAAS (Cloud Functions), if the service is not called for a long time, it will enter a cold start when called again, which may not respond within Slack's 3-second limit
    # Plus, OpenAI API requests take a certain amount of time to respond (depending on the response length, it may take up to 1 minute to complete)
    # If Slack does not receive a response within the time limit, it will consider the request lost and will call again
    # This can cause duplicate requests and responses, so we can set X-Slack-No-Retry: 1 in the Response Headers to inform Slack not to retry even if it does not receive a response within the time limit
    headers = {'X-Slack-No-Retry':1}

    # If it's a Slack Retry request...ignore it
    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 the event source is the App and App ID == Slack App ID, it means the event was triggered by the Slack App itself
        # Ignore it to avoid infinite loops 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 (related to messages), app_mention (mentioned)....
        eventType = request_json['event']['type']

        # SubType, e.g., message_changed (edited message), message_deleted (deleted message)...
        # New messages do not have a Sub Type
        eventSubType = None
        if 'subtype' in request_json['event']:
            eventSubType = request_json['event']['subtype']
        
        if eventType == 'message':
            # Messages with Sub Type are edited, deleted, or replied to...
            # Ignore them
            if eventSubType is not None:
                return ("OK!", 200, headers)
               
            # Message sender of the Event
            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']
            # TS (message ID) of the Event message
            eventTS = request_json['event']['event_ts']
                
            # TS (message ID) of the parent message in the thread of the Event message
            # Only new messages in the thread will have this data
            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 Stop OpenAI API response Shortcut
                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!")
                        return ("OK!", 200, headers)


    return ("Access Denied!", 400, headers)

def openAIRequest(apiAppID, eventChannel, eventTS, eventThreadTS, eventText):
    
    # Set Custom instructions
    # Thanks to my colleague (https://twitter.com/je_suis_marku) for the support
    messages = [
        {"role": "system", "content": "I can only understand Traditional Chinese and English"},
        {"role": "system", "content": "I cannot understand Simplified Chinese"},
        {"role": "system", "content": "If I speak Chinese, I will respond in Traditional Chinese used in Taiwan, and it must conform to common usage in Taiwan."},
        {"role": "system", "content": "If I speak English, I will respond in English."},
        {"role": "system", "content": "Do not respond with pleasantries."},
        {"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 if 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 it's a Slack App (OpenAI API Response), mark it as assistant
                if appID and appID == apiAppID:
                    messages.append({
                        "role": "assistant", "content": threadMessageText
                    })
                else:
                # Mark the user's message content 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
    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 the message every 0.8 seconds to avoid frequent calls to the Slack Update Message API, which may fail or waste Cloud Functions requests
            if debounceSlackUpdateTime is None or time.time() - debounceSlackUpdateTime >= 0.8:
                response = slackUpdateMessage(eventChannel, eventTS, None, result+"...")
                debounceSlackUpdateTime = time.time()

                # If the message has metadata & metadata event_type == aborted, it means the response has been marked as terminated by the user
                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]*"
                # If the message has been 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": "Prompt"
            },
            "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:

  • The left image shows a new conversation when asking a follow-up question without adding Context.
  • The right image shows that with Context added, it can understand the entire conversation context and the new question.

Done!

At this point, we have built a ChatGPT (via OpenAI API) Slack App Bot.

You can also refer to Slack API and OpenAI API Custom instructions to integrate them into Cloud Functions Python programs according to your needs. For example, training a channel to answer team questions and find project documents, a channel dedicated to translation, a channel dedicated to data analysis, etc.

Supplement

Marking the bot to answer questions outside of 1:1 messages

  • You can mark the bot to answer questions in any channel (the bot needs to be added to the channel).

First, you need to add the app_mention Event Subscription:

After adding, click “Save Changes” to save, then “reinstall your app” to complete.

In the main.py program mentioned above, in the #Handle Event Subscriptions Events… Code Block, add a new Event Type judgment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
        # Mention Event (@SlackApp hello)
        if eventType == 'app_mention':
            # Event message sender
            eventUser = request_json['event']['user']
            # Event message channel
            eventChannel = request_json['event']['channel']
            # Event message content, remove the leading tag string <@SLACKAPPID>
            eventText = re.sub(r"<@\w+>\W*", "", request_json['event']['text'])
            # Event message TS (message ID)
            eventTS = request_json['event']['event_ts']
                
            # Parent message TS of the event message thread (message ID)
            # Only new messages in the thread will have this data
            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 will be completed.

Deleting messages sent by Slack App

You cannot directly delete messages sent by Slack App on Slack. You can refer to the above “ Stop OpenAI API Response “ Shortcut method, and add a “delete message” Shortcut.

And in the Cloud Functions main.py program:

In the # Handle Shortcut Code Block, add a callback_id judgment. If it equals the “delete message” Shortcut Callback ID you defined, pass the parameters into the following method to delete:

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
  • Ensure Cloud Functions are fully deployed
  • Verify if the Slack App is in the channel you are asking questions in (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 the Slack API Response under the SlackRequest method

Cloud Functions Public URL Not Secure Enough

  • If you are concerned about the security of the Cloud Functions URL, 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)

Billing Method

Different regions, CPU, RAM, capacity, traffic… have different prices. Please refer to the official pricing table.

The free tier is as follows: (2024/02/15)

1
2
3
4
5
6
7
8
9
10
Cloud Functions offers a permanent free tier for compute time resources,
including allocations of GB-seconds and GHz-seconds. In addition to 2 million invocations,
this free tier also provides 400,000 GB-seconds and 200,000 GHz-seconds of compute time,
and 5 GB of internet data transfer per month.

The usage quota of the free tier is calculated in equivalent USD amounts at the above tier 1 prices.
Regardless of whether the function execution region uses tier 1 and/or tier 2 prices, the system will allocate the equivalent USD amount to you.
However, when deducting the free tier quota, the system will use the tier (tier 1 or tier 2) of the function execution region as the standard.

Please note that even if you are using the free tier, you must have a valid billing account.

btw. Slack App is free, you don’t necessarily need Premium to use it.

Slack App Response Too Slow, Timeout

(Excluding the issue of slow response during OpenAI API peak times), if it’s a Cloud Function bottleneck, you can expand the settings on the first page of the Cloud Function editor:

You can adjust CPU, RAM, Timeout time, Concurrent number… to improve request processing speed.

*But it may incur additional costs

Development Stage Testing & Debug

Click “Test Function” to open a Cloud Shell window in the bottom toolbar. Wait about 3–5 minutes (the first startup takes longer), and after the build is completed and the following authorization is agreed upon:

Once you see “Function is ready to test,” you can click “Run Test” to execute the method for debugging.

You can use the “Triggering event” block on the right to input a JSON Body that will be passed into the request_json parameter for testing, or directly modify the program to inject a test object for testing.

*Please note that Cloud Shell/Cloud Run may incur additional costs.

It is recommended to run a test before deploying (Deploy) to ensure that the build can succeed.

Build Failed, What to Do When Code Disappears?

If you accidentally write incorrect code causing Cloud Function Deploy Build Failed, an error message will appear. At this point, clicking “EDIT AND REDEPLOY” to return to the editor will find 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 that just Build Failed:

View Runtime print Logs

*Please note that Cloud Logging and Querying Logs may incur additional costs.

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
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 defined security verification Token
# The URL must carry the ?token=SAFE_ACCESS_TOKEN parameter to accept the request  
SAFE_ACCESS_TOKEN = "nF4JwxfG9abqPZCJnBerwwhtodC28BuC"

# 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 will be given 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 record runtime logs, which can be viewed in Logs
    # For advanced Logging Level usage, refer to: https://cloud.google.com/logging/docs/reference/libraries
    # print(payload)

    # Due to the nature of FAAS (Cloud Functions), if the service is not called for a long time, it will enter a cold start when called again, which may not respond within Slack's 3-second limit
    # Additionally, it takes a certain amount of time for the OpenAI API to respond (depending on the response length, it may take up to 1 minute to complete)
    # If Slack does not receive a response within the time limit, it will consider the request lost and will call again
    # This will cause repeated requests and responses, so we can set X-Slack-No-Retry: 1 in the Response Headers to inform Slack that even if it does not receive a response within the time limit, it does not need to retry    
    headers = {'X-Slack-No-Retry':1}

    # 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)

    # If it is a Slack Retry request...ignore
    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 the event source is the App and the App ID == Slack App ID, it means the event was triggered by its own Slack App
        # Ignore and do not process, otherwise it will fall into an 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, for example: message (related to messages), app_mention (mentioned)....
        eventType = request_json['event']['type']

        # SubType, for example: message_changed (edited message), message_deleted (deleted message)...
        # New messages do not have a Sub Type
        eventSubType = None
        if 'subtype' in request_json['event']:
            eventSubType = request_json['event']['subtype']
        
        # Message type Event
        if eventType == 'message':
            # Messages with Sub Type are edited, deleted, replied to...
            # 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 message thread parent message TS (message ID)
            # Only new messages in the thread will have this data
            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 type Event (@SlackApp hello)
        if eventType == 'app_mention':
            # Event message sender
            eventUser = request_json['event']['user']
            # Event message channel
            eventChannel = request_json['event']['channel']
            # Event message content, remove the leading tag string <@SLACKAPPID> 
            eventText = re.sub(r"<@\w+>\W*", "", request_json['event']['text'])
            # Event message TS (message ID)
            eventTS = request_json['event']['event_ts']
                
            # Event message thread parent message TS (message ID)
            # Only new messages in the thread will have this data
            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 is 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 is a stop OpenAI API response Shortcut
                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!")
                        return ("OK!", 200, headers)
                # If it is a 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 a colleague (https://twitter.com/je_suis_marku) for support
    messages = [
        {"role": "system", "content": "I can only understand Traditional Chinese and English"},
        {"role": "system", "content": "I do not understand Simplified Chinese"},
        {"role": "system", "content": "If I speak Chinese, I will respond in Traditional Chinese, and it must conform to common Taiwanese usage."},
        {"role": "system", "content": "If I speak English, I will respond in English."},
        {"role": "system", "content": "Do not respond with pleasantries."},
        {"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 if 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 it is a Slack App (OpenAI API Response), mark it as assistant
                if appID and appID == apiAppID:
                    messages.append({
                        "role": "assistant", "content": threadMessageText
                    })
                else:
                # User's 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
    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 the message every 0.8 seconds to avoid frequent calls to the Slack Update Message API, which may cause failures or waste Cloud Functions request counts
            if debounceSlackUpdateTime is None or time.time() - debounceSlackUpdateTime >= 0.8:
                response = slackUpdateMessage(eventChannel, eventTS, None, result+"...")
                debounceSlackUpdateTime = time.time()

                # If the message has metadata & metadata event_type == aborted, it means this response has been marked as terminated by the user
                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'] == "aborted":
                    break
                    result += "...*[Terminated]*"
                # The message has been 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": "Prompt"
            },
            "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 Settings

OAuth & Permissions

  • The items with the delete button grayed out 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 🤘🤘🤘

[Reddit](https://www.reddit.com/r/ChatGPT/comments/154l9z1/are_you_polite_with_chatgpt/){:target="_blank"}

Reddit

Commercial Time

If you and your team have automation tool and process integration needs, whether it’s Slack App development, Notion, Asana, Google Sheet, Google Form, GA data, various integration needs, feel free to contact for development.

If you have any questions or comments, feel free to contact me.

===

本文中文版本

===

This article was first published in Traditional Chinese on Medium ➡️ View Here


This post is licensed under CC BY 4.0 by the author.

Travelogue 2023 Hiroshima Okayama 6-Day Free Trip

Implementing Google Services RPA Automation with Google Apps Script