11/3更新:ここ1ヶ月ほど、DuckDuckGo APIの不具合?によりレートリミットエラーが発生していましたが、当該ライブラリを最新化することで解消されることが確認できました。
この事象に該当する方は、エラー発生時にWeb検索LambdaのCloudWatchログを見ると、202エラーが発生しているはずです。その場合、Lambdaレイヤーをもう一度作成すれば、最新のDuckDuckGoライブラリが取得されるため、それをWeb検索Lambdaに設定しなおせばOKです。これからハンズオンを新たに実施される方は問題ありません。
この記事は何?
先日公開した以下ハンズオンが大変好評だったので、今回はバリエーション版を作成しました。
GoogleスライドではなくPowerPoint資料を作成するようにアレンジしています。
- MS Officeを普段使いしている会社で、より実務に活かしやすくなります。
- Google関連の初期設定が不要となり、ハンズオンの所要時間を短縮できます。
(デモ動画)
本ハンズオンの概要
生成AIブームは終わりが見えませんが、そろそろRAGは十分試したよという方も多いのではないでしょうか。次のトレンドと目されているのが、人間の代わりに自動で仕事してくれる「AIエージェント」です。
AWSクラウドの生成AIサービス「Amazon Bedrock」には、そんなエージェントを簡単に作れるマネージドサービス 「Agents for Amazon Bedrock」 という機能があります。
これを使えば、難しいPythonのコードをたくさん書かなくても、AWSのマネジメントコンソールでGUIをポチポチやるだけで賢いエージェントが作れてしまいます。
今回作るアプリ
ユーザーが「xxxについて資料にまとめて」と依頼すると、
- Web検索して情報を集める
- PowerPointで資料を作成してS3に保存し、署名付きURLを発行する
- 発行したURLをユーザーにメールで送る
という流れを自動でやってくれます。
不明点があればユーザーに聞き返しますし、AIは各作業の結果をうけて次のアクションを柔軟に調整します。(例:メールの送信に失敗したら結果をチャット画面上に表示するなど)
アーキテクチャ
Web検索に利用しているDuckDuckGoは、APIキーが不要で使えるためお手軽です。
今回は含めていませんが、ここに「ナレッジベース」機能でRAGを組み合わせて社内文書を検索させることも可能です。
そもそもBedrockって何?
色んなAIモデルをAPIとしてサーバーレスで利用できる、AWSの機能です。
概要資料をまとめていますので、ご興味ある方はご覧ください!
ハンズオン手順
基本的にWebブラウザがあれば実施できます。
すべてサーバーレスで構築していますので、費用もかなり少額で済むはずです。
1. AWSアカウント作成
以下を参考に、AWSアカウントを新規作成しましょう。
アカウント作成したら、以下URLよりAWSマネジメントコンソールにサインインします。
サインインしたら、右上のリージョンを「バージニア北部」に切り替えておきましょう。
このハンズオンでは、バージニア北部リージョンのみを利用します。
(Claude 3.5 Sonnetを使えば東京リージョンでも実施可能です)
2. Bedrock設定
次はいよいよ生成AIサービスの設定です。
マネジメントコンソール上部の検索ボックスに bedrock
と入力して、Amazon Bedrockのコンソールへ移動しましょう。
モデル有効化
画面左下の「モデルアクセス」から、Anthropic社のClaude 3 Sonnetを有効化しましょう。
「Enable spesific models」をクリックして、Anthropic > Claude 3 Sonnet
にチェックを入れて進みます。
最新モデル「Claude 3.5 Sonnet」も、今回利用するBedrockエージェントで選択可能となりました!余裕のある方はぜひ試してみてください。
用途の申告が求められるので、以下を簡単に入力して送信しましょう。
- 会社名: あなたの所属会社名
- 会社サイトのURL: あなたの会社のHPなど
- 業界: あなたの会社の所属業界名
- 対象ユーザー:
社内の従業員
- ユースケースの説明:
個人検証
など
完了後、1〜2分でモデルが有効化されます。待たずに次の作業を進めましょう。
エージェント作成
次に「エージェント > エージェントを作成」を実施します。
エージェント名はデフォルトのまま「作成」をクリックします。
エージェントビルダーという編集画面が開くので、以下のとおり設定します。
- モデルを選択: Anthropic > Claude 3 Sonnet
- エージェント向けの指示:
- ユーザーからの依頼をもとに、クエリーを考案してWeb検索を行い、PowerPointに調査結果をまとめてください。
- スライドは必ず6ページ以上作成し、各ページには必ず見出しをつけ、内容を箇条書きで複数含めてください。
- 作ったスライドのURLをユーザーにメールで送信してください。
- すべての処理が終わったら、ユーザーへはメールを送信したことを伝えてください。
- Additional settings
- ユーザー入力: Enabled
上記が設定できたら、画面上部の「保存」をクリックします。
アクショングループ作成
Bedrockのエージェントでは、AIが実行できるタスクを「アクショングループ」として定義します。今回は「Web検索」「スライド作成」「メール送信」の3つのアクショングループを作成します。
エージェントビルダー画面下部のアクショングループにある「追加」ボタンをクリックしましょう。
アクショングループを以下のとおり、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 |
---|---|---|---|
signed_url | PPTXファイルの署名付きURL | string | True |
上記を設定したら、他はデフォルトのまま右下の「作成」をクリックします。
3つのアクショングループを作成すると、エージェントビルダーで以下のように表示されます。
3. S3バケット作成
エージェントが作ったパワポ資料を格納する場所を作成します。
Amazon S3のコンソールに移動し、バージニア北部リージョンでバケットを新規作成してください。
他の人とかぶらない好きな名前( pptx-あなたのニックネームなど-日付8桁
など)にしましょう。他の設定はすべてデフォルトのままでOKです。
S3バケット名はまた後で使いますので、作業PCのメモ帳などにコピペしておきましょう。
4. SNS設定
Lambdaからメールを送信するために、Amazon SNSを事前に設定します。
SNSコンソールのトップより、bedrock-agent
という名前のトピックを作成します。他はデフォルト設定のままで大丈夫です。
トピックが作成されたら、「ARN」を作業PCのメモ帳などにコピーしておきましょう。後で使います。
次に、このトピックに届いたメッセージを配信する「サブスクリプション」を作成しましょう。
- プロトコル: Eメール
- エンドポイント: 自分のメールアドレス
上記を設定したら、設定したメールアドレス宛てに確認メールが届くので「Confirm subscription」リンクをクリックしましょう。これで通知が配信されるようになります。
5. Lambda設定
先ほどBedrockエージェントのアクショングループを作成した時点で、Lambda関数が自動で作成されているのですが、中身のコードがほぼ空っぽなので編集します。
AWS Lambdaのコンソールに移動して「関数」を開き、バージニア北部リージョンに3つの関数が作成されていることを確認します。
Lambdaレイヤーの準備
各アクショングループ用のLambda関数を作成する前に、Lambdaが必要とするPythonの外部ライブラリを「レイヤー」として事前作成しておきます。
最初にマネコン右上の [>.]
アイコンより、CloudShellを起動します。
以下コマンドを実行して、Pythonライブラリ2つ(DuckDuckGoとpython-pptx)をZIP化します。
# レイヤー用のディレクトリを作成
mkdir python
# 作成したディレクトリに、必要なライブラリをインストール
pip install -t python --platform manylinux2014_x86_64 --only-binary=:all: duckduckgo_search python-pptx
# インストールしたライブラリをZIP圧縮
zip -r layer.zip python
CloudShell右上の「アクション > ファイルのダウンロード」をクリックし、ファイルパスとして layer.zip
を入力して、作業PCのローカルにZIPファイルをダウンロードしておきます。
ダウンロードが完了したら、CloudShellのウィンドウは閉じてしまっても大丈夫です。
Lambdaレイヤーの作成
まずは「レイヤー > レイヤーの作成」より、いま作成したZIPでLambdaレイヤーを作成します。
- 名前:
ddg-pptx
- .zipファイルをアップロード: ダウンロードしたZIPファイルを指定(目安:14MB)
- 互換性のあるアーキテクチャ: x86_64
- 互換性のあるランタイム: Python 3.9
他はそのままで「作成」をクリックします。10秒ちょっとかかります。
Lambda関数の設定
その後、左サイドバーの「関数」より、3つの関数をそれぞれ設定していきます。
search-web 関数
- コードソース:以下で上書きし、「Deploy」をクリックします。
# 必要なPythonライブラリをインポート
import json
from duckduckgo_search import DDGS
# メインのLambda関数
def lambda_handler(event, context):
# イベントパラメーターから検索クエリを取得
query = next(
(item["value"] for item in event["parameters"] if item["name"] == "query"), ""
)
# DuckDuckGoを使用して検索を実行
results = list(
DDGS().text(keywords=query, region="jp-jp", safesearch="off", timelimit=None, max_results=10)
)
# 検索結果をフォーマット
summary = "\n\n".join(
[f"タイトル: {result['title']}\n要約: {result['body']}" for result in results]
)
# エージェント用のレスポンスを返す
return {
"messageVersion": "1.0",
"response": {
"actionGroup": event["actionGroup"],
"function": event["function"],
"functionResponse": {
"responseBody": {
"TEXT": {
"body": json.dumps({"summary": summary}, ensure_ascii=False)
}
}
},
},
}
その後、下にスクロールして次の設定をします。
- ランタイム設定:「編集」をクリック
- ランタイム: Python 3.9
- レイヤー:「レイヤーの追加」をクリック
- カスタムレイヤー: ddg-pptx
- バージョン: 1
完了したら、次の関数を探して以下の設定に進みましょう。
create-pptx 関数
- コードソース:以下で上書きし、「Deploy」をクリックします。
# 必要なPythonライブラリをインポート
import os, json, boto3
from pptx import Presentation
from datetime import datetime
# 環境変数からS3バケット名を取得
S3_BUCKET_NAME = os.getenv("S3_BUCKET_NAME")
# Lambdaのメイン関数
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
- レイヤー:「レイヤーの追加」をクリック
- カスタムレイヤー: ddg-pptx
- バージョン: 1
その後、「設定」タブから以下を設定します。
- 一般設定:「編集」をクリック
- タイムアウト: 0分30秒
- 環境変数:「編集 > 環境変数の追加」をクリック
- キー:
S3_BUCKET_NAME
- 値: 先ほど作成したS3バケット名(メモっていない場合は別タブで確認)
- キー:
- アクセス権限:実行ロールの「ロール名」をクリック
IAMロールの編集画面に飛ぶので、「許可を追加 > ポリシーをアタッチ」をクリックします。
AmazonS3FullAccess
を探してチェックを入れ、「許可を追加」をクリックします。
完了したら、次の関数を探して以下の設定に進みましょう。
send-email 関数
- コードソース:以下で上書きし、「Deploy」をクリックします。
# 必要なPythonライブラリをインポート
import os, json, boto3
# 環境変数からSNSトピックARNを取得
SNS_TOPIC_ARN = os.environ.get("SNS_TOPIC_ARN")
# Lambdaのメイン関数
def lambda_handler(event, context):
# イベントパラメーターからURLを取得
signed_url = event.get("parameters", [{}])[0].get("value")
# SNSメッセージの発行
boto3.client("sns").publish(
TopicArn=SNS_TOPIC_ARN,
Message=f"Bedrockエージェントがスライドを作成しました。URLの有効期限は1時間です:\n{signed_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": signed_url,
}
)
}
}
},
},
}
※ランタイムやレイヤーの設定は不要です。
その後「設定」タブから以下を設定します。
- 環境変数:「編集」をクリック
- キー:
SNS_TOPIC_ARN
- 値: 先ほど作成したSNSトピックのARN(メモっていない場合は別タブで確認)
- キー:
- アクセス権限:実行ロールの「ロール名」をクリック
IAMロールの編集画面に飛ぶので、「許可を追加 > ポリシーをアタッチ」をクリックします。
AmazonSNSFullAccess
にチェックを入れ、「許可を追加」をクリックします。
これで3つの関数の設定が完了です。まとめると以下のようになります。
search-web | create-pptx | send-email | |
---|---|---|---|
ランタイム | Python 3.9 | Python 3.9 | (変更不要) |
レイヤー | ddg-pptx | ddg-pptx | (変更不要) |
タイムアウト | (変更不要) | 30秒 | (変更不要) |
環境変数 | (変更不要) | S3_BUCKET_NAME | SNS_TOPIC_ARN |
アクセス権限 | (変更不要) | S3を追加 | SNSを追加 |
6. エージェント動作確認
ここまで設定できたら、アプリ画面に組み込む前にエージェントがうまく動くか動作確認をしてみましょう。
Bedrockのコンソールに移動し、「エージェント」より先ほど作成したエージェント名をクリックして開きます。
右側のテスト用サイドバーにある「準備」をクリックします。
チャットボックスに KAGという会社を調べてパワポにまとめて
と送信してみましょう。
30秒〜1分ほど待つと、エージェントから返事が来ます。
確かにAmazon SNSからメールが届いています。
URLをクリックすると、パワポがダウンロードできました!
社長の名前だけ少し惜しいのですが、Web検索を行っているので社名略称だけで情報をちゃんと集めており、かなりハルシネーションは少なくなっています。
ここまで動いたら、エージェント画面下部の「エイリアス > 作成」より、エイリアスを作成しておきます。エージェントの新バージョンをリリースする、といった作業です。
- エイリアス名: v1
エイリアスが登録されたら、画面上部にあるエージェントの「ID」と、画面下部にある「エイリアスID」を作業PCのメモ帳などにコピペしておきましょう。後で使います。
うまく動かないときは、以下を確認して原因を切り分けてみましょう。
- エージェントのトレースを確認する
- Lambdaの「モニタリング > CloudWatchログを表示」より、ログストリームを確認する
ハマりやすい点は以下です。
- モデルは有効化できているか?
- バージニア北部以外のリージョンで作業していないか?
- アクショングループのパラメーターに設定ミス(変数名の誤植など)はないか?
- Lambda関数のコードを「デプロイ」し忘れていないか?
- Lambdaレイヤーの中身は問題ないか?
- Lambdaのランタイム設定、レイヤー、環境変数、IAMロールに漏れはないか?
- SNSからの確認メールをちゃんと一度クリックしているか?
7. フロントエンド開発
エージェントが単体でうまく動いたので、これを実際にアプリケーションに組み込んでみましょう。
まず、以下のソースコードを作業PCのメモ帳などに貼り付けて、中段の XXXXXXXXXX
部分に先ほど作成したエージェントのIDとエイリアスIDを記載してください。
その後、frontend.py
という名前で作業PCのローカルに保存しましょう。
# 必要なPythonライブラリをインポート
import uuid, boto3
import streamlit as st
# Bedrockクライアントを作成
if "client" not in st.session_state:
st.session_state.client = boto3.client("bedrock-agent-runtime")
client = st.session_state.client
# セッションIDを作成
if "session_id" not in st.session_state:
st.session_state.session_id = str(uuid.uuid4())
session_id = st.session_state.session_id
# メッセージ格納用のリストを作成
if "messages" not in st.session_state:
st.session_state.messages = []
messages = st.session_state.messages
# タイトルを表示
st.title("パワポ作ってメールで送るマン")
# 過去のメッセージを表示
for message in messages:
with st.chat_message(message['role']):
st.markdown(message['text'])
# チャット入力欄を定義
if prompt := st.chat_input("例:通信業界の生成AI活用事例をリサーチして"):
# ユーザーの入力をメッセージに追加
messages.append({"role": "human", "text": prompt})
# ユーザーの入力を画面に表示
with st.chat_message("user"):
st.markdown(prompt)
# Bedrockエージェントの呼び出し設定
response = client.invoke_agent(
agentId="XXXXXXXXXX", # エージェントID
agentAliasId="XXXXXXXXXX", # エージェントエイリアスID
sessionId=session_id,
enableTrace=True,
inputText=prompt,
)
# エージェントの回答を画面に表示
with st.chat_message("assistant"):
for event in response.get("completion"):
# トレースイベントが更新されたら画面に表示
if "trace" in event:
if "orchestrationTrace" in event["trace"]["trace"]:
orchestrationTrace = event["trace"]["trace"]["orchestrationTrace"]
if "modelInvocationInput" in orchestrationTrace:
with st.expander("思考中…", expanded=False):
st.write(orchestrationTrace)
if "rationale" in orchestrationTrace:
with st.expander("次のアクションを決定しました", expanded=False):
st.write(orchestrationTrace)
if "invocationInput" in orchestrationTrace:
with st.expander("次のタスクへのインプットを生成しました", expanded=False):
st.write(orchestrationTrace)
if "observation" in orchestrationTrace:
with st.expander("タスクの結果から洞察を得ています…", expanded=False):
st.write(orchestrationTrace)
# エージェントの回答が出力されたら画面に表示
if "chunk" in event:
chunk = event["chunk"]
answer = chunk["bytes"].decode()
st.write(answer)
messages.append({"role": "assistant", "text": answer})
CloudShellを再度開き、右上の「アクション > ファイルのアップロード」よりこのファイルをアップロードします。
その後、以下のコマンドを実行します。
# Pythonの外部ライブラリをインストール
pip install boto3 streamlit
# Streamlitアプリを起動
streamlit run frontend.py
Streamlitのアクセス用URLが表示されたら、うまく起動しています。
その後、CloudShell上部の「+」をクリックして、2つ目の「us-east-1」ターミナルを起動して以下を実行します。
# PinggyにSSH接続し、インターネットからアクセス可能なURLを生成する
ssh -p 443 -R0:localhost:8501 a.pinggy.io
確認メッセージが出力されたら yes
と入力してEnterを押すと、Pinggyという外部サービスを通じてこのアプリにアクセス可能なURLが発行されます。
下側のHTTPSの方のURLをコピーして、ブラウザの別タブからアクセスしてみましょう。
「Enter site」をクリックすると、先ほどアップロードしたPythonアプリにアクセスできます。Streamlitというフレームワークを使って、フロントエンドを表示しています。
実際にこのアプリを使ってみましょう。
エージェントのトレース情報が、Streamlitのフロントエンドにリアルタイムで出力されるようになっています。
エラーが発生するときは、アップロードするPythonファイルにエージェントIDとエイリアスIDを正しくコピペできているか再確認ください。
ファイルを再アップする際は、以下コマンドで既存ファイルを削除できます。
rm frontend.py
ちなみに、CloudShell上のPythonアプリにアクセスするため、今回はPinggyという外部サービスを利用し、一時的にインターネットからアクセス可能なURLを生成しています。
※セキュリティ上、意図せぬ第三者にURLを知られないよう注意ください。
先ほどコピーしたURLを同僚に共有すると、アプリを触って試してもらうこともできます。
ちなみにCloudShellは、20〜30分で自動停止してしまうため、StreamlitとPinggyの再実行が必要となります。また、Pinggyは無料版の制約としてURLにアクセス可能な時間が60分間となります。
余力があれば、プロンプトチューニングやLambdaの改修にも挑戦してみましょう。
- エージェント向けの指示を修正してみる
- アクショングループの説明を修正してみる
- ナレッジベースを作成し、RAG機能も追加してみる
- Lambda関数を修正して、画像生成などの機能を追加してみる
お片付け
今回はサーバーレス構成のため、環境を放置してもほぼ課金は発生しませんが、セキュリティ事故を防ぐためにAWSアカウントの閉鎖(閉鎖しない場合、ルートユーザーへのMFA設定)をおすすめします。
※アカウント閉鎖すると同じメールアドレスでAWSアカウントが二度と作成できなくなるため、事前に捨てアドレスなどへ変更しておくことをおすすめします。
次のステップ
Bedrockにはナレッジベースという、RAGを簡単に構築できる機能もあります。これも実施して、今回のエージェントから呼んでみるとさらに面白くなります!
また、今回作ったアプリをコンテナにデプロイして、Webアプリとして公開してみたい方は以下を活用ください。
宣伝
もしBedrockに興味を持たれた方は、先日、入門書を出版しましたのでお手に取ってみてくださいますと幸いです!