AWSのKendraとGPTで自社ドキュメントを学習させたLINEbotを作る

# はじめに

慶應大学 理工学部 情報工学科 B3のぽこちゃんです

今回は技術系の記事を書きます!
学校の実験でAWSを一万円分くらい自由に使わせてもらえたので

タイトルにもあるとおり



### AWSのKendraとGPTで自社ドキュメントを学習させたLINEbotを作った話

https://x.com/tomato_boy77/status/1745732359184380262?s=20

ソースコードも晒すのでぜひ作ってみてね

:::note warn
お金そこそこかかるので気をつけましょう
:::


Xもやってるのでぜひフォローお願いします
フォロバします!

https://twitter.com/tomato_boy77


# アーキテクチャについて

![アーキテクチャ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/9f5e1c19-e904-a425-989e-3c6a344ac08a.png)

全体的にはこんな感じ

1. LINEで質問を投げる
2. S3に保存してあるドキュメント(PDF)からKendraでindexを作る
3. LambdaでLINEから得た質問をクエリにしてKendraで類似するチャンクを3つ取得する
4. チャンクのテキストとLINEの質問を組み合わせてGPTに投げる
5. GPTから得た回答をLINEに出力する


# 早速作っていきましょうか

## Kendraの設定から
AWSのアカウントは用意している前提で話を進めていきます


まずが上部の検索欄から Kendraと調べて下記のページに辿り着きましょう

その後右側の**Create an Index**を押してください

![スクリーンショット 2024-01-12 2.02.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/c1dc2a24-a16b-ffdc-e040-8903cfd3cd21.png)

設定画面に移るのでこのページで設定するのは二つ
1. Index name
1. IAM role

その他はオプションなので必要の応じて設定してください

![スクリーンショット 2024-01-07 9.45.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/20d4865c-2dc3-6021-dc48-c58d5ab3be2c.png)![スクリーンショット 2024-01-12 2.04.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/51f96efe-1a32-d66e-4207-519f4edf0e22.png)



:::note info
IAM roleは青部分で言われているように
**Create a new role**が勧められています
IAM roleについて詳しくなければそうした方が便利です
(自分で権限設定しなければいけないので)
:::

![スクリーンショット 2024-01-12 1.48.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/3bd3558b-251f-4b69-991f-c0f3ea4fa10e.png)

画面下部の**Next**を押して次に進みます
Step2,Step3と進んでいきますが特にこだわりがなければデフォルトで大丈夫です

![スクリーンショット 2024-01-07 9.45.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/0235b2dc-05d4-03f2-c257-d8501af1f24b.png)


**Review and create**まで進んだら
画面下部の**create**を押してIndexを作成します
これ結構時間かかります

その間にデータソースとなるS3の設定をしましょう

## S3の設定

今回はデータソースとしてS3を用います
他にもWebCrawlerの設定もできるので自動でクロールしてきてってのもできます

さっきと同様にS3と検索して
**バケットを作成**を押しましょう

![スクリーンショット 2024-01-12 2.04.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/919a0db6-9acd-50cb-5a17-7c1b60aebd7c.png)

バケットの設定をしていきますが基本的にはデフォルトでいいです
設定したのはバケット名ぐらいですかね

![スクリーンショット 2024-01-07 9.49.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/e3698a0f-8c61-9871-8037-fc94a5d9af37.png)

バケットを作成したら何かドキュメントをアップロード(pdf)しておきましょう
その際に
**s3://hogehoge/**
(hogehogeはバケット名)というS3のURIを得ることができますが
後で使うのでメモっておいてください

そろそろKendraのIndex作成が終わった頃だと思うので戻りましょう

## KendraとS3のSync

Kendraの画面に戻って左側のメニューを見てください

![スクリーンショット 2024-01-12 2.22.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/ce027a87-afbb-0a92-a98f-f778f3fa461e.png)

**Data sources**を選択すると
データソースの候補がたくさん出てきますが今回はS3なので
**S3 connector**というのを探してください
**Add connector** をポチッと

![スクリーンショット 2024-01-07 9.46.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/33e32c6e-5e95-abf5-26d9-3f714e066e5a.png)

connectorの設定画面に移るのでStepに従って進めていきます

ここで変えるのは

- Step1 
    - Data souece name (任意の名前)
    - Default language (データソースの言語)

 

![スクリーンショット 2024-01-07 9.51.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/fc0c5600-4054-e30a-7888-7aa949e1a75c.png)

- Step2
    - IAM role (さっきと同じように新規作成で平気です)

![スクリーンショット 2024-01-07 9.52.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/3d2e609b-4bef-2f34-87ae-660fa6093ba2.png)

- Step3
    - data source location
        - さっき取得した**s3://hogehoge/**
    - maxfile size
        - テキトーに100 MBにしてます
    - Sync run schedule
        - Run on demandにしています
        - 手動で**sync now**ボタンを押したら同期する設定にしてます

![スクリーンショット 2024-01-07 9.52.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/66a544ac-7b3e-6c37-eb1a-b140c9b5380c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/13bcf824-2ffb-3b34-834f-39e14a5b94b1.png)


- Step4,5
    - デフォルトでOKだがfield mappingした方が精度上がるかも?

**Sync now** を押して同期をスタート!
これまたかなり時間かかります(30分くらい??)

その間にLambdaの設定をしちゃいましょう!

## Lambdaの設定

Lambdaの設定をしていきます
ここでは
1. 関数名
1. ランタイム (Python 3.9)
1. アーキテクチャ (x86_64,arm64どちらでも可)
を選択します

:::note alert
Pythonのバージョンは3.9にしましょう!
OpenAI,boto,LINESDKと色々importするのですが
各ライブラリの互換が取れるのが3.9だったと思います
3.11でやってたらクソ苦労しました
まじで半日かけた
:::

![スクリーンショット 2024-01-12 2.40.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/5237442d-30d2-4dfe-ccde-6f915f9483e1.png)

関数ができたらこっから設定していきます


![スクリーンショット 2024-01-12 2.48.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/4646b73a-36ba-2203-eb73-dfde4c942591.png)

## Lambdaのコードについて
コードは以下の通り
設定していただく必要があるのは
1. "LINE_CHANNEL_ACCESS_TOKEN"
1. "OPNEAIのAPIキー"
1. 'KendraのindexID'
1. 種々のプロンプト

KendraのindexIDってなんだって話ですが
kendraに戻っていただいて以下の赤丸で囲った部分です!


![スクリーンショット 2024-01-12 3.01.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/c200b5d1-6feb-dde3-9a90-0d0d517dc091.png)

``` python
import boto3
import json
import langchain
import os
import openai
from openai import OpenAI
from linebot import LineBotApi
from linebot.models import TextSendMessage
kendra = boto3.client('kendra')

LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
LINE_BOT_API = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
openai_API_key = "OPNEAIのAPIキー"
os.environ["OPENAI_API_KEY"] = openai_API_key
openai.api_key = os.environ.get('OPENAI_API_KEY')
client = OpenAI()
def lambda_handler(event, context):
    replyToken = event['events'][0]['replyToken']

    
    if event['events'][0]['type'] == 'message':
        if event['events'][0]['message']['type'] == 'text':
            # LINEからのメッセージをクエリにする
            query_text = event['events'][0]['message']['text']

    index_id = 'KendraのindexID'  

    response = kendra.retrieve(
        QueryText=query_text,
        IndexId=index_id,
        AttributeFilter={
            "EqualsTo": {
                "Key": "_language_code",
                "Value": {"StringValue": "ja"},
            },
        },
    )

    # Kendra の応答から最初の3つの結果を抽出
    results = response['ResultItems'][:3] if response['ResultItems'] else []

    extracted_results = []
    for item in results:
        content = item.get('Content')
        document_uri = item.get('DocumentURI')

        extracted_results.append({
            'Content': content,
        })
    print("Kendra extracted_results:" + json.dumps(extracted_results, ensure_ascii=False))
    
    prompt = get_prompt(
        ref_list=extracted_results,
        query=query_text)
        
    completion = client.chat.completions.create(
    model="gpt-4",
    messages=[
    {"role": "user",
    "content":str(prompt)}
        ]
    )
    print(completion.choices[0].message)
    # LINEにChatGPTの回答を送る
    messageText = completion.choices[0].message
    LINE_BOT_API.reply_message(
        replyToken,
        TextSendMessage(text=messageText.content))
    return None

    
def get_Hypothesis(
    query:str
    )->str:
        assert query != None
        hypothesis = hypothesis_dict(query=query)
        completion = client.chat.completions.create(
        model="gpt-4",
        messages=[
        {"role": "user",
        "content":str(hypothesis)}
                ]
                                                    )
        return str(completion.choices[0].message)
def get_prompt(
    ref_list:list,
    query:str
    )->str:
        assert ref_list != None
        refs_template = ""
        for i,item in enumerate(ref_list):
            content = item['Content']
            refs_template += "・{}".format(content) + "\n"
        prompt = prompt_dict(
            refs_template=refs_template,
            query_text=query
            )
        return prompt


def hypothesis_dict(
    query:str
    ) -> str:
        prompt = """
        背景情報を与えるプロンプト
                 """
        prompt.format(query=query)
        return prompt
def prompt_dict(
    refs_template:str,
    query_text:str
    )->str:
    prompt = """
    欲しい回答を与えるプロンプト
            """
                
    return prompt.format(ref_template=refs_template,input_text=query_text)

```

:::note warn
もしかしたら get_Hypothesis使ってないかもです
API周り友達に任せちゃってたので聞いてみます
:::

## Lambdaのレイヤー設定について

コードはこれでOKですがライブラリが入ってないのでこれを追加します
**ここがいちばんの難関なんですわ**
チームで作る場合はできる人に任せた方がいいです

参考にしたのは記事の以下の部分です

(参考情報) Boto3のバージョンが古い場合の対応方法

https://dev.classmethod.jp/articles/invoke-bedrock-form-lambda-function/

基本的には
1. ローカル環境で仮想環境を用意する
1. ディレクトリを作成
1. 2で作成したディレクトリにライブラリを入れる
1. 3のディレクトリをpythonという名前に変える
1. 4のディレクトリをzipする

:::note warn
- 4でpythonという名前にしないとレイヤーで読み込んでくれない
- zipファイルは大きすぎるとuploadできないのでレイヤー3つぐらいに分けた方がいいです
    - 特にopenaiデカすぎなので
:::

**installするものリスト**
```txt
line-bot-sdk
boto3
autopep8==1.6.0 
certifi==2021.10.8 
charset-normalizer==2.0.7 
click==8.0.3 
et-xmlfile==1.1.0 
idna==3.3 
itsdangerous==2.0.1 
Jinja2==3.0.2 
MarkupSafe==2.0.1 
numpy==1.21.3 
openai==1.3.6 
openpyxl==3.0.9 
pandas==1.3.4 
pandas-stubs==1.2.0.35 
pycodestyle==2.8.0 
python-dateutil==2.8.2 
python-dotenv==0.19.2 
pytz==2021.3 
requests==2.26.0 
six==1.16.0 
toml==0.10.2 
tqdm==4.62.3 
langchain==0.0.344
tiktoken==0.5.1
```
もしかするといらんもの入ってるかも

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1436034/a122b6ed-eca0-2bb1-bb75-85ceef523260.png)

互換性のランタイムの部分はpython3.9を選ぶようにしてください!

## LINEBotの設定
ここまですると話長くなってしまうので以下の記事を参考にしてください
あんまり難しくないです
APIGatewayの設定も必要です!

https://staff-blog.faith-sol-tech.com/aws%E3%81%A7-line-bot-%E3%82%92%E4%BD%9C%E3%82%8B/

**LINEとの接続もできたら完成です!**
**お疲れ様でした!**

## まとめ
AWSを用いて自社ドキュメントを学習させたLINEbotを作ってみました!
今回はS3に保存したデータを用いましたが
他にもデータソース選べるので無限にアイデアが膨らんできますね

ただ一点注意事項として
**お金まあまあ使うので気をつけてください**

Xもやってるのでぜひフォローお願いします

https://twitter.com/tomato_boy77

ご読了ありがとうございました!