【パワポ対応版】資料作成はAIにまかせよう！AWSでBedrockエージェント入門ハンズオン

:::note info
2025/2/4更新：Web検索に利用していたDuckDuckGoのAPIが秋ごろからレートリミットエラーを出すようになっていたため、Tavilyという別サービスを利用する手順へ変更しました！
:::

# この記事は何？

先日公開した以下ハンズオンが大変好評だったので、今回はバリエーション版を作成しました。

https://qiita.com/minorun365/items/9b4ae4d50a93123dd8c2

GoogleスライドではなくPowerPoint資料を作成するようにアレンジしています。

- MS Officeを普段使いしている会社で、より実務に活かしやすくなります。
- Google関連の初期設定が不要となり、ハンズオンの所要時間を短縮できます。

![スクリーンショット 2024-08-27 22.01.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/04ea0c51-cb8a-f83e-2b78-53ddd2c0c74b.png)

（デモ動画）

- https://www.youtube.com/watch?v=78DPihePNUo


# 本ハンズオンの概要

生成AIブームは終わりが見えませんが、そろそろRAGは十分試したよという方も多いのではないでしょうか。次のトレンドと目されているのが、人間の代わりに自動で仕事してくれる「AIエージェント」です。

https://xtech.nikkei.com/atcl/nxt/column/18/00001/09497/

AWSクラウドの生成AIサービス「Amazon Bedrock」には、そんなエージェントを簡単に作れるマネージドサービス **「Agents for Amazon Bedrock」** という機能があります。

これを使えば、難しいPythonのコードをたくさん書かなくても、AWSのマネジメントコンソールでGUIをポチポチやるだけで賢いエージェントが作れてしまいます。


### 今回作るアプリ

ユーザーが「xxxについて資料にまとめて」と依頼すると、
- Web検索して情報を集める
- PowerPointで資料を作成してS3に保存し、署名付きURLを発行する
- 発行したURLをユーザーにメールで送る

という流れを自動でやってくれます。

不明点があればユーザーに聞き返しますし、AIは各作業の結果をうけて次のアクションを柔軟に調整します。（例：メールの送信に失敗したら結果をチャット画面上に表示するなど）


### アーキテクチャ

![スクリーンショット 2024-08-27 19.25.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/c242fc16-5d5f-a007-2890-f0d1107db064.png)

Web検索にはAPIキーが不要なDuckDuckGoというサービスを利用していましたが、最近レートリミットでまともに使えなくなったため、Tavilyという別サービスに手順変更しました。

:::note info
今回は含めていませんが、ここに「ナレッジベース」機能でRAGを組み合わせて社内文書を検索させることも可能です。
:::


### そもそもBedrockって何？

色んなAIモデルをAPIとしてサーバーレスで利用できる、AWSの機能です。
概要資料をまとめていますので、ご興味ある方はご覧ください！

https://speakerdeck.com/minorun365/awsnosheng-cheng-aisabisu-amazon-bedrockru-men-2025nian-1yue-ban


# ハンズオン手順

基本的にWebブラウザがあれば実施できます。
すべてサーバーレスで構築していますので、費用もかなり少額で済むはずです。


## 1. AWSアカウント作成

以下を参考に、AWSアカウントを新規作成しましょう。

https://aws.amazon.com/jp/register-flow/

アカウント作成したら、以下URLよりAWSマネジメントコンソールにサインインします。

- https://console.aws.amazon.com

サインインしたら、右上のリージョンを「オレゴン」に切り替えておきましょう。

![スクリーンショット 2024-08-25 14.01.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/6ae31700-3d18-a842-3bc2-005ac926ef27.png)

:::note info
このハンズオンでは、オレゴンリージョンのみを利用します。（Bedrockのモデル利用クォータ制限が最も緩いため）
:::


## 2. Bedrock設定

次はいよいよ生成AIサービスの設定です。

![スクリーンショット 2024-08-28 0.50.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/8d51236e-074e-3e02-e736-24e3babda329.png)

マネジメントコンソール上部の検索ボックスに `bedrock` と入力して、Amazon Bedrockのコンソールへ移動しましょう。

![スクリーンショット 2024-08-27 20.01.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/2ab5d27d-3dcb-cdde-bc38-56b177da18d3.png)



### モデル有効化

画面左下の「モデルアクセス」から、Anthropic社のClaudeを有効化しましょう。

![スクリーンショット 2024-08-25 14.05.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/d7816fb9-3977-ef52-429e-a934a62a826f.png)

「Enable spesific models」をクリックして、`Anthropic > Claude 3.5 Sonnet v2` にチェックを入れて進みます。

用途の申告が求められるので、以下を簡単に入力して送信しましょう。

- 会社名： あなたの所属会社名
- 会社サイトのURL： あなたの会社のHPなど
- 業界： あなたの会社の所属業界名
- 対象ユーザー： `社内の従業員`
- ユースケースの説明： `個人検証` など

![スクリーンショット 2024-08-27 19.31.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/e7ee3e00-f5d3-f7fc-3eb8-e244bf1da66f.png)

完了後、1〜2分でモデルが有効化されます。待たずに次の作業を進めましょう。


### エージェント作成

次に「エージェント > エージェントを作成」を実施します。

![スクリーンショット 2024-08-25 14.09.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/d2501e69-3488-53c7-2f1b-1ba351564a1b.png)

エージェント名はデフォルトのまま「作成」をクリックします。

エージェントビルダーという編集画面が開くので、以下のとおり設定します。

- モデルを選択： Anthropic > Claude 3.5 Sonnet v2 > US Claude 3.5 Sonnet v2　※オンデマンドではなく推論プロファイルを選択
- エージェント向けの指示： 

```
- ユーザーからの依頼をもとに、クエリーを考案してWeb検索を行い、PowerPointに調査結果をまとめてください。
- スライドは必ず6ページ以上作成し、各ページには必ず見出しをつけ、内容を箇条書きで複数含めてください。
- 作ったスライドのURLをユーザーにメールで送信してください。
- すべての処理が終わったら、ユーザーへはメールを送信したことを伝えてください。
```

- Additional settings
    - ユーザー入力： Enabled

![スクリーンショット 2024-08-27 19.36.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/9512471c-dc77-85c6-b115-281769dd492e.png)


上記が設定できたら、画面上部の「保存」をクリックします。


### アクショングループ作成

Bedrockのエージェントでは、AIが実行できるタスクを「アクショングループ」として定義します。今回は「Web検索」「スライド作成」「メール送信」の3つのアクショングループを作成します。

エージェントビルダー画面下部のアクショングループにある「追加」ボタンをクリックしましょう。

![スクリーンショット 2024-08-25 14.26.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/85d75d7d-5ca7-7bb4-b015-eb652a172163.png)

アクショングループを以下のとおり、3つ作成します。


#### アクショングループ（1つ目）

- アクショングループ名： search-web
- 説明： `与えられたクエリーでWeb検索を行い、結果を返します。`
- Action group function 1
    - Name： search-web
    - 説明： `与えられたクエリーでWeb検索を行い、結果を返します。`
    - Parameters： 以下のとおり

| Name | Description | Type | Required |
|:-|:-|:-|:-|
| query  | Web検索クエリー  | string  | True  |

上記を設定したら、他はデフォルトのまま右下の「作成」をクリックします。
同様に、アクショングループの「追加」ボタンから次も作成します。


#### アクショングループ（2つ目）

- アクショングループ名： create-pptx
- 説明： `与えられたトピックについて、PowerPointで日本語の解説資料を作成します。`
- Action group function 1
    - Name： create-pptx
    - 説明： `与えられたトピックについて、PowerPointで日本語の解説資料を作成します。`
    - Parameters： 以下のとおり

| Name | Description | Type | Required |
|:-|:-|:-|:-|
| topic  | スライドのメイントピック  | string  | True  |
| content  | スライドの内容  | string  | True  |

上記を設定したら、他はデフォルトのまま右下の「作成」をクリックします。
同様に、アクショングループの「追加」ボタンから次も作成します。


#### アクショングループ（3つ目）

- アクショングループ名： send-email
- 説明： `作成されたPPTXファイルのURLをユーザーにメール送信します。`
- Action group function 1
    - Name： send-email
    - 説明： `作成されたPPTXファイルのURLをユーザーにメール送信します。`
    - Parameters： 以下のとおり

| Name | Description | Type | Required |
|:-|:-|:-|:-|
| url  | PPTXファイルの署名付きURL  | string  | True  |

上記を設定したら、他はデフォルトのまま右下の「作成」をクリックします。
3つのアクショングループを作成すると、エージェントビルダーで以下のように表示されます。

![スクリーンショット 2024-08-27 19.43.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/ebfb0afd-9a8d-35c8-a400-86dbf1fa5c8b.png)


## 3. S3バケット作成

エージェントが作ったパワポ資料を格納する場所を作成します。

![スクリーンショット 2024-08-28 0.51.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/54b066b2-47d4-1311-07d1-d67d55f60103.png)

Amazon S3のコンソールに移動し、バケットを新規作成してください。

![スクリーンショット 2024-08-27 19.46.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/00ef7c60-d0a9-4ce8-0982-12c58dc0c1dd.png)

他の人とかぶらない好きな名前（ `pptx-あなたのニックネームなど-日付8桁` など）にしましょう。他の設定はすべてデフォルトのままでOKです。

![スクリーンショット 2024-08-27 19.47.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/6e623633-5986-c249-1ad3-3a5817c7be3d.png)

S3バケット名はまた後で使いますので、作業PCのメモ帳などにコピペしておきましょう。


## 4. SNS設定

Lambdaからメールを送信するために、Amazon SNSを事前に設定します。

![スクリーンショット 2024-08-28 0.51.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/af5af87f-3115-066d-2cd3-7e899839669d.png)

SNSコンソールのトップより、`bedrock-agent` という名前のトピックを作成します。他はデフォルト設定のままで大丈夫です。

![スクリーンショット 2024-08-25 15.40.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/b20c5dd3-8f05-6762-6326-daaf68654791.png)

トピックが作成されたら、「ARN」を作業PCのメモ帳などにコピーしておきましょう。後で使います。

![スクリーンショット 2024-08-27 19.52.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/6b11e8e1-5768-c652-ec4b-c6a42b5639b0.png)

次に、このトピックに届いたメッセージを配信する「サブスクリプション」を作成しましょう。

![スクリーンショット 2024-08-25 15.42.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/9c86839f-9201-f283-f902-90945e2d1537.png)

- プロトコル： Eメール
- エンドポイント： 自分のメールアドレス

上記を設定したら、設定したメールアドレス宛てに確認メールが届くので「Confirm subscription」リンクをクリックしましょう。これで通知が配信されるようになります。

![スクリーンショット 2024-08-27 19.56.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/975a3cbe-7a59-0632-c16e-b793e3ecd458.png)



## 5. Lambda設定

先ほどBedrockエージェントのアクショングループを作成した時点で、Lambda関数が自動で作成されているのですが、中身のコードがほぼ空っぽなので編集します。

![スクリーンショット 2024-08-28 0.52.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/bc73d1dc-f79a-5ca1-deb9-e03805a2b45b.png)

AWS Lambdaのコンソールに移動して「関数」を開き、オレゴンリージョンに3つの関数が作成されていることを確認します。

![スクリーンショット 2024-08-27 20.03.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/029f16e6-9c3a-b0f6-79ec-1a725ed69d75.png)


### Lambdaレイヤーの準備

各アクショングループ用のLambda関数を作成する前に、Lambdaが必要とするPythonの外部ライブラリを「レイヤー」として事前作成しておきます。

最初にマネコン右上の `[>.]` アイコンより、CloudShellを起動します。

![スクリーンショット 2024-08-25 15.17.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/bbf6a842-0faa-6107-21e2-f2e488261a2a.png)

以下コマンドを実行して、Pythonライブラリ2つ（DuckDuckGoとpython-pptx）をZIP化します。

```bash: CloudShell
# レイヤー用のディレクトリを作成
mkdir python

# 作成したディレクトリに、必要なライブラリをインストール
pip install tavily-python python-pptx -t python --platform manylinux2014_x86_64 --only-binary=:all:

# インストールしたライブラリをZIP圧縮
zip -r layer.zip python
```

CloudShell右上の「アクション > ファイルのダウンロード」をクリックし、ファイルパスとして `layer.zip` を入力して、作業PCのローカルにZIPファイルをダウンロードしておきます。

![スクリーンショット 2024-08-27 20.09.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/9903e4e0-1b48-aab1-65a4-c699007e0575.png)

ダウンロードが完了したら、CloudShellのウィンドウは閉じてしまっても大丈夫です。


### Lambdaレイヤーの作成

まずは「レイヤー > レイヤーの作成」より、いま作成したZIPでLambdaレイヤーを作成します。

- 名前： `tavily-pptx`
- .zipファイルをアップロード： ダウンロードしたZIPファイルを指定（目安：14MB）
- 互換性のあるアーキテクチャ： x86_64
- 互換性のあるランタイム： Python 3.9

![スクリーンショット 2024-08-27 20.12.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/6e4e812f-f3fc-e127-6bc1-a91aee159794.png)

他はそのままで「作成」をクリックします。10秒ちょっとかかります。


### Lambda関数の設定

その後、左サイドバーの「関数」より、3つの関数をそれぞれ設定していきます。


#### search-web 関数

- コードソース：以下で上書きし、「Deploy」をクリックします。

```py: dummy_lambda.py
import os
import json
from tavily import TavilyClient

def lambda_handler(event, context):
    # 環境変数からAPIキーを取得
    tavily_api_key = os.environ.get('TAVILY_API_KEY')
    
    # eventからクエリパラメータを取得
    parameters = event.get('parameters', [])
    for param in parameters:
        if param.get('name') == 'query':
            query = param.get('value')
            break
    
    # Tavilyクライアントを初期化して検索を実行
    client = TavilyClient(api_key=tavily_api_key)
    search_result = client.get_search_context(
        query=query,
        search_depth="advanced",
        max_results=10
    )
    
    # 成功レスポンスを返す
    return {
        'messageVersion': event['messageVersion'],
        'response': {
            'actionGroup': event['actionGroup'],
            'function': event['function'],
            'functionResponse': {
                'responseBody': {
                    'TEXT': {
                        'body': json.dumps(search_result, ensure_ascii=False)
                    }
                }
            }
        }
    }

```

![スクリーンショット 2024-08-27 20.28.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/1304bafa-a7c1-cd5a-179d-0fde4b305328.png)

その後、下にスクロールして次の設定をします。

- ランタイム設定：「編集」をクリック
    - ランタイム： Python 3.9
- レイヤー：「レイヤーの追加」をクリック
    - カスタムレイヤー： tavily-pptx
    - バージョン： 1

その後「設定」タブから以下を設定します。

- 一般設定：「編集」をクリック
    - タイムアウト： 0分30秒
- 環境変数：「編集」をクリック
    - キー： `TAVILY_API_KEY`
    - 値： 以下サイトにサインアップし、APIキーをコピペ

https://tavily.com/

TavilyはLLMに特化したWeb検索サービスです。なんと無料でも月1,000回まで検索APIを実行できます。

:::note warn
今回は簡易検証のためAPIキーを環境変数に設定していますが、本格利用の際はAWS Secrets Managerなどを利用してセキュアに運用してください。
:::

完了したら、次の関数を探して以下の設定に進みましょう。


#### create-pptx 関数

- コードソース：以下で上書きし、「Deploy」をクリックします。

```py: dummy_lambda.py
import os
import json
import boto3
from pptx import Presentation
from datetime import datetime

# 環境変数からS3バケット名を取得
S3_BUCKET_NAME = os.getenv("S3_BUCKET_NAME")

def lambda_handler(event, context):

    # イベントパラメータからトピックとコンテンツを抽出
    topic = next((item["value"] for item in event["parameters"] if item["name"] == "topic"), "")
    content = next((item["value"] for item in event["parameters"] if item["name"] == "content"), "")
    
    # 空白文字を削除し、コンテンツを空行で分割
    content = content.strip()
    slides_content = content.split('\n\n')
    
    # プレゼンテーションオブジェクトを作成
    prs = Presentation()
    
    # タイトルスライドの作成
    title_slide_layout = prs.slide_layouts[0]
    slide = prs.slides.add_slide(title_slide_layout)
    title = slide.shapes.title
    subtitle = slide.placeholders[1]
    title.text = topic
    subtitle.text = f"作成日: {datetime.now().strftime('%Y年%m月%d日')}"
    
    # コンテンツスライドの作成
    for i, slide_content in enumerate(slides_content):
        content_slide_layout = prs.slide_layouts[1]
        slide = prs.slides.add_slide(content_slide_layout)
        title = slide.shapes.title
        content_shape = slide.placeholders[1]
        
        # 見出しと本文を作成
        lines = slide_content.split('\n')
        title.text = lines[0].lstrip('- ')
        content_shape.text = '\n'.join([line.lstrip('- ') for line in lines[1:]])
    
    # S3にファイルを保存する準備
    s3 = boto3.client("s3")
    bucket_name = S3_BUCKET_NAME
    file_name = f"{topic.replace(' ', '_')}.pptx"
    file_path = f"/tmp/{file_name}"

    # S3バケットにファイルをアップロード
    prs.save(file_path)
    s3.upload_file(file_path, bucket_name, file_name)
    
    # ファイルへの署名付きURLを生成
    url = s3.generate_presigned_url(
        'get_object',
        Params={'Bucket': bucket_name, 'Key': file_name},
        ExpiresIn=3600
    )
    
    # エージェント用のレスポンスを返す
    return {
        "messageVersion": "1.0",
        "response": {
            "actionGroup": event["actionGroup"],
            "function": event["function"],
            "functionResponse": {
                "responseBody": {
                    "TEXT": {
                        "body": json.dumps(
                            {"signed_url": url}
                        )
                    }
                }
            },
        },
    }
```

- ランタイム設定：「編集」をクリック
    - ランタイム： Python 3.9
- レイヤー：「レイヤーの追加」をクリック
    - カスタムレイヤー： tavily-pptx
    - バージョン： 1

その後、「設定」タブから以下を設定します。

- 一般設定：「編集」をクリック
    - タイムアウト： 0分30秒
- 環境変数：「編集 > 環境変数の追加」をクリック
    - キー： `S3_BUCKET_NAME`
    - 値： 先ほど作成したS3バケット名（メモっていない場合は別タブで確認）
- アクセス権限：実行ロールの「ロール名」をクリック

![スクリーンショット 2024-08-27 20.36.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/5bdb6b15-a641-61df-ccc3-79180dd16d00.png)

IAMロールの編集画面に飛ぶので、「許可を追加 > ポリシーをアタッチ」をクリックします。

![スクリーンショット 2024-08-27 20.36.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/530a07be-b9b3-0c44-ec97-3bc1f9a29d6e.png)

`AmazonS3FullAccess` を探してチェックを入れ、「許可を追加」をクリックします。

![スクリーンショット 2024-08-27 20.37.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/1ac428e0-9e71-45e4-162b-8bd7a51c68c4.png)

完了したら、次の関数を探して以下の設定に進みましょう。


#### send-email 関数

- コードソース：以下で上書きし、「Deploy」をクリックします。

```py: dummy_lambda.py
import os
import json
import boto3

# 環境変数からSNSトピックARNを取得
SNS_TOPIC_ARN = os.environ.get("SNS_TOPIC_ARN")

def lambda_handler(event, context):
    # イベントパラメーターからURLを取得
    url = event.get("parameters", [{}])[0].get("value")

    # SNSメッセージの発行
    boto3.client("sns").publish(
        TopicArn=SNS_TOPIC_ARN, 
        Message=f"Bedrockエージェントがスライドを作成しました。URLの有効期限は1時間です：\n{url}",
        Subject="スライド作成通知"
    )

    # エージェント用のレスポンスを返す
    return {
        "messageVersion": "1.0",
        "response": {
            "actionGroup": event.get("actionGroup", "send-email"),
            "function": event.get("function", "send-email"),
            "functionResponse": {
                "responseBody": {
                    "TEXT": {
                        "body": json.dumps(
                            {
                                "message": "Email sent successfully",
                                "presentationUrl": url,
                            }
                        )
                    }
                }
            },
        },
    }
```

※ランタイムやレイヤーの設定は不要です。

その後「設定」タブから以下を設定します。

- 環境変数：「編集」をクリック
    - キー： `SNS_TOPIC_ARN`
    - 値： 先ほど作成したSNSトピックのARN（メモっていない場合は別タブで確認）
- アクセス権限：実行ロールの「ロール名」をクリック

![スクリーンショット 2024-08-25 16.21.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/3af5716c-30f7-79fc-81be-3b60c33d06cd.png)

IAMロールの編集画面に飛ぶので、「許可を追加 > ポリシーをアタッチ」をクリックします。

`AmazonSNSFullAccess` にチェックを入れ、「許可を追加」をクリックします。

![スクリーンショット 2024-08-25 16.24.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/ae8ccb7d-e133-1810-7ee3-e882c17d7535.png)

これで3つの関数の設定が完了です。まとめると以下のようになります。

|  | search-web | create-pptx | send-email |
|:-|:-|:-|:-|
| ランタイム | Python 3.9 | Python 3.9 | （変更不要） |
| レイヤー | tavily-pptx | tavily-pptx | （変更不要） |
| タイムアウト | 30秒 | 30秒 | （変更不要） |
| 環境変数 | （変更不要） | S3_BUCKET_NAME | SNS_TOPIC_ARN |
| アクセス権限 | （変更不要） | S3を追加 | SNSを追加 |


## 6. エージェント動作確認

ここまで設定できたら、アプリ画面に組み込む前にエージェントがうまく動くか動作確認をしてみましょう。

![スクリーンショット 2024-08-28 0.54.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/0eaa1336-3415-31cb-f663-3daef2bad154.png)

Bedrockのコンソールに移動し、「エージェント」より先ほど作成したエージェント名をクリックして開きます。

![スクリーンショット 2024-08-27 20.44.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/92d80551-91ab-2c70-208e-33d565b91d31.png)

右側のテスト用サイドバーにある「準備」をクリックします。

![スクリーンショット 2024-08-25 16.36.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/edf5b8e6-349b-e5b4-3c6d-974a3d3cb746.png)

チャットボックスに `KAGという会社を調べてパワポにまとめて` と送信してみましょう。

![スクリーンショット 2024-08-27 21.16.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/4dc4a726-5d67-e655-c0dc-d4b1c9b1edd2.png)

30秒〜1分ほど待つと、エージェントから返事が来ます。
確かにAmazon SNSからメールが届いています。

![スクリーンショット 2024-08-27 21.19.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/45d3ba82-fa67-fee1-d806-fd596dba7560.png)

URLをクリックすると、パワポがダウンロードできました！

社長の名前だけ少し惜しいのですが、Web検索を行っているので社名略称だけで情報をちゃんと集めており、かなりハルシネーションは少なくなっています。

![スクリーンショット 2024-08-27 21.20.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/29991ff6-833b-09df-536c-4fa31fea34e7.png)

ここまで動いたら、エージェント画面下部の「エイリアス > 作成」より、エイリアスを作成しておきます。エージェントの新バージョンをリリースする、といった作業です。

- エイリアス名： v1

![スクリーンショット 2024-08-25 17.32.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/7181d1d3-3580-8a6a-6892-5dbb848945ab.png)

エイリアスが登録されたら、画面上部にあるエージェントの「ID」と、画面下部にある「エイリアスID」を作業PCのメモ帳などにコピペしておきましょう。後で使います。

:::note info
うまく動かないときは、以下を確認して原因を切り分けてみましょう。

- エージェントのトレースを確認する
- Lambdaの「モニタリング > CloudWatchログを表示」より、ログストリームを確認する

ハマりやすい点は以下です。

- モデルは有効化できているか？
- オレゴン以外のリージョンで作業していないか？
- アクショングループのパラメーターに設定ミス（変数名の誤植など）はないか？
- Lambda関数のコードを「デプロイ」し忘れていないか？
- Lambdaレイヤーの中身は問題ないか？
- Lambdaのランタイム設定、レイヤー、環境変数、IAMロールに漏れはないか？
- SNSからの確認メールをちゃんと一度クリックしているか？
- Claude 3.5 Sonnet v2のレートリミット（Service Quotas）に抵触していないか？
:::

なお、Bedrockは世界的に需要が高く、新規作成したばかりのAWSアカウントでは初期クォータがかなり低く設定されています。

もしレートリミットに抵触してしまう場合は、Claude 3 Sonnetなど別のモデルに変更するか、数日かかりますがAWSサポートにリクエストを上げるとクォータを緩和してもらえます。

https://aws.amazon.com/jp/blogs/news/generative-ai-amazon-bedrock-handling-quota-problems/


## 7. フロントエンド開発

エージェントが単体でうまく動いたので、これを実際にアプリケーションに組み込んでみましょう。

![スクリーンショット 2024-08-28 0.52.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/161a8269-3620-3078-cdeb-c2a1282937a2.png)

まず、以下のソースコードを作業PCのメモ帳などに貼り付けて、8-9行目の `XXXXXXXXXX` 部分に先ほど作成したエージェントのIDとエイリアスIDを記載してください。

その後、`frontend.py` という名前で作業PCのローカルに保存しましょう。

```py: frontend.py
import json
import uuid
import boto3
import streamlit as st
from botocore.exceptions import ClientError
from botocore.eventstream import EventStreamError

agent_id = "XXXXXXXXXX" # あなたのエージェントIDをここにコピペしてね
agent_alias_id = "XXXXXXXXXX" # あなたのエイリアスIDをここにコピペしてね

def initialize_session():
    """セッションの初期設定を行う"""
    if "client" not in st.session_state:
        st.session_state.client = boto3.client("bedrock-agent-runtime")
    
    if "session_id" not in st.session_state:
        st.session_state.session_id = str(uuid.uuid4())
    
    if "messages" not in st.session_state:
        st.session_state.messages = []
    
    if "last_prompt" not in st.session_state:
        st.session_state.last_prompt = None
    
    return st.session_state.client, st.session_state.session_id, st.session_state.messages

def display_chat_history(messages):
    """チャット履歴を表示する"""
    st.title("パワポ作ってメールで送るマン")
    st.text("Web検索の結果をスライドにまとめて、メールで送るよ！")
    
    for message in messages:
        with st.chat_message(message['role']):
            st.markdown(message['text'])

def handle_trace_event(event):
    """トレースイベントの処理を行う"""
    if "orchestrationTrace" not in event["trace"]["trace"]:
        return
    
    trace = event["trace"]["trace"]["orchestrationTrace"]
    
    # 「モデル入力」トレースの表示
    if "modelInvocationInput" in trace:
        with st.expander("🤔 思考中…", expanded=False):
            input_trace = trace["modelInvocationInput"]["text"]
            try:
                st.json(json.loads(input_trace))
            except:
                st.write(input_trace)
    
    # 「モデル出力」トレースの表示
    if "modelInvocationOutput" in trace:
        output_trace = trace["modelInvocationOutput"]["rawResponse"]["content"]
        with st.expander("💡 思考がまとまりました", expanded=False):
            try:
                thinking = json.loads(output_trace)["content"][0]["text"]
                if thinking:
                    st.write(thinking)
                else:
                    st.write(json.loads(output_trace)["content"][0])
            except:
                st.write(output_trace)
    
    # 「根拠」トレースの表示
    if "rationale" in trace:
        with st.expander("✅ 次のアクションを決定しました", expanded=True):
            st.write(trace["rationale"]["text"])
    
    # 「ツール呼び出し」トレースの表示
    if "invocationInput" in trace:
        invocation_type = trace["invocationInput"]["invocationType"]
                
        if invocation_type == "ACTION_GROUP":
            with st.expander("💻 Lambdaを実行中…", expanded=False):
                st.write(trace['invocationInput']['actionGroupInvocationInput'])
    
    # 「観察」トレースの表示
    if "observation" in trace:
        obs_type = trace["observation"]["type"]
        
        if obs_type == "ACTION_GROUP":
            with st.expander(f"💻 Lambdaの実行結果を取得しました", expanded=False):
                st.write(trace["observation"]["actionGroupInvocationOutput"]["text"])
                
def invoke_bedrock_agent(client, session_id, prompt):
    """Bedrockエージェントを呼び出す"""
    return client.invoke_agent(
        agentId=agent_id,
        agentAliasId=agent_alias_id,
        sessionId=session_id,
        enableTrace=True,
        inputText=prompt,
    )

def handle_agent_response(response, messages):
    """エージェントのレスポンスを処理する"""
    with st.chat_message("assistant"):
        for event in response.get("completion"):
            if "trace" in event:
                handle_trace_event(event)
            
            if "chunk" in event:
                answer = event["chunk"]["bytes"].decode()
                st.write(answer)
                messages.append({"role": "assistant", "text": answer})

def show_error_popup(exeption):
    """エラーポップアップを表示する"""
    if exeption == "throttlingException":
        error_message = "【エラー】Bedrockのモデル負荷が高いようです。1分ほど待ってから、ブラウザをリロードして再度お試しください🙏（改善しない場合は、モデルを変更するか[サービスクォータの引き上げ申請](https://aws.amazon.com/jp/blogs/news/generative-ai-amazon-bedrock-handling-quota-problems/)を実施ください）"
    st.error(error_message)

def main():
    """メインのアプリケーション処理"""
    client, session_id, messages = initialize_session()
    display_chat_history(messages)
    
    if prompt := st.chat_input("例：日本のBedrock最新事例をリサーチして"):
        messages.append({"role": "human", "text": prompt})
        with st.chat_message("user"):
            st.markdown(prompt)
        
        try:
            response = invoke_bedrock_agent(client, session_id, prompt)
            handle_agent_response(response, messages)
            
        except (EventStreamError, ClientError) as e:
            if "throttlingException" in str(e):
                show_error_popup("throttlingException")
            else:
                raise e

if __name__ == "__main__":
    main()
```

CloudShellを再度開き、右上の「アクション > ファイルのアップロード」よりこのファイルをアップロードします。

![スクリーンショット 2024-08-27 21.34.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/fe36590e-f1bb-bec1-6b6c-9ce070d6664b.png)

その後、以下のコマンドを実行します。

```bash: CloudShell
# Pythonの外部ライブラリをインストール
pip install boto3 streamlit

# Streamlitアプリを起動
streamlit run frontend.py
```

Streamlitのアクセス用URLが表示されたら、うまく起動しています。

![スクリーンショット 2024-08-25 17.22.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/88d3c146-1acb-a552-e3f1-133942038f0a.png)

その後、CloudShell上部の「＋」をクリックして、2つ目の「us-east-1」ターミナルを起動して以下を実行します。

```bash: CloudShell
# PinggyにSSH接続し、インターネットからアクセス可能なURLを生成する
ssh -p 443 -R0:localhost:8501 a.pinggy.io
```

確認メッセージが出力されたら `yes` と入力してEnterを押すと、Pinggyという外部サービスを通じてこのアプリにアクセス可能なURLが発行されます。

下側のHTTP**S**の方のURLをコピーして、ブラウザの別タブからアクセスしてみましょう。

![スクリーンショット 2024-08-25 17.26.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/e88d75ac-6b34-696a-c68b-4787a9140a06.png)

「Enter site」をクリックすると、先ほどアップロードしたPythonアプリにアクセスできます。Streamlitというフレームワークを使って、フロントエンドを表示しています。

![スクリーンショット 2025-02-04 1.58.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1633856/a838ed7a-11a2-5bd2-83ba-fc9ddd831065.png)


実際にこのアプリを使ってみましょう。

エージェントのトレース情報が、Streamlitのフロントエンドにリアルタイムで出力されるようになっています。

:::note info
エラーが発生するときは、アップロードするPythonファイルにエージェントIDとエイリアスIDを正しくコピペできているか再確認ください。

ファイルを再アップする際は、以下コマンドで既存ファイルを削除できます。

```bash: CloudShell
rm frontend.py
```
:::

ちなみに、CloudShell上のPythonアプリにアクセスするため、今回はPinggyという外部サービスを利用し、一時的にインターネットからアクセス可能なURLを生成しています。

※セキュリティ上、意図せぬ第三者にURLを知られないよう注意ください。

https://pinggy.io/

先ほどコピーしたURLを同僚に共有すると、アプリを触って試してもらうこともできます。

ちなみにCloudShellは、20〜30分で自動停止してしまうため、StreamlitとPinggyの再実行が必要となります。また、Pinggyは無料版の制約としてURLにアクセス可能な時間が60分間となります。

:::note info
余力があれば、プロンプトチューニングやLambdaの改修にも挑戦してみましょう。

- エージェント向けの指示を修正してみる
- アクショングループの説明を修正してみる
- ナレッジベースを作成し、RAG機能も追加してみる
- Lambda関数を修正して、画像生成などの機能を追加してみる
:::


## お片付け

今回はサーバーレス構成のため、環境を放置してもほぼ課金は発生しませんが、セキュリティ事故を防ぐためにAWSアカウントの閉鎖（閉鎖しない場合、ルートユーザーへのMFA設定）をおすすめします。

※アカウント閉鎖すると同じメールアドレスでAWSアカウントが二度と作成できなくなるため、事前に捨てアドレスなどへ変更しておくことをおすすめします。


# 次のステップ

Bedrockにはナレッジベースという、RAGを簡単に構築できる機能もあります。これも実施して、今回のエージェントから呼んでみるとさらに面白くなります！

https://qiita.com/minorun365/items/24dfb0ea3afde6ed0a56

また、今回作ったアプリをコンテナにデプロイして、Webアプリとして公開してみたい方は以下を活用ください。

https://qiita.com/minorun365/items/84bef6f06e450a310a6a


# 宣伝

もしBedrockに興味を持たれた方は、先日、入門書を出版しましたのでお手に取ってみてくださいますと幸いです！

https://www.sbcr.jp/product/4815626440/