ChatGPT APIをSlackから利用できるようにしてみた

# はじめに
最近ChatGPTが流行ってますよね。
筆者も論文の要約や検索、英語翻訳などに利用させていただいております。
そんな[ChatGPTのAPI](https://openai.com/blog/introducing-chatgpt-and-whisper-apis)が2023/3/1に一般公開されたとのことで、Slackから簡単に利用できるようにしたい！！ということでAWSの勉強も兼ねて実装してみました。
備忘録的に記事を書いてみます！
今回のシステムの簡単なアーキテクチャ図はこちらになります！！
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/36271b1b-c1dc-8025-68f4-4120ba576d0d.png)
実際に遊んでみるとこんな感じです笑
![16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/1e71b259-8b8e-986f-1822-89e3f01a9878.png)
# 実装
早速ですが、実装について説明していきます。
ステップとしては以下の３つです。
1\. OpenAIのAPIキーの取得
2\. SlackAppの作成
3\. SlackAppの機能実装(AWS)

## 1. OpenAIのAPIキーの取得
これがないとChatGPT APIが利用できません。
ということで、[OpenAI](https://platform.openai.com/overview)にアクセスして、アカウントを作成しましょう。

作成が完了したら、右上の`Personal`から`View API keys`をクリックします。
![１８.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/0b74b4a9-053c-a616-c13e-2723075c9e68.png)

その後`Create new secret key`をクリックして、API keyを作成します。（**この際に生成されたkeyは一度しか見れないので、忘れないようにどこかにメモしておきましょう！！**）
![スクリーンショット 2023-03-02 20.53.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/8a8ac908-0166-51e2-8375-dc68c8139c91.png)

OpenAIのAPIキーについてはこれで完了です！！

## 2. SlackAppの作成
次はSlack上でユーザからのメッセージを受け取り、ChatGPTに流すためのSlack Appを作成します。
ここでは、Slack Appの作成のみにとどめ、具体的な機能実装については3.で説明します。

[Slack Apps](https://api.slack.com/apps)にアクセスし、`Create New App`をクリックします。
その後、`From scratch`を選択します。
名前やWorkspaceに関しては自由に設定してください。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/51ca1948-da31-a487-8ced-8dd2ccb3d58e.png)

### Webhook URLの作成
アプリの作成が完了したら、外部サービスからSlackにメッセージを送信するためのWebhook URLを作成します。
`Add New Webhook to Workspace`をクリックし、メッセージを送信するチャンネルを選択します。
**作成された、URLはアプリの機能実装で利用するのでメモしておきましょう！！**
![スクリーンショット 2023-03-02 21.08.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/198517a9-4091-254b-daca-40c0c3c12ed2.png)

### Event Subscriptionsの設定
Slackでは、ユーザのメッセージ送信などのイベントを受け取るために予めSubscribeするイベントを登録しておく必要があります。
以下の画像のように、ボットが追加されているチャンネルのメッセージ送信イベントを受け取るために`message.channels`イベントを登録するのですが、この操作を行うためにはイベントを送信するLambda関数の機能が必要となるため、後回しにします。
![３.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/b1127def-dd7c-7ceb-d414-87ae4f036514.png)

### Slack Appをチャンネルに追加
作成したSlack Appをメッセージを送受信するためのチャンネルに追加しましょう。
Slackの追加したいチャンネルを開き左上にあるチャンネル名をクリックすると以下のような画面が出てきます。
そこで`Integrations`タブに移動し、`Apps`の`Add apps`から作成したSlack Appを選択することでチャンネルに追加できます。
これをしていないと、Slack Appがメッセージを受信できないのでメッセージを送信しても何も反応してくれなくなります。
![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/4c7cd210-f9a4-b63b-291c-da4062332e9d.png)

とりあえず、これでSlack Appの作成は完了です。

## 3. Slack Appの機能実装
ここから、Slack Appの機能実装に入っていきます！！
今回はAWS LambdaとAPI gateway、Cloud9を利用していきます。
参考にした記事は、[AWSを使ったslackチャットbotの構築](https://note.com/arumon_team/n/n95a05aeb8229)と[Cisco WebexとAWS LambdaでOpenAI GPT-3のチャットボットを作る](https://qiita.com/masanobu48154/items/5e939290a7bbdd94ca0d#aws-cloud9)になります。

### AWS Cloud9
まずはAWS Cloud9を使って、AWS Lambdaにデプロイするためのzipファイルを作成します。
pythonの`openai`ライブラリのような標準ライブラリ以外を用いたい場合はAWS Lambdaでpipを使うことができないため、ファイルアップロード機能を用いる必要があります。
[【AWS】Lambda上のPythonで外部ライブラリを使用する方法](https://bftnagoya.hateblo.jp/entry/2021/06/23/074120)のように様々な方法でアップロードは可能ですが、今回はコマンド一つでアップロード可能ということで、AWS Cloud9を利用しました。

#### AWS Cloud9の作成
AWSのCloud9の画面に行き、`Create environment`をクリックすると、以下のような画面に遷移します。
適当な名前を入力し、`create`をクリックします。（その他の設定に関してはデフォルトのままでいいと思います。）
![スクリーンショット 2023-03-02 21.54.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/73f41b1a-cd6d-c502-f7f0-bc0cd69168fd.png)

作成後は、インスタンスが立ち上がるまでに少し時間がかかると思います。
作成が完了し、EnvironmentsページからCloud9 IDEを開くために`Open`をクリックすると以下のような画面が出力されます。
![スクリーンショット 2023-03-02 21.59.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/af40fc25-09d3-1aeb-4a4b-2097d1f1e12c.png)

#### ライブラリのインストール
[openai](https://github.com/openai/openai-python)をインストールします。
まず、python3.9のコンテナを起動し、そのコンテナ上でプロジェクトディレクトリの作成、ライブラリのインストールを行います。
Cloud9 IDEのコンソール上で以下のコマンドを入力します。
```bash:bash 
# コンテナ作成
$ docker run -itd --name py3.9 python:3.9
# コンテナに入る
$ docker exec -it py3.9 bash
# ------コンテナ内------
$ cd
$ mkdir slack_chatgpt # プロジェクトディレクトリの作成
$ cd slack_chatgpt/
$ mkdir package #ライブラリをインストールするpackageディレクトリの作成
$ pip3 install --target ./package openai #openaiライブラリのインストール
$ exit #コンテナから出る
# ------コンテナ内------
$ docker cp py3.9:/root/slack_chatgpt ./ #コンテナ上のプロジェクトディレクトリをホストOSへコピー
$ cd slack_chatgpt/package/
$ zip -r ../my-deployment-package.zip . #ライブラリのzip化
$ cd ../
$ touch lambda_function.py #SlackAppの機能を実装するためのファイルを作成
```

#### Slack Appの機能を実装するpythonファイルの作成
ソースコードは以下の通りです。
基本的には、Slackメッセージの最初でSlack Appがメンションされているメッセージに対して、ChatGPTを用いた回答を返信するという機能となっています。
[Slackからのリクエスト形式](https://platform.openai.com/docs/guides/chat/response-format)や[ChatGPTからのレスポンス形式](https://platform.openai.com/docs/guides/chat/response-format)を参考に実装しています。
```python:lambda_function.py
import json 
import urllib.request 
import ssl
import logging
import openai
import os

openai.api_key = os.environ["OPENAI_API_KEY"]

# CloudwatchLogsのログ記憶ライブラリの使用
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context): 

    # CloudwatchLogsで詳細なログを取得
    logging.info(json.dumps(event))
    
    # Slack Events APIの再送回避(参考URL:https://dev.classmethod.jp/articles/slack-resend-matome/)
    if "x-slack-retry-num" in event["headers"]:
        return {"statusCode": 200}
    
    # メッセージイベントのbodyを取得
    body = json.loads(event["body"])
    
    msg = ""
    
    # Event Subscriptionsのチャレンジステータスを返す
    if "challenge" in body:
        return {
          "statusCode": 200,
          "body": body["challenge"],
          "headers": {
            "content-type": "text/plain"
          }
        }
    elif "user" == body["event"]["blocks"][0]["elements"][0]["elements"][0]["type"]:
        if os.environ["SLACK_APP_ID"] == body["event"]["blocks"][0]["elements"][0]["elements"][0]["user_id"]:
            # メッセージの最初でSlackAppがメンションされていた場合
            for block in body["event"]["blocks"]:
                for elements in block["elements"]:
                    for element in elements["elements"]:
                        if element["type"] == "text":
                            # Slackのテキストやコードブロックなどを一つのmsgとして変数に格納
                            msg += element["text"]
        # ChatGPTにリクエストを送信
        completion = openai.ChatCompletion.create(
          model="gpt-3.5-turbo", 
          messages=[
              {"role": "user", "content": msg}
              ]
        )
        logging.info(completion)
        # ChatGPTからの返答をmsgに格納
        return_msg = completion["choices"][0]["message"]["content"]
    else:
        return
    
    # リクエストヘッダをJSONにする。
    req_headers = { 
        "Content-Type": "application/json", 
    } 

    # JSONにメッセージをつめる。 
    req_json_slack = { 
        "text": return_msg
    } 

    #リクエストを生成してSlackへ投げる。 
    req_slack = urllib.request.Request(os.environ["WEBHOOK_URL"], json.dumps(req_json_slack).encode(), req_headers) 
    urllib.request.urlopen(req_slack) 
```

ファイル作成が完了したら、zipパッケージにlambda_function.pyを追加します。
```bash:bash
$ zip -g my-deployment-package.zip lambda_function.py
```
このあとはLambdaの作業に移りますが、Cloud9からzipファイルのLambdaへのアップロードを行うのでCloud9 IDEは開いたままが良いと思います。

### AWS Lambda
#### AWS Lambdaの作成
AWSのLambdaのページに行き、`関数の作成`を選択すると以下のような画面が出てきます。
適当な関数名とランタイムを`python3.9`に設定して、`関数の作成`をクリックします。
![スクリーンショット 2023-03-02 22.31.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/c3f0c729-4c89-c49d-a9bf-8b46e581bfc2.png)

####　環境変数の設定
lambda_function.pyで使用している環境変数の設定を行います。
作成したLamdaの設定タブに移動して、`編集`ボタンをクリックして、設定を行います。
キーの`OPENAI_API_KEY`と`WEBHOOK_URL`はそれぞれ[1. OpenAIのAPIキーの取得](##-1.-OpenAIのAPIキーの取得)と[Webhook URLの作成](###-Webhook-URLの作成)で作成したものを設定します。
`SLACK_APP_ID`は以下の画像のような画面がSlack上で作成したアプリをクリックすることで表示されるので`Member ID`をコピーして設定します。
![スクリーンショット 2023-03-02 22.37.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/e3bfb824-703e-8b7a-8c23-200123877771.png)
設定が完了すると以下のような画面になります。
![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/9c0003a4-0acd-437f-a6ac-c4a8a55ab99e.png)


#### ファイルのアップロード
ここで先程Cloud9で作成した、zipファイルをLambdaにアップロードします。
Cloud9のコンソールで以下のコマンドを入力します。（FunctionNameは自信がLambda作成時に設定した名前に置き換えてください）
```bash:bash
aws lambda update-function-code --function-name <FunctionName> --zip-file fileb://my-deployment-package.zip
```

### AWS API Gateway
AWSのAPI Gatewayのページに行き、`APIを作成`をクリックします。
以下のような画面に遷移するのでHTTP APIの`構築`をクリックします。
![スクリーンショット 2023-03-02 22.53.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/f594d082-e2b5-87c1-cb60-5e215cb1e613.png)
次の画面で`統合を追加`をクリックすると以下のような画面になるので、先程作成したLambda関数を選択します。
API名は適当なものをつけてください。
![20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/14ce886c-b4c7-9b24-3c8c-c1862de370d1.png)
次のルートを設定ではデフォルトのままで良いですが、リソースパスは後々必要となるので覚えておいてください。
![スクリーンショット 2023-03-02 22.53.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/7af08cfe-0402-d3fe-3a3e-b4b99d381199.png)
Step3のステージを定義はデフォルトのままで良いので、Step4で`作成`をクリックすると完了です。

### Event Subscriptionsの設定
ここで、[Event Subscriptionsの設定](###-Event-Subscriptionsの設定)で後回しにしていた設定を行います。
Event Subscriptionsのページに行き、`Enable Events`をonにすると以下のような画面になると思います。
![３.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/b1127def-dd7c-7ceb-d414-87ae4f036514.png)
ここで、Request URLに下記のAPI Gatewayで`URLを呼び出す`のURLに[AWS API Gateway](###-AWS-API-Gateway)でのリソースパスを追加したURLを入力します。
入力後、Verifiedと緑の文字が表示されると認証成功です。
[url verification](https://api.slack.com/events/url_verification)に関して詳しく知りたい方はどうぞ
![12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/c8d46476-b282-c70b-6f2f-6d8eaaaf5011.png)
成功後は、Subscribe to bot eventsタブから`Add Bot User Event`をクリックし、`message.channels`を選択し、画面下部の`Save Changes`をクリックし、アプリのワークスペースへの再インストールを行います。

# 動作確認
皆様、お疲れさまでした。（筆者も書いてて疲れました笑）
また、ここまで、お読みいただき誠にありがとうございます！！
では、実装したSlackAPPを利用してみましょう！
メッセージの最初で作成したアプリをメンションして、メッセージを送信すると回答が返ってきます！！！（作成する前に聞いてみるべきでしたね．．．）
![１３.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/a24795a2-91e2-6279-9356-3da9b22f81b3.png)

# おわりに
今回、「ChatGPT APIを使いたい！」、「だったら普段から作りなれているSlack Appで作ってみるか！！」、「どうせならあまり使ったことないAWSで実装してみるか！！！」と思いつきで始め、なんとなくまとめてみたくなって筆をとった次第です笑
読みにくい、または分かりづらい、記事通りに実装したのに動かない、もっとこうした方が良い等がございましたらコメントいただけますとできる範囲で対応、修正していきたいと思います！
最後に、煽りスキルの高いChatGPTさんとの会話を載せて示させていただきます。
お付き合いいただき誠にありがとうございました:bow_tone1:
![15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1784051/46079524-685a-dd17-fc58-9aca2d1aa062.png)