Agents for Amazon Bedrockの振る舞いがなんとなく分かってきたのでLINEから呼び出してみます。まずは基本形として(?)LINEから自然言語で乗換検索を行えるようにします。
過去に作ったものを流用しているものが多いですが、少し手を入れている部分もあるので関係ある部分を一通り掲載(再掲)します。
乗換案内用のLambda関数の作成
requestsとbeautifulsoup4をLambdaレイヤーで追加し、以下のLambda関数を作成します。
import requests
from bs4 import BeautifulSoup
import json
def lambda_handler(event, context):
print(event) # パラメータの出力(確認用)
# パラメータの取得
parameters = event.get('parameters')
dep_station = next(p for p in parameters if p['name'] == 'dep_station')['value']
dest_station = next(p for p in parameters if p['name'] == 'dest_station')['value']
yyyy = next(p for p in parameters if p['name'] == 'yyyy')['value']
mm = next(p for p in parameters if p['name'] == 'mm')['value']
dd = next(p for p in parameters if p['name'] == 'dd')['value']
H = next(p for p in parameters if p['name'] == 'H')['value']
M = next(p for p in parameters if p['name'] == 'M')['value']
type = next(p for p in parameters if p['name'] == 'type')['value']
# Yahoo乗換案内のURL設定
url = "https://transit.yahoo.co.jp/search/print?from="+dep_station+"&to="+dest_station+"&y="+yyyy+"&m="+mm+"&d="+dd+"&hh="+H+"&m1="+M[:1]+"&m2="+M[1:]+"&type="+type+"&ticket=ic&expkind=1&userpass=1&ws=3&s=0&al=1&shin=1&ex=1&hb=1&lb=1&sr=1"
print(url) # 確認用
# 乗換案内のページからメイン部分のみ取得
page = requests.get(url).text
body = str(BeautifulSoup(page, 'html.parser').select('#srline'))
# agentの形式でreturn
response_body = {"application/json": {"body": body}}
action_response = {
"actionGroup": event["actionGroup"],
"apiPath": event["apiPath"],
"httpMethod": event["httpMethod"],
"httpStatusCode": 200,
"responseBody": response_body,
}
api_response = {"messageVersion": "1.0", "response": action_response}
return api_response
念の為にタイムアウトを30秒ほどに伸ばしておきます。
以下の感じでリソースベースのポリシーを追加します。プリンシパルはbedrock.amazonaws.com
を指定し、Bedrockから実行可能にします。
「AWSのサービス」を選択したくなりますが、一番左の「AWSアカウント」で大丈夫です。
Agentの作成
Amazon Bedrock
のAgents
からCreate Agent
を実行します。
名前を適当に付けつつ、セッションタイムアウトは私は最小の1分にしました。無操作の場合にセッション(今回はLINEのユーザー単位)を維持する時間であり、この時間の間、過去の入力履歴がプロンプトに残り続けます。あまり長くすると回答が過去の問合せに左右される事になり、応答がちょっと不安定になる(再検索して欲しい時に履歴から回答してきたりする)ので、私は短い方が好みです。
Claude2ではなくInstantを選んでいる部分ですが、Agentはプロンプトが相当長く課金額的な意味もありますが、それ以上に、Agentの実行時間はLLMの実行時間に左右されるので速度優先の意味でClaude Instantを選んだ方が良いと思います。
Instructionは以下です。
ユーザーからの質問に対して回答します。現在日時が必要な場合は<question>のSystem:から取得します。
言語の指定が無い場合は日本語で回答します。
Functionを呼び出す必要が無い場合は知っている知識で回答します。知識では回答出来ないものはWeb上の情報を検索するFunctionを呼び出して回答します。
乗換案内を回答する際は、時刻、路線、乗換駅、料金、所要時間を含めて箇条書きで回答します。
後はAction groups(後で設定します)とKnowledge baseに何も設定せずCreate Agent
まで進んで完了します。
後で使用するので作成したAgentのIDをメモしておきます。
Agentの設定
Working draft
から設定していきます。
Pre-processingの無効化
Advanced prompts
のEdit
から、Pre-processing
を無効にしていきます。
Override pre-processing template defaults
をON(右側)にします。
Activate pre-processing template
が選択可能になるので、OFF(左側)にします。
Save and exit
で保存します。
何をやっているかというと、元々のAgentは実行の最初に「問い合わせ内容が関数(Lambda or Knowledge base)で解決可能か」というのをLLMで判断しており、これがPre-processingに該当します。一見、賢そうです。
が。
ちょっとここの判定が厳しめなのと、関数に関係ない(と判断された)問い合わせが跳ねられたりするので、(少し行儀が悪い気もしますが)ここをOFFにして何でも素通しにしちゃいます。また、AgentはLLMを何度も呼ぶ関係で結構処理時間が長いので、LLMの実行を1回減らす意図もあります。
前はプロンプトを書き換えて無理矢理判定OKにしていたのですが、そもそもプロンプト自体を飛ばせる事に気が付いたので飛ばすようにしました。
例えば以下のような、作成した関数に関係ない話が跳ねられないようになります。
用意した関数以外の会話をさせたくない場合もあると思うので、その場合はもちろんこの設定変更は不要です(でもプロンプトはチューニングした方が良いかも)。
Action groupの追加
Action groups
からAdd
していきます。
名前を適当に付けつつ、Select Lambda function
は先ほど作成した乗換案内のLambda関数を選択します。
Select API schema
はDefine with in-line OpenAPI schema editor
を選択し、以下の内容で上書きし、Add
で反映します。
Working draft
からPrepare
で反映します。
openapi: 3.0.0
info:
title: Lambda
version: 1.0.0
paths:
/search:
get:
summary: Transit Search
description: 乗換案内を検索します。<question>のSystem:の現在日時を参考に引数を考えてください。日時の指定が無い場合は今日の日付、今の時刻を使用してください。終電や始発を検索する際は、引数のH(hour)とM(minute)は現在の時間と分を数字2桁で設定してください。
operationId: search
parameters:
- name: dep_station
in: path
description: 出発駅(日本語で指定)
required: true
schema:
type: string
- name: dest_station
in: path
description: 到着駅(日本語で指定)
required: true
schema:
type: string
- name: yyyy
in: path
description: 出発時刻で検索する場合は出発する年、到着時刻で検索する場合は到着したい年(year)
required: true
schema:
type: string
minLength: 4
maxLength: 4
- name: mm
in: path
description: 出発時刻で検索する場合は出発する月、到着時刻で検索する場合は到着したい月(month)
required: true
schema:
type: string
minLength: 2
maxLength: 2
- name: dd
in: path
description: 出発時刻で検索する場合は出発する日、到着時刻で検索する場合は到着したい日(day)
required: true
schema:
type: string
minLength: 2
maxLength: 2
- name: H
in: path
description: 出発時刻で検索する場合は出発する時刻、到着時刻で検索する場合は到着したい時刻(hourの24時間表記)。始発または終電を検索する場合は現在時刻
required: true
schema:
type: string
minLength: 2
maxLength: 2
- name: M
in: path
description: 出発時刻で検索する場合は出発する時刻、到着時刻で検索する場合は到着したい時刻の分(minute)。始発または終電を検索する場合は現在時刻
required: true
schema:
type: string
minLength: 2
maxLength: 2
- name: type
in: path
description: 検索方法。1:出発時刻を指定して検索、4:到着時刻を指定して検索、2:終電を検索、3:始発を検索
required: true
schema:
type: string
minLength: 1
maxLength: 1
responses:
"200":
description: 検索成功
content:
application/json:
schema:
type: object
properties:
body:
type: string
Agent Aliasの作成
作成したAgentの選択直後の画面まで戻り、Create Alias
を実行します。
名前を適当に入れて、Create Alias
で反映します。
画面最下部のAliases
に反映されるので、Alias ID
をメモしておきます。後で使用します。
これでAgentの設定は完了です。
LINEチャンネルの作成
下記を参考にLINEチャネルを作成します。MessagingAPIを使用します。
後で使用するのでチャネルアクセストークンとチャネルシークレットをメモしておきます。
Webhook URLは最後に設定します。
Agentを実行するLambda関数の作成
boto3とline-bot-sdkをLambdaレイヤーで追加し、以下のLambda関数を追加します。
import os
import json
import boto3
import base64
import hashlib
import hmac
from linebot import LineBotApi
from linebot.models import TextSendMessage
import datetime
def lambda_handler(event, context):
# LINE用 初期処理
LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
CHANNEL_SECRET = os.environ['LINE_CHANNEL_SECRET']
LINE_BOT_API = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
# LINEからの正規リクエスト以外は終了
x_line_signature = event["headers"].get("x-line-signature") or event["headers"].get("X-Line-Signature")
req_body_str = event["body"]
hash = hmac.new(CHANNEL_SECRET.encode('utf-8'),
req_body_str.encode('utf-8'), hashlib.sha256).digest()
signature = base64.b64encode(hash)
if signature != x_line_signature.encode(): raise Exception
# リクエストの取得
req_body = json.loads(event['body'])
# Agentのsession_idとしてLINEのUserIdを使用する
session_id = req_body['events'][0]['source']['userId']
# LINEからのメッセージを受信
if req_body['events'][0]['type'] == 'message':
if req_body['events'][0]['message']['type'] == 'text':
line_message = req_body['events'][0]['message']['text']
# Agentの定義
agent_id = os.environ['AGENT_ID']
agent_alias_id = os.environ['AGENT_ALIAS_ID']
# Agent用Client
agent_client = boto3.client("bedrock-agent-runtime")
# 現在日時の取得(ISO8601形式)
now = str(datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9))).isoformat())
# LINEからのメッセージに現在日時をくっ付けてAgent呼出し
query = "System: 現在の日時は "+now+"(ISO8601形式)です。現在日付や現在時刻が必要な場合は左記から取得してください。もしチャット履歴に対して質問された場合は、現在時刻に対してではなくその前のチャット履歴に対して回答してください。\n\nHuman: "+line_message
# Agentの実行
response = agent_client.invoke_agent(
inputText=query,
agentId=agent_id,
agentAliasId=agent_alias_id,
sessionId=session_id,
enableTrace=False
)
# Agent実行結果の取得
answer = ''
event_stream = response['completion']
for event in event_stream:
if 'chunk' in event:
answer += event['chunk']['bytes'].decode("utf-8")
# Agentの回答をLINEに返却
replyToken = req_body['events'][0]['replyToken']
LINE_BOT_API.reply_message(replyToken, TextSendMessage(text=answer))
return {'statusCode': 200, 'body': json.dumps('Reply ended normally.')}
Agentの実行時間は結構長いので、タイムアウトを5分ほどに伸ばしておきます。乗換案内のみであればこんなに長くありませんが、今後長い処理を追加した時の為に長めにしています。
また、LambdaからAgentを実行できるように、Lambdaの実行ロールにAdministratorAccessを追加しておきます(リソースベースのポリシーではなく、実行ロール)。
環境変数として、ここまでメモした以下の4つを設定します。
- AGENT_ID : AgentのID
- AGENT_ALIAS_ID : AgentのAlias ID
- LINE_CHANNEL_ACCESS_TOKEN : LINEのチャネルアクセストークン
- LINE_CHANNEL_SECRET : LINEのチャネルシークレット
前にAgentを作ったときは、現在日時の取得は専用のLambda関数を作ってAgentから実行していたのですが、LLMの実行回数が多くなると処理時間がなかなかなので、Agentを呼び出す際に毎回渡すように変更しています。
API gatewayの作成
API GatewayからLambda関数が呼べるように設定していきます。
Lambda関数の関数URLでも良いといえば良いのですが、行儀の問題です。プロキシ統合なら同じじゃないかという気もしつつ。
APIの作成
からREST API
を構築
名前を適当に付けてAPIを作成
メソッドを作成
から、POST
Lambda関数
Lambda プロキシ統合
を選択、Lambda関数として上で作成した「Agentを実行するLambda関数」を選択し(乗換案内では無いので注意)、メソッドを作成
を実行。
APIをデプロイ
から、適当なステージ名を入力してデプロイ
これでAPI Gatewayの準備は出来たので、URL を呼び出す
のURLをメモしておきます。
最後のLINEの設定
- LINE DeveloperのWebhook URLに先ほどメモしたAPI GatewayのURLを設定します
- QRコードからご自身のLINEにチャネル登録します
これでLINEチャネルから自然言語で乗換案内が検索できるようになります。
まとめ
これで基本形は出来たので、後は必要なLambda関数の作成とAction groupの追加を行っていけば、LINEから自然言語で情報の収集であろうがトランザクションの実行であろうが何でも出来ます!
と言いたいところですが、実際にやってみると分かるのですが、プロンプト(Claude Instant)がなかなか挙動不審で、弄っていると平均台の上を歩いているような気分が味わえます。くどくどと謎の日本語指示が書いてある部分は、何かしら特定の問い合わせをした際に平均台から落ちた名残だと思ってください。パッチワークをしている感が強く、これがスマートなノーコードなのかと聞かれるとちょっとアレですが、前にもどこかで書きましたがLangChainを使わなくて済むのとAWS純正のClaude用プロンプトをベースに弄れるのは良いような気がします。
(もうちょっとだけ続くんか?)