LoginSignup
9
3

More than 1 year has passed since last update.

Amazon Lexで受け答えした情報を、Lambdaを用いてDynamoDBに書き込む

Last updated at Posted at 2022-01-15

はじめに

Lexで答えた情報をLambdaを用いて、DynamoDBに書き込むことをやります。

スクリーンショット 2022-01-15 15.58.25.png

流れ

  1. DynamoDBのテーブル作成
  2. Lex作成
  3. Lambda作成
  4. LexにLambdaを設定

DynamoDBのテーブル作成
以下の設定で作成します。
テストですので、オンデマンドモードにして、安く済ませます。

  • テーブル名:Lex-Write-Lambda
  • パーティションキー:telephoneNumber文字列
  • ソートキー:date文字列
  • 項目:age数値 (テーブル作成後)
  • 項目:timeStamp数値 (テーブル作成後)
  • テーブルクラス:DynamoDB 標準 – IA
  • キャパシティーモード:オンデマンド
    スクリーンショット 2022-01-15 16.07.25.png

agetimeStampを含めた項目を作成します。

スクリーンショット 2022-01-15 19.50.42.png

Lexの作成

bot作成

botを作成します。
名前は適当に作成し、言語は日本語に設定します。それ以外はデフォルトのままです。

スクリーンショット 2022-01-15 16.15.34.png
スクリーンショット 2022-01-15 16.15.49.png
スクリーンショット 2022-01-15 16.16.28.png

インテント作成

インテント名は、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インテントが発動するトリガーとなる言葉を設定します。
スクリーンショット 2022-01-15 16.23.13.png

スロット

まず、telephoneNumberを以下の設定で作成します。
スクリーンショット 2022-01-15 16.36.08.png

ここでポイントですが、 スロットタイプ:AMAZON.PhoneNumberは、番号のみを聞き取ります。
userが数値のみを言うと、Lexは聞き取ってくれますが、userが「09011111111です。」と文字列も含めて答えると、Lexは聞き取れなくなります。

そのため、telephoneNumberスロットの詳細オプションサンプル発話 -オプションで、{telephoneNumber}です。」と設定することで、文字列のです`を省いた数値を聞き取ってくれるようになります。

スクリーンショット 2022-01-15 16.33.16.png

注意

スロットを更新を押した後、スロットのプロンプト等を修正すると、先程設定したサンプル発話 -オプションが削除される場合がありますので、再度入力して更新してください。

同様に、dateageのスロットを作成します。
必要に応じて、詳細オプションサンプル発話 -オプションで設定しましょう。

スクリーンショット 2022-01-15 16.37.53.png
スクリーンショット 2022-01-15 16.41.56.png

ちなみに、AMAZON.Dateで聞き取れるのは、下記の通りです。

反応 言葉
2日後
明日
明後日
17日(未来の直近の月の17日になる)
来週の月曜日
明々後日

確認プロンプトと応答拒否

以下のように設定します。
スクリーンショット 2022-01-15 19.51.57.png

テスト

インテントを保存し、構築をクリック後、テストしてみましょう。
スクリーンショット 2022-01-15 16.53.54.png

テスト時の検査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"
}

応答

dialogActiontypestateconfirmationStateは、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ポリシーをアタッチしてください。
スクリーンショット 2022-02-06 21.47.07.png

  • 設定のタイムアウト時間は、3秒から1分に変更します。
  • Lambdaのテストイベントは、先程のlexのjsonを貼り付けておきます。

Node.jsバージョン

lambda
// 現在時刻の取得
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)
スクリーンショット 2022-01-15 17.22.10.png

インテントのフルフィルメントの詳細オプションフルフィルメントにLambda関数を使用チェックしないでください。
スクリーンショット 2022-01-31 22.52.54.png

初期化と検証に Lambda 関数を使用にチェックします

スクリーンショット 2022-01-31 22.53.53.png

スクリーンショット 2022-01-15 17.25.48.png

テストしてみましょう。
スクリーンショット 2022-01-15 17.30.00.png
予約完了し、DynamoDBにも保存されており、成功ですね。

20歳未満の場合、予約できず、DBに書き込まれないようになっています。

スクリーンショット 2022-02-01 0.09.51.png

LexとLambdaでできるテクニック

参考

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3