はじめに
こんにちは! yu-Matsuと申します。
先月末のAWS re:InventでGAが発表された Agents for Amazon Bedrock を使ってLINE Botを作ってみましたので、記事にしたいと思います! テーマは、ちょうどDLCが配信されたばかりということで、ポケモン図鑑Botです!
Agents for Amazon Bedrockとは
既に他にも解説記事が上がっていますので簡単な説明に留めますが、以下のような処理を自分で判断して実行(オーケストレーション)してくれるエージェントを作成することが出来ます。
- ユーザーの入力の意図を汲み取り、必要であればAPIを呼び出して、その結果を元に回答を生成する
- 用意された Knowleage base を検索して、その結果を元に回答を生成する
OpenAI の Assistants API をイメージしてもらえると分かりやすいかと思います。もちろん、AWSのフルマネージドサービスになりますので、AWSに閉じて利用することができ、SDKが提供されているため他のサービスとの連携も比較的簡単です。
現状出来ているもの
先に現状出来ているものをお見せしたいと思います。図鑑番号を指定すると対応したポケモンの情報とイラストが返ってくるイメージです。 またキャラ付けに関しては、ポケモン図鑑といえば、ゲーム、アニメが好きな方にはお馴染みの ロトム図鑑 を意識しています!
Agentの作成
作成の流れ
現在は東京リージョンでは利用不可のため、バージニア北部リージョンでAmazon Bedrockのコンソールを見てみると、、、 左メニューに Agents が追加されています!ここでAgentの作成と管理を行います。
[Create Agent]を開くと、Agentの作成画面が開きます。Stepに従って情報を入力していきます。
Step1では、Agentの名前の入力や、AgentにつけるIAMロールの作成、セッションタイムアウトの設定を行います。
Step2では、利用するモデルの選択と、Instructionsの設定を行います。Anthoropic社のモデルは現在 Instant V1 と V2 のみが選択可能で、V2.1はまだ利用出来ないようです。
Instructionsでは、Agentがどういうものかの定義を記載します。Agentの方向性を決定付けるものなのになります。
今回はロトム図鑑ですので、以下のように設定しました。冒頭でも説明した通り、今回は図鑑番号を指定することで回答してくれるようにしています。
あなたはポケモン(ポケットモンスター/pokemon)に詳しい「ロトム図鑑」という図鑑エージェントです。語尾は「〜ロト」、「〜ロ」になります。
会話サンプルを次に提示します。「こんにちはロト〜!」「何か用ロト?」「お役に立つロト〜!」「これからよロトしく!」「ケテー! 呼ばれた! 学校の2階に 行くロ!」「マラサダも スカル団も 両方 気になっちゃうロ……!」
あなたは次のような機能を持っています。
1. 「図鑑番号1000のポケモンは?」のように、図鑑番号を数字で指定した質問をされた場合、該当のポケモンの情報を検索して回答することが出来ます。ポケモンの画像のURLも回答に含めて下さい。
2. 「こんにちは」など、質問形式の発話ではない場合、そのまま雑談をおこないます。
Step3で、 Action groups を設定します。こちらが今回の記事の肝となります! Actionとは Agentが回答を生成する際に利用するAPI(実装はLambda) になります。詳しい設定内容に関しては後ほど!
Step4は、Knowledge base の設定になります。こちらはまだ検証中のため本記事では触れないので省略します。
最後にStep5でReviewを確認して、問題がなければ[Create Agent]すれば完成です! Action groupsの設定はOptionalであることを考えると、Agentの作成自体は非常に簡単に出来ます。作成完了後、以下のようなAgentの情報とテスト用のPlaygroundが表示されます。
Action groupsの設定
それでは、先ほど省略した Action groups について見ていきたいと思います。 Agentが利用するAPIの設定ということでしたが、具体的には以下の2つを用意します。 (Agentに合わせて バージニア北部 で作成することに注意!)
- APIの処理を実装したLambda
- APIの定義を記述したOpenAPIスキーマファイル(yaml形式)
APIの処理を実装したLambdaですが、処理内容は「図鑑番号(数字)を受け取り、対応するポケモンの情報を返す」となります。このポケモンの情報を取得する処理に、今回は PokeAPI というAPIを利用しました。
こちらは、ポケモン、というよりは「ポケットモンスター」というゲームに関する様々なデータを取得出来るOSSのAPIになります。弊社のアドベントカレンダーの初日にはこのPokeAPIのコントリビュートに挑戦した記事が公開されています!
実際に実装したLambda(invoke-pokeapi.py)は以下のようになります。実行するActionのAPIのパスと図鑑番号を元にPokeAPIを実行、実行結果の中から必要なデータを取り出し、返しています。
import json
import requests
BASE_URL="https://pokeapi.co/api/v2/"
def lambda_handler(event, context):
# APIのパスと図鑑番号をeventから受け取る
api_path = event["apiPath"]
pokemon_id = event["parameters"][0]["value"]
endpoint = BASE_URL + "pokemon/"
# https://pokeapi.co/api/v2/pokemon/図鑑番号 で対象のポケモンの情報が取得出来る
url = endpoint + pokemon_id
# APIのパスが"/search"であればPokeAPIを実行し、結果を返す
if api_path == "/search":
# PokeAPIの実行
response = requests.get(url)
response = response.json()
# タイプが複数ある場合の処理に連結して返す(「xxx/yyy...」の形式)
formatted_types = [ poke_type["type"]["name"] for poke_type in response["types"] ]
pokemon_type = "/".join(formatted_types)
# とくせいが複数ある場合の処理に連結して返す(「xxx/yyy...」の形式)
formatted_abilities = [ ability["ability"]["name"] + "*" if ability["is_hidden"] else ability["ability"]["name"]) for ability in response["abilities"] ]
abilities = "/".join(formatted_abilities)
# レスポンスを整形
# ポケモンのID、名前、タイプ、特性、各種族値
result = {
"id": response["id"],
"名前": response["name"],
"タイプ": pokemon_type,
"とくせい": abilities,
"HP": response['stats'][0]['base_stat'],
"こうげき": response['stats'][1]['base_stat'],
"ぼうぎょ": response['stats'][2]['base_stat'],
"とくこう": response['stats'][3]['base_stat'],
"とくぼう": response['stats'][4]['base_stat'],
"すばやさ": response['stats'][5]['base_stat'],
}
response_body = {"application/json": {"body": json.dumps(result, ensure_ascii=False)}}
# Actionとしてのレスポンスを作成
action_response = {
"actionGroup": event["actionGroup"],
"apiPath": event["apiPath"],
"httpMethod": event["httpMethod"],
"httpStatusCode": 200,
"responseBody": response_body,
}
else:
# APIのパスが"/search"でない場合はとりあえずエラーメッセージを返す
response_body = {"application/json": {"body": json.dumps({"errorMessage": "エラーが発生しました"}, ensure_ascii=False)}}
action_response = {
"actionGroup": event["actionGroup"],
"apiPath": event["apiPath"],
"httpMethod": event["httpMethod"],
"httpStatusCode": 400,
"responseBody": response_body,
}
# Actionのレスポンスを返す
return {
"messageVersion": "1.0",
"response": action_response
}
次に、APIの定義を記述したOpenAPIスキーマファイルになりますが、こちらはS3バケットを作成し、以下のyamlファイルを配置しました。AgentがAPIを実行する際に参照するものになりますので、ここも重要なポイントになります! コツはAPIの description をなるべく丁寧に書いてあげることのようです。
※ OpenAIの Function Calling を触ったことがある方は、既視感がある設定かと思います。
openapi: 3.0.0
info:
title: "invoke pokeapi API"
version: 1.0.0
description: "APIs for testing Agents' behavior"
paths:
"/search":
get:
summary: "invoke pokeapi"
description: "質問の中から図鑑番号に対応する数字文字列を抽出し、それを元にAPIを実行して情報を取得します。取得した内容からid、名前、タイプ、とくせい、HP、こうげき、ぼうぎょ、とくこう、とくぼう、すばやさをJSONで返します。"
operationId: search
parameters:
- name: pokemon_id
in: path
description: "ポケモンの図鑑番号"
required: true
schema:
type: string
responses:
200:
description: "検索成功"
content:
application/json:
schema:
type: object
properties:
id:
type: string
description: "ポケモンの図鑑番号"
name:
type: string
description: "ポケモンの名前"
type:
type: string
description: "ポケモンのタイプ"
ability:
type: string
description: "ポケモンのとくせい"
hp:
type: string
description: "ポケモンのHPの種族値"
attack:
type: string
description: "ポケモンのこうげきの種族値"
defense:
type: string
description: "ポケモンのぼうぎょの種族値"
special_attack:
type: string
description: "ポケモンのとくこうの種族値"
special_defense:
type: string
description: "ポケモンのとくぼうの種族値"
speed:
type: string
description: "ポケモンのすばやさの種族値"
以上2つの準備が完了しましたら、先ほどのAgentの作成手順のStep3で Lambda の選択とOpenAPIスキーマファイルのS3のURLを指定します。(再掲)
なお、Action groupsは複数設定することも可能で、例えばポケモンの情報以外に「どうぐ」や「わざ」の検索なんかもさせることが出来そうです!
動作確認
それでは、作成したAgentの動作を確認したいと思います。作成完了時に現れるテスト用Playgroundに「こんにちは!」と挨拶をしてみました。すると、、、
ロトム図鑑から挨拶が返ってきました! 語尾もしっかり反映されています。
このPlaygroundではトレース情報も見ることが出来ますので、[Show trace]から見てみました。
Trace を見ると、 Agentの回答生成は以下のステップで処理されているようです。
- Pre-processing : 前処理。こちらの発話を分類して、Action groups や Knowledge base を使って回答可能かを判断している
- Orchestration & knowledge base : このステップで action や knowledge base を利用した場合の実行結果や検索結果を取得し、回答を生成
- Post-processing : Agentの回答の最終の処理
Pre-processing trace の Step1を見てみると、 「こんにちは!」は質問ではなく挨拶に分類されていますね! そのため以降のステップ(Orchestration & knowledge base)には進んでいません。
いよいよポケモンを検索して見たいと思います。「図鑑番号149のポケモンの情報を教えて」と入力して見ました。すると、、、
お、ちゃんと回答が返ってきた!!、、、と思ったら名前と特性が変ですね。
図鑑番号149は「カイリュー」なのですが、英名の「Dragonite」がそのまま日本語読みになっている感じです。(とくせいも英名が無理矢理翻訳されている)
これは、PokeAPIのレスポンスが基本的に英語で返ってくるため、Agentが上手くポケモン特有の翻訳をできていないことが原因のようです。 しかし、情報としては正しいものが返ってきています!
こちらもトレースを見てみます。Pre-processingのトレースを見ると、ちゃんと Action groups を使って回答する質問だと判断され、APIが呼び出されていることが分かります。
今回は Action groups が使用されたので、 Orchestration のトレースも見てみます。Step2を見ると、 Action groups で設定したAPIが呼び出され、OpenAPIスキーマで定義したAPI仕様通りの結果が返ってきていることが分かります。
Step3を見ると、Action groups の実行結果を元に回答が生成されていることが分かります。
Advanced prompts について
Pre-processingにて、こちらの発話を分類していると言いましたが、この分類は Advanced prompts で定義されています。作成した Agent の [Working draft] から確認出来ます。
ここでは、先程のトレース結果で見た各StepのプロンプトやTemparatureなどが定義されており、編集することも出来ます。
(編集したい場合は、[Override pre-processing template defaults] を有効にします。)
Pre-processingのプロンプトを見てみると、発話の分類が以下のようにされていることが分かります。
Action groups を利用するような質問と判断された場合は、Category C に、質問ではないと判断された場合は、Category E に分類されるようです。 Category C の定義を見てみると、Agent groups を利用して回答できない場合も分類されるようですので、もし思ったようにAgentが回答を返さない場合はこちらを編集してみると良いかもしれませんね。
因みに、Orchestration のプロンプトを編集して勝手に日本語訳をしないように指定してみましたが、ダメでした...。 奥が深そうなので、試行錯誤してみたいと思います。
LINE Botの作成
Agentの作成が完了しましたので、LINE Botの作成をしていきます。LINE Developpers で提供されている Messaging API を利用します。
手順に関しては、本記事の本題ではないため割愛させていただきます。実際に作成する際に以下の記事を参考にさせていただきましたので、詳しくはこちらをご覧下さい。
因みに、ちゃんとロトム図鑑で作成しています!
Messaging APIのボットサーバーの実装
参考にさせていただいた記事と同様に、 API Gateway + Lambda(Python) でボットサーバーを実装しています。Lambdaのコードは以下になります!
import os
import boto3
import json
import re
from linebot import LineBotApi
from linebot.models import TextSendMessage
LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
CHANNEL_SECRET = os.environ['LINE_CHANNEL_SECRET']
AGENT_ID = os.environ['AGENT_ID']
AGENT_ALIAS_ID = os.environ['AGENT_ALIAS_ID']
LINE_BOT_API = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
def chat(message, session_id):
# Agent用のSDKが用意されている
client = boto3.client("bedrock-agent-runtime")
# Agentを実行する
response = client.invoke_agent(
inputText=message,
agentId=AGENT_ID, # AgentのID
agentAliasId=AGENT_ALIAS_ID, # AgentのaliasのID
sessionId=session_id, # セッションのID
enableTrace=False
)
# Agentの実行結果を取得し、返す
results = response['completion']
for result in results:
if 'chunk' in result:
data = result['chunk']['bytes'].decode("utf-8")
return data
def lambda_handler(event, context):
body = json.loads(event['body'])
if len(body['events']) > 0:
if body['events'][0]['type'] == 'message':
if body['events'][0]['message']['type'] == 'text':
# Messaging APIのイベントからメッセージとuser idを取り出す
message = body['events'][0]['message']['text']
user = body['events'][0]['source']['userId']
# Agentを実行し、回答を取得
resp = chat(message, user)
# Agentの回答を、返答の形でユーザーに返す
LINE_BOT_API.reply_message(
body['events'][0]['replyToken'],
messages
)
return {
"statusCode": 200,
"body": json.dumps('Success!'),
}
Agentの呼び出しは chat関数 内で実行しています。SDKが用意されていますので、Pythonの場合は boto3 を利用して呼び出すことが出来ます。
環境変数に設定しているのは、以下の4つになります。
- LINE_CHANNEL_ACCESS_TOKEN : Messaging APIのチャンネルアクセストークン
- CHANNEL_SECRET : Messaging APIのチャンネルシークレット
- AGENT_ID : Agent for AmazonBedrock の Agent ID
- AGENT_ALIAS_ID : Agent for AmazonBedrock の Agent alias ID
LINE_CHANNEL_ACCESS_TOKEN、CHANNEL_SECRET は、Messaging APIのチャンネルを作成後にコンソールから確認することが出来ます。
AGENT_IDは、作成した Agent の overview から確認することができ、AGENT_ALIAS_IDに関しては、[Create Alias] から Alias を作成後に生成されます。
API GatewayとLambdaの作成が完了しましたら、Messaging APIコンソールの[Messaging API設定]における、Webhook設定 で、作成したAPIのURLを登録します。(その下の Webhoookの利用 を有効にするのも忘れずに!)
動作確認
一通り設定が完了しましたので、動作確認してみたいと思います。[Messaging API設定] にQRコードがありますので、そこから作成したBotを友達追加します。実際に会話をしてみた際のキャプチャが以下になります。
問題なくLINE上でロトム図鑑と会話出来ています! 図鑑番号を指定して質問しても、ちゃんとポケモンの情報が返ってきていますね! (相変わらず変な翻訳が入っていますが...)
これで想定していた機能が一通り実装出来ました! 👏🎉
最後に少し修正
LINE Botが動くようになりましたが、最後に少し修正を加えたいと思います。
- ポケモンの名前、タイプ、特性が変な感じになっているので修正
- イラストも表示できるように修正
イラストに関しては、PokeAPIのレスポンスに画像URLを含めることが出来るので、そちらで対応します。
また、ポケモンの名前、タイプ、特性に関しては、Action groupsで設定しているLambdaにて、PokeAPIの実行結果を日本語対応させたいと思います。以下の記事を参考に、invoke-pokeapi.pyを修正してみました。
+ def translate_info(info, value):
+ '''
+ 変換したい情報のタイプと値を引数として、日本語に変換して返す
+ ※ 変換できない場合なそのままの値を返す
+
+ param:
+ - info: 変換したい情報のタイプ( type or ability or pokemon-species )
+ - value: 変換したい値
+ '''
+
+ # PokeAPIを実行してinfo/valueに対応した情報を取得
+ url = "{base_url}{info}/{value}".format(base_url=BASE_URL, info=info, + value=value)
+ response = requests.get(url)
+
+ if response.ok:
+ data = response.json()
+ # レスポンスの中に日本語の情報が含まれているので、取り出して返す
+ for item in data['names']:
+ if item['language']['name'] == 'ja-Hrkt':
+ return item['name']
+ return info
+ else:
+ return info
def lambda_handler(event, context):
...
# タイプが複数ある場合の処理
+ formatted_types = [ translate_info("type", poke_type["type"]["name"]) for poke_type in response["types"] ]
- formatted_types = [ poke_type["type"]["name"] for poke_type in response["types"] ]
...
# とくせいが複数ある場合の処理
+ formatted_abilities = [ translate_info("ability", ability["ability"]["name"]) + "*" if ability["is_hidden"] else translate_info("ability", ability["ability"]["name"]) for ability in response["abilities"] ]
- formatted_abilities = [ ability["ability"]["name"] + "*" if ability["is_hidden"] else ability["ability"]["name"] for ability in response["abilities"] ]
...
result = {
"id": response["id"],
+ "名前": translate_info("pokemon-species", response["name"]),
- "名前": response["name"],
...
+ # レスポンスに画像URLを追加
+ "画像URL": response["sprites"]["other"]['official-artwork']['front_default']
}
...
レスポンスに画像URLを追加しましたので、OpenAPIスキーマの定義ファイル(invoke-pokeapi.yaml)も修正しました。
...
paths:
"/search":
...
- description: "質問の中から図鑑番号に対応する数字文字列を抽出し、それを元にAPIを実行して情報を取得します。取得した内容からid、名前、タイプ、とくせい、HP、こうげき、ぼうぎょ、とくこう、とくぼう、すばやさをJSONで返します。"
+ description: "質問の中から図鑑番号に対応する数字文字列を抽出し、それを元にAPIを実行して情報を取得し、返します。id、名前、タイプ、とくせい、HP、こうげき、ぼうぎょ、とくこう、とくぼう、すばやさ、画像URLをJSONで返します。"
...
responses:
200:
description: "検索成功"
content:
application/json:
schema:
type: object
properties:
...
+ image_url:
+ type: string
+ description: "ポケモンの画像URL"
また、イラストを表示させるために、Messaging APIボットサーバー側も以下のように修正しました。
...
def lambda_handler(event, context):
body = json.loads(event['body'])
...
resp = chat(message, user)
+ img_url = re.findall('https.*png', resp)
+ # Agentのレスポンスに画像URLが含まれている場合は、メッセージに加え画像も送信する
+ if len(img_url) > 0:
+ messages = [TextSendMessage(text=resp), +ImageSendMessage(original_content_url=img_url[0], +preview_image_url=img_url[0])]
+ else:
+ messages = [TextSendMessage(text=resp)]
LINE_BOT_API.reply_message(
body['events'][0]['replyToken'],
messages
)
以上の修正を実施し、動作確認をしてみた結果が以下になります。
最初の方で少し失敗していますが、ポケモンについての質問に対して、名前、タイプ、特性が正しい日本語で表示されています! また、カイリューのイラストも表示されるようになりました! 🎉
まとめ
今回は Agents for Amazon Bedrock を使ってロトム図鑑LINE Botを作ってみました。冒頭にも説明した通り、OpenAIの Assistans API のようにローコードで(Actionの実装を除き)簡単にAgentを作成できるのはかなり魅力的でした。また、以下の点は他にはない特徴かなと考えています。
- Playground上ではあるが、Agentの回答に関してトレース情報を確認しやすいので、裏でどのようなことが起きているかを把握しやすい。
- SDKが用意されているので、既存のシステムにも比較的組み込みやすい
- Advance prompts で Action groups や Knowledge base を利用する際の処理を細かく調整出来そう
今まで LangChain などを利用して頑張って実装していたものが、ここまで簡単になったのかと驚いております。(Assistans APIも含め)
今後の展望
今回は図鑑番号の指定のみを受け付け、返す情報も最低限のものだけでしたので、PokeAPIの多種多様なデータをAPIを活用して、ポケモンだけではなく「どうぐ」や「わざ」などのデータも検索出来るようにしたいです。
また、現在検証中ですが、PokeAPIには無いような、対戦等のよりディープな情報に関して、今回は触れなかった Knowledge base for Amazon Bedrock を利用した RAG を用いて回答出来るようにしたいと考えています!(データソースはポケモンの対戦wikiなどを利用予定)
本記事はこれで以上となります。最後までお読みいただき、ありがとうございました!!(記事も無事書き終わったので、ポケモンのDLCを全力で楽しみたいと思います!!)