はじめに
現時点でまだSlackのChatGPT公式アプリはリリースされておりませんが
OpenAIからAPIは提供されていますのでこれを使ってSlack上でChatGPTとやりとりができるボットアプリの作成方法を共有したいと思います。
今回はPythonのFlaskを利用した例となります。
OpenAIの提供しているPython公式SDKを利用します。
処理仕様
ユーザーからAIへの質問の入力を受ける方法はアプリへのメンションイベントとしてEvent Subscriptionsのapp_mention
を拾うことにします。
処理設計の中で重要なポイントとしてはデータベースを極力利用せずに会話履歴を保持する方法です。
ChatGPTのWeb画面ではトピックごとにNew chatを立てていくと思いますがこれをどうやってSlackのインターフェースの中で実現していくのかがポイントとなります。
これについてはNew chatごとにスレッドを立ててもらう方法を取りました。
スレッドの内容を取得できるAPIエンドポイントconversations.replies
で会話履歴を構築できます。
以上を考慮した処理フローは以下のようになりました。
処理内容
必要なPythonライブラリは以下となります。
pip install requests
pip install openai
import requests
import openai
メンションイベント受信
@app.route("/openai_mention", methods=["POST"])
def openai_mention():
# リトライを無視する対応
if "X-Slack-Retry-Num" in request.headers and request.headers["X-Slack-Retry-Reason"] == "http_timeout":
print('ignore retry!!!')
return {'text': 'ignore retry!!!'}
request_json = request.json
print(request_json)
# jsonから必要なパラメータを取得する
text = request_json["event"]["text"]
user_id = request_json["event"]["user"]
channel_id = request_json["event"]["channel"]
thread_ts = ''
url = ''
# スレッド内のメンションの場合はスレッドIDを取得する
if "thread_ts" in request_json["event"]:
thread_ts = request_json["event"]["thread_ts"]
Event SubscriptionsのRequest URLは/openai_mention
をコールするエンドポイントを指定してあげてください。
Event Subscriptionsでは3秒以内にレスポンスしないとリトライする仕様があります。
サーバーレス環境などでの運用ではアプリがコールドスタートした場合に返答にどうしてもタイムラグが発生してしまいます。
これを食らってしまうと重複処理が起こるのでリトライのリクエストをチェックし無視する対策を入れています。
イベントのパラメータからどのチャンネルchannel_id
で誰user_id
が何を発言text
をどのスレッドthread_ts
でしたのかなどの情報を取得します。
スレッド情報取得
APP_USER_ID_RAW = '@UXXXXXXXXXX'
APP_USER_ID = f"<@{APP_USER_ID_RAW}>"
payload = {'token':APP_TOKEN, 'channel':channel, 'ts': thread_ts}
res = post("https://slack.com/api/conversations.replies", data=payload)
res_json = res.json()
messages = res_json['messages']
for message in messages:
# アプリへのメンションの場合
if APP_USER_ID in message['text']:
# 制御文字を抜く
input = message['text'].replace(APP_USER_ID, '')
q_message = {"role": "user", "content": input}
prompt.append(q_message)
# AIの回答の場合
elif APP_USER_ID_RAW in message['user']:
input = message['text']
a_message = {"role": "assistant", "content": input}
prompt.append(a_message)
チャンネルとスレッドの情報をパラメータとしてconversations.replies
を実行するとスレッド情報一覧が取得できます。
スレッドの内のメッセージを一つずつ走査して以下の処理を行います。
- 自分の質問内容の文字列内に
APP_USER_ID
と一致するものが存在しているかで判定しそれを除去する(これを'role': 'user'
としている) - AIの返答については
APP_USER_ID_RAW
がuser
と一致しているかで判定する(これを'role': 'assistant'
としている)
APP_USER_ID_RAW
にはアプリのメンバーIDを設定します。
APP_USER_ID
にはメンションされた際にメッセージの中に含まれるアプリのメンバーID文字列を格納しておきます。
なぜこのような制御をしてるかというとスレッド内に関係ないメッセージが紛れ込んでくる可能性がありますので明確にAIへの質問と回答だけを正しくパラメータにセットするためです。
プロンプトパラメータサンプル
{'role': 'user', 'content': ' 令和元年の西暦は?'},
{'role': 'assistant', 'content': '令和元年は、西暦2019年にあたります。'},
{'role': 'user', 'content': ' 平成は?'},
{'role': 'assistant', 'content': '平成元年は、西暦1989年にあたります。'},
{'role': 'user', 'content': ' 昭和は?'}
スレッド内容を解析後最終的にはこのようなパラメータが構築されることになります。
一番最後の部分が新しいAIへの質問内容となります。
ChatGPTプロンプト送信&回答をSlackに投稿
result = openai.ChatCompletion.create(model=AI_ENGINE, messages=prompt)
ai_response = result.choices[0].message.content
print(ai_response)
# メッセージ送信用のPOST
payload = {'token':APP_TOKEN, 'channel':channel, 'text': ai_response}
# スレッド内の場合はそこに打ち返すこと
if thread_ts != '':
payload['thread_ts'] = thread_ts
res = requests.post("https://slack.com/api/chat.postMessage", data=payload)
AIから受け取った回答をchat.postMessage
を指定のスレッドthread_ts
に投稿します。
万が一thread_ts
が未指定の場合はチャンネルにそのまま投稿しておくことにします。
その他
必要なBot Token Scopesは以下です。
- app_mentions:read
- channels:history
- chat:write
最後に作成したアプリをチャンネルにジョインさせてあげます。
これでアプリにメンションするだけの非常にシンプルな使い方になります。