Amazon Lexの第4回です。今回は受け取った発話をバックエンドのLambdaに渡してみたいと思います。
目次
LexでHello World
引き続き"BookTrip"のサンプルを使って、LexからLambdaにユーザからの入力を渡すようにしてみましょう。
まずはかんたんなHello Worldをやってみます。以下のようなLambda関数を作成します。
'use strict'; exports.handler = async (event, context, callback) => { console.log(JSON.stringify(event)); const response = { sessionAttributes: event.sessionAttributes, dialogAction: { type: 'Close', fulfillmentState: 'Fulfilled', message: { contentType: 'PlainText', content: 'ハローワールド' } } }; callback(null, response); };
responseがLexに返すレスポンスの中身です。この中で、dialogActionというのが必須になっています。見た感じ、なんとなくわかりますよね。
type: 'Close'
セッションが終了することを伝えて、ユーザからの入力を受けないようにします。AlexaでいうところのSessionEndedRequest: true
みたいなものですかね。fulfillmentState: 'Fulfilled'
で収集中のスロットが全て満たされたことを伝えます。スロットを使っていないとしてもこのステータスは必須のようです。message
がLexの応答になるようです。なければLexのインテント側に設定されたレスポンスが使われるようです。
セッションアトリビュートなども使えるようですが、詳細はドキュメントを御覧ください。
関数名は"LexTest"にしました。
Lex側でLamdaのレスポンスを使うように設定します。「Fulfillment」で「AWS Lambda function」を選択して、Lambdaの関数名とバージョンを設定します。
下にスクロールして「Save Intent」をクリックして保存、「Build」をクリックして対話モデルをビルドします。
ではテストしてみましょう。
最後に「ハローワールド」が返ってきているのがわかりますね。
ちなみに、LexからLambdaに渡されたイベントの中身はこんな感じでした。
- イベント
{ "messageVersion": "1.0", "invocationSource": "FulfillmentCodeHook", "userId": "XXXXXXXXXXXXXXXXXXXXXXXXXX", "sessionAttributes": {}, "requestAttributes": null, "bot": { "name": "BookTrip_jaJP", "alias": "$LATEST", "version": "$LATEST" }, "outputDialogMode": "Text", "currentIntent": { "name": "BookCar_jaJP", "slots": { "PickUpDate": "2022-05-01", "ReturnDate": "2022-05-02", "DriverAge": "40", "CarType": "エコノミー", "PickUpCity": "神戸" }, "slotDetails": { "PickUpDate": { "resolutions": [ { "value": "2022-05-01" } ], "originalValue": "5/1" }, "ReturnDate": { "resolutions": [ { "value": "2022-05-02" } ], "originalValue": "5/2" }, "DriverAge": { "resolutions": [ { "value": "40" } ], "originalValue": "40" }, "CarType": { "resolutions": [ { "value": "エコノミー" } ], "originalValue": "エコノミー" }, "PickUpCity": { "resolutions": [ { "value": "神戸" } ], "originalValue": "神戸" } }, "confirmationStatus": "Confirmed", "nluIntentConfidenceScore": 1 }, "alternativeIntents": [ { "name": "AMAZON.FallbackIntent", "slots": {}, "slotDetails": {}, "confirmationStatus": "None", "nluIntentConfidenceScore": null }, { "name": "BookHotel_jaJP", "slots": { "RoomType": null, "CheckInDate": null, "Nights": null, "Location": null }, "slotDetails": { "RoomType": null, "CheckInDate": null, "Nights": null, "Location": null }, "confirmationStatus": "None", "nluIntentConfidenceScore": 0.15 } ], "inputTranscript": "はい", "recentIntentSummaryView": [ { "intentName": "BookCar_jaJP", "checkpointLabel": null, "slots": { "PickUpDate": "2022-05-01", "ReturnDate": "2022-05-02", "DriverAge": "40", "CarType": "エコノミー", "PickUpCity": "神戸" }, "confirmationStatus": "None", "dialogActionType": "ConfirmIntent", "fulfillmentState": null, "slotToElicit": null } ], "sentimentResponse": { "sentimentLabel": "NEUTRAL", "sentimentScore": "{Positive: 0.052890804,Negative: 0.0028756545,Neutral: 0.9416867,Mixed: 0.002546887}" }, "kendraResponse": null }
スロットはevent.currentIntent.slots.スロット名
で取れますね。nluIntentConfidenceScoreでインテントの信頼度や、sentimentResponseで感情分析が見えるのも面白いです。
スロットを使う
ということでスロットの値を使って、BookCarインテントを実装してみましょう。上に書いたとおり、スロットはevent.currentIntent.slots.スロット名
で取れますので、こういう感じ。
'use strict'; exports.handler = async (event, context, callback) => { const slots = event.currentIntent.slots; const PickUpCity = slots.PickUpCity; const PickUpDate = slots.PickUpDate; const ReturnDate = slots.ReturnDate; const CarType = slots.CarType; const DriverAge = slots.DriverAge; let message; if (DriverAge >= 20 ) { message = `${PickUpCity} で ${CarType} を、${PickUpDate} から ${ReturnDate} までご予約いたしました。`; } else { message = `申し訳ございません。レンタカーのご利用は20歳以上とさせていただいております。`; } const response = { sessionAttributes: event.sessionAttributes, dialogAction: { type: 'Close', fulfillmentState: 'Fulfilled', message: { contentType: 'PlainText', content: message } } }; callback(null, response); };
すこし動的な要素を含めるために、年齢でレスポンスの内容を変えてみました。テストしてみるとこんな感じです。
最後まで聞いてから20歳未満はNGというのはインタフェース的にはイマイチなので、年齢を確認するインテントを別に用意して先に確認するような会話フローにすべきだと思いますが、まあ一応動いてますね。
予約完了した場合はこちらです。
ちゃんと動いてますね。
インテントのルーティング
上の例では1インテントに1つのLambda関数になっていました。1つのLambda関数の中でインテントごとに呼び出す処理をルーティングしてまとめて書きたいですよね。公式のサンプルによいものがありました。
上記を参考に書いてみたのがこれ。
'use strict'; function close(sessionAttributes, fulfillmentState, message) { return { sessionAttributes, dialogAction: { type: 'Close', fulfillmentState, message, }, }; } function BookCarIntent(intentRequest, callback) { const sessionAttributes = intentRequest.sessionAttributes || {}; const slots = intentRequest.currentIntent.slots; const PickUpCity = slots.PickUpCity; const PickUpDate = slots.PickUpDate; const ReturnDate = slots.ReturnDate; const CarType = slots.CarType; const DriverAge = slots.DriverAge; let message; if (DriverAge >= 20 ) { message = `${PickUpCity} で ${CarType} を、${PickUpDate} から ${ReturnDate} までご予約いたしました。`; } else { message = `申し訳ございません。レンタカーのご利用は20歳以上とさせていただいております。`; } callback(close( sessionAttributes, 'Fulfilled', { contentType: 'PlainText', content: message })); } function BookHotelIntent(intentRequest, callback) { const sessionAttributes = intentRequest.sessionAttributes || {}; const slots = intentRequest.currentIntent.slots; const Location = slots.Location; const Nights = slots.Nights; const CheckInDate = slots.CheckInDate; const RoomType = slots.RoomType; let message = `${Location} で ${CheckInDate} から ${Nights} 泊、${RoomType} タイプのお部屋を予約しました。`; callback(close( sessionAttributes, 'Fulfilled', { contentType: 'PlainText', content: message })); } function dispatch(intentRequest, callback) { console.log(JSON.stringify(intentRequest, null, 2)); console.log(`dispatch userId=${intentRequest.userId}, intentName=${intentRequest.currentIntent.name}`); const intentName = intentRequest.currentIntent.name; if (intentName === "BookCar_jaJP") { return BookCarIntent(intentRequest, callback); } else if (intentName === "BookHotel_jaJP") { return BookHotelIntent(intentRequest, callback); } else { throw new Error(`Intent with name ${intentName} not supported`); } } exports.handler = (event, context, callback) => { try { dispatch(event, (response) => callback(null, response)); } catch (err) { callback(err); } };
1インテント=1関数として作成して、function dispatch
でインテントごとに別の関数を呼び出す形ですね。ASK-SDK v2とかに慣れているとこちらのほうがなんとなくしっくりきますが、LexのほうがレスポンスをJSONでガッツリ書かないといけない印象があります。このあたりは上のfunction close
のようにレスポンスをテンプレートとしてラップしてあげればいいかなと思います。
テストしてみましょう。LexではインテントごとにFulfilmentの設定が必要なのでそれぞれ設定変更するのをお忘れなく。
ちゃんと動いてますね!
まとめ
Lambdaで動的にレスポンス生成できるようになるといろいろ夢が広がりますね。ASK-SDK v2に慣れていれば、比較的違和感なく書けるのではないかと思います。
いや、もっとASK-SDKっぽく書きたい!という人は以下も試してみてはいかがでしょうか?