🤖

ChatGPT API を使って Cloud Functions で ChatGPT風SlackBot を作ってみる

2023/03/05に公開6

💡はじめに

2023年3月2日に OpenAI より ChatGPT に使われている言語モデル gpt-3.5-turbo を有料で利用できる Chat Completion API が公開されました🎊
早速、Slack で ChatGPT との会話ができる Slack Bot を作ってみました。

[使い方] Bot にメンションを飛ばすと ChatGPT風に返答してくれます。
animation

📐アーキテクチャ

FaaS を利用すると、簡単なアプリケーションを気軽に構築できます。
今回は、GCP を使いたかったので Cloud Functions[第2世代] を利用しています。
SlackBot にメンション付きで質問すると、Cloud Functions が起動し、Chat Completion API を利用してGPT-3.5 に質問を送信し、受信した回答を Slack にメンション付きで返信します。

architecture_img

🛠構築

Slack app を作成したことがない人でも作成できることを目指しているため、
Slack app の作成から Cloud Functions のデプロイまで細かく説明しています。

SlackBot のコードだけ確認したい人は、コード まで飛ばしてください。

Slack app の作成

Slack api にアクセスして Slack app を作成してください。

slack_create_app

from scratch を選択

slack_create_from_scratch

Basic Information で Bots, Event Subscriptions, Permissions を設定

slack_create_bot

OAuth & Permissions で Bot User OAuth Token を発行する

slack_OAuth_token

Scopes でアプリが持つ権限を設定する
Bot Token Scopes

最低限

  • app_mentions:read
  • chat:write

今後は会話継続機能を実装したいのでこちらも

  • channels:history
  • chat:write.customize
  • groups:history

OpenAI API キーの取得

OpenAI の HP にアクセスして OpenAI API key を取得してください。

openai_api_key

Cloud Functions のデプロイ

Google Cloud Platform にアクセスして チュートリアルを参考にして Cloud Functions 関数を作成してください。

公式ドキュメント:Cloud Functions のチュートリアル

以下のコードを Cloud Functions[第2世代] にデプロイしてください。
ランタイム設定は Python3.10 です。
なお、発行した API キーは Secret Manager に登録して環境変数として参照します。

公式ドキュメント:Cloud Functions でのシークレットの使用

コード

https://github.com/y-tsuritani/SlackChatbot_like_ChatGPT

main.py
import json
import logging
import os
import re
from typing import Union

import functions_framework
import google.cloud.logging
import openai
from box import Box
from flask import Request
from slack_bolt import App, context
from slack_bolt.adapter.google_cloud_functions import SlackRequestHandler

# Google Cloud Logging クライアント ライブラリを設定
logging_client = google.cloud.logging.Client()
logging_client.setup_logging(log_level=logging.DEBUG)

# 環境変数からシークレットを取得
slack_token = os.environ.get("SLACK_BOT_TOKEN")
openai_api_key = os.environ.get("OPENAI_API_KEY")
openai.api_key = openai_api_key

# FaaS で実行する場合、応答速度が遅いため process_before_response は True でなければならない
app = App(token=slack_token, process_before_response=True)
handler = SlackRequestHandler(app)


# Bot アプリにメンションしたイベントに対する応答
@app.event("app_mention")
def handle_app_mention_events(body: dict, say: context.say.say.Say):
    """アプリへのメンションに対する応答を生成する関数

    Args:
        body: HTTP リクエストのボディ
        say: 返信内容を Slack に送信
    """
    logging.debug(type(body))
    logging.debug(body)
    box = Box(body)
    user = box.event.user
    text = box.event.text
    only_text = re.sub("<@[a-zA-Z0-9]{11}>", "", text)
    # TODO: 単発の質問に返信するのみで、会話の履歴を参照する機能は未実装
    message = [{"role": "user", "content": only_text}]
    logging.debug(only_text)

    # OpenAI から AIモデルの回答を生成する
    (openai_response, total_tokens) = create_completion(message)
    logging.debug(openai_response)
    logging.debug(f"total_tokens: {total_tokens}")

    # 課金額がわかりやすいよう消費されたトークンを返信に加える
    say(f"<@{user}> {openai_response}\n消費されたトークン:{total_tokens}")


def create_chat_completion(messages: list) -> tuple[str, int]:
    """OpenAI API を呼び出して、質問に対する回答を生成する関数

    Args:
        messages: チャット内容のリスト

    Returns:
        GPT-3.5 の生成した回答内容
    """
    # openai の GPT-3 モデルを使って、応答を生成する
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",  # gpt-3.5-turbo を指定すると ChatGPT に近い文章が生成されます
        messages=messages,
        # TODO: max_tokens を指定すると token over になる
        # max_tokens=4096,  # 生成する応答の長さ 大きいと詳細な回答が得られますが、多くのトークンを消費します
        stop=None,
    )
    openai_response = response["choices"][0]["message"]["content"]
    total_tokens = response["usage"]["total_tokens"]
    # 応答のテキスト部分を取り出して返す
    return (openai_response, total_tokens)

# Cloud Functions で呼び出されるエントリポイント
@functions_framework.http
def slack_bot(request: Request):
    """slack のイベントリクエストを受信して各処理を実行する関数

    Args:
        request: Slack のイベントリクエスト

    Returns:
        SlackRequestHandler への接続
    """
    header = request.headers
    logging.debug(f"header: {header}")
    body = request.get_json()
    logging.debug(f"body: {body}")

    # URL確認を通すとき
    if body.get("type") == "url_verification":
        logging.info("url verification started")
        headers = {"Content-Type": "application/json"}
        res = json.dumps({"challenge": body["challenge"]})
        logging.debug(f"res: {res}")
        return (res, 200, headers)
    # 応答が遅いと Slack からリトライを何度も受信してしまうため、リトライ時は処理しない
    elif header.get("x-slack-retry-num"):
        logging.info("slack retry received")
        return {"statusCode": 200, "body": json.dumps({"message": "No need to resend"})}

    # handler への接続 class: flask.wrappers.Response
    return handler.handle(request)
requirements.txt
functions-framework==3.3.0
openai == 0.27.0 # ChatCompletion は 0.27.0 以降で対応
slack-bolt == 1.16.1
google-cloud-logging == 3.5.0
flask == 2.2.2
python-box == 7.0.0

呼び出し URL の設定

Cloud Functions の呼び出し URL を Slack app に設定する
(URL を入力するとテストが送信され、確認済みになると「Verified」に✅がつきます)
slack_event_url

Subscribe to bot events で app_mention を設定

slack_event_appmention

これで、アプリにメンションを送信すると、Cloud Functions が起動するようになります。

最後に、ワークスペースにアプリをインストールしましょう。

アプリの設定が完了したので、SlackBot にメンションを送ってみましょう。

slack_install_app

問題なければ、しばらくして返信が返ってくるはずです。

chat_test

📌おわりに

API キーの発行やら SlackApp の準備やらは少し面倒ですが、Cloud Functions を利用すればかなり簡単に ChatGPT風の SlackBot が作成できました。
実際の ChatGPT のようにセッションの会話履歴から継続して回答を生成してくれる機能は実装できていないので、次の段階はセッション機能の実装ですね。
ChatGPT API はかなり安価なのでぜひ皆さんも試してみてください。

読んでくださってありがとうございました。
https://twitter.com/tsunotto

📕API ドキュメント

Slack

GCP

OpenAI API

GitHubで編集を提案

Discussion

munimurahomunimuraho

こんにちは。参考にさせていただき、実装してみました。
slackから作成したファンクションのURLの呼び出しに失敗するのですが、何か心当たりはありますでしょうか?
slack側の設定は全て確認済みです。

Cloud Function側に問題があるのでは、と思っています。(ソース、もしくは設定)

・試してみたこと
シークレットの呼び出しに失敗しているのでは?と思い、ソースにbotトークンを直接記入するも同じく失敗。

slack_token = "xoxb..."

・心当たり?
エントリポイントは「slack_bot」で大丈夫でしょうか?

ランタイム、ビルド、接続は変更していません。
もし何か心当たりがあれば教えていただけないでしょうか。

Yuji TsuritaniYuji Tsuritani

閲覧ありがとうございます!
Slack の設定は無事されているとのことなので、GCP側の認証設定についてご確認をお願いします。
エラーメッセージがあるとより正確にコメントできると思いますが、Cloud Run の呼出しの認証設定は「未認証の呼び出しを許可」を選択されておりますでしょうか?
デフォルトでは、サービスアカウントやそのほかの認証されたユーザーしかURLを呼び出せない設定になっていると思います。
Cloud Functions 第2世代は裏でCloud Runが走っているので、そちらの認証設定も合わせて確認されるといいと思います。

Cloud Functions の設定

Cloud Runの設定

munimurahomunimuraho

お返事ありがとうございます!
cloud Runの設定、アドバイス通りにしたところ、slack側の方でのEvent Subscriptionの認証リクエストは通るようになりましたが、いざbotにメンションをしてみるとレスが帰ってこず、以下のようなCloud Function側で以下のような401エラーを吐いていました。もし何かヒントがありましたらいただけると幸いです・・・!

httpRequest: {
latency: "0.004401535s"
protocol: "HTTP/1.1"
remoteIp: "34.204.173.142"
requestMethod: "POST"
requestSize: "1681"
requestUrl: "https://chotgpt-5ovjtda36a-an.a.run.app/"
responseSize: "685"
serverIp: "216.239.38.53"
status: 401
userAgent: "Slackbot 1.0 (+https://api.slack.com/robots)"
}

Yuji TsuritaniYuji Tsuritani

401は認証エラーなので、どこかで認証ができてないと思われます。
Slack ➡ GCP はクリアしているので、次は GCP ➡ Slack もしくは、 GCP ➡ OpenAI API あたりかとおもいます。

MK00MK00

つんさん
初めまして、わかりやすい記事ありがとうございます。

SlackのEvent Subscriptionsで
Your URL didn't respond with the value of the challenge parameter.
のエラーが返ってきてしまいますが、原因分かりますでしょうか。。?

特にソースコードは変更しておりません。

ShintaroAmaikeShintaroAmaike

次のように修正後うまく受信できるようになりました。
Slack App →Basic Information→Signing Secretを取得
Cloud Functionsのランタイム環境変数にSLACK_SIGNING_SECRETを設定

コードを一部修正

slack_signing_secret = os.environ.get("SLACK_SIGNING_SECRET")
app = App(token=slack_token, signing_secret=slack_signing_secret, process_before_response=True)