⚙️

ChatGPT の GPT-3 API を使って Slack bot を作ってみた

2023/01/21に公開5

背景

ChatGPT を皆さんは既に活用しているでしょうか。
ChatGPT が利用するAPIと一般公開されている GPT-3 API は異なるものですが、兎に角性能が高いので皆さんにも使って頂きたいという思いで SlackApp で使えるようにしてみました。

概要

目指すアーキテクチャ


⇒ 流れは数字の通りで、登場人物としては以下の4つです。

Slack(SlackApp)

SlackApp は Event Subscriptions により bot へのメンションイベントをフックして Lambda にコメントを送信します。

Lambda

Slack コメントを受け取り、最初に GPT-3 API にそのままコメントを送信、そのレスポンスを SlackAPI に送信します。

OpenAI(GPT-3)

Lambda からコメントを受信し、GPT-3 のエンジンが回答を返します。

SlackAPI

Lambda から GPT-3 からの回答を受信し、Slack に投稿します。

手順

さっそく手順に沿って説明していきます。

1. Slack Apps (Slack bot) で Token 取得 (OAuth & Permissions)

Slack にログインした状態で https://api.slack.com/apps へアクセスします。

1-1.

⇒ [Create New App] をクリックします。

1-2.

⇒ 「From scratch」をクリックします。

1-3.

⇒ App Name : 任意
⇒ Pick a workspace to develop your app in : 導入したい Slack ワークスペース
⇒ 上記の通り設定し、[Create App] をクリックします。

1-4.

⇒ 「OAuth & Permissions」をクリックします。

1-5.

⇒ Scopes を上記のように設定します。

1-6.

⇒ [Install to Workspace] をクリックします。

1-7.

⇒ Slack の権限リクエストのウィンドウが開くので[許可する]をクリックします。

1-8.

⇒ Token が発行されるので「Bot User OAuth Token」のトークンを保持しておいてください。
⇒ この後のLambda関数の環境変数 SLACK_BOT_TOKEN で利用します。

2. OpenAI APIキー 取得

ChatGPT にログインして https://beta.openai.com/account/api-keys へアクセスします。

2-1.
⇒ 「+ Create new sercret key」をクリックします。

2-2.
⇒ API key generated のウィンドウに APIキー が発行されるので保持しておいてください。
⇒ この後のLambda関数の環境変数 OPENAI_API_KEY で利用します。

2-3.

⇒ APIキー 発行後の画面。

3. Lambda関数の作成

Slack Apps と疎通による認証が必要な為、先に必要な設定だけしてしまいます。
関数URL(Lambda Function urls) を利用しますので設定してください。

3-1. 一般設定

3-2. 関数URL


⇒ 実運用の際は認証タイプを設定してください。

3-3. ランタイム設定


Node.js 18.x を選択します。

3-4. コード

export const handler = async (event) => {
    let json = JSON.stringify(event.body);
    console.log(json);
    return { statusCode: 200, body: json };
};

4. Slack Apps の Event Subscriptions を設定

4-1.

⇒ Enable Events を ON にします。

4-2.

⇒ 前述した 関数URL を設定します。

4-3.

⇒ 前述した通り Lambda関数 が設定してあればこの時点で Verified になるはずです。

4-4.

⇒ Subscribe to bot events をこのように設定して [Save Changes] をクリックします。

5. Lambda関数のレイヤー作成

Lambda関数の作成の前にやっておきます。

5-1. nvm の導入

nodejs のバージョンを Lambda のランタイムと合わせやすいように nvm を導入します。

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
. ~/.nvm/nvm.sh
nvm --version
nvm install v18.13.0

⇒ Lambdaが Node.js 18.x を使うので v18.13.0 を指定しています。

5-2. Lambda関数のレイヤーをアップロード

こここでは S3 経由でアップロードしますので、S3バケットは先に作成しておいてください。

cd nodejs/
npm install @slack/web-api
npm install openai
zip -r nodejs.zip .
aws s3 cp nodejs.zip s3://{bucket}/

⇒ レイヤーに含めるのは @slack/web-apiopenai の2つです。

5-3. Lambda関数レイヤーの作成


⇒ 5-2. でアップロードした zipファイルを AmazonS3のリンクURLに設定します。

6. Lambda関数の作成(続き)

6-1. 環境変数


OPENAI_API_KEY に 4-3. のキーを、 SLACK_BOT_TOKEN に 1-8. のキーをそれぞれ設定します。

6-2. レイヤー


⇒ 5. で作成したレイヤーを選択してください。

6-3. コード

3-4. のコードは削除してしまって大丈夫です。

import { WebClient } from '@slack/web-api';
import { Configuration, OpenAIApi } from "openai";

const slackClient = new WebClient(process.env.SLACK_BOT_TOKEN);
const openaiConfig = new Configuration({
  apiKey: process.env.OPENAI_API_KEY,
});
const openaiClient = new OpenAIApi(openaiConfig);

export const handler = async (event, context) => {
    console.log('event: ', event);
    if (event.headers['x-slack-retry-num']) {
        return { statusCode: 200, body: JSON.stringify({ message: "No need to resend" }) };
    }

    const body = JSON.parse(event.body);
    const text = body.event.text.replace(/<@.*>/g, "");
    console.log('input: ', text);
    
    const openaiResponse = await createCompletion(text);

    const thread_ts = body.event.thread_ts || body.event.ts;
    await postMessage(body.event.channel, openaiResponse, thread_ts);

    return { statusCode: 200, body: JSON.stringify({ message: openaiResponse }) };
};

async function createCompletion(text) {
    try {
        const response = await openaiClient.createCompletion({
          model: "text-davinci-003",
          prompt: text,
          temperature: 0.5,
          max_tokens: 2048,
        });
        console.log('openaiResponse: ', response);
        return response.data.choices[0].text;
    } catch(err) {
        console.error(err);
    }
}

async function postMessage(channel, text, thread_ts) {
    try {
        let payload = {
            channel: channel,
            text: text,
            as_user: true,
            thread_ts: thread_ts
        };
        const response = await slackClient.chat.postMessage(payload);
        console.log('slackResponse: ', response);
    } catch(err) {
        console.error(err);
    }
}

使い方

  1. チャンネルにアプリを追加します。
  2. bot に対してメンション付きでコメントします。
  3. bot からスレッドに回答が届きます。

ポイント

今回の流れのポイントは「親切過ぎたリトライ処理」です。
リトライ処理は LambdaEvent Subscriptions 2つのポイントで行われます。
これに気が付かない限りは延々と「同じような回答が複数届く」現象に悩まされます。

Lambda リトライ

Lambda & node ではお馴染みですが、非同期に処理できない為、同期する為のコードになっています。
これを正しく行えないと GPT-3 API の回答が届いていないのに Slack API に回答を返そうとして失敗、失敗により合計3回のリトライが走ってしまいます。
今回のコードではその辺りは考慮されているので心配しなくて大丈夫です。

Event Subscriptions リトライ

Lambda のリトライは有名過ぎて検知するのに時間は掛かりませんが、普段使わない Event Subscriptions にまさかのリトライ機能が付いていることに気が付くまでには時間が掛かりました。

参考にさせて頂いたのは Slack Events APIの再送仕様と回避方法まとめ(Serverless on AWS) のサイトです。

ここでリトライの事を知り、 x-slack-retry-num で制御する必要性を確認できました。
要約すると、 Event Subscriptions のAPIは3秒以内にレスポンスが無いと自動でリトライしてくれるというものです。

    if (event.headers['x-slack-retry-num']) {
        return { statusCode: 200, body: JSON.stringify({ message: "No need to resend" }) };
    }

⇒ この部分で制御しています。

一言

コード部分はほぼ ChatGPT が考えてくれました。(なのでコードコメント無いのはそのせいです)
素晴らしい時代になったものです。

Discussion

tanabebetanabebe

おかげさまでサクッと作成出来ました!知見をありがとうございます!

___nix______nix___

コメントありがとうございます^^
今後も情報発信していく予定ですので Twitter フォローも宜しければ。

chima91chima91

Slack bot実装できました,ありがとうございます!!
ただ,返信に数分ほどかかるのですが,これが普通なのでしょうか...?

___nix______nix___

コメントありがとうございます!

Slack -①-> Lambda -②-> OpenAI-API

の ①,② のいずれか、或いは OpenAI-API のレスポンスにボトルネックがありそうです。
原因を探るのであれば Lambda関数 のログを見て頂くのが良いかと思います。
(ログが不足でしたら追加してください)

まずはどこが遅い原因になっているのかの調査からしてみてください!

chima91chima91

先ほど試したら,何も変えていないのですが,数秒で返信が返ってくるようになってました...
また遅くなることがあったらログを調査してみます!
いい記事をありがとうございました...!