はじめに
Lexで答えた情報をLambdaを用いて、DynamoDBに書き込むことをやります。
流れ
- DynamoDBのテーブル作成
- Lex作成
- Lambda作成
- LexにLambdaを設定
DynamoDBのテーブル作成
以下の設定で作成します。
テストですので、オンデマンドモードにして、安く済ませます。
- テーブル名:
Lex-Write-Lambda
- パーティションキー:
telephoneNumber
:文字列
- ソートキー:
date
:文字列
- 項目:
age
:数値
(テーブル作成後) - 項目:
timeStamp
:数値
(テーブル作成後) - テーブルクラス:DynamoDB 標準 – IA
- キャパシティーモード:オンデマンド
age
とtimeStamp
を含めた項目を作成します。
Lexの作成
bot作成
botを作成します。
名前は適当に作成し、言語は日本語に設定します。それ以外はデフォルトのままです。
インテント作成
インテント名は、book
とします。
lexとの会話の流れは以下の通りです。
予約したい日付と電話番号と年齢の情報をlexに渡します。
user「予約したいです。」
lex「あなたの電話番号を教えて下さい」
user「09011111111」
lex「予約したい日付を教えて下さい」
user「明日」
lex「年齢を教えて下さい?」
user「22歳です」
lex「2022-01-16に、電話番号09011111111の年齢22歳で予約します。よろしいでしょうか?よろしければ、はい、予約をキャンセルしたい場合、いいえとお伝え下さい」
user「はい」
lex「予約を完了しました」
バリデーションもかけます。
年齢20未満は利用できないようにする。
user「予約したいです。」
lex「あなたの電話番号を教えて下さい」
user「09011111111」
lex「予約したい日付を教えて下さい」
user「明日」
lex「年齢を教えて下さい?」
user「18歳です」
lex「ご利用は20歳以上とさせていただいております。」
user「25歳です」
lex「2022-01-16に、電話番号09011111111の年齢25歳で予約します。よろしいでしょうか?よろしければ、はい、予約をキャンセルしたい場合、いいえとお伝え下さい」
user「はい」
lex「予約を完了しました」
発話
サンプル発話は、bookインテントが発動するトリガーとなる言葉を設定します。
スロット
まず、telephoneNumberを以下の設定で作成します。
ここでポイントですが、 スロットタイプ:AMAZON.PhoneNumber
は、番号のみを聞き取ります。
userが数値のみを言うと、Lexは聞き取ってくれますが、userが「09011111111です。」と文字列も含めて答えると、Lexは聞き取れなくなります。
そのため、telephoneNumberスロットの詳細オプション
→サンプル発話 -オプション
で、{telephoneNumber}です。」と設定することで、文字列の
です`を省いた数値を聞き取ってくれるようになります。
注意
スロットを更新を押した後、スロットのプロンプト等を修正すると、先程設定したサンプル発話 -オプション
が削除される場合がありますので、再度入力して更新してください。
同様に、date
とage
のスロットを作成します。
必要に応じて、詳細オプション
→サンプル発話 -オプション
で設定しましょう。
ちなみに、AMAZON.Date
で聞き取れるのは、下記の通りです。
反応 | 言葉 |
---|---|
○ | 2日後 |
○ | 明日 |
○ | 明後日 |
○ | 17日(未来の直近の月の17日になる) |
○ | 来週の月曜日 |
✕ | 明々後日 |
確認プロンプトと応答拒否
テスト
インテントを保存し、構築
をクリック後、テストしてみましょう。
テスト時の検査
のJSON 入力と出力
の最下部の応答
にjsonデータがあります。
これは、Lambdaを使用する際に、Lambdaが受け取るデータになります。
後でLambdaを作成するにあたり、Lambdaのテストで、このjsonデータを貼り付けて、Lambdaのコードを書くとスムーズにLambdaが作成できるためコピーしておきましょう。
{
"messages": [
{
"content": "予約を承りました。",
"contentType": "PlainText"
}
],
"sessionState": {
"dialogAction": {
"type": "Close"
},
"intent": {
"name": "book",
"slots": {
"age": {
"value": {
"originalValue": "22",
"interpretedValue": "22",
"resolvedValues": ["22"]
}
},
"date": {
"value": {
"originalValue": "3",
"interpretedValue": "2022-01-16",
"resolvedValues": ["2022-01-16"]
}
},
"telephoneNumber": {
"value": {
"originalValue": "09011111111",
"interpretedValue": "09011111111",
"resolvedValues": ["09011111111"]
}
}
},
"state": "Fulfilled",
"confirmationState": "Confirmed"
},
"originatingRequestId": "2cc08e3e-9b67-4baa-b21f-972c09d15188"
},
"interpretations": [
{
"nluConfidence": {
"score": 1
},
"intent": {
"name": "book",
"slots": {
"age": {
"value": {
"originalValue": "22",
"interpretedValue": "22",
"resolvedValues": ["22"]
}
},
"date": {
"value": {
"originalValue": "3",
"interpretedValue": "2022-01-16",
"resolvedValues": ["2022-01-16"]
}
},
"telephoneNumber": {
"value": {
"originalValue": "09011111111",
"interpretedValue": "09011111111",
"resolvedValues": ["09011111111"]
}
}
},
"state": "Fulfilled",
"confirmationState": "Confirmed"
}
},
{
"intent": {
"name": "FallbackIntent",
"slots": {}
}
}
],
"sessionId": "586569454024704"
}
応答
dialogAction
、type
、state
、confirmationState
は、Lambdaのコードを書く上で、意識する必要がありますので、確認しておきましょう。
スロット値の入力をユーザーに促す
"sessionState": {
"dialogAction": {
"type": "ElicitSlot",
"slotToElicit": "age"
},
"state": "InProgress",
"confirmationState": "None"
},
確認プロンプト
"sessionState": {
"dialogAction": {
"type": "ConfirmIntent"
},
"state": "InProgress",
"confirmationState": "None"
},
####確認プロンプトの返答「はい」
"sessionState": {
"dialogAction": {
"type": "Close"
},
"state": "Fulfilled",
"confirmationState": "Confirmed"
},
確認プロンプトの返答「いいえ」
"sessionState": {
"dialogAction": {
"type": "Close"
},
"state": "Failed",
"confirmationState": "Denied"
},
FallbackIntent
インテントの発話が聞き取れなかった場合、発動します。
応答を閉じる
にうまく聞き取れませんでした。
などの言葉を返答するとよいでしょう。
IAMポリシー
LambdaのIAMロール用に、以下のIAMポリシーを作成します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:DeleteItem",
"dynamodb:GetItem",
"dynamodb:Scan",
"dynamodb:Query",
"dynamodb:UpdateItem"
],
"Resource": "arn:aws:dynamodb:ap-northeast-1:xxxxxxxx:table/*"
}
]
}
Lambdaを作成
以下の通りに作成します。
IAMロールは、基本的な Lambda アクセス権限で新しいロールを作成
を指定してください。
作成後、先程作成したIAMポリシーをアタッチしてください。
- 設定の
タイムアウト時間
は、3秒から1分
に変更します。 - Lambdaのテストイベントは、先程のlexのjsonを貼り付けておきます。
Node.jsバージョン
// 現在時刻の取得
var dt = new Date();
// 日本の時間に修正
dt.setTime(dt.getTime() + 32400000); // 1000 * 60 * 60 * 9(hour)
// 日付を数字として取り出す
var year = dt.getFullYear();
var month = dt.getMonth() + 1;
var day = dt.getDate();
var hour = dt.getHours();
var min = dt.getMinutes();
// 値が1桁であれば '0'を追加
if (month < 10) {month = "0" + month;}
if (day < 10) {day = "0" + day;}
if (hour < 10) {hour = "0" + hour;}
if (min < 10) {min = "0" + min;}
// 出力
var dateTime = year + month + day + hour + min;
console.log("現在の日付" + dateTime);
exports.handler = (event, context, callback) => {
try {
dispatch(event, (response) => {
callback(null, response);
});
} catch (err) {
callback(err);
}
};
// DynamoDBに書き込み
function putDynamoDB(intentRequest, age,date,telephoneNumber,callback){
const AWS = require("aws-sdk");
const dynamo = new AWS.DynamoDB.DocumentClient({
region: "ap-northeast-1", // DynamoDBのリージョン
});
// DynamoDBのテーブル名
var tableName = "Lex-Write-Lambda";
var item = {
age: age,
date: date,
telephoneNumber: telephoneNumber,
timeStamp: dateTime,
};
var params = {
TableName: tableName,
Item: item,
};
// DynamoDBに書き込み処理
dynamo.put(params).promise().then((data) => {
console.log("書き込み完了");
})
.catch((err) => {
console.log(err);
callback(err);
});
}
// インテントのルーティング
function dispatch(intentRequest, callback) {
const intentName = intentRequest.sessionState.intent.name;
if (intentName === "book") {
return bookIntent(intentRequest, callback);
} else if (intentName === "anotherintent") {
return anotherintentIntent(intentRequest, callback);
}
}
// 別のインテント
function anotherintentIntent(intentRequest, callback) {
console.log("anotherintentログです。");
}
// bookインテント
function bookIntent(intentRequest, callback) {
console.log('Received event:', JSON.stringify(intentRequest, null, 2));
const intentName = intentRequest.sessionState.intent.name;
const slots = intentRequest.sessionState.intent.slots;
// 各スロットの値
const date = slots.date?.value?.interpretedValue;
const telephoneNumber = slots.telephoneNumber?.value?.interpretedValue;
const age = slots.age?.value?.interpretedValue;
//スロット値が埋まっていない場合、聞く
if (typeof telephoneNumber == "undefined") {
return callback(ElicitSlot("telephoneNumber", intentName, slots));
}
if (typeof date == "undefined") {
return callback(ElicitSlot("date", intentName, slots));
}
if (typeof age == "undefined") {
return callback(ElicitSlot("age", intentName, slots));
}
// バリデーション 20歳以上
if (age < 20) {
console.log("20歳未満と判定");
return callback(ValidateSlot("age", "ご利用は20歳以上とさせていただいております。",intentName, slots));
}
// 確認プロンプトで「はい」と返答すると、confirmationStateのNone→Confirmedになる
const confirmationState = intentRequest.sessionState.intent.confirmationState;
if (confirmationState === "Confirmed"){
// DynamoDBに書き込み
putDynamoDB(intentRequest, age,date,telephoneNumber,callback);
return callback(Close("Fulfilled", '予約を承りました。', intentName, slots));
// 確認プロンプトで「いいえ」」と返答すると、confirmationStateのNone→Deniedになる
}else if (confirmationState === "Denied") {
return callback(Close("Failed",'追加分の予約をキャンセルしました。',intentName, slots) );
}else{
// 確認プロンプトに移行。confirmationState = None
return callback(ConfirmIntent(intentName, slots));
}
}
function Close(fulfillmentState, messages, intentName, slots) {
return {
"messages": [
{
"content": messages,
"contentType": "PlainText"
}
],
sessionState: {
dialogAction: {
type: 'Close'
},
intent: {
name: intentName,
slots,
state: fulfillmentState,
},
},
};
}
function ElicitSlot(slotToElicit, intentName, slots) {
return {
sessionState: {
dialogAction: {
type: 'ElicitSlot',
slotToElicit,
},
intent: {
name: intentName,
slots,
},
},
};
}
function ValidateSlot(slotToElicit,messages, intentName, slots) {
return {
"messages": [
{
"content": messages,
"contentType": "PlainText"
}
],
sessionState: {
dialogAction: {
type: 'ElicitSlot',
slotToElicit,
},
intent: {
name: intentName,
slots,
},
},
};
}
function ConfirmIntent(intentName, slots) {
return {
sessionState: {
dialogAction: {
type: 'ConfirmIntent'
},
intent: {
name: intentName,
slots,
state: 'Fulfilled',
},
},
};
}
下記の記事を参考にしました。
dispach関数は、インテントが複数あるときに、振り分ける役割があります。
dialogAction
- Close — 会話を終了させる
- ConfirmIntent — Yes/Noを促す
- Delegate — 次のActionをLexにまかせる
- ElicitSlot - Slotを話すように促す
- ElicitIntent - Intentを話すように促す
- Failed - 会話失敗(指定回数を超えて要求に答えられなかった)
- Fulfilled - インテントが完了した事を伝える
- ReadyForFulfillment - 後の処理は、クライアントが処理する必要があることを伝える
confirmationStatus
confirmationStatusも使いますので、記載しておきます。詳細は、ドキュメントを。
confirmationStatusは、確認のプロンプトが発生した場合、それに応じてユーザーレスポンスを提供します。
確認のプロンプト時、ユーザー「はい」と答えると、この値は、Confirmed
ユーザーが「いいえ」と答えると、この値は、Denied となります。
それ以外を答えると、このフィールドの値は None になります。
Pythonバージョン
import json
import boto3
from decimal import Decimal
from datetime import datetime, timedelta
dynamodb = boto3.resource('dynamodb')
lex_write_lambda_table = dynamodb.Table("Lex-Write-Lambda")
def created():
dateJST = datetime.today() + timedelta(hours=9)
return dateJST
def put_item(telephoneNumber, date, age):
response = lex_write_lambda_table.put_item(Item={
"telephoneNumber": telephoneNumber,
"date": date,
"age": age,
"timeStamp" : str(created())
})
print("Send ResponseMetadata:"+ json.dumps(response, default=decimal_to_int, ensure_ascii=False))
# json形式でログを出力するため、Decimalがある場合取り除く
def decimal_to_int(obj):
if isinstance(obj, Decimal):
return int(obj)
def elicit_slot(slot_to_elicit, intent_name, slots):
return {
'sessionState':{
'dialogAction': {
'type': 'ElicitSlot',
'slotToElicit': slot_to_elicit,
},
'intent':{
'name': intent_name,
'slots': slots,
'state': 'InProgress'
}
}
}
def validation_slot(slot_to_elicit, message_content, intent_name, slots):
return {
'messages': [{'contentType': 'PlainText', 'content': message_content}],
'sessionState':{
'dialogAction': {
'type': 'ElicitSlot',
'slotToElicit': slot_to_elicit,
},
'intent':{
'name': intent_name,
'slots': slots,
}
}
}
def confirm_intent(intent_name, slots):
return {
'sessionState':{
'dialogAction': {
'type': 'ConfirmIntent',
},
'intent':{
'name': intent_name,
'slots': slots,
'state': 'Fulfilled'
}
}
}
def close(fulfillment_state, message_content, intent_name, slots):
return {
'messages': [{'contentType': 'PlainText', 'content': message_content}],
"sessionState": {
'dialogAction': {
'type': 'Close',
},
'intent':{
'name': intent_name,
'slots': slots,
'state': fulfillment_state
}
}
}
# "book"インテント
def book_reserve(intent_request):
print("Received event:" + json.dumps(intent_request, default=decimal_to_int, ensure_ascii=False))
intent_name = intent_request['sessionState']['intent']['name']
slots = intent_request['sessionState']['intent']['slots']
print('Received intent_name:' + json.dumps(intent_name, default=decimal_to_int, ensure_ascii=False))
print('Received slots:' + json.dumps(slots, default=decimal_to_int, ensure_ascii=False))
if slots['telephoneNumber'] is None:
return elicit_slot('telephoneNumber', intent_name, slots)
telephoneNumber = slots['telephoneNumber']['value']['interpretedValue']
if slots['date'] is None:
return elicit_slot('date',intent_name, slots)
date = slots['date']['value']['interpretedValue']
if slots['age'] is None:
return elicit_slot('age',intent_name, slots)
age = slots['age']['value']['interpretedValue']
if int(age) < 20:
return validation_slot(
'age',
'ご利用は20歳以上とさせていただいております。\
年齢を教えて下さい',
intent_name,
slots
)
confirmation_status = intent_request['sessionState']['intent']['confirmationState']
print(confirmation_status)
if confirmation_status == "Confirmed":
# DynamoDBに書き込み処理
put_item(telephoneNumber, date, age)
return close( "Fulfilled", '予約を承りました', intent_name, slots)
elif confirmation_status == "Denied":
return close( "Failed", '追加分の予約をキャンセルしました。', intent_name, slots)
elif confirmation_status == "None":
return confirm_intent( intent_name, slots)
# インテントのルーティング
def dispatch(intent_request):
# 他にインテントを使用する場合
# intent_name = intent_request['sessionState']['intent']['name']
# if intent_name == 'book':
# "book"インテント
return book_reserve(intent_request)
def lambda_handler(event, context):
return dispatch(event)
Lambdaのテストしてみましょう。
先程、Lexでの応答でコピーしたjsonをペーストして、テストしましょう。年齢を22歳にしていますので、予約完了しました
と返答するはずです。
Response
{
"sessionState": {
"dialogAction": {
"type": "Close"
},
"intent": {
"name": "book",
"slots": {
"age": {
"value": {
"originalValue": "30",
"interpretedValue": "30",
"resolvedValues": [
"30"
]
}
},
"date": {
"value": {
"originalValue": "明日",
"interpretedValue": "2022-01-16",
"resolvedValues": [
"2022-01-16"
]
}
},
"telephoneNumber": {
"value": {
"originalValue": "09011111111",
"interpretedValue": "09011111111",
"resolvedValues": [
"09011111111"
]
}
}
},
"state": "Fulfilled"
}
},
"messages": [
{
"contentType": "PlainText",
"content": "予約完了しました"
}
]
}
jsonのageのinterpretedValue
を10にすると、ご利用は20歳以上とさせていただいております。
と返答するはずです。
{
"sessionState": {
"dialogAction": {
"type": "Close"
},
"intent": {
"name": "book",
"slots": {
"age": {
"value": {
"originalValue": "90",
"interpretedValue": "10",
"resolvedValues": [
"10"
]
}
},
"date": {
"value": {
"originalValue": "明日",
"interpretedValue": "2022-01-16",
"resolvedValues": [
"2022-01-16"
]
}
},
"telephoneNumber": {
"value": {
"originalValue": "09011111111",
"interpretedValue": "09011111111",
"resolvedValues": [
"09011111111"
]
}
}
},
"state": "Failed"
}
},
"messages": [
{
"contentType": "PlainText",
"content": "ご利用は20歳以上とさせていただいております。"
}
]
}
LexにLambdaを設定
作成したLambdaをボットに設定します。
Lambdaの設定場所がわかりにくいですね。
Lex > ボット > ボット:Lex-Write-Lambda-bot > エイリアス > エイリアス: TestBotAlias > 日本語 (JP)
インテントのフルフィルメントの詳細オプションフルフィルメントにLambda関数を使用
にチェックしないでください。
初期化と検証に Lambda 関数を使用
にチェックします
テストしてみましょう。
予約完了し、DynamoDBにも保存されており、成功ですね。
20歳未満の場合、予約できず、DBに書き込まれないようになっています。
LexとLambdaでできるテクニック
参考