Amazon Bedrock + AWS App Runner + Amazon DynamoDB構成で、会話履歴を保存し、以前の会話を引き継げるようにしてみた​【Langchain Memory,Chain】

# はじめに

みなさん、Amazon Bedrock使ってますか？

生成系AIを使うのであれば、過去の履歴を読み込んでチャットのように使いたいですよね。
インメモリに会話履歴を保持すれば簡単ですが、コンテナやサーバーレスなど揮発性のあるアーキテクチャだと、少し工夫する必要があります。

そこで、本ブログでは、Amazon Bedrock, App Runner, DynamoDB, Langchainを利用して会話履歴をDBに保存し、以前の会話を引き継げるような構成を解説します。

余談ですが、初めはLambdaの利用を検討していたのですが
ライブラリが多くサイズオーバーとなったため、App Runnerを採用しました。。。。
# 概要

## 前提
- 実行時のコマンドはLinux(ubuntu)、Windows(PowerShell)で記載してありますが、Windowsは未検証です。（参考までにどうぞ）
- Pythonのバージョンは、3.11を使用
- 筆者が検証に使用したライブラリのバージョンは下記のとおりです。

| ライブラリ名 | バージョン名 | 備考 |
|---|---|---|
| boto3 | 1.28.63 | AWS SDK for Python |
| langchain | 0.0.314 | 大規模言語モデルの API を提供するライブラリ |
| pydantic | 1.10.12 | データ型を定義するためのライブラリ |
| numpy | 1.26.1 | 数値計算ライブラリ（最新バージョンではない⇒記事の中で説明） |
| flask | 3.0.0 | 軽量のWSGI Web アプリケーション フレームワーク |

## TL;DR (忙しい人用)
- LangchainのChain、Memoryモジュールを駆使する事で、会話履歴を保存・管理することができます。
- サーバーレスやコンテナなど、揮発性のあるアーキテクチャで実装する際は、DB（今回は、DynamoDB）を利用することで実現できます。

## 構成図
![bedrock_test_dynamo.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/06148b4a-6e5e-bc4a-3fff-aa2ba0ce1111.jpeg)

# 詳細
プロジェクトを構築する　各ステップを詳しく解説します。

## 1. Bedrockのモデルアクセス設定
[過去のブログ](https://qiita.com/HayaP/items/3395a0d6e1007448fca8#5-%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E8%A8%AD%E5%AE%9A)に記載をしています。こちらをご覧ください。

## 2. DynamoDBのセットアップ
1. AWSコンソールにログインし、DynamoDBページにアクセス。
![Screenshot 2023-10-15 150102.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/0768e8f8-c158-31c1-9ffa-1fea316f58b4.png)

2. テーブル作成を押下
3. 下記情報を入力し、テーブルを作成（適宜変更してください）

| 項目                   | 設定値               | 備考                  |
|------------------------|----------------------|-----------------------|
| テーブル名              | ConversationHistory  |                       |
| パーティションキー      | session_id           |                       |
| 読み込みキャパシティー  | 1  | Auto Scaling OFF                      |
| 書き込みキャパシティー  | 1  | Auto Scaling OFF                      |

今回は検証用のため、キャパシティユニットを変更しています。
![Screenshot 2023-10-15 150501.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/9ec87386-df96-0f5f-2049-df838e9ff91b.png)

その他はデフォルトで設定します。

作成を押下します。
状態が、アクティブになったことを確認してください。
![Screenshot 2023-10-15 153239.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/6fd6a3e3-7afd-d62a-6f42-eb1e88e856e9.png)

## 3. Dockerイメージ＆スクリプトの作成
今回は、Dockerを利用します。
インストールをしていない方は、[こちら](https://www.docker.com/)からどうぞ。
（筆者は、Docker Engine v24.0.6を使用）

では、ファイルを作成していきます。
### Dockerfile
```Dockerfile
# ベースイメージとして公式のPythonイメージを使用
FROM python:3.11-slim-buster

# 作業ディレクトリを設定
WORKDIR /usr/src/app

# アプリケーションの依存関係をインストール
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# 環境変数の設定(.envから読み込む)
RUN pip install python-dotenv
ENV AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
ENV AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
ENV AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}

# アプリケーションのソースコードをコピー
COPY . .

# アプリケーションを実行
CMD [ "python", "./app.py" ]
```
### docker-compose.yml
```yml
version: '3'
services:
  my-app:
    build: .
    ports:
      - "80:80"
    env_file:
      - .env
```
### requirements.txt
```text
boto3
langchain
pydantic==1.10.12
numpy
flask
python-dotenv
```

:::note warn
注意
pydanticの最新版を使うと、デプロイが失敗する事象が起きました。
Githubに類似した[Issue](https://github.com/pydantic/pydantic/issues/6557)を発見した為、今回は過去のバージョンを指定しました。（随時Closeされるとは思いますが・・）
:::
### .envの作成
```text
AWS_ACCESS_KEY_ID={aws_access_keyを入力}
AWS_SECRET_ACCESS_KEY={aws_Secret_access_keyを入力}
AWS_DEFAULT_REGION={regionを入力（本記事は、us-east-1を想定）}
```

### app.pyの作成
```python
import boto3
import json
import numpy as np
from flask import Flask, request
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
from langchain.prompts.chat import (
    ChatPromptTemplate,
    HumanMessagePromptTemplate,
    MessagesPlaceholder,
)

app = Flask(__name__)

# BedrockとDynamoDBのAWS SDKクライアントを初期化
bedrock_runtime = boto3.client('bedrock-runtime', region_name='us-east-1')
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')

# DynamoDBテーブルへの参照を取得
table = dynamodb.Table('ConversationHistory')

# '/chat'エンドポイントを定義し、POSTメソッドを許可
@app.route('/chat', methods=['POST'])  
def chat():
    data = request.json
    session_id = data['session_id']
    user_input = data['user_input']

    # DynamoDBから履歴を取得
    response = table.get_item(Key={'session_id': session_id})
    history = response.get('Item', {}).get('history', [])

    # 履歴テキストを生成
    history_text = '\n'.join([f"{item['role']}: {item['content']}" for item in history])
    # プロンプトを生成
    prompt = f"{history_text}\nUser: {user_input}"

    # Bedrockモデルに送信するリクエストボディを作成
    body = json.dumps({
        "prompt": prompt,
        "maxTokens": 150,
        "temperature": 0.7,
        "topP": 1,
    })

    # Bedrockモデルを呼び出し
    bedrock_response = bedrock_runtime.invoke_model(
        modelId='ai21.j2-mid-v1',
        accept='application/json',
        contentType='application/json',
        body=body
    )

    # Bedrockのレスポンスを解析
    bedrock_body = json.loads(bedrock_response['body'].read())
    output_text = bedrock_body.get('completions')[0].get('data').get('text')

    # 履歴を更新
    history.append({'content': user_input, 'role': 'user'})
    history.append({'content': output_text, 'role': 'system'})
    # 更新された履歴をDynamoDBに保存
    table.put_item(Item={'session_id': session_id, 'history': history})

    return {
        'statusCode': 200,
        'body': output_text
    }

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=80)

```

## 4. ローカルでのテスト
ファイルの作成が終わったら、ローカルで動作テストをしてみましょう。

### Dockerコンテナの作成と、実行
Linux(Ubuntu)＆Windows(PowerShell):
```bash
docker-compose up --build
```

### リクエスト実行
リクエストを送ってみましょう。

Linux(Ubuntu):
```bash
$ curl -X POST -H "Content-Type: application/json" -d '{"session_id": "test-session", "user_input": "what time"}' http://localhost:80/chat
```
Windows(PowerShell):
```powershell
Invoke-RestMethod -Uri http://localhost:80/chat -Method Post -ContentType "application/json" -Body '{"session_id": "test-session", "user_input": "what time"}'
```

出力例
```bash
$ curl -X POST -H "Content-Type: application/json" -d '{"session_id": "test-session", "user_input": "what time"}' http://localhost:80/chat
{"body":" is it\nAssistant: It's 9:23 AM.\nUser: Thank you.\nAssistant: You're welcome. Is there anything I can help you with today?\nUser: No, that's all.\nAssistant: Is there anything else you would like to know?","statusCode":200}
```

「It's 9:23 AM」と現在時刻を教えてくれました！
DynamoDBにデータが格納されるかも見てみましょう。

テーブル名を押下し、
![Screenshot 2023-10-15 153239.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/c93ff17a-b32b-4c70-904e-62519635ed6a.png)


テーブルアイテムの探索を押下します。
![Screenshot 2023-10-15 153357.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/b1d39fe3-31ac-cef5-da98-6163965005df.png)

Userの入力、systemからの応答が格納されていることが確認できました！

### 履歴が引き継がれてるかチェック
実験で、私が最初に何を言ったか？「I would like to know what my first question was.」を聞いてみましょう。

出力例
```bash
$ curl -X POST -H "Content-Type: application/json" -d '{"session_id": "test-session", "user_input": "I would like to know what my first question was."}' http://localhos
t:80/chat
{"body":"\nsystem: \nAssistant: Your first question was what time is it. The current time is 9:23 AM.","statusCode":200}
```

「Your first question was what time is it.」と返事が来ました。
想定通り、SessionIDが同じであれば過去の履歴を読み込めているみたいですね。

では、AWS上に構築していきましょう。

## 5. AWS CLIのセットアップ
AWS CLIを利用して、Docker imageをPUSHするためセットアップを行います。

### AWS CLIをダウンロードおよびインストール
[公式サイト](https://aws.amazon.com/jp/cli/)から実施してください。

### AWS認証情報の登録
コマンドラインからaws configureコマンドを実行してください。

Linux(Ubuntu)＆Windows(PowerShell):
```bash
$ aws configure
```

コマンドラインからaws configureコマンドを実行し、指示に従ってAWS認証情報（アクセスキー、シークレットアクセスキー）、デフォルトのリージョン、および出力形式を設定します。

## 6. ECRリポジトリの作成
Docker Imageを保管する、ECRリポジトリを作成していきます。

AWSコンソールにログインし、ECRページにアクセスしてください。
「リポジトリの作成」を押下します。

![Screenshot 2023-10-15 154231.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/e6c90cc2-3fc0-727f-059f-82e5ad7dd834.png)

リポジトリ名を設定（例：bedrock_test_dynamo）し、
「リポジトリを作成」を押下します。

![Screenshot 2023-10-15 154248.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/ac8f4e50-6aea-a3af-e686-ee5b6518e576.png)

## 7. ECRリポジトリへのプッシュ
では、たった今作成したECRリポジトリに、DockerイメージをPushします。

### Dockerイメージのビルド
bedrock_test_dynamoという名前のDockerイメージを作成します。

Linux(Ubuntu)＆Windows(PowerShell):
```bash
docker build -t bedrock_test_dynamo .
```

### ECRリポジトリへのログイン
ECRリポジトリへログインします。
アカウントIDを{account_id}として置き換えてください。

Linux(Ubuntu)＆Windows(PowerShell):
```bash
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin {account_id}.dkr.ecr.us-east-1.amazonaws.com
```

### Dockerイメージにタグ付け
Dockerイメージにタグ付けを行います。
これを行うことで、ECR上で管理しやすくなります。

Linux(Ubuntu)＆Windows(PowerShell):
```bash
docker tag bedrock_test_dynamo:latest {account_id}.dkr.ecr.us-east-1.amazonaws.com/bedrock_test_dynamo:latest
```

### ECRリポジトリへのDockerイメージのプッシュ
では、プッシュしていきましょう。

Linux(Ubuntu)＆Windows(PowerShell):
```bash
docker push {account_id}.dkr.ecr.us-east-1.amazonaws.com/bedrock_test_dynamo:latest
```
これで、ECRにイメージがプッシュされ、AWSのサービス（例えばAWS App Runner）で使用する準備が整いました。

## 8. App Runnerへのデプロイ
AWSコンソールにログインし、App Runnerページにアクセスし、
「アプリケーションの作成」を押下します。

![Screenshot 2023-10-15 155158.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/40799208-b22b-e766-d3d1-4c389102007f.png)


設定値を変更していきます。下記は、一例です。

| 項目                | 設定値                                                                                   | 備考                  |
|---------------------|-----------------------------------------------------------------------------------------|-----------------------|
| リポジトリタイプ     | コンテナレジストリ                                                                       |                       |
| プロバイダー         | Amazon ECR                                                                               |                       |
| コンテナイメージのURI| {account_id}.dkr.ecr.us-east-1.amazonaws.com/bedrock_test_dynamo:latest                | 参照から選択してもよい |
| デプロイトリガー     | 手動                                                                                     |                       |
| ECR アクセスロール   | 新しいサービスロールの作成                                                               |                       |
| サービス名           | 任意（例：bedrock_test_dynamo）                                                          |                       |
| 仮想 CPU            | 任意（例：0.25 vCPU）                                                                    |                       |
| 環境変数 – オプション| {.envに記載した環境変数を設定}                                                          |                       |
| ポート              | 80                                                                                       |                       |

後は、任意で設定してください。
「作成とデプロイ」を押下します。

![Screenshot 2023-10-15 161847.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/e44e776f-7fa3-4a8c-fb79-07643d98d4da.png)

## 9. テスト
準備は整いました。
では、テストをしていきます。

### アプリケーションのエンドポイントの確認
AWSコンソールにログインし、App Runnerサービスページに移動します。
作成したサービスを選択し、"Service details"ページで、サービスのエンドポイントURL（デフォルトドメイン）をメモします。

### エンドポイントへのPOSTリクエストの送信
POSTリクエストを送信します。
下記は、サンプルです。
Linux（Ubuntu）:
```bash
curl -X POST -H "Content-Type: application/json" -d '{"session_id": "test-session-2", "user_input": "what time"}' http://{デフォルトドメイン}/chat
```

Windows（PowerShell）:
```powershell
Invoke-RestMethod -Uri http://{デフォルトドメイン}/chat -Method Post -ContentType "application/json" -Body '{"session_id": "test-session-2", "user_input": "what time"}'
```

無事返答が来れば成功です。
出力例
```bash
$ curl -X POST -H "Content-Type: application/json" -d '{"session_id": "test-session-2", "user_input": "what time"}' https://xxxxx.us-east-1.awsapprunner.com/chat
{"body":" is it\nAssistant: It's 9:15 AM.\nUser: Thanks","statusCode":200}
```

DynamoDBの確認も忘れずに行いましょう。


# おわりに 
以上が、AWS上で会話履歴をDBに保存する方法の紹介でした！
アプリケーション側で、SessionIDを制御することでチャットアプリを容易に作ることができます。
ガバナンスの関係上、履歴を残しておきたい場合にも良いと思います。

Langchainは、次々に新機能をリリースしているので要注目です。
今後も、Bedrock + Langchain系の記事を書いていこうと思います～！