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

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

# 概要

## 目指すアーキテクチャ
![](https://storage.googleapis.com/zenn-user-upload/795b553f01ad-20230121.png)
⇒ 流れは数字の通りで、登場人物としては以下の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.
![](https://storage.googleapis.com/zenn-user-upload/3661f1480d2d-20230120.png)
⇒ [Create New App] をクリックします。

1-2.
![](https://storage.googleapis.com/zenn-user-upload/b81ad8d3ae58-20230121.png)
⇒ 「From scratch」をクリックします。

1-3.
![](https://storage.googleapis.com/zenn-user-upload/d9aab9e57c67-20230121.png)
⇒ App Name : 任意
⇒ Pick a workspace to develop your app in : 導入したい Slack ワークスペース
⇒ 上記の通り設定し、[Create App] をクリックします。

1-4.
![](https://storage.googleapis.com/zenn-user-upload/49d4f7d3f85d-20230121.png)
⇒ 「OAuth & Permissions」をクリックします。

1-5.
![](https://storage.googleapis.com/zenn-user-upload/2d29de07f169-20230121.png)
⇒ Scopes を上記のように設定します。

1-6.
![](https://storage.googleapis.com/zenn-user-upload/3f4b95ed81d5-20230121.png)
⇒ [Install to Workspace] をクリックします。

1-7.
![](https://storage.googleapis.com/zenn-user-upload/31e7f06bac51-20230121.png)
⇒ Slack の権限リクエストのウィンドウが開くので[許可する]をクリックします。

1-8.
![](https://storage.googleapis.com/zenn-user-upload/257a0cb923eb-20230121.png)
⇒ 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.
![](https://storage.googleapis.com/zenn-user-upload/9fc431093a3c-20230121.png)
⇒ APIキー 発行後の画面。


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

### 3-1. 一般設定
![](https://storage.googleapis.com/zenn-user-upload/b80b779ba09d-20230121.png)

### 3-2. 関数URL
![](https://storage.googleapis.com/zenn-user-upload/3cd85c11a061-20230121.png)
⇒ 実運用の際は認証タイプを設定してください。

### 3-3. ランタイム設定
![](https://storage.googleapis.com/zenn-user-upload/32f03aecbca7-20230121.png)
⇒ `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.
![](https://storage.googleapis.com/zenn-user-upload/e802fb72c7ee-20230121.png)
⇒ Enable Events を ON にします。

4-2.
![](https://storage.googleapis.com/zenn-user-upload/c8e23bccd393-20230121.png)
⇒ 前述した 関数URL を設定します。

4-3.
![](https://storage.googleapis.com/zenn-user-upload/440770d79d53-20230121.png)
⇒ 前述した通り Lambda関数 が設定してあればこの時点で `Verified` になるはずです。

4-4.
![](https://storage.googleapis.com/zenn-user-upload/0fe19679ee99-20230121.png)
⇒ 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-api` と `openai` の2つです。

### 5-3. Lambda関数レイヤーの作成
![](https://storage.googleapis.com/zenn-user-upload/d6897a715eaa-20230121.png)
⇒ 5-2. でアップロードした zipファイルを AmazonS3のリンクURLに設定します。


## 6. Lambda関数の作成（続き）

### 6-1. 環境変数
![](https://storage.googleapis.com/zenn-user-upload/fa303e38d8b6-20230121.png)
⇒ `OPENAI_API_KEY` に 4-3. のキーを、 `SLACK_BOT_TOKEN` に 1-8. のキーをそれぞれ設定します。 

### 6-2. レイヤー
![](https://storage.googleapis.com/zenn-user-upload/6a9e1b84df80-20230121.png)
⇒ 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 からスレッドに回答が届きます。

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

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

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

参考にさせて頂いたのは [Slack Events APIの再送仕様と回避方法まとめ(Serverless on AWS)](https://dev.classmethod.jp/articles/slack-resend-matome/) のサイトです。

ここでリトライの事を知り、 `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 が考えてくれました。（なのでコードコメント無いのはそのせいです）
素晴らしい時代になったものです。