LambdaでLangChain(0.2.11)を動かして、Bedrock・Kendra・DynamoDBのRAGアプリを実装する

# はじめに
お疲れ様です。@yuki_inkです。

**Bedrock・Kendra・DynamoDBを利用してRAGアプリを作りたい！**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/ac6019bd-7730-f514-3d94-fe15fd19a91c.png)

ということで、LangChainを使って実装してみました。
Kendraを利用したRAGに加え、会話履歴のセッション管理を行えるアプリを目指しています。
今回の開発は、以下の記事で紹介されているアプリを参考に行いました。

https://dev.classmethod.jp/articles/amazon-bedrock-with-langchain-invoking-claude3-from-lambda/

>今回は、LangChainが提供する機能を使って以下のようなことを実現したいと思います。
・チャットの会話履歴を記憶して、生成AIに文脈を踏まえた回答をさせる。
・会話履歴をDynamoDBに保存して、Lambda関数を呼び出す度に記憶がリセットされないようにする。
・会話履歴のセッション管理を行うことで、複数ユーザーの会話履歴を生成AIが区別できるようにする。

勉強がてら、今回は以下のクラスを利用しました。
- ChatBedrock
- AmazonKendraRetriever
- DynamoDBChatMessageHistory
- ConversationalRetrievalChain


# やったこと

1. [BedrockからClaudeを利用可能にする](https://qiita.com/yuki_ink/items/62f28d40b726918207c9#1-bedrock%E3%81%8B%E3%82%89claude%E3%82%92%E5%88%A9%E7%94%A8%E5%8F%AF%E8%83%BD%E3%81%AB%E3%81%99%E3%82%8B)
1. [Kendraの設定](https://qiita.com/yuki_ink/items/62f28d40b726918207c9#2-kendra%E3%81%AE%E8%A8%AD%E5%AE%9A)
1. [DynamoDBテーブルの作成](https://qiita.com/yuki_ink/items/62f28d40b726918207c9#3-dynamodb%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90)
1. [Lambda用IAMロールの作成](https://qiita.com/yuki_ink/items/62f28d40b726918207c9#4-lambda%E7%94%A8iam%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90)
1. [Lambdaレイヤーの作成](https://qiita.com/yuki_ink/items/62f28d40b726918207c9#5-lambda%E3%83%AC%E3%82%A4%E3%83%A4%E3%83%BC%E3%81%AE%E4%BD%9C%E6%88%90)
1. [Lambda関数の作成](https://qiita.com/yuki_ink/items/62f28d40b726918207c9#6-lambda%E9%96%A2%E6%95%B0%E3%81%AE%E4%BD%9C%E6%88%90)

今回使用した環境は以下の通りです。
基本的に、記事執筆時点で最新のバージョンを利用しています。

| 項目 | 環境 |
|-|-|
|Lambda ランタイム	|Python 3.12 (x86_64)|
|langchain バージョン	|0.2.11　|
|langchain-aws バージョン	|0.1.15　|
|LLM モデル	|Claude 3.5 Sonnet|
|LLM バージョン	|anthropic.claude-3-5-sonnet-20240620-v1:0|
|リージョン  |バージニア北部 (us-east-1)|



## 1. BedrockからClaudeを利用可能にする
マネジメントコンソールの「モデルアクセス」画面から、利用したいモデルを使えるようにしておきます。
今回はバージニア北部リージョン（us-east-1）のClaudeを利用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/7dec249d-5445-9843-3075-26afffbddca5.png)




## 2. Kendraの設定
非常に便利なCloudFormationテンプレートが公開されていたので、これを利用してリソースを作成しました。

https://dev.classmethod.jp/articles/kendra-s3-cfn/

公開されているCloudFormationで作成できるリソースは以下の通りです。
KendraのデータソースとしてS3が利用されています。

- インデックス用Kendra用IAMロール
- データソース用Kendra用IAMロール
- Kendraインデックス
- Kendra用のCloudWatch Logsロググループ
- Kendraデータソース
- S3バケット
- S3バケットポリシー

### CloudFormationスタックのデプロイ
上記記事で公開されているCloudFormationテンプレートをそのまま流して、リソースを作成します。

<details><summary>CloudFormationテンプレート全量（参考記事から転載）</summary>

```SearchSystemWithKendraAndS3.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Kendra and S3

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Parameters:
          - SysName
          - Env
          - KendraEdition
          - KendraLogRetentionDays
          - KendraDSBucketPrefix

Parameters:
  SysName:
    Type: String
    Default: 'cm'
    Description: 'System name for this stack.'
  Env:
    Type: String
    Default: 'prd'
    Description: 'Environment for this stack.'
    AllowedValues:
      - 'prd'
      - 'stg'
      - 'dev'
  KendraEdition:
    Type: String
    Default: 'ENTERPRISE_EDITION'
    Description: 'ENTERPRISE_EDITION is suitable for production environments. DEVELOPER_EDITION is suitable for development environments.'
    AllowedValues:
      - 'ENTERPRISE_EDITION'
      - 'DEVELOPER_EDITION'
  KendraLogRetentionDays:
    Type: Number
    Default: 365
    Description: 'Retention days for Kendra logs.'
    AllowedValues:
      - 1
      - 3
      - 5
      - 7
      - 14
      - 30
      - 60
      - 90
      - 120
      - 150
      - 180
      - 365
      - 400
      - 545
      - 731
      - 1096
      - 1827
      - 2192
      - 2557
      - 2922
      - 3288
      - 3653
  KendraDSBucketPrefix:
    Type: String
    Default: 'awsdoc'
    Description: 'Bucket prefix for search range.'


Resources:
  ##Role for Kendra Index
  KendraIndexRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: !Sub '${SysName}-${Env}-kendra-index-role'
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: ''
            Effect: Allow
            Principal:
              Service: kendra.amazonaws.com
            Action: 'sts:AssumeRole'

  ##Policy for Kendra Index
  KendraIndexPolicy:
    Type: 'AWS::IAM::ManagedPolicy'
    Properties:
      ManagedPolicyName: !Sub '${SysName}-${Env}-kendra-index-policy'
      Roles:
        - !Ref KendraIndexRole
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Resource: '*'
            Condition:
              StringEquals:
                'cloudwatch:namespace': 'AWS/Kendra'
            Action:
              - 'cloudwatch:PutMetricData'
          - Effect: Allow
            Resource: '*'
            Action: 'logs:DescribeLogGroups'
          - Effect: Allow
            Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/kendra/*'
            Action: 'logs:CreateLogGroup'
          - Effect: Allow
            Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/kendra/*:log-stream:*'
            Action:
              - 'logs:DescribeLogStreams'
              - 'logs:CreateLogStream'
              - 'logs:PutLogEvents'

  ##Kendra Index
  KendraIndex:
    Type: 'AWS::Kendra::Index'
    Properties:
      Name: !Sub '${SysName}-${Env}-kendra-index'
      Edition: !Ref KendraEdition
      RoleArn: !GetAtt KendraIndexRole.Arn

  ##CloudWatch LogGroup for Kendra Index
  KendraIndexLogs:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: !Sub
        - '/aws/kendra/${IndexId}'
        -  IndexId: !GetAtt KendraIndex.Id
      RetentionInDays: !Ref KendraLogRetentionDays

  ##Role for Kendra Data Source
  KendraDSRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: !Sub '${SysName}-${Env}-kendra-ds-role'
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: ''
            Effect: Allow
            Principal:
              Service: kendra.amazonaws.com
            Action: 'sts:AssumeRole'

  ##Policy for Kendra Data Source
  KendraDSPolicy:
    Type: 'AWS::IAM::ManagedPolicy'
    Properties:
      ManagedPolicyName: !Sub '${SysName}-${Env}-kendra-ds-policy'
      Roles:
        - !Ref KendraDSRole
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Resource:
              - !Join
                - ''
                - - 'arn:aws:s3:::'
                  - !Ref KendraDSBucket
                  - '/*'
            Action:
              - 's3:GetObject'
          - Effect: Allow
            Resource: !GetAtt KendraDSBucket.Arn
            Action:
              - 's3:ListBucket'
          - Effect: Allow
            Resource: !Sub
              - 'arn:aws:kendra:${AWS::Region}:${AWS::AccountId}:index/${IndexId}'
              -  IndexId: !GetAtt KendraIndex.Id
            Action:
              - 'kendra:BatchPutDocument'
              - 'kendra:BatchDeleteDocument'

  ##Kendra Data Source
  KendraDS:
    Type: 'AWS::Kendra::DataSource'
    Properties:
      DataSourceConfiguration:
        S3Configuration:
          BucketName: !Ref KendraDSBucket
          InclusionPrefixes:
            - !Ref KendraDSBucketPrefix
      IndexId: !GetAtt KendraIndex.Id
      LanguageCode: 'ja'
      Name: !Sub '${SysName}-${Env}-kendra-ds'
      RoleArn: !GetAtt KendraDSRole.Arn
      Type: 'S3'

  ##Data Source Bucket
  KendraDSBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Sub '${SysName}-${Env}-kendra-ds-bucket-${AWS::AccountId}'
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
            BucketKeyEnabled: true
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced

  ##Data Source Bucket Policy
  KendraDSBucketPolicy:
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      Bucket: !Ref KendraDSBucket
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Deny
            Action:
              - 's3:GetObject'
            Resource:
              - !Join
                - ''
                - - 'arn:aws:s3:::'
                  - !Ref KendraDSBucket
                  - '/*'
            Principal: '*'
            Condition:
                StringNotEquals:
                  aws:PrincipalArn:
                    - !GetAtt KendraDSRole.Arn

Outputs:
  KendraIndexID:
    Value: !GetAtt KendraIndex.Id
  KendraDSID:
    Value: !GetAtt KendraDS.Id
  KendraDSBucketName:
    Value: !Ref KendraDSBucket
```

</details>

パラメータは以下のように指定しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/a7350a1b-baef-331a-955e-4c25f7e9f118.png)

### S3バケットへのデータ投入
[参考記事](https://dev.classmethod.jp/articles/kendra-s3-cfn/#S3%25E3%2583%2590%25E3%2582%25B1%25E3%2583%2583%25E3%2583%2588%25E3%2581%25B8%25E3%2581%25AE%25E3%2583%2587%25E3%2583%25BC%25E3%2582%25BF%25E6%258A%2595%25E5%2585%25A5)に記載されている通り進めていきます。
RAGで利用するドキュメントも参考記事に倣います。
CloudFormationスタックのデプロイが完了したら、CloudShell上で以下のコマンドを実施し、AWSの公式ドキュメント（PDFファイル）を作成したS3バケットにアップロードします。

:::note warn
後ほど詳述しますが、記事執筆時点で `DynamoDB.pdf` についてはKendraへの同期が失敗します。
S3バケットへのデータ投入の対象から外して構いません。
:::

```
# S3 バケット名を設定
BUCKET_NAME=<バケット名>

# S3 プレフィックスを設定
BUCKET_PREFIX=awsdoc

# AWS の公式ドキュメントの PDF ファイルをダウンロード
mkdir ${BUCKET_PREFIX}
cd ${BUCKET_PREFIX}
wget https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/dynamodb-dg.pdf -O DynamoDB.pdf
wget https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-dg.pdf -O Lambda.pdf
wget https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/vpc-ug.pdf -O VPC.pdf
wget https://docs.aws.amazon.com/ja_jp/kendra/latest/dg/kendra-dg.pdf -O Kendra.pdf
wget https://docs.aws.amazon.com/ja_jp/Route53/latest/DeveloperGuide/route53-dg.pdf -O Route53.pdf
cd ..

### S3 バケットに PDF ファイルをアップロード
aws s3 cp ${BUCKET_PREFIX} s3://${BUCKET_NAME}/${BUCKET_PREFIX}/ --recursive
```
マネジメントコンソールから、S3バケットにPDFファイルがアップロードされていることを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/46f7bfe4-bdb4-665b-c6f9-b902a2c31897.png)



### Kendraデータソースの同期
[参考記事](https://dev.classmethod.jp/articles/kendra-s3-cfn/#Kendra%25E3%2583%2587%25E3%2583%25BC%25E3%2582%25BF%25E3%2582%25BD%25E3%2583%25BC%25E3%2582%25B9%25E3%2581%25AE%25E5%2590%258C%25E6%259C%259F)に記載されている通り進めていきます。
データソースの画面から、`Sync now` ボタンを押し、しばらく待ちます。

**・・・おや(^^;)**
5件のドキュメントをスキャンして、そのうち1件の処理に失敗したようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/6df82d11-1809-85a5-b6bf-7f7f6cd5055f.png)

CloudWatch Logsでログを確認すると、`DynamoDB.pdf` について以下のエラーが出ていました。
```
"IndexingStatus":"DocumentFailedToIndex","ErrorMessage":"Extracted document content size 5.902100 MB is larger than Kendra supported size 5.000000 MB"
```

AWSの公式ページでは、以下のように案内されています。

>オリジナルのドキュメントは最大 50 MB のサイズで、ドキュメントごとに最大 5 MB のテキストを含めることができます。
（出典）[Amazon Kendra の料金 - Amazon Web Services](https://aws.amazon.com/jp/kendra/pricing/)

`DynamoDB.pdf` に含まれるテキスト情報が5.9MBで、Kendra側の上限をオーバーしてしまったというオチのようです。
ドキュメントのサイズについては注意が必要ですね。

:::note
`DynamoDB.pdf` の同期は失敗しましたが、その他のドキュメントについては問題なく同期できているようなので、このまま前に進みます。
:::

### Kendraでの検索実行
[参考記事](https://dev.classmethod.jp/articles/kendra-s3-cfn/#Kendra%25E3%2581%25A7%25E3%2581%25AE%25E6%25A4%259C%25E7%25B4%25A2%25E5%25AE%259F%25E8%25A1%258C)に記載されている通り進めていきます。

Search indexed content の画面に移り、Default language of source documentsの設定を `Japanese(ja)` に変えて、検索。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/6c37c242-882a-e8da-d5cf-7675720e069c.png)

大丈夫そう！

## 3. DynamoDBテーブルの作成
チャット履歴をDynamoDBに保存させるため、DynamoDBのテーブルを作っておきます。
以下のCloudFormationテンプレートを使ってください。
```DynamoDBforLangChain.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: DynamoDB for LangChain

Resources:
  DynamoDBTable:
    Type: AWS::DynamoDB::Table
    Properties: 
      AttributeDefinitions: 
        - AttributeName: SessionId
          AttributeType: S
      KeySchema: 
        - AttributeName: SessionId
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST
      TableName: ChatMessageHistory
```
マネジメントコンソールから `ChatMessageHistory` が作成されていることを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/5455fafc-99e1-568a-ccab-0808307aad91.png)



## 4. Lambda用IAMロールの作成
Lambdaから各種サービスを利用するためのIAMロールを作成します。
以下のCloudFormationテンプレートを使ってください。
```IAMRoleforLambda.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: IAM Role for Lambda

Resources:
  LangChainFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName : LangChainFunctionRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AmazonKendraFullAccess
      Policies:
        - PolicyName: LangChainFunctionRolePolicy0
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: 'bedrock:*'
                Resource: '*'
              - Effect: Allow
                Action: 'dynamodb:*'
                Resource: "arn:aws:dynamodb:*:*:table/ChatMessageHistory"
```
マネジメントコンソールから `LangChainFunctionRole` が作成されていることを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/e160fb11-4e26-a936-23b6-711dc7dbbdfd.png)




## 5. Lambdaレイヤーの作成
Lambda関数の標準状態では langchain / langchain-aws が利用できないため、Lambdaレイヤーを使ってパッケージを追加します。
今回はPython 3.12のLambdaを利用するので、Lambdaレイヤーに登録するzipファイルもPython 3.12の環境で作成する必要があります。

Lambdaレイヤーの作成は、以下の記事を参考にしながら実施しました。

https://zenn.dev/issy/articles/zenn-lambda-layer-psycopg2-tried-it

:::note
CloudShell上のDockerを使う方法になっています。
この方法を試す前は、CloudShellの環境に直接Python 3.12を入れて langchain / langchain-aws をzip化しようとしていましたが、色々エラーが出て諦めました（Python 3.12のインストールで躓いた…）
:::

:::note
Lambdaレイヤーの概要については以下の記事が分かりやすかったです。
[【2024年春Ver.】AWS Lambda Layerの作成方法をわかりやすく解説【Python3.12】](https://qiita.com/__DASHi__/items/b03d3efd401260d8add4)
:::

### Dockerfile作成
CloudShellを開いて、Dockerfileを作ります。
私はCloudShell上で `vi Dockerfile-langchain-0.2.11` と叩いてファイルを作成しました。

```Dockerfile-langchain-0.2.11
FROM public.ecr.aws/lambda/python:3.12

WORKDIR /work

# システム更新と必要なパッケージのインストール
RUN dnf update && dnf install -y zip

RUN pip install --upgrade pip && \
    pip install langchain==0.2.11 -t /python/lib/python3.12/site-packages/ && \
    pip install langchain-community==0.2.11 -t /python/lib/python3.12/site-packages/ && \
    pip install langchain-core==0.2.11 -t /python/lib/python3.12/site-packages/ && \
    pip install langchain-aws==0.1.15 -t /python/lib/python3.12/site-packages/

# boto3など、不要なパッケージを削除
RUN rm -rf /python/lib/python3.12/site-packages/boto3* \
           /python/lib/python3.12/site-packages/botocore* \
           /python/lib/python3.12/site-packages/s3transfer* \
           /python/lib/python3.12/site-packages/docutils* \
           /python/lib/python3.12/site-packages/jmespath* \
           /python/lib/python3.12/site-packages/chardet* \
           /python/lib/python3.12/site-packages/six* \
           /python/lib/python3.12/site-packages/python_dateutil*

ENTRYPOINT [""]
CMD zip -r langchain-0.2.11.zip /python/lib/python3.12/site-packages/
```
:::note warn
一部のパッケージを `rm -rf` で削除していますが、これは、zipファイルが大きすぎると、Lambdaレイヤー登録時に `Layers consume more than the available size of 262144000 bytes` と怒られるためです。
とりあえず消しても大丈夫そうなものを削除していますが、他にも消せるパッケージはあると思います。
:::


### イメージをビルドして、zip ファイルを生成
CloudShell上で以下コマンドを実行します。
```
$ docker build -t langchain-0.2.11 . -f Dockerfile-langchain-0.2.11

$ docker run -v "${PWD}":/work langchain-0.2.11
```
カレントディレクトリに `langchain-0.2.11.zip` が作成されていますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/d8c531bb-f285-f26c-8d22-8a0d31ff722c.png)


### Lambda レイヤーを登録
`langchain-0.2.11.zip` をLambdaレイヤーに登録します。
CloudShellからzipファイルをダウンロードしてLambdaレイヤーの登録画面でアップロードする、という方法も可能ですが、CloudShellからAWS CLIコマンドで直接Lambdaレイヤーを登録する方が楽ですね。
```
$ aws lambda publish-layer-version \
    --layer-name langchain-0-2-11 \
    --zip-file fileb://langchain-0.2.11.zip \
    --compatible-runtimes python3.12 \
    --region us-east-1
```
マネジメントコンソールから、Lambdaレイヤーが登録されていることを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/b73f60ee-1198-338d-9ef5-192fbca51bfd.png)




## 6. Lambda関数の作成
マネジメントコンソールからLambda関数を作っていきます。
ランタイムで `Python 3.12` を、アーキテクチャで `x86_64` を選択し、先ほど作成したIAMロールを関連づけます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/17bace6b-4281-6c63-2969-8ba27f7b87e1.png)

関数が作成できたら、`レイヤーの追加` ボタンを押して、先ほど登録したLambdaレイヤーを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/246be384-f9bd-775f-e88f-d2b1f22c9077.png)



タイムアウト値を伸ばしておきましょう。
今回は長めに設定して、5分にしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/f4d032a3-4bf6-99e0-5117-508fc371e3ea.png)

次に、環境変数を設定します。

| キー | 値 |
|-|-|
|INDEX_ID	|※先ほど作成したKendraインデックスのID|
|MAX_TOKENS	|4096|
|MODEL_NAME	|anthropic.claude-3-5-sonnet-20240620-v1:0|

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/6229a732-1460-fdcd-2270-8eac6d15a63e.png)

最後に、コードソース欄に以下のコードを入れて、`Deploy` ボタンを押せば完了です。


```Langchain_RAG.py
import os
import json
import uuid
from langchain_aws import ChatBedrock
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationalRetrievalChain
from langchain_community.chat_message_histories import DynamoDBChatMessageHistory
from langchain_community.retrievers import AmazonKendraRetriever
from langchain_community.retrievers.kendra import clean_excerpt

# 環境変数の値を取得
INDEX_ID = os.getenv('INDEX_ID')
MAX_TOKENS = os.getenv('MAX_TOKENS')
MODEL_NAME = os.getenv('MODEL_NAME')


# 使用するLLMモデルを宣言
llm = ChatBedrock(
    model_id=MODEL_NAME,
    model_kwargs={
        "temperature": 0,
        "max_tokens": int(MAX_TOKENS)
    }
)


# Lambdaハンドラー
def lambda_handler(event, context):
    # for debug
    print(f"Received event: {json.dumps(event, ensure_ascii=False)}")

    # イベントJSONから入力パラメーターを取得
    session_id = event.get("session_id")
    input_text = event.get("input_text")

    # セッションIDが未設定の場合、新規にセッションIDをセットする (UUIDを使ったランダムな文字列)
    if session_id is None:
        session_id = str(uuid.uuid4())

    # メインの処理を呼び出す
    output_text = chat_conversation(session_id, input_text)

    # Lambda関数のレスポンスを返す
    return {
        "session_id": session_id,
        "output_text": output_text.encode('utf-8')
    }


# メイン処理
def chat_conversation(session_id: str, input_text: str) -> str:
    # Historyモジュール (Memoryの内容を外部記憶を使って永続化する)
    history = DynamoDBChatMessageHistory(
        table_name="ChatMessageHistory",
        session_id=session_id,
    )

    # Retrieverモジュール (Kendraの情報を参照する)
    retriever = AmazonKendraRetriever(
        index_id=INDEX_ID,
        attribute_filter={
            "EqualsTo": {
                "Key": "_language_code",
                "Value": {"StringValue": "ja"},
            },
        },
        page_content_formatter=original_content_formatter,
    )

    # Memoryモジュール (会話履歴を記憶する)
    memory = ConversationBufferMemory(
        chat_memory=history,
        memory_key="chat_history",
        input_key="question",
        output_key="answer",
        return_messages=True
    )

    # Chainモジュール (複数のモジュールを組み合わせて処理を行わせる)
    chain = ConversationalRetrievalChain.from_llm(
        llm,
        chain_type="stuff",
        retriever=retriever,
        memory=memory,
        return_source_documents=True,
        verbose=True
    )
    chat_history = []

    # LLMにプロンプトを与えて、応答を含む結果を得る
    result = chain.invoke({"question": input_text})
    # for debug
    # print(f"Result: {result}")

    # 得られた結果を型変換 (dictからstrへの変換)
    output_text = json.dumps(result, default=str, ensure_ascii=False)
    
    return output_text
    

# source などのメタデータもLLMに渡せるようにする
# (参考) https://qiita.com/s3kzk/items/be046a2ed62cd0663dbb#
def original_content_formatter(item):
    text = ""
    title = item.get_title()
    if title:
        text += f"Document Title: {title}\n"
    excerpt = clean_excerpt(item.get_excerpt())
    if excerpt:
        text += f"Document Excerpt: \n{excerpt}\n"
    ## 以下2行を追加    
    if item.DocumentURI:
        text += f"Document URI: {item.DocumentURI}\n"
    return text
```


# 動かしてみる
最初は `session_id` を指定せずに、メッセージ内容だけを指定して実行します。
```
{
  "input_text": "Amazon Kendraの利点はなんですか"
}
```

以下の応答が返ってきました。
セッションIDとして `f8f1ca03-6f49-4214-9edd-ff6109717ad8` が発行されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/0807ec96-6711-455c-fc17-460620866f94.png)

`output_text` の中身をJSONチックに表示すると以下の通りです。
`session_id` を指定していない最初のやり取りなので、`chat_history` の中身が空になっています。
`answer` の中身が最終的な回答になりますが、情報が多いに越したことはないので、今回は `source_documents` の内容も返すようにしています。
```
{
  "question": "Amazon Kendraの利点はなんですか",
  "chat_history": [],
  "answer": "Amazon Kendraの主な利点は以下の通りです：\n\n1. シンプルさ：\n   - ドキュメントを管理するためのコンソールとAPIを提供しています。\n   - シンプルな検索APIを使用して、ウェブサイトやモバイルアプリケーションなどのクライアントアプリケーションに簡単に統合できます。\n\n2. 接続性：\n   - Microsoft SharePointなどのサードパーティのデータリポジトリやデータソースに接続できます。\n   - データソースを使用して、ドキュメントのインデックス作成と検索を簡単に行うことができます。\n\n3. 高度な検索機能：\n   - キーワードだけでなく、自然言語による複雑な質問にも対応できます。\n   - 意味が曖昧な単語（例：「address」）の文脈を正しく推測し、関連情報を返すことができます。\n\n4. スケーラビリティとパフォーマンス：\n   - 非常にスケーラブルで、高いパフォーマンス要求を満たすことができます。\n\n5. AWS統合：\n   - Amazon S3やAmazon Lexなどの他のAWSサービスと緊密に統合されています。\n\n6. セキュリティ：\n   - エンタープライズレベルのセキュリティを提供します。\n\nこれらの特徴により、Amazon Kendraは効率的で高度な企業向け検索ソリューションとなっています。",
  "source_documents": [
    {
      "page_content": "Document Title: Kendra\nDocument Excerpt: \n. 1 Amazon Kendra のクエリ ..  1 Amazon Kendra の利点 . . 1 Amazon Kendra の利点 \nDocument URI: https://s3.us-east-1.amazonaws.com/yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf\n",
      "metadata": {
        "result_id": "272801e4-d46f-414a-84e2-b61b6f151f64-9a12050f-9f4d-487a-aa0a-396abfa5a1c2",
        "document_id": "s3://yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf",
        "source": "https://s3.us-east-1.amazonaws.com/yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf",
        "title": "Kendra",
        "excerpt": "................... 1 Amazon Kendra のクエリ ................................................................. ................................................................................. 1 Amazon Kendra の利点 ............................... .............................................. 1 Amazon Kendra の利点 ..................................................................",
        "document_attributes": {
          "_source_uri": "https://s3.us-east-1.amazonaws.com/yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf",
          "_excerpt_page_number": 3
        },
        "score": "NOT_AVAILABLE"
      }
    },
    {
      "page_content": "Document Title: Kendra\nDocument Excerpt: \n 1 Amazon Kendra の利点 . . 1 Amazon Kendra の利点   2 Amazon Kendra Editions ..\nDocument URI: https://s3.us-east-1.amazonaws.com/yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf\n",
      "metadata": {
        "result_id": "272801e4-d46f-414a-84e2-b61b6f151f64-86099966-2015-4f81-95c9-696724cd15d7",
        "document_id": "s3://yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf",
        "source": "https://s3.us-east-1.amazonaws.com/yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf",
        "title": "Kendra",
        "excerpt": "................................................................................. 1 Amazon Kendra の利点 ............................... .............................................. 1 Amazon Kendra の利点 .................................................................. .................................................................................... 2 Amazon Kendra Editions ................................",
        "document_attributes": {
          "_source_uri": "https://s3.us-east-1.amazonaws.com/yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf",
          "_excerpt_page_number": 3
        },
        "score": "NOT_AVAILABLE"
      }
    },
    {
      "page_content": "Document Title: Kendra\nDocument Excerpt: \nAmazon Kendra 開発者ガイド キーワードおよび自然言語に関する質問 - 意味がはっきりしない、複雑な会話内容を含む質問。 例え ば、キーノートのアドレス。 Amazon Kendra では、複数の文脈上の意味を持つ「address」のよう な単語が見つかると、検索クエリの意味を正しく推測し、関連情報を返します。 Amazon Kendra の利点 Amazon Kendra は非常にスケーラブルで、パフォーマンス要求を満たすことができ、Amazon S3 や Amazon Lex などの他の AWS のサービスと緊密に統合され、エンタープライズレベルのセキュリ ティを提供します。 Amazon Kendra を使用する利点のいくつかを以下に示します。 シンプルさ - Amazon Kendra は、検索するドキュメントを管理するためのコンソールと API を提供 します。 シンプルな検索 API を使用して、Amazon Kendra をウェブサイトやモバイルアプリケー ションなどのクライアントアプリケーションに統合できます。 接続性 - Amazon Kendra は、Microsoft SharePoint などのサードパーティのデータリポジトリまたは データソースに接続できます。 データソースを使用して、ドキュメントのインデックス作成と検索を 簡単に行うことができます。\nDocument URI: https://s3.us-east-1.amazonaws.com/yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf\n",
      "metadata": {
        "result_id": "272801e4-d46f-414a-84e2-b61b6f151f64-ad75a0ad-6ebd-4336-8b21-f9288b15a519",
        "document_id": "s3://yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf",
        "source": "https://s3.us-east-1.amazonaws.com/yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf",
        "title": "Kendra",
        "excerpt": "Amazon Kendra 開発者ガイド キーワードおよび自然言語に関する質問 - 意味がはっきりしない、複雑な会話内容を含む質問。 例え ば、キーノートのアドレス。 Amazon Kendra では、複数の文脈上の意味を持つ「address」のよう な単語が見つかると、検索クエリの意味を正しく推測し、関連情報を返します。 Amazon Kendra の利点 Amazon Kendra は非常にスケーラブルで、パフォーマンス要求を満たすことができ、Amazon S3 や Amazon Lex などの他の AWS のサービスと緊密に統合され、エンタープライズレベルのセキュリ ティを提供します。 Amazon Kendra を使用する利点のいくつかを以下に示します。 シンプルさ - Amazon Kendra は、検索するドキュメントを管理するためのコンソールと API を提供 します。 シンプルな検索 API を使用して、Amazon Kendra をウェブサイトやモバイルアプリケー ションなどのクライアントアプリケーションに統合できます。 接続性 - Amazon Kendra は、Microsoft SharePoint などのサードパーティのデータリポジトリまたは データソースに接続できます。 データソースを使用して、ドキュメントのインデックス作成と検索を 簡単に行うことができます。",
        "document_attributes": {
          "_source_uri": "https://s3.us-east-1.amazonaws.com/yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf",
          "_excerpt_page_number": 15
        },
        "score": "NOT_AVAILABLE"
      }
    }
  ]
}

```


続いて、先ほど発行された `session_id` を指定して、文脈の理解が問われそうな質問をしてみます。
※先ほどKendraの話をしていたので、言外にKendraのコストについて質問している。
```
{
  "session_id": "f8f1ca03-6f49-4214-9edd-ff6109717ad8",
  "input_text": "コストはどれくらいかかりますか。"
}
```

以下の応答が返ってきました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/6202070b-258d-ff2e-3bdb-c681f5d527d8.png)

`output_text` の中身をJSONチックに表示すると以下の通りです。
`chat_history` に前回のやり取りが記録されているのが分かります。
また、`answer` の内容から、ちゃんとKendraに関する話題と認識して回答してくれたようです。

```
{
  "question": "コストはどれくらいかかりますか。",
  "chat_history": [
    {
      "content": "Amazon Kendraの利点はなんですか"
    },
    {
      "content": "Amazon Kendraの主な利点は以下の通りです：\n\n1. シンプルさ：\n   - ドキュメントを管理するためのコンソールとAPIを提供しています。\n   - シンプルな検索APIを使用して、ウェブサイトやモバイルアプリケーションなどのクライアントアプリケーションに簡単に統合できます。\n\n2. 接続性：\n   - Microsoft SharePointなどのサードパーティのデータリポジトリやデータソースに接続できます。\n   - データソースを使用して、ドキュメントのインデックス作成と検索を簡単に行うことができます。\n\n3. 高度な検索機能：\n   - キーワードだけでなく、自然言語による複雑な質問にも対応できます。\n   - 意味が曖昧な単語（例：「address」）の文脈を正しく推測し、関連情報を返すことができます。\n\n4. スケーラビリティとパフォーマンス：\n   - 非常にスケーラブルで、高いパフォーマンス要求を満たすことができます。\n\n5. AWS統合：\n   - Amazon S3やAmazon Lexなどの他のAWSサービスと緊密に統合されています。\n\n6. セキュリティ：\n   - エンタープライズレベルのセキュリティを提供します。\n\nこれらの特徴により、Amazon Kendraは効率的で高度な企業向け検索ソリューションとなっています。"
    }
  ],
  "answer": "申し訳ありませんが、提供された文脈の中にAmazon Kendraの具体的な価格情報は含まれていません。Amazon Kendraの正確な価格設定については、AWSの公式ウェブサイトやAmazon Kendraの価格ページを確認するのが最良の方法です。価格は使用量、選択したエディション、データ量などの要因によって変わる可能性があります。",
  "source_documents": [
    {
      "page_content": "Document Title: Kendra\nDocument Excerpt:\n例 えば、各都市のカテゴリでファセット検索を行うには、すべての都市カテゴリを含む _category カ スタムドキュメント属性を使用します。 提案する回答 機械学習で生成された回答をユーザーのクエリに追加します。 例: 「このコースはどれほど難しいで すか?」 Amazon Kendra はコースの難しさを指しているすべてのドキュメントで最も関連性の高い テキストを取得し、最も関連性の高い回答を提案できます。 よくある質問 よくある質問への回答を得るには、よくある質問ドキュメントを追加します。 例: 「このコースを完 了するのに何時間かかりますか?」 Amazon Kendra はこの質問に対する回答を含むよくある質問ド キュメントを使用して、正しい答えを出すことができます。 Sort 検索結果のソートを追加して、ユーザーが関連性、作成時刻、最終更新時刻、その他のソート基準で 結果を整理できるようにします。\nDocument URI: https://s3.us-east-1.amazonaws.com/yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf\n",
      "metadata": {
        "result_id": "d7699b54-62d5-4e50-9f1c-9a764b136b03-77b435eb-abff-4036-84f9-2cf548814913",
        "document_id": "s3://yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf",
        "source": "https://s3.us-east-1.amazonaws.com/yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf",
        "title": "Kendra",
        "excerpt": "例 えば、各都市のカテゴリでファセット検索を行うには、すべての都市カテゴリを含む _category カ スタムドキュメント属性を使用します。 提案する回答 機械学習で生成された回答をユーザーのクエリに追加します。 例: 「このコースはどれほど難しいで すか?」 Amazon Kendra はコースの難しさを指しているすべてのドキュメントで最も関連性の高い テキストを取得し、最も関連性の高い回答を提案できます。 よくある質問 よくある質問への回答を得るには、よくある質問ドキュメントを追加します。 例: 「このコースを完 了するのに何時間かかりますか?」 Amazon Kendra はこの質問に対する回答を含むよくある質問ド キュメントを使用して、正しい答えを出すことができます。 Sort 検索結果のソートを追加して、ユーザーが関連性、作成時刻、最終更新時刻、その他のソート基準で 結果を整理できるようにします。",
        "document_attributes": {
          "_source_uri": "https://s3.us-east-1.amazonaws.com/yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf",
          "_excerpt_page_number": 152
        },
        "score": "NOT_AVAILABLE"
      }
    },
    {
      "page_content": "Document Title: Kendra\nDocument Excerpt:\n例: \"Amazon Kendra\" \"pricing\" を含むド キュメント。 上のいずれかの演算子を組み合わせて使用できます。 演算子や非常に複雑なクエリを過度に使用すると、クエリのレイテンシーに影響を与える可能性があ るので注意してください。 ワイルドカードは、レイテンシーの点で非常にコストのかかる演算子の 1 高度なクエリ構文による検索 1069 Amazon Kendra 開発者ガイド つです。 一般的に、使用する語句や演算子が多いほど、レイテンシーへの影響が大きくなります。 レ イテンシーに影響するその他の要因には、インデックス作成されたドキュメントの平均サイズ、イン デックスのサイズ、検索結果のフィルタリング、 Amazon Kendra インデックスの全体的な負荷など があります。 ブール値 ブール値演算子の AND、OR、NOT を使用して、単語を組み合わせたり、除外したりすることができ ます。 ブール演算子を使用した例を次に示します。 amazon AND sports Amazon Prime Video のスポーツまたはその他の類似コンテンツなど、テキストに「amazon」と\nDocument URI: https://s3.us-east-1.amazonaws.com/yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf\n",
      "metadata": {
        "result_id": "d7699b54-62d5-4e50-9f1c-9a764b136b03-a1bd1300-0a18-412e-a1ab-08d0e2e4442f",
        "document_id": "s3://yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf",
        "source": "https://s3.us-east-1.amazonaws.com/yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf",
        "title": "Kendra",
        "excerpt": "例: \"Amazon Kendra\" \"pricing\" を含むド キュメント。 上のいずれかの演算子を組み合わせて使用できます。 演算子や非常に複雑なクエリを過度に使用すると、クエリのレイテンシーに影響を与える可能性があ るので注意してください。 ワイルドカードは、レイテンシーの点で非常にコストのかかる演算子の 1 高度なクエリ構文による検索 1069 Amazon Kendra 開発者ガイド つです。 一般的に、使用する語句や演算子が多いほど、レイテンシーへの影響が大きくなります。 レ イテンシーに影響するその他の要因には、インデックス作成されたドキュメントの平均サイズ、イン デックスのサイズ、検索結果のフィルタリング、 Amazon Kendra インデックスの全体的な負荷など があります。 ブール値 ブール値演算子の AND、OR、NOT を使用して、単語を組み合わせたり、除外したりすることができ ます。 ブール演算子を使用した例を次に示します。 amazon AND sports Amazon Prime Video のスポーツまたはその他の類似コンテンツなど、テキストに「amazon」と",
        "document_attributes": {
          "_source_uri": "https://s3.us-east-1.amazonaws.com/yuki-ink-dev-kendra-ds-bucket-xxxxxxxxxxxx/awsdoc/Kendra.pdf",
          "_excerpt_page_number": 14
        },
        "score": "NOT_AVAILABLE"
      }
    }
  ]
}

```

# 終わりに
以上、LangChainでRAGアプリを作ってみました。
LangChainのバージョンによってエラーが出たり出なかったりしたので、その点が一番苦労しました…
この機会に最新のLangChainを触れてよかったなぁと思います。

何かの助けになれば幸いです。



# 参考にした記事

https://qiita.com/hayao_k/items/18d8bb7d59942a240782

https://qiita.com/s3kzk/items/be046a2ed62cd0663dbb

https://qiita.com/mashmoeiar11/items/5aa2c1f3381e5184c919

https://blog.elcamy.com/posts/cca2905b/

https://tech.affordigitalife.com/langchain_x_amazon-kendra/#index_id7

https://tex2e.github.io/blog/python/json-dumps-ensure-ascii-false

https://www.bioerrorlog.work/entry/python-datetime-not-json-serializable-error

https://note.com/astropomeai/n/n393c954ad1cc