小原 達也/ 2023年 7月 24日/ 技術

はじめまして!7月よりNorthTorchにジョインさせていただきました小原です。

本記事ではSlackにChatGPTを搭載する方法をご紹介します。

弊社では現在Pythonをメインに開発を行っており、今回のスクリプトもPythonで作成いたしました。

※ 記事の内容はデプロイを含まずngrokでの動作確認までとなっております。ご了承ください。

実装の流れ

少し長い記事になりますので、まずは実装の全体像をお伝えします。

1.APIキーの取得
2.コーディング
 2-1.Flask
 2-2.Slack Bolt / SQL Alchemy
3.Slack APPの設定
4.ngrokによる動作確認
5.Slack側でエンドポイントの登録

結論となるコード

続いて、今回結論となるコードを掲載します。

import os

import openai
from dotenv import load_dotenv
from flask import Flask, request
from slack_bolt import App
from slack_bolt.adapter.flask import SlackRequestHandler

# ".env"ファイルより環境変数を読み込み
load_dotenv()
bot_token = os.environ["SLACK_BOT_TOKEN"]
slack_signing_secret = os.environ["SLACK_SIGNING_SECRET"]
openai_api_key = os.environ["OPENAI_API_KEY"]

app = Flask(__name__)

openai.api_key = openai_api_key
GPT_3_5_TUROBO_MODEL = "gpt-3.5-turbo"

slack_app = App(
    token=bot_token,
    signing_secret=slack_signing_secret,
)
handler = SlackRequestHandler(slack_app)

# SQLAlchemy初期設定
Base = declarative_base()

class Chat(Base):
    __tablename__ = "chat"
    id = Column(Integer, primary_key=True)
    question = Column(String)
    reply = Column(String)

engine = create_engine("sqlite:///chat.db")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()

# 会話履歴
conversations = {}

def generate_gpt_reply(messages):
    try:
        response = openai.ChatCompletion.create(
            model=GPT_3_5_TUROBO_MODEL,
            messages=messages,
            n=1,
            stop=None,
            temperature=0.5,
        )
    except openai.error.OpenAIError as e:
        return None
    reply = response["choices"][0]["message"]["content"].strip()
    return reply

def handle_conversation(thread_timestamp, question):
    """
    GPTからの返信の生成と会話履歴の格納
    """
    if thread_timestamp not in conversations:
        # チャット開始時にBotの振る舞いを調整
        conversations[thread_timestamp] = [
            {"role": "system", "content": "あなたはベテランのプログラマです。質問に対して平易な文章で解説します。"},
        ]

    # 質問を記録
    conversations[thread_timestamp].append({"role": "user", "content": question})

    reply = generate_gpt_reply(conversations[thread_timestamp])
    if reply is None:
        return "OpenAI APIからのレスポンスが得られませんでした。"

    # 返答を記録
    conversations[thread_timestamp].append({"role": "assistant", "content": reply})

    return reply

@slack_app.event("app_mention")
def command_handler(body, say):
    """
    Botへのメンションにより発火
    - body: payloadと同値
    - say: メッセージ送信用の関数
    """
    question = body["event"]["text"]
    thread_timestamp = body["event"].get("thread_ts", None) or body["event"]["ts"]

    # 返信の生成と会話履歴の記録
    reply = handle_conversation(thread_timestamp, question)

    try:
        # Databaseに反映
        new_chat = Chat(question=question, reply=reply)
        session.add(new_chat)
        session.commit()
    except SQLAlchemyError:
        session.rollback()

    # Slackに返答を送信
    say(text=reply, thread_ts=thread_timestamp)

@app.route("/", methods=["POST"])
def slack_events():
    payload = request.json
    # SlackへのRequest URLの登録に必要
    if "challenge" in payload:
        return payload["challenge"]

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=False)

主な使用ライブラリと採用理由は以下の通りです。

ライブラリ 採用理由
openai ChatGPT組み込み
dotenv APIキーの読み込み
Flask Slackからのリクエスト受付 / レスポンスの生成
slack_bolt Botへのメンションに反応
sqlalchemy 会話履歴をデータベースに格納

実装に至るまで

ここからは結論のコードに至るまでの流れを深堀して解説します。

OpenAI API Keyの取得方法


OpenAI公式ページにアクセスします。
>> OpenAI公式ページ


APIを選択します。


View API Keysを選択します。


「Create new secret key」を選択します。


API Keyの名前を決めると、上記のような形でAPI Keyが発行されます。
※"sk-"で始まるAPIキーが取得できると思います。(2023年7月現在)

ライブラリのインストール

今回使用するライブラリは次の通りです。

・slack-bolt
・openai
・flask
・python-dotenv
・sqlalchemy

pipやpoetryなどを用いて、上記ライブラリをインストールしておきましょう。

Slack APIキーの取得

Slack側では、次の2つのAPIキーを取得します。

・Bot Token
・Signing Secret Key

この2つのAPI Keyを取得するため、Slack APIページにアクセスします。
Slack API


Create an Appをクリック。


続いてFrom scratchをクリックします。


App Nameには任意のアプリ名を、Pick a workspace to develop your app inにはChatGPTを導入したいワークスペースを選択します。


続いて、先ほど作成したアプリ名をクリックします。(ここではgptという名前にしました)


サイドバーより"Oauth & Permissions"を選択。


"Add an OAuth Scope"から必要なScopeを追加します。
今回はメンションに反応することと、チャットに書き込むため、"app_mentions:read"と"chat:write"の二つを追加。


"xoxb"から始まるBotトークンが発行されますので、コピーしておきます。
こちらがBot Tokenになります。


Signing Secret Keyを取得するため、サイドバーから"Basic Information"をクリックします。


少し下にスクロールすると"Signing Secret"というテキストボックスがありますので、Showボタンをクリックして内容をコピーします。

APIキーを.envファイルに格納

取得してきたAPIキーを".env"にファイルに書き込みます。

SLACK_BOT_TOKEN=*******
SLACK_SIGNING_SECRET=*******
OPENAI_API_KEY=*******

.envはスクリプトと同じ階層に配置して実行してください。

FlaskAPP

今回は質問に対して応答を返す簡単なアプリケーションのため、フレームワークはDjangoではなくFlaskで構築しました。

大まかな構造としては次の通りです。

from flask import Flask, request
app = Flask(__name__)

@app.route("/", methods=["POST"])
def slack_events():
    payload = request.json
    print(payload)

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=False)

このコードにより、エンドポイントに対するPOSTリクエストに含まれるpayloadを受け取ることができます。
if __name__ == "__main__":内は、本スクリプトが直接実行された場合に実行される、おなじみの定型文です。

ここでは単純化のため、payloadをprintするだけの実装にとどめています。

なお、app.run()は開発用サーバーの立ち上げるメソッドで、オプションの意味は以下の通りです。

host="0.0.0.0":すべての公開IPアドレスからアクセスできるようにする。
post=5000: アプリが受け付けるポート番号を5000番に指定。Flaskのデフォルトポートは5000番。
debugFalse: デバッグモードを無効にする。開発中は詳細なエラーを出力するためTrueにする。

この骨格に対して、ChatGPTとSlack連携を加えていきます。

ChatGPTとの連携

ChatGPTとの連携にはOpenAIのライブラリを使います。
今回のスクリプトのうち、OpenAIに関する部分は以下の通りです。

import openai
from dotenv import load_dotenv

load_dotenv()

openai.api_key = openai_api_key
GPT_3_5_TUROBO_MODEL = "gpt-3.5-turbo"

def generate_gpt_reply(messages):
    try:
        response = openai.ChatCompletion.create(
            model=GPT_3_5_TUROBO_MODEL,
            messages=messages,
            n=1,
            stop=None,
            temperature=0.5,
        )
    except openai.error.OpenAIError as e:
        return None
    reply = response["choices"][0]["message"]["content"].strip()
    return reply

たったこれだけのコードでChatGPTが実装できます。

補足として、openai.ChatCompletion.create()メソッドの引数を解説します。

model: OpenAIのモデルを選択。
messages: プロンプトと呼ばれる会話履歴をPythonの辞書型で渡す。
n: 返答として返すメッセージの個数を指定。
stop: 返答が止まるトリガーを指定。例えば'。'とすると生成された文章に"。"が出現したときに返答をやめる。
temperature: 返答のランダム性を制御。今回は中間の0.5を採用。

この関数に対して会話履歴を格納した辞書を与えると、それに対する返答が受け取ることができます。

※ modelについては、最近GPT4のAPIも一般公開されました。価格などの違いもありますので必要に応じて使い分けてみてください。

Slackとの連携 / データベースへの登録

Slackとの連携にあたってはSlack_boltというライブラリを採用しました。
ここでのSlack_boltの目的は、SlackBot(ChatGPT)へのメンションに反応することです。
データベースへの反映は、SqlAlchemyを使っています。

from slack_bolt import App
from slack_bolt.adapter.flask import SlackRequestHandler

from dotenv import load_dotenv
load_dotenv()

bot_token = os.environ["SLACK_BOT_TOKEN"]
slack_signing_secret = os.environ["SLACK_SIGNING_SECRET"]

slack_app = App(
    token=bot_token,
    signing_secret=slack_signing_secret,
)
handler = SlackRequestHandler(slack_app)

def handle_conversation(thread_timestamp, question):
    """
    GPTからの返信の生成と会話履歴の格納
    """
    if thread_timestamp not in conversations:
        # チャット開始時にBotの振る舞いを調整
        conversations[thread_timestamp] = [
            {"role": "system", "content": "あなたはベテランのプログラマです。質問に対して平易な文章で解説します。"},
        ]
    # 質問を記録
    conversations[thread_timestamp].append({"role": "user", "content": question})
    reply = generate_gpt_reply(conversations[thread_timestamp])
    if reply is None:
        return "OpenAI APIからのレスポンスが得られませんでした。"
    # 返答を記録
    conversations[thread_timestamp].append({"role": "assistant", "content": reply})
    return reply

@slack_app.event("app_mention")
def command_handler(body, say):
    """
    Botへのメンションにより発火
    - body: payloadと同値
    - say: メッセージ送信用の関数
    """
    question = body["event"]["text"]
    thread_timestamp = body["event"].get("thread_ts", None) or body["event"]["ts"]

    # 返信の生成と会話履歴の記録
    reply = handle_conversation(thread_timestamp, question)

    try:
        # Databaseに反映
        new_chat = Chat(question=question, reply=reply)
        session.add(new_chat)
        session.commit()
    except SQLAlchemyError:
        session.rollback()

    # Slackに返答を送信
    say(text=reply, thread_ts=thread_timestamp)

@app.route("/", methods=["POST"])
def slack_events():
    payload = request.json
    # SlackへのRequest URLの登録に必要
    if "challenge" in payload:
        return payload["challenge"]

Slackではタイムラインを区別するために「タイムスタンプ」で管理しています。
そのため、本スクリプトでもSlackで管理しているタイムスタンプをKey、会話履歴をValueとする辞書を作成の上、OpenAIAPIに渡すようにしました。

これにより、同じスレッド内でBotにメッセージを送った場合には、過去の会話履歴を考慮した返答が得られるようになっています。

なお、if "challenge" in payload:の部分はSlack上の認証プロセスである「チャレンジ-レスポンス認証」プロセスに対応するためのものです。
この部分がないと、後の「Slack側へのエンドポイントの登録」が失敗します。

動作検証

以上のスクリプトが動くか動作検証してみます。

今回は簡易的に動作検証するためにngrok(エングロック)を活用します。
ngorkとは外部から開発サーバーに対してアクセスできるようにトンネルを作る便利なツールです。
初期設定はこちらなどを参考に済ませてみてください。

終わりましたら開発サーバーを動かした後に、以下のコマンドをたたいていきましょう。

ngrok http 5000

Flaskではポート5000番を指定して開発用サーバーを立ち上げたため、ngrokでも5000番を指定します。

これでローカルで起動した開発サーバーに対してngrok経由で外部からアクセスができるようになります。
つまり、slack => ngrok => flask開発サーバーの経路でアクセスができるようになりました。

Slack側での設定

Slack上でエンドポイントの設定をする必要があります。

サイドバーより"Event Subscriptions"をクリックします。

続いて、Request URLとしてngrokから配布されたURLを入力しましょう。

Slack上からBotに質問してみる


@gptのようにメンションを付けてメッセージをすると、スレッド単位で返信してくれます。

まとめ

以上がSlackとChatGPTを連携する方法でした。

手順を簡潔にまとめると、以下の通りです。

・各種APIキーを取得する
・スクリプトを書く
・開発サーバー起動
・ngrokでローカルへのトンネルを確保
・Slack側でエンドポイントの設定

ぜひ、チャレンジしてみてください。

お知らせ

弊社は通年通して、一緒に働く仲間を募集しています。
興味を持たれた方がいらっしゃいましたら、こちらからご連絡をお待ちしております!