🛠️

SlackとChatGPT APIでチャットボットを作る パート1(基礎編)

2023/08/04に公開

(2023-12-13 追記)最近のOpenAI SDKは仕様が変わっており、現在載せているコードは動かないので互換性のある古いOpenAI SDKを含むrequirents.txtを掲載します。Pythonコードも細部を少し修正しました。

requirements.txt
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を作る

  1. ログインしている状態でSlack APIのトップ・ページから右上の方のYou AppsをクリックしてYour Appsページに飛び、右上のCreate New AppをクリックするとCreate an appポップアップが出ますので、From Scratchを選びます。
    Create an app
  2. Name app & choose workspaceに遷移します。今回はAppの名前をchatbotとします。workspaceは適宜選んでください。
    Name app & choose workspace
  3. Create AppをクリックするとAppのページに遷移します。次にページ左のメニューからSocket Modeを選びEnable Socket ModeをONにします。そうするとApp Level Tokenを作成するポップアップがでますので、作成後、トークンをコピーして保存しておき、ポップアップを閉じます。
    Alt text
  4. ページ左のメニューからApp Homeをクリックし、Show Tabsのペインにスクロールダウンして、Message TabがONになっているのを確認しAllow users to send Slash commands and messages from the messages tabのチェックボックスをチェックします。
  5. ページ左のメニューからOAuth & PermissionsをクリックしスクロールダウンしてScopesのペインに移動します。Bot Token ScopesAdd an OAuth Scopeをクリックすると出てくるメニューからスコープをクリックして加えます。加えるスコープは次の3つです: app_mentions:readchat:writegroup:history
    Alt text
  6. ページ左のメニューからEvent Subscriptionをクリックします。Enable EventsをONにした後、Subscribe to bot eventsapp_mentionmessage.imを加えます。
    Alt text
  7. ページ左のメニューから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_TOKENSLACK_BOT_TOKENに格納したうえで、次のようなファイルを作成し、

chatbot.py
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を書くので英語になってしまいました)

utils.py
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を次のように書き換えます。

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に移動してチャットボットと会話できるか試してみましょう。だいたい次のような会話ができれば成功です。
ChatGPT
パート1はここまでにします。パート2ではfunction callingを使ってデータベースにアクセスする機能を実装したいと思います。パート1のコードはtf-koichi/slack-chatbot at part1に置いてあります。以上、何かお気づきの点がありましたらフィードバックよろしくお願いします。

Discussion