Bedrockを使って、S3に置いた独自データに応答してくれるチャットボットを作ってみた。

# はじめに

AWS re:Invent 2023 でGAとなったKnowledge Bases for Amazon BedrockとAgents for Amazon Bedrockを使用するとS3バケットに配置したオブジェクトを対象としたRAGが簡単に構築できるということで、試してみました。Bedrockのコンソールで試すだけでなく、Agents for Amazon Bedrock(以下、Agents)をチャットボットで呼び出すSlack Appも作ってみました。

本記事では、Vector storeに[Pinecone](https://www.pinecone.io/)(Free Plan)を使用します。Knowledge Bases for Amazon Bedrock(以下、knowledge base)のVectore database作成画面ではデフォルトでOpenSearch ServerlessにVector storeが構築される流れになっていますが、時間単位の課金で思わぬ費用が発生しないようPineconeを選択しました。

今回は､FIAとJAFモータースポーツのサイトで公開されているF1世界選手権の技術規則書と競技規則書をもとにRAGを作成し､F1のレギュレーションについて応答するチャットボットを作成します｡

# デモ画面

Agentsのレスポンスに参照ファイルのパスと参照箇所が含まれていれば、それらも出力できます。もちろん､会話の履歴を踏まえた回答が得られます。
参照元に同じ内容が2回出力されていますが､おそらく参照した2箇所が含まれる広い範囲をレスポンスしているためこのようになっているのだと思います｡

(ボットの名前にbacklogと入っていますが､今回はbacklogは関係ありません｡)

![Agentsとの会話を行うSlackボット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/4fa1608d-46ec-3a2f-fe12-9d7735356989.png)

# 構成図

![構成図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/d7169deb-cb54-7799-5736-18ff138f888e.png)

1. PDFなどのファイルをS3バケットにアップロード
1. knowledge baseが埋め込みを作成し､Pineconeに登録/更新
1. SlackからのクエリをAgentsが受け取る
1. Agentsはknowledge baseと対話し、クエリを送る。
1. knowledge baseはクエリに応じて意味的に最も関連性の高いコンテンツをPineconeから取得しAgentsに返す
1. クエリの結果をSlackに返す

# 参考情報

https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html

https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base-setup.html

https://docs.aws.amazon.com/bedrock/latest/userguide/agents.html

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-agent-runtime/client/invoke_agent.html

https://aws.amazon.com/jp/blogs/aws/knowledge-bases-now-delivers-fully-managed-rag-experience-in-amazon-bedrock/

https://docs.aws.amazon.com/secretsmanager/latest/userguide/auth-and-access_examples.html

https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html

https://github.com/aws-samples/bedrock-kb-rag-workshop/blob/main/rag_w_bedrock_agent.ipynb

https://www.pinecone.io/blog/amazon-bedrock-integration/

https://dev.classmethod.jp/articles/agents-for-amazon-bedrock-ga/

https://dev.classmethod.jp/articles/update-reinvent2023-bedrock-rag/

https://blog.serverworks.co.jp/agents-for-amazon-bedrock

https://qiita.com/yu-Matsu/items/5f26eb630620ec42342e

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

https://qiita.com/miu_crescent/items/b913683ff202ca34b019

# 前提条件

- 以下のリソースを作成･削除･変更できる権限をもつAWSユーザーを利用すること
  - AWS IAM
  - AWS Lambda
  - AWS CloudFormation
  - AWS Secrets Manager
  - Amazon API Gateway
  - Amazon S3
  - Amazon CloudWatch Logs
  - Amazon Bedrock
- 使用するAWSリージョンは、us-east-1 (記事執筆時点でknowledge baseとAgentsが利用可能なリージョンがus-east-1,us-west-2のみのため)
- Slack Appを作成するためのアカウントや権限を持っている
- Pineconeにサインイン可能なアカウント(Google, GitHub, Microsoftのいずれか)を持っている

# 環境構築

以降の手順は､[Pinecone as a Knowledge Base for Amazon Bedrock](https://www.pinecone.io/blog/amazon-bedrock-integration/)の記事を参考にしました｡記事に書かれている手順に従って進めていきます｡

## PineconeのサインアップとIndexの作成

[Pinecone](https://www.pinecone.io/)のサイトにアクセスし、`Get Started` (もしくは画面右上の `Sign Up Free`)からアカウント作成画面に進みます。

![www.pinecone.io Pineconeトップページ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/2fe686bb-7494-b6a0-61f4-5c0450fec80f.png)

Google, GitHub, Microsoftのいずれかのアカウントでログインします。もしくは、メールアドレスを入力しアカウントを作成します。

![Pineconeログイン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/22ba468f-9503-1c4b-4c35-7338f7992cb8.png)

質問に回答しサインアップを完了させます。どれを選択してもよいと思いますが、今回は以下のとおり選択しました。

|質問|回答|
|-- |-- |
| Waht is your goal? | Personal Lerning and development |
| What are you making? | Chatbot application |
| What is your preferred coding launguage? | Pyhon |

![Pineconeサインアップ時のアンケート](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/5d3c7194-ebdd-9b67-8e87-9e57de014167.png)

Create IndexをクリックしてIndexを作成します｡
![Pinecone サインアップ後の画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/116b1870-c5bc-7c47-ef98-124c67ae51a6.png)

|入力/選択項目|内容|
|-- |-- |
|Name|任意のIndex名｡ 使用可能な文字は小文字､数字､ハイフンのみ|
|Dimensions|1536|
|Metric|cosine|
|Pod Type|starter|

後ほど作成するknowledge baseの埋め込みモデル`Titan Embeddings G1 - Text v1.2`が1536次元のため､Indexを1536次元で作成します｡Pod Typeは､Free Planではstarterのみ選択可能です｡Free Planは作成可能なIndex数が1個なので､別のIndexを作成する場合は既存のものを削除するか上位プランの契約が必要です｡

![Pinecone インデックス作成画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/66fb7280-3388-fdbc-4257-44ceec3eda3c.png)

Indexの作成が完了すると､このようにIndexes画面にHOSTアドレスが､API Keys画面にAPIキーが表示されます｡Environmentがawsではなくgcpとなっていますが､Free Planではこの選択しかできません｡HOSTアドレスとAPIキーがあればknowledge baseから接続できるので､Environmentは気にしなくてよさそうです｡
HOSTアドレスは後ほど作成するknowledge baseの設定で使用します｡

![Pinecone インデックスのHOSTアドレス](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/8294c3e3-c64a-85f5-1434-a620ea1542fe.png)

![Pinecone APIキー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/90bb213a-915f-8abd-f61a-2a29d3622b2d.png)


### PineconeのAPI KeyをSecrets Managerに登録

[AWS Secrets Manager](https://us-east-1.console.aws.amazon.com/secretsmanager/newsecret?region=us-east-1)を開き､シークレットの登録を行います｡

|入力/選択項目|内容|
|-- |-- |
|シークレットのタイプ|その他のシークレットのタイプ|
|キー|apiKey|
|値|PineconeのAPIキー|

![PineconeのAPI KeyをSecrets Managerに登録](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/e246846c-59a6-1a65-54fe-0d2dd8954a78.png)


シークレットの名前と説明を入力します｡

![シークレットの名前と説明を入力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/839ff283-e3e1-c3a1-15cc-dabe1d4b953e.png)

ローテーションの設定はすべてデフォルトのままにします｡

![ローテーションの設定はすべてデフォルトのままにします](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/706988bc-2835-6443-2686-aeefcc9eac4f.png)

シークレットの作成が完了すると､Secret ARNが発行されます｡
Secret ARNは後ほど作成するknowledge baseの設定で使用します｡

![シークレットの作成が完了すると､Secret ARNが発行されます](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/40bc94be-d9fe-5f4d-8e63-c283ee775c93.png)

## Bedrock環境構築

### Knowledge Bases for Amazon Bedrock

#### ファイルをS3バケットにアップロード

任意のS3バケットにファイルをアップロードします｡knowledge baseをus-east-1に作成するためリージョン間の通信費用を考慮し､S3バケットのリージョンもus-east-1としたほうが良いと思います｡
knowledge baseがサポートするファイルフォーマットは､[Set up your data for ingestion](https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base-setup.html)に記載されています｡
引用すると以下のとおりです｡また､1ファイルあたりの最大ファイルサイズは50MBです｡

- Plain text (.txt)
- Markdown (.md)
- HyperText Markup Language (.html)
- Microsoft Word document (.doc/.docx)
- Comma-separated values (.csv)
- Microsoft Excel spreadsheet (.xls/.xlsx)
- Portable Document Format (.pdf)

ここでは､JAFモータースポーツのサイト内の[国際モータースポーツ諸規則など](https://motorsports.jaf.or.jp/regulations/information/international)で公開されているFIAレース世界選手権等規則書のなかから`2023年競技規則_日本語版_20230425`と`2023年技術規則_日本語版_20230425`の2つのPDFファイルを、FIAのサイト内のREGULATIONSで公開されている[FIA FORMULA ONE WORLD CHAMPIONSHIP](https://www.fia.com/regulation/category/110)のドキュメントのなかから`FIA 2023 Formula 1 Sporting Regulations - Issue 8 - 2023-12-06`と`FIA 2023 Formula 1 Technical Regulations - Issue 7 - 2023-08-31`をそれぞれS3バケットにアップロードしています｡

#### knowledge baseを作成

[knowledge base](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/knowledge-bases/create-knowledge-base)を開き､knowledge baseを作成します｡

|入力/選択項目|内容|
|-- |-- |
|Knowledge base name|任意のknowledge base名|
|Runtime role|Create and use a new service role|

ロールは新規に作成します｡以前に作成したknowledge baseのロールが存在していても､Use an existing service roleで既存のロールが選択肢に表示されませんでした｡

![knowledge base名の入力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/1470492d-da91-2c91-18cf-7a8d2d24543b.png)

データソースとして､ファイルをアップロードしたS3バケットを指定します｡

|入力/選択項目|内容|
|-- |-- |
|Data source name|任意のデータソース名|
|S3 URI|S3バケットのURI|
|Chunking strategy|Default chunking|

S3バケットのURIは､直接URIを入力するか､`Browse S3`をクリックしてバケット一覧からバケットを選択することで入力できます｡
Chunking strategyは､チャンク分割の設定を選択します｡knowledge base作成後に設定を変更することができないため､この時点で選択が必要です｡それぞれどのように分割するのかはプルダウンメニュー内に記載されています｡また､[Set up your data for ingestion](https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base-setup.html)にも記載があります｡

![データソースとして､ファイルをアップロードしたS3バケットを指定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/7e7ee2b5-2231-f405-f142-bf7e39385316.png)

#### Vectore storeの設定

つづいて､Vector storeを設定します｡
埋め込みモデルは､`Titan Embeddings G1 - Text v1.2`のみ利用可能です｡PineconeのIndexを作成する際に設定した Dimensions: 1536 はここに明記されています｡

![埋め込みモデルの選択](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/08ddcebe-1fb6-70e3-4ff4-4c4c48260a59.png)

|入力/選択項目|内容|
|-- |-- |
|Select how you want to create your vector store|Choose a vector store you have created|
|Select an existing database|Pinecone|
|By selecting "Pinecone"...| ✓ |
|Connection String|PineconeのIndexのHOSTアドレス|
|Credentials secret ARN|PineconeのAPIキーを登録したSecretsのARN|
|Text field|Vector databaseのテキストフィールドの任意の名前｡ `text`など|
|Bedrock-managed metadata field|Vector databaseのメタデータフィールドの任意の名前｡ `metadata`など|

By selecting "Pinecone"... の項目は､AWSがユーザーに代わってサードバーティーのソースにアクセスすることを許可することに同意する旨が書かれています｡

![Vectore storeの設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/a0c70745-a993-f961-07de-4cf1ad4f9502.png)

knowledge baseは作成されましたが､まだS3バケットのデータはPineconeに登録されていません｡作成したknowledge baseのData Sourceの項目に移動し､`Sync`ボタンをクリックします｡S3バケット内のデータに追加/更新があった場合も同様にSyncを実行します｡

![PineconeとSync](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/94899930-df86-cf91-48ed-ec951e6b7628.png)

Statusが Ready となれば完了です｡PineconeのIndex画面にアクセスすると､このようにデータが登録されていることが分かります｡

![Pineconeに構築されたインデックス](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/b5bea35a-722b-6800-cffa-5e1e391e01da.png)

### Agents for Amazon Bedrock

#### Agentsの設定

[Agents](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/agents/create)を開き､Agentを作成します｡

|入力/選択項目|内容|
|-- |-- |
|Agent name|任意のAgents名|
|Agent description|Agentsの説明文|
|User Input| Yes |
|IAM Permissins| 新規にロールを作成する場合は､Create and use a new service role｡既存のロールを利用する場合は Use an existing service role |
|Idle session timeout| 30 Minutes |

![Agent作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/852dc03b-dc9e-6c11-d5ad-65e2eb22cce3.png)

"User Input" は､Agentsが回答するための十分な情報がない場合に､ユーザーに追加の情報を求めることができるかを選択します｡Yes,Noのどちらを選択しても良いのですが､Yesのほうが会話らしくなると思います｡
"Idle session timeout"は､入力が何もない状態でセッションを維持する時間｡セッションが有効な間であれば､以前の会話履歴を引き継いで会話を続けることができます｡

Agentsが使用する言語モデルを選択します｡記事執筆時点では､Anthropic社の"Claude instant V1","Claude instant V2", "Claude instant V2.1"のみが選択可能です｡
"Amazon Titan Express"というモデルが近々公開されるようです｡

![Agentsの言語モデルを選択](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/3ce91fc8-5401-6869-476e-86737abed6c8.png)

![Amazon Titan Expressは選択不可](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/41ea801c-7b69-567d-718b-a4d72c92f8ae.png)


"Instructions for the Agent"には､エージェントが実行するタスクについて明確かつ具体的な指示を入力します｡スタイルやトーンを指示することも可能なので､特定の口調や語尾で会話するように指示することができるようです｡

Add Action groupsの設定はスキップします｡APIスキーマとそれを処理するLambda関数を指定することでOpenAI APIのFunction Callingと同じようなことが実現できるようです｡

![Action groupsの設定はスキップ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/91fae929-b7a4-2811-7c77-4e160be116e3.png)

Agentsが連携するknowledge baseを選択し､それをいつ使用するのかどういった情報が含まれているのかをAgentsに伝えるための指示を入力します｡

|入力/選択項目|内容|
|-- |-- |
|Select knowledge base|先ほど作成したknowledge baseを選択|
|knowledge base instructions for Agent|Agentsがknowledge baseと対話する際の指示を定義|

今回の例では使用するknowledge baseはひとつだけですが､Add another knowledge baseを操作することで複数のknowledge baseをひとつのAgentsに紐付けることができます｡


![knowledge baseをいつ使用するのかどういった情報が含まれているのかをAgentsに伝えるための指示を入力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/ea86b82b-9fad-2910-81b0-91f98b4cf4c4.png)


Agentsの設定が完了すると､概要がこのように表示されます｡APIからAgentsを呼び出す際に`ID`を使用します｡ **AgentsのIDは後述のSecret Managerのシークレット登録に使用します。**
右上のEditからAgentsの設定を変更することができます｡

![Agentsの設定概要](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/c0941f5b-f274-1acb-2cb0-b63bacc7d4ca.png)

#### Agentsの動作確認

画面右にあるTest playgroundを使用すると､Agentsとのチャットを試す事ができます｡この例では､S3バケットに置いたPDFドキュメントの内容について質問し､Agentsが応答しています｡"もう少し詳細に説明してください｡"という問い合わせに対して､会話履歴を踏まえた応答が得られました｡

![Agentsの動作確認](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/ab3c5b91-9b7b-4eb0-803a-be0fde8ea2a7.png)

#### Agents エイリアスの作成

AgentsをAPIから呼び出す際は､Agents本体ではなくエイリアスを使用します｡そこで､Agentsに対するエイリアスを作成します｡
右上のCreateから新規作成画面に進みます｡

![Agents エイリアスの作](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/56917617-5697-c855-2de5-b1ad730d7620.png)

エイリアスの情報を入力します。
|入力/選択項目|内容|
|-- |-- |
|Alias name|任意のエイリアス名|
|Description|エイリアスの説明文|
|Create a new version and associate it to this alias. / Use an existing version to associate this alias.| エイリアスを新しいバージョンと既存バージョンのどちらに紐付けるか|

![エイリアスの情報](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/b5a8ca18-50f8-17ef-9546-fc8dce85d5ac.png)

エイリアスを作成すると､バージョンとともに以下のようにリストが表示されます｡APIからAgentsを呼び出す際に`Alias ID`を使用します｡ **Alias IDは後述のSecret Managerのシークレット登録に使用します。**

![エイリアス一覧](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/e3ca6698-6c49-555e-d5f0-e5024ff16765.png)

knowledge baseの設定変更は､Working draftの項目から行えます｡なかでもAdvanced promptsでは､言語モデルに対して前処理や後処理の詳細な指示テンプレートを上書き/拡張できます｡
設定を変更した際はplaygroundで動作確認が行ったあと､エイリアスを作成します｡

![Agentsのドラフト](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/43163851-1d91-f516-e258-52556b74bf1b.png)

![AgentsのAdvanced prompts](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/640e47af-8c6c-46be-fba5-cf9bb54e1239.png)


## 開発環境構築

### 作業環境のOSバージョン

Windows 11上のWSLでUbuntu 23.04を動かしています。

```console
$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 23.04"
```

### Python環境

```console
$ python3 --version
Python 3.11.6
```

```console
$ python3 -m venv .venv
source .venv/bin/activate
```

```console
$ pip3 install --upgrade pip
$ pip3 --version
pip 23.3.1 from /home/xxx/.venv/lib/python3.11/site-packages/pip (python 3.11)
```

### AWS環境構築

aws configureコマンドでデフォルトのリージョンやクレデンシャルを設定するか､もしくは~/.aws/configや~/.aws/credentialsを用意します。

#### AWS SAM CLIインストール

AWS上でサーバーレスアプリケーションを構築､実行するAWS SAMを使用します｡

[Installing the AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) の手順に従い、AWS SAM CLIをインストールします。今回はx86_64環境でLinux OSを使用するため、x86_64 - command line installerの手順を実行します｡

```console
$ sam --version
SAM CLI, version 1.107.0
```

version 1.104.0 ~ 1.106.0では[ビルド時に不必要なWarningメッセージが出力される](https://github.com/aws/aws-sam-cli/issues/6444)ため修正パッチが適用された1.107.0を使用します。

https://github.com/aws/aws-sam-cli/issues/6444

### Slack Appの作成

[Slac API](https://api.slack.com/apps?new_app=1)を開き､From scratchからSlack Appを作成します｡ここでは、App Nameを`aws-sam-bedrock-slack-rag`とします｡

Basic Information画面のApp Credentialsに表示されているクレデンシャルは`SlackSigningSecret`として次のSecret Managerのシークレット登録に使用します。

![App Credentials](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/dcdec7e0-a9f6-6109-8e01-b4798c9716ab.png)

OAuth & Permissions画面のOAuth Tokens for Your WorkspaceにあるBot User OAuth Tokenは、`SlackBotToken`として次のSecret Managerのシークレット登録に使用します。

![OAuth Tokens for Your Workspace](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/7d19fb7c-908b-4c6b-ce5a-5ccfe807da7d.png)

OAuth & Permissions画面のBot Token Scopesにapp_mentions:readとchat:writeを追加します｡

![Bot Token Scopes](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/ac765a8c-ac58-17f4-f370-a1e24ca8fb85.png)

### シークレット情報をSecret Managerに登録

あらたにシークレットを作成し、ここまでの手順で作成した以下のシークレット情報を登録します。

|シークレットキー|値|
|-- |-- |
|SlackSigningSecret|前項のSlackのSigning Secret|
|SlackBotToken|前項のSlackのBot User OAuth Token|
|BedrockAgentId|AgentsのID|
|BedrockAgentAliasId|AgentsのエイリアスID|

![シークレット作成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/f3375dcc-bd44-921f-c149-cafc8a2009f4.png)


### アプリケーションの構築

ディレクトリ構造は以下のとおりです｡

```console
.
├── bedrock_slack_rag_app
│   ├── __init__.py
│   ├── app.py
│   └── requirements.txt
├── samconfig.toml
└── template.yaml
```

`__init__.py`は空のファイルです｡

requirements.txtは以下のとおりです｡

```console
slack-bolt
slack-sdk
boto3
```

<details><summary>template.yaml (長いので折りたたんでいます｡クリックして展開)</summary>

```python
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Slack Bedrock Assitant.

Resources:
  BedrockAssitantFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: bedrock_slack_rag_app/
      Handler: app.lambda_handler
      Runtime: python3.11
      Role: !GetAtt LambdaRole.Arn
      Timeout: 300
      MemorySize: 384
      Architectures:
        - x86_64
      Environment:
        Variables:
          SECRET_NAME: 'Bedrock-sam-secrets' # Name of the secret in Secrets Manager
          REGION_NAME: 'us-east-1' # Region of the secret in Secrets Manager
      Events:
        Slack:
          Type: Api
          Properties:
            Method: POST
            Path: /slack/events
      Layers:
        # https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html#ps-integration-lambda-extensions-add
        - arn:aws:lambda:us-east-1:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11

  LambdaRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: bedrock-slack-rag-app-lambda-role
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: allow-lambda-invocation
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                  - lambda:InvokeAsync
                Resource: "*"
        - PolicyName: SecretsManagerPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: 'secretsmanager:GetSecretValue' # Required for Lambda to retrieve the secret
                Resource: "*"
        - PolicyName: allow-bedrock-access
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - bedrock:InvokeModel
                  - bedrock:InvokeModelWithResponseStream
                Resource: "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-v2"
        - PolicyName: allow-bedrock-agent-access
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - bedrock:InvokeAgent
                Resource: "*"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  BacklogAssitantLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${BedrockAssitantFunction}
      RetentionInDays: 14 # Optional. Default retention is 30 days.

Outputs:
  BedrockAssitantApi:
    Description: "The URL of Slack Event Subscriptions"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/slack/events"
  BedrockAssitantFunction:
    Description: "Bedrock Assistant Lambda Function ARN"
    Value: !GetAtt BedrockAssitantFunction.Arn
  BedrockAssitantFunctionIamRole:
    Description: "Implicit IAM Role created for Bedrock Assistant function"
    Value: !GetAtt LambdaRole.Arn
```

</details>

AWS SAM テンプレートファイル(template.yaml)に､作成するAWSリソースを定義します｡Lambda関数用ロールやLambdaの環境変数､API Gatewayの設定が含まれます｡
Lambda関数からSecrets ManagerにアクセスするためにはAWSレイヤーが必要で､手動で追加する場合は以下の画面で`AWS-Parameters-and-Secrets-Lambda-Extension`を選択し､バージョンは11とします(11以外に選択肢がない)｡

![AWSレイヤーの設定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/f5dbf142-40d3-bc78-1ff8-617414041fc3.png)

template.ymlで定義する場合は､LayersにAWS Parameters and Secrets Lambda ExtensionのARNを設定します｡ARNは､以下のドキュメントに記載されています｡

https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html#ps-integration-lambda-extensions-add


ほかにも､LambdaからBedrockやSecrets Managerを扱うためのポリシー設定を行っています｡Environmentにある`SECRET_NAME`と`REGION_NAME`にはそれぞれ先ほど作成したSecrets Managerのシークレットの名前とリージョンを設定します｡IAMポリシーの定義において`Resource: "*"`としていますが、本番環境などでは許可するリソースを特定したほうがよいでしょう。例えば、AgentsのResourceを特定する場合は以下のように指定します。

```text
Resource: "arn:aws:bedrock:{Region}:{AWS Account ID}:agent-alias/{Agents ID}/{Agents Alias}"
```


<details><summary>samconfig.toml (長いので折りたたんでいます｡クリックして展開)</summary>

```python
# More information about the configuration file can be found here:
# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html
version = 0.1

[default]
[default.global.parameters]
stack_name = "bedrock-slack-rag-app"

[default.build.parameters]
cached = true
parallel = true

[default.validate.parameters]
lint = true

[default.deploy.parameters]
capabilities = "CAPABILITY_NAMED_IAM"
confirm_changeset = true
resolve_s3 = true
region = "us-east-1"

[default.package.parameters]
resolve_s3 = true

[default.sync.parameters]
watch = true

[default.local_start_api.parameters]
warm_containers = "EAGER"

[default.local_start_lambda.parameters]
warm_containers = "EAGER"
```

</details>

SAM CLIの実行設定ファイル(samconfig.toml)に､SAM CLIを実行する際の設定を定義します｡AWS SAMのチュートリアル: Hello World アプリケーションのデプロイを実行した際に作成されるsamconfig.tomlをもとにしています｡今回の例では､以下の点を変更しています｡

- `[default.global.parameters]`セクションのstack_nameを"sam-app"から"bedrock-slack-rag-appt"に変更
- `[default.deploy.parameters]`セクションにregion指定を追加
- `[default.deploy.parameters]`セクションのcapabilitiesを"CAPABILITY_IAM"から"CAPABILITY_NAMED_IAM"に変更


<details><summary>app.py (長いので折りたたんでいます｡クリックして展開)</summary>

```python
import ast
import json
import logging
import os
import re

import boto3
from botocore.config import Config
from botocore.exceptions import ClientError
from slack_bolt import App
from slack_bolt.adapter.aws_lambda import SlackRequestHandler

SlackRequestHandler.clear_all_log_handlers()
logging.basicConfig(
    format="%(asctime)s [%(levelname)s] %(message)s",
    level=logging.INFO
)

logger = logging.getLogger(__name__)


class SecretsManager:
    def __init__(self, secret_name, region_name):
        self.secret_name = secret_name
        self.region_name = region_name
        self.client = boto3.client(
            service_name='secretsmanager',
            region_name=region_name
        )

    def get_secret(self, key):
        try:
            get_secret_value_response = self.client.get_secret_value(
                SecretId=self.secret_name
            )
        except ClientError as e:
            raise e

        secret_data = get_secret_value_response['SecretString']
        secret = ast.literal_eval(secret_data)

        return secret[key]


secrets_manager = SecretsManager(
    secret_name=os.environ.get("SECRET_NAME"),
    region_name=os.environ.get("REGION_NAME")
)

app = App(
    signing_secret=secrets_manager.get_secret("SlackSigningSecret"),
    token=secrets_manager.get_secret("SlackBotToken"),
    process_before_response=True,
)


def agent_client(message, session_id):
    agetn_id = secrets_manager.get_secret("BedrockAgentId")
    agent_alias_id = secrets_manager.get_secret("BedrockAgentAliasId")
    enable_trace = False

    client = boto3.client(
            service_name='bedrock-agent-runtime',
            region_name="us-east-1",
            config=Config(
                read_timeout=180,
            )
        )

    response = client.invoke_agent(
            inputText=message,
            agentId=agetn_id,
            agentAliasId=agent_alias_id,
            sessionId=session_id,
            enableTrace=enable_trace,
    )

    event_stream = response['completion']
    data, uri, text, references = "", "", "", ""
    try:
        for event in event_stream:
            if 'chunk' in event:
                data = event['chunk']['bytes'].decode("utf-8")

            # Check if 'attribution' and 'citations' exist in 'chunk'
            if 'attribution' in event['chunk']:
                for citation in event['chunk']['attribution']['citations']:
                    # Check if 'retrievedReferences' exist in 'citation'
                    if 'retrievedReferences' in citation:
                        for refs in citation['retrievedReferences']:
                            if 'location' in refs:
                                uri = refs['location']['s3Location']['uri']
                            if 'content' in refs:
                                text = refs['content']['text']
                            references += uri + "\n" + text[:100].replace('\n', '') + "...\n\n"
            if 'trace' in event:
                logger.info(json.dumps(event['trace'], indent=2))
    except Exception as e:
        raise Exception("Error in processing response: ", e) from e

    return data + "\n\n" + "参照元:\n" + references if references else data


def handle_app_mentions(event, say):
    thread_ts = event["ts"]
    userid = event["user"]
    message = re.sub("<@.*>", "", event["text"])

    say("\n\nお待ちください...", thread_ts=thread_ts)
    if "thread_ts" in event:
        thread_ts = event["thread_ts"]

    response = agent_client(message, thread_ts)
    say(f"<@{userid}> {response}", thread_ts=thread_ts)


def respond_to_slack_within_3_seconds(ack):
    ack()


app.event("app_mention")(
    ack=respond_to_slack_within_3_seconds,
    lazy=[handle_app_mentions]
)


def lambda_handler(event, context):
    logging.info(json.dumps(event))

    retry_counts = event.get("multiValueHeaders", {}).get("X-Slack-Retry-Num", [0])

    if retry_counts[0] != 0:
        logging.info("Skip slack retrying(%s).", retry_counts)
        return {}

    slack_handler = SlackRequestHandler(app=app)
    return slack_handler.handle(event, context)
```

</details>

ボットプログラムの本体です｡Bolt for Python を使ったアプリをAWS Lambda上で動かしやすくするためのLazy Listenersを利用しています。SlackのクレデンシャルやAgentsのIDなどはSecrets Managerから取得しています。

### ビルド

template.yamlがあるディレクトリで､ビルドコマンドを実行します｡

```console
$ sam build
```

ビルドに成功すると､以下のようなメッセージが表示されます｡

```console
Starting Build use cache
Manifest is not changed for (BedrockAssitantFunction), running incremental build
Building codeuri: /home/xxx/aws-sam-bedrock-slack-rag-app/bedrock_slack_rag_app runtime: python3.11 metadata: {} architecture:
x86_64 functions: BedrockAssitantFunction
 Running PythonPipBuilder:CopySource
 Running PythonPipBuilder:CopySource

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch
[*] Deploy: sam deploy --guided
```

### デプロイ

ビルドでエラーがなければsam deployコマンドを実行し､デプロイを行います。

```console
$ sam deploy
```

デプロイが成功すると､以下のような情報がコンソールに出力されます｡

```text
CloudFormation outputs from deployed stack
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 BedrockAssitantApi
Description         The URL of Slack Event Subscriptions
Value               https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/Prod/slack/events

Key                 BedrockAssitantFunction
Description         Bedrock Assistant Lambda Function ARN
Value               arn:aws:lambda:us-east-1:xxxxxxxxxxxx:function:bedrock-slack-rag-app-BedrockAssitantFunction-xxxxxxxxxxxx

Key                 BedrockAssitantFunctionIamRole
Description         Implicit IAM Role created for Bedrock Assistant function
Value               arn:aws:iam::xxxxxxxxxxxx:role/bedrock-slack-rag-app-lambda-role
-------------------------------------------------------------------------------------------------------------------------------------------------------------
```

### Slack Appの設定

メンションイベントに応答するために､Event Subscriptions画面のSubscribe to bot eventsにapp_mentionを追加します｡

![Event Subscriptions](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/27e712e6-9106-66cc-b28c-cc8158f88e01.png)

Event Subscriptions画面のEnable EventsをOnにし､Request URLにさきほど出力されたURL https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/Prod/slack/events を入力します｡
`Verified✓` と表示されれば､正しいURLが入力されたことになります｡レスポンスが得られるようになるまで時間がかかる場合があります。正しいURLを入力しているにもかかわらずVerifiedとならない場合は、時間をおいて再試行します。

![Enable_events.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/d0daeada-23c8-c170-0898-6672773f9717.png)

設定を追加後､画面最下部にあるSave Changesをクリックし内容を保存します｡

### Slack Workspaseにアプリをインストール

Install App画面のInstall to Workspaceをクリックし､Slack AppをWorkspaceにインストールします｡
インストールに成功すると､Thank you!画面が表示されます｡Slackアプリをインストールしている場合は`click here`のリンク､Webブラウザを使用している場合は､`this link`をクリックしてSlackを開きます｡

## 動作確認

任意のチャンネンルに､`@aws-sam-bedrock-slack-rag` (Slack Appの作成時に設定した名前)を招待し､メンション形式で依頼をポストします｡動作しない場合のログやOpenAI APIが返すレスポンスは CloudWatch Logs に出力されているログが参考になります｡

## 作成したリソースの削除

最後に､作成したアプリケーションを削除する手順です｡リソースを削除するには sam delete コマンドを実行します｡

```console
$ sam delete
```

## まとめ

Knowledge Bases for Amazon BedrockとAgents for Amazon bedrock､Pineconeを使うことでRAGを手軽に構築できました｡Vector Databaseを構築しAgentsで応答を試す工程まではノーコードで構築できるため､手元のPDFやWordドキュメントに対して生成AIがどのような応答を示すのか容易に試すことができます｡knowledge baseにはあらかじめ前処理や後処理、応答のプロンプトが設定済みなので、複雑なプロンプトを意識しなくてよいのも利点です。もちろん、カスタマイズもできるため、より高度な利用も可能です。

少し手を加えて､S3オブジェクトの更新をトリガーにしてknowledge baseのSyncを実行するような仕組みを入れると､最新の情報に対してAgentsが応答するようなこともできそうです｡
また､Agentsに対してboto3クライアントで接続できるため､既存のアプリケーションから呼び出したり､今回のようにチャットボットを作ったり用途はいろいろとありそうです｡

記事執筆時点では､knowledge baseの埋め込みモデルやAgentsの言語モデルが限定されているため､日本語のドキュメントに対する応答が不自然に感じるところもありました｡この点は､今後AWS側で改善されると期待しています｡