Amazon BedrockとLangChainとAWS LambdaでLINEチャットボットを作成する

LLMとLangChainを好きに触れる環境が欲しかったので、Amazon Bedrockを使用してチャットボットを作成しました。LLMはClaude2を使用しています。
UIとしてはLINEチャネルを使用します（作らなくて済むので）。LINEから送られたメッセージをそのままAPI Gateway-Lambda-Bedrockと素通しして応答します。チャットの履歴はLangChainの機能でDynamoDBに格納します。

:::note warn
2023/10/11
DynamoDBのテーブル名に誤記があったので修正しました
:::
:::note warn
2023/10/16
boto3とlangchainのバージョンを上げたところプログラム内でboto3を呼ぶ必要が無くなったのでプログラムとrequirements.txtを修正しました
:::

Bedrockの価格は以下をご確認ください。

https://aws.amazon.com/jp/bedrock/pricing/

動作イメージ
<img width="200" alt="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/b3c811dc-1e83-c9d7-865c-0f4015c2df82.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/b3c811dc-1e83-c9d7-865c-0f4015c2df82.jpeg">　<img width="200" alt="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/b6083115-bc32-070a-1214-68ba589a93d1.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/b6083115-bc32-070a-1214-68ba589a93d1.jpeg">

アーキテクチャ
![line_architecture.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/d1535630-59fe-11e6-9282-610909d6d948.gif)


# 目次
1. LINEチャネルの作成
1. AWS環境の前準備
1. ローカル環境の準備
1. Dockerイメージの作成
1. Lambda関数の作成
1. DynamoDBの設定
1. API Gatewayの設定
1. LINEの設定

# LINEチャネルの作成
下記を参考にLINEチャネルを作成します。MessagingAPIを使用します。

https://developers.line.biz/ja/docs/messaging-api/getting-started/#step-three-confirm-channel

https://developers.line.biz/ja/docs/messaging-api/building-bot/

チャネルアクセストークンとチャネルシークレットを後で使用します。
Webhook URLは最後に設定します。

# AWS環境の前準備
- バージニア北部(us-east-1)リージョンのClaude2を使用可能にします。マネコンからRequestしてください

https://dev.classmethod.jp/articles/if-you-want-to-try-out-amazon-bedrock-from-the-management-console-you-can-set-up-access-to-base-models/


:::note info
2023/10/9現在 LLMの選択肢が多いバージニア北部リージョンを使用します
:::
- Bedrockアクセス用のIAMポリシーの作成
```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Allow",
            "Action": "bedrock:*",
            "Resource": "*"
        }
    ]
}
```
- IAMユーザーの作成とcredential(アクセスキー、シークレットアクセスキー)の発行
- 同IAMユーザーに対するECRポリシー(AmazonEC2ContainerRegistryFullAccess)のアタッチ
- 同IAMユーザーでバージニア北部(us-east-1)リージョンにECR(Amazon Elastic Container Registry)のプライベートリポジトリを作成（line_bedrock_chat)
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/8b7f0df1-d9f5-b3a9-2486-5f2d13c9435a.gif">

# ローカル環境の準備
- Amazon CLIのインストール (上述のcredentialを設定)
- Docker Desktopのインストール
- 作業用フォルダの作成

# Dockerイメージの作成
LangChainが巨大でLambdaの250MB制限を超える為、コンテナイメージを使用したLambda関数の作成を行う必要があります。

ローカル環境の作業用フォルダに以下の3ファイルを作成します。

```Python:line_bedrock_chat.py
import os
import json
from langchain.llms import Bedrock
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.memory import ConversationBufferMemory
from langchain.memory.chat_message_histories import DynamoDBChatMessageHistory
import base64
import hashlib
import hmac
from linebot import LineBotApi
from linebot.models import TextSendMessage

def handler(event, context):
    
    # LINE用 初期処理
    LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
    CHANNEL_SECRET = os.environ['LINE_CHANNEL_SECRET']
    LINE_BOT_API = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)

    # LINEからの正規リクエスト以外は終了
    x_line_signature = event["headers"].get("x-line-signature") or event["headers"].get("X-Line-Signature")
    req_body_str = event["body"]
    hash = hmac.new(CHANNEL_SECRET.encode('utf-8'),
        req_body_str.encode('utf-8'), hashlib.sha256).digest()
    signature = base64.b64encode(hash)
    if signature != x_line_signature.encode(): raise Exception

    # リクエストの取得
    req_body = json.loads(event['body'])

    # チャット履歴をDynamoDB.SessionTableに格納する
    # SessionIdにはLINEのUserIdを使用する（個人単位でチャット履歴を管理する）
    session_id = req_body['events'][0]['source']['userId']
    message_history = DynamoDBChatMessageHistory(table_name="SessionTable", session_id=session_id)
    memory = ConversationBufferMemory(
        memory_key="chat_history", chat_memory=message_history, return_messages=True
    )

    # LINEからのメッセージを受信
    if req_body['events'][0]['type'] == 'message':
        if req_body['events'][0]['message']['type'] == 'text':
            query = req_body['events'][0]['message']['text']

            # promptの定義
            prompt = PromptTemplate(
                input_variables=["chat_history","Query"],
                template="""あなたは優秀なAIアシスタントです。名前はChatClaudeです。\\n\\n{chat_history}\\nHuman: {Query}\\nAssistant:"""
            )

            # LLMの定義
            llm = Bedrock(
                model_id='anthropic.claude-v2', # Claude2
                model_kwargs={'max_tokens_to_sample': 4096} # Outputの最大トークン
            )
            
            # LLMChainの定義
            llm_chain = LLMChain(memory=memory,prompt=prompt,llm=llm)

            # LLMChainの実行
            answer = llm_chain.predict(Query=query)

            # Bedrockの回答をLINEに返却
            replyToken = req_body['events'][0]['replyToken']
            LINE_BOT_API.reply_message(replyToken, TextSendMessage(text=answer))

    return {'statusCode': 200, 'body': json.dumps('Reply ended normally.')}
```
```Dockerfile:Dockerfile
FROM public.ecr.aws/lambda/python:3.11

# Copy requirements.txt
COPY requirements.txt ${LAMBDA_TASK_ROOT}

# Copy function code
COPY line_bedrock_chat.py ${LAMBDA_TASK_ROOT}

# Install the specified packages
RUN pip install -r requirements.txt

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "line_bedrock_chat.handler" ]  
```
```txt:requirements.txt
boto3==1.28.63
langchain==0.0.313
line-bot-sdk==3.5.0
```
:::note warn
- Claude2高いよ!という方は`claude-v2`を`claude-instant-v1`に置き換えてください
:::

:::note info
- ECRのコンテナイメージはPython3.11としました。Python 3.10で構築した時には古いBoto3を参照し続けるという事象がありました
- boto3とlangchainはバージョンアップが速いので最新が出ている場合は最新で試してみてください
- LLMに与えているpromptは`template`が全てなのでカスタマイズしたい場合は変更してみてください
:::

作成が終わったら該当フォルダに移動して、Docker Desktopが起動した状態で以下のコマンドを実行します。${ACCOUNTID}はご自身のAWSアカウントIDに置き換えてください。
```sh
aws ecr get-login-password | docker login --username AWS --password-stdin ${ACCOUNTID}.dkr.ecr.us-east-1.amazonaws.com
docker build -t line_bedrock_chat.py .
docker tag line_bedrock_chat.py:latest ${ACCOUNTID}.dkr.ecr.us-east-1.amazonaws.com/line_bedrock_chat:latest
docker push ${ACCOUNTID}.dkr.ecr.us-east-1.amazonaws.com/line_bedrock_chat:latest
```

# Lambda関数の作成
バージニア北部リージョンにコンテナイメージでLambda関数の作成を行います。
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/e6248059-0ba6-0d6b-438c-413228676846.gif">
コンテナイメージは先ほど作成したリポジトリのlatestを選択します。
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/53e0317b-f92c-246f-19ec-0774d1f56712.png">
作成が終わったら幾つか設定を行います。

- 設定-一般設定-タイムアウトを2分に変更 ※回答が揃ってから返信する為、少し時間がかかります
- 設定-環境変数に`LINE_CHANNEL_ACCESS_TOKEN`と`LINE_CHANNEL_SECRET`を設定。値は冒頭のLINE Developersから取得します。
- 設定-アクセス権限からLambdaの実行ロールに対して以下を設定します。
    - 前準備で作成したBedrockのポリシーのアタッチ
    - AmazonDynamoDBFullAccessポリシーのアタッチ（フルじゃなくても良いですが）

これでLambdaの設定は完了です。

# DynamoDBの設定
バージニア北部リージョンにテーブル名 `SessionTable`、パーテーションキー `SessionId` で作成します。
このテーブルにチャット内容がユーザー単位で全て保存されますので、削除は適宜ご対応ください。
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/bf90dc06-9dd4-3577-781a-7b96362f555b.gif">

# API Gatewayの設定
バージニア北部リージョンで
- APIの作成 → REST API → 構築 → API名に任意の値を入力してAPIの作成
- 以下のように設定して保存(ポップアップはOKしてください)
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/7ee1dba8-e562-012d-50fd-2d072ad43331.gif">

:::note warn
Lambdaプロキシ統合の使用は重要です。リクエストのRAWデータをそのままProxyしないとLINEからのリクエストの正当性確認（LINEの署名の確認）がNGになります
:::
- APIのデプロイ
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/76ae5ad2-5671-b705-977f-f27793736324.gif">

- URLをコピーしておきます。
<img width="30%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3573242/f9abf97a-2041-4470-5cc8-0d413d63a09e.gif">

# LINEの設定
- LINE DeveloperのWebhook URLにAPI GatewayのURLを設定します
- QRコードからご自身のLINEにチャネル登録します

これで冒頭のように使用可能になります。

# 最後に
本当はLangChainのAgent機能を使用したWeb検索等を行いたかったのですが、Claude2ではどうしてもpromptが正しく生成されない（場合がある）ので一旦中断しました。LangChainのUpdateかBedrockAgentに期待しています。

書いた後にこちらを読んで気付きましたがLINE周りはもう少し頭の良い実装が出来ましたね。
~~そのうち直すかも。~~ 今のままの方が処理をそのまま書いているので可読性の観点からこのままとしておきます。

https://qiita.com/moritalous/items/6e0864d724d04d4ecf22

https://qiita.com/cyberBOSE/items/fe66c2049aaa04568d61