Slackで質問に自動回答！ChatGPTとLangChainで社内FAQボットを作ってみた Part3

# 概要
本記事では、Slack BoltとChatGPT、LangChainを用いて社内FAQボットを開発する過程を、Partに分けて解説します。
Part1では独自データを回答する。Part2ではSlack Bolt for Pythonを使ってSlack Botの実装について解説しました。今回のPart3では、Dockerを使ったLambda環境でのデバッグとPart2でできていないslackから独自データを回答するところまでを解説します。

## Dockerでデバッグ
Part2までで作ったコードは最終的にはAWS Lambda上で動かしたいのですが、ここで2つの課題をクリアする為に、Dockerを使います。
1. AWS Lambdaはアップロードしたデプロイパッケージの解凍後のサイズが250MBを超えるとエラーとなります。複数のライブラリを入れるとすぐに上限を超えてしまうため、デプロイ方法を考える必要があります。コンテナイメージは、この制限を回避することができます。（その他にもやり方はいくつかありますが、今回はこれを選択します。）
2. 開発環境では動いたのに本番環境では動かないというトラブルがよくあります。コンテナイメージをデプロイすることで、開発・本番と同じ状態が作れます。

### 1.Docker Desktopインストール
私の環境はWindows10なので、Windows用のDockerをインストールします。参考になる記事を貼り付けておきます。

https://qiita.com/R_R/items/a09fab09ce9fa9e905c5

インストールが完了したら起動します。起動だけで特に操作は必要ありません。

### 2.IDEと拡張機能
VSCodeを使用します。
拡張機能に以下を追加してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536199/6ff239a6-4373-9a3d-049e-42d6443ecce7.png)

Dev Containers拡張機能を使うことで、コンテナをフルタイムの開発環境として使用できます。実行中のコンテナにアタッチしてデバッグもできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536199/5abe7a0a-2f7a-9241-c674-fc1a04b6c684.png)
（画像引用元：https://code.visualstudio.com/docs/devcontainers/containers ）

### 3.コンテナ環境構築と接続
構築にあたり、この記事を参考にさせて頂きました。

https://qiita.com/sasayakadopono/items/cfc7479fff3b685b3932

一応、私の手順も載せておきます。
以下、ファイル構成です。これらのファイルを作ります。
```
qiita/
│
├── .devcontainer/
│   └── devcontainer.json
│
├── docker-compose.yml
├── Dockerfile
├── ...
```

まずDockerfileを作ります。これはコンテナイメージを管理、構築するためのファイルです。
Part1から作っているプロジェクト内に __Dockerfile__ という名前のファイルを作ります。
中身はこんな感じです。

```Dockerfile
# Amazon ECR Public Gallery からイメージを取得します。
FROM public.ecr.aws/lambda/python:3.11

# ワーキングディレクトリを設定
WORKDIR /var/task

# PYTHONPATH の設定
ENV PYTHONPATH="/var/task/app"

# 依存関係のファイルコピー
COPY requirements.txt .

# 依存関係のインストール
RUN pip install --upgrade pip && \
    pip install -r requirements.txt

# appディレクトリを/var/task/app/へコピー
COPY app/ /var/task/app/

# Lambdaの呼び出す関数を指定
CMD ["slack_app.handler"]
```

次にComposeファイルを作ります。これはアプリケーション全体の構成を記述するためのYMLファイルです。Dockerfileと同じパスに __docker-compose.yml__ という名前のファイルを作成します。以下、docker-composeの中身です。詳しい解説は省きますが、後ほどAWSのECRへアップロードしますので、事前にAWSの環境とアクセス用のキーが必要です。
```yml
version: "3"
services:
  dev:
    build:
      context: .
      dockerfile: Dockerfile
    image: faqbot_image
    container_name: faqbot_container
    working_dir: /var/task
    volumes:
      - ./:/var/task
    ports:
      - "3000:3000"
    environment:
      AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY #環境変数に登録したものを参照しています。
      AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY #環境変数に登録したものを参照しています。
    env_file:
      - ./.env
```

最後にdevcontainerファイルを用意して接続に行きましょう。
画面左下の「__><__」のマークをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536199/c28b2d19-c039-7099-dc59-a3fc41552c39.png)

「__開発コンテナー構成ファイルを追加…__」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536199/d3105b13-54e1-1c92-92e7-6a389703e334.png)

「__ワークスペースに構成を追加する__」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536199/f314c86f-3726-633d-c041-667529ce0b70.png)

なにも選択せずに「__OK__」をクリックします。 __devcontainer.json__ という構成ファイルが作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536199/a7199516-f6ca-f255-0317-893a09cf524a.png)

デフォルトで色々書かれてますが、自分が必要な内容に書き換えます。
```json
{
	"name": "slack faqbot app",
	"dockerComposeFile": "../docker-compose.yml",
	"service": "dev",
	"workspaceFolder": "/var/task",
	"settings": {
	  "terminal.integrated.shell.linux": "/bin/bash"
	},
	"customizations": {
	  "vscode": {
		"extensions": [
		  "amazonwebservices.aws-toolkit-vscode",
		  "ms-python.python",
		  "magicstack.MagicPython",
		  "ms-python.vscode-pylance",
		  "microsoft.vscode-docker"
		]
	  }
	},
	"remoteUser": "root",
	"shutdownAction": "stopCompose"
  }  
```

準備が多かったですが、やっと実行できます。画面左下の「__><__」をクリックし、「__コンテナーで再度開く__」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536199/24027533-5e98-8095-b98f-f6e4ddcec3e6.png)

少し時間がかかりますが、開発コンテナーが起動します。これでローカルと同じようにデバッグができます。※docker desktopを起動していないとエラーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536199/f149c113-bb11-d2a3-ee81-03afa2dd2b3b.png)

指定した名前でdockerイメージが作成されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536199/3c7b9586-d876-b960-1687-ad970689fc84.png)

コンテナーも起動しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536199/28ce72cd-805c-ac79-cad3-a4550ed0d28a.png)


### 4.デバッグ
vscodeに戻り、デバッグします。コンテナー上で実行されていることが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536199/705de361-b056-44ca-81aa-42439e87c9fa.png)

ここからは<ins>[Part2](https://qiita.com/i04046/items/88aa605c00d64da0e6a6#bolt-for-python%E3%81%A7bot%E5%AE%9F%E8%A3%85)</ins>で使ったngrokを起動し、Forwarding のアドレスをこれまたPart2で作った<ins>[slackアプリ作成](https://qiita.com/i04046/items/88aa605c00d64da0e6a6#slack%E3%82%A2%E3%83%97%E3%83%AA%E4%BD%9C%E6%88%90)</ins>の「Request URL」にセットして更新します。Slackからのイベントを正常に受信できています。これでDockerコンテナーを利用したデバッグができるようになりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536199/6c267ee8-e5e7-8949-262f-bccd57bacfc3.png)


## Slackで独自データを回答
さて、ここまでで、Dokcerを使ったSlackとのやり取りが完成したので、Slackから質問して独自データを回答してもらいましょう。

### 1.独自データ回答クラス
まず、<ins>[Part1]()</ins>で作成したソースを修正して、Faissインデックスファイルの読み込みと、独自データの回答を返信できるクラスを作ります。以下、ソースです。(ファイル名はchatgpt_client.pyとしています。)
FaissインデックスファイルはPart1で作成したものを使います。「<INS>[Windowsで開発環境を設定する](https://learn.microsoft.com/pdf?url=https%3A%2F%2Flearn.microsoft.com%2Fja-jp%2Fwindows%2Fdev-environment%2Ftoc.json) </INS>」のPDFファイルを読み込ませたものです。
```python
import os
from dotenv import load_dotenv
from langchain_community.vectorstores.faiss import FAISS
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

class ChatGptClient():

    # OPENAI API KEYを環境変数から取得
    ChatOpenAI.openai_api_key = os.environ.get("OPENAI_API_KEY")
    # ベクターストア
    vector_store: FAISS
    
    faiss_index_dir = 'ベクトルデータのファイルを保存したフォルダを指定してください'

    def answer(self, question):
        """質問に対して、独自ドキュメントデータから該当する内容を回答する
        Args:
            question(str): 質問事項
        """

        retriever = self.vector_store.as_retriever()

        # promptの作成
        template = """
        contextに従って回答してください:{context}
        質問: {question}
        """
        prompt = ChatPromptTemplate.from_template(template)
        model = ChatOpenAI(temperature=0.0, max_tokens=1000)

        # ChatGPTへの質問生成
        chain = (
            {"context": retriever, "question": RunnablePassthrough()}
            | prompt
            | model
        )

        # ChatGPTによる回答生成
        ret = chain.invoke(question)

        # 回答を返す
        return ret.content

    def __init__(self):

        index_path = os.path.join(
            os.path.dirname(self.faiss_index_dir), 'index.faiss')

        if os.path.isfile(index_path):
            # Faissインデックス読み込み
            self.vector_store = FAISS.load_local(
                os.path.dirname(self.faiss_index_dir),
                OpenAIEmbeddings(),
                allow_dangerous_deserialization=True)
        else:
            # ファイルがなければindexファイルを作成する処理を書く
            pass
```

### 2.Slackとのやり取りと独自データの回答
次に<ins>[Part2](https://qiita.com/i04046/items/88aa605c00d64da0e6a6#bolt-for-python%E3%81%A7bot%E5%AE%9F%E8%A3%85)</ins>で作成したslack_app.pyのソースを修正して、上のChatGptClinetクラスを使って回答を取得できるようにしましょう。
```python
import os
from chatgpt_client import ChatGptClient
from slack_bolt import App


# ボットトークンと署名シークレットを使ってアプリを初期化します
# SLACK_BOT_TOKENはBot User OAuth Tokenを環境変数に登録したものを参照しています
# SLACK_SIGNING_SECRETはSigning Secretを環境変数に登録したものを参照しています
app = App(token=os.environ.get('SLACK_BOT_TOKEN'),
          signing_secret=os.environ.get('SLACK_SIGNING_SECRET'),
          process_before_response=True)

# ChatGptClientのインスタンス作成
gpt_client = ChatGptClient()

# chatbotにメンションが付けられたときのハンドラ
@app.event("app_mention")
def handle_mentions(event, say, request):

    # 再送リクエストには200を返す
    if request.headers.get("x-slack-retry-num") is not None:
        return {"statusCode": 200}
    
    #ChatGPTを使って独自データを回答する
    res = gpt_client.answer(event["text"])
    say(text=res)


if __name__ == "__main__":
    # python app.py のように実行すると開発用 Web サーバーで起動します
    app.start()
```
>⚡️ Bolt app is running! (development server)

それでは、SlackからFAQボットに対して質問を投稿して回答をもらいます。
FAQボットにメンションをつけてメッセージを送ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536199/0837d4a9-7982-16e4-5077-4de887a479be.png)

回答が返ってきました。Djangoプロジェクトの作成手順を回答していますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536199/98c71d44-c5a7-5a6b-75c3-fbd3a9dcff37.png)

デバッグではありますが、概ねFAQボットの開発は完了しました。
次回はAmazon ECRにコンテナーイメージをアップロードして、AWS Lambda上で動作するようにします。

## 参考文献
https://qiita.com/sasayakadopono/items/cfc7479fff3b685b3932

https://qiita.com/R_R/items/a09fab09ce9fa9e905c5

https://code.visualstudio.com/docs/devcontainers/containers