SlackとChatGPT APIでチャットボットを作る パート1(基礎編)
(2023-12-13 追記)最近のOpenAI SDKは仕様が変わっており、現在載せているコードは動かないので互換性のある古いOpenAI SDKを含む
requirents.txt
を掲載します。Pythonコードも細部を少し修正しました。
wheel
tenacity
slack_bolt
openai==0.28.1
tiktoken
pandas
matplotlib
japanize_matplotlib
seaborn
scikit-learn
ipykernel
SlackとChatGPT APIでチャットボットを作る パート1(基礎編)
表記のテーマについて数回にわたって記事を書きます。パート1では基礎部分を作成し、以降のパートで徐々に機能を追加して行きたいと考えています。あえてLangChainなどを使わずに実装します。
Slack上でNew Appを作る
- ログインしている状態でSlack APIのトップ・ページから右上の方のYou AppsをクリックしてYour Appsページに飛び、右上のCreate New AppをクリックするとCreate an appポップアップが出ますので、From Scratchを選びます。
-
Name app & choose workspaceに遷移します。今回はAppの名前をchatbotとします。workspaceは適宜選んでください。
-
Create AppをクリックするとAppのページに遷移します。次にページ左のメニューからSocket Modeを選びEnable Socket ModeをONにします。そうするとApp Level Tokenを作成するポップアップがでますので、作成後、トークンをコピーして保存しておき、ポップアップを閉じます。
- ページ左のメニューからApp Homeをクリックし、Show Tabsのペインにスクロールダウンして、Message TabがONになっているのを確認しAllow users to send Slash commands and messages from the messages tabのチェックボックスをチェックします。
- ページ左のメニューからOAuth & PermissionsをクリックしスクロールダウンしてScopesのペインに移動します。Bot Token ScopesのAdd an OAuth Scopeをクリックすると出てくるメニューからスコープをクリックして加えます。加えるスコープは次の3つです:
app_mentions:read
、chat:write
、group:history
- ページ左のメニューからEvent Subscriptionをクリックします。Enable EventsをONにした後、Subscribe to bot eventsに
app_mention
とmessage.im
を加えます。
- ページ左のメニューからInstall Appをクリックし、Install to workspaceをクリックし、許可をするとBOT User OAuth Tokenが発行されます。トークンは保存しておきます。
動作テスト
最初に必要なライブラリをインストールします。
pip install slack_bolt
pip install openai
ちなみにチャットボットアプリが動作する環境ですが、slack_bolt
がSlackにアクセスしてWebSocketを確立してくれるのですが、こちらからSlackが見えればOKで、こちらがグローバルIPアドレスを持っている必要はありません。なので典型的な企業や家庭のLAN内にあるPCでOKです(ファイアウォールの設定などでうまくいかないケースもあるかもしれませんが)。この記事はローカルPCのWSL/Ubuntu22.04上でテストしながら書いています。
まずはチャットボットがSlackと交信できることを確認しましょう。App作成時に記録したトークンを次の環境変数CHATBOT_APP_TOKEN
とSLACK_BOT_TOKEN
に格納したうえで、次のようなファイルを作成し、
import os
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
chatbot_app_token = os.environ["CHATBOT_APP_TOKEN"]
slack_bot_token = os.environ["SLACK_BOT_TOKEN"]
app = App(token=slack_bot_token)
@app.message()
def handle(message, say):
say(f"Hi there, <@{message['user']}>!")
SocketModeHandler(app, chatbot_app_token).start()
実行します。
python chatbot.py
⚡️ Bolt app is running!
と出力されたらSlackに移動し、chatbotを選んでメッセージ・タブでメッセージを入力してチャットボットが反応したら成功です。
ChatGPTと接続
それではチャットボットをChatGPTに接続しましょう。chatbot.py
と同じディレクトリに次のようなファイルを作成します。(GitHub Copilotは放っておくと英語でdocstringを書くので英語になってしまいました)
from typing import Optional, Any, Generator
import os
import openai
from openai.error import InvalidRequestError
from tenacity import retry, retry_if_not_exception_type, wait_fixed
class ChatEngine:
"""Chatbot engine that uses OpenAI's API to generate responses."""
@classmethod
def setup(cls, model: str) -> None:
"""Basic setup of the class.
Args:
model (str): The name of the OpenAI model to use, i.e. "gpt-3-0613" or "gpt-4-0613"
"""
cls.model = model
openai.api_key = os.getenv("OPENAI_API_KEY")
def __init__(self) -> None:
"""Initializes the chatbot engine.
"""
self.messages = [{
"role": "system",
"content": "ユーザーを助けるチャットボットです。博多弁で答えます。"
}]
@retry(retry=retry_if_not_exception_type(InvalidRequestError), wait=wait_fixed(10))
def _process_chat_completion(self, **kwargs) -> dict[str, Any]:
"""Processes ChatGPT API calling."""
response = openai.ChatCompletion.create(model=self.model, messages=self.messages, **kwargs)
assert isinstance(response, dict)
message = response["choices"][0]["message"]
self.messages.append(message)
return message
def reply_message(self, user_message: str) -> Generator:
"""Replies to the user's message.
Args:
user_message (str): The user's message.
Yields:
(str): The chatbot's response(s)
"""
self.messages.append({"role": "user", "content": user_message})
try:
message = self._process_chat_completion()
except InvalidRequestError as e:
yield f"## Error while Chat GPT API calling with the user message: {e}"
return
yield message['content']
reply_message()
はyield
で返答を返しますが、これは機能追加で複数メッセージを返すようにできるためです。
次にchatbot.py
を次のように書き換えます。
import os
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
from utils import ChatEngine
chatbot_app_token = os.environ["CHATBOT_APP_TOKEN"]
slack_bot_token = os.environ["SLACK_BOT_TOKEN"]
app = App(token=slack_bot_token)
@app.message()
def handle(message, say):
global chat_engine_dict
if message["user"] not in chat_engine_dict.keys():
chat_engine_dict[message["user"]] = ChatEngine()
for reply in chat_engine_dict[message["user"]].reply_message(message['text']):
say(reply)
model = "gpt-4-0613"
ChatEngine.setup(model)
chat_engine_dict = dict()
SocketModeHandler(app, chatbot_app_token).start()
chat_engine_dict[<user_id>]
がそのユーザー向けのChatEngine
のインスタンスを保持する仕組みで、メッセージを送って来たユーザーのインスタンスがあるかどうかチェックして無ければ新規にインスタンスを作ります。
環境変数OPENAI_API_KEY
をセットしたのち、チャットボットを起動します。
python chatbot.py
⚡️ Bolt app is running!
Slackに移動してチャットボットと会話できるか試してみましょう。だいたい次のような会話ができれば成功です。
パート1はここまでにします。パート2ではfunction callingを使ってデータベースにアクセスする機能を実装したいと思います。パート1のコードはtf-koichi/slack-chatbot at part1に置いてあります。以上、何かお気づきの点がありましたらフィードバックよろしくお願いします。
Discussion