ダイアログモデルを使用すると、マルチターンの会話スキルを簡単に開発できます。1つ以上のインテントのスロットで、このインテントを完了させるためこのスロットは必須ですか?を選択すると、Alexa音声サービスはダイアログの状態と収集する必要がある必須スロットを常に追跡します。バックエンドがDialog.Delegateディレクティブを返すと、Alexa音声サービスは自動的に次のスロットを入力するよう促します。次の候補を上書きする場合は、Dialog.ElicitSlotディレクティブを使用します。
たとえば、コーヒーショップのスキルを作成し、ユーザーはコーヒーかお茶を選択できるとします。ユーザーがコーヒーを選択すると、スキルはロースト(焙煎の種類)をライト、ミディアム、ミディアムダーク、ダークのどれにするか尋ねます。ユーザーがお茶を選択すると、スキルはユーザーに紅茶、緑茶、ウーロン茶、白茶から選んでもらいます。ユーザーがコーヒーを頼んだのに、スキルがお茶の種類を尋ねるのは変ですよね?
この記事では、ダイアログモデルを使用して、ユーザーの前の答えに基づいて動的にスロットを引き出す方法を詳しく説明します。
ここでの対話モデルは、OrderIntentというインテントを作成し、コーヒーショップで注文する発話を処理します。
ユーザーとスキルのやり取りでは、次のように注文します。
上記の発話を見ると、ユーザーの注文をキャプチャするために必要な3つのスロット、drink、coffeeRoast、teaTypeがあるのがわかります。
これでスロットが決まったので、発話を更新して値をスロットに置き換え、スキルのバックエンドコードで値をキャプチャできるようにします。
「{teaType} {drink} をください」という発話は、すべての情報をワンフレーズで指定できますが、「{drink}をください」では、drinkに指定された値に基づいて追加の質問を行う必要があります。これを行う方法はバックエンドの説明のときに合わせて説明します。
スロット値はコーヒーショップで注文できる選択肢に対応しています。次の表に、各スロットの値を示します。
drink | coffeeRoast | teaType |
---|---|---|
コーヒー | ライト | 紅 |
茶 | ミディアム | 緑 |
ミディアムダーク | 白 | |
ダーク | ウーロン |
発話とスロットを設定したら、次はダイアログモデルを有効にして、Alexaがユーザーに新しいスロットについて尋ねる処理を行うようにします。ダイアログモデルを有効にするには、OrderIntentの1つ以上のスロットを必須に設定します。
設定したら、次にスロットを埋めるためにAlexaがユーザーに尋ねるプロンプトと、ユーザーがそれに答えるサンプル発話を指定します。技術的には3つのスロットをすべて必須にして、最低限必要なスロット値、drinkとcoffeeRoastまたはdrinkとteaType、つまり(drink || coffeeRoast && teaType) が得られたら手動で会話を終了することもできますが、ここではElicitSlotディレクティブを使用して、ユーザーが飲み物を選択した後に追加で質問をすることにして、drinkのみを必須にします。質問は「コーヒーにしますか、お茶にしますか?」とし、サンプル発話は次のとおりです。
スロットの発話はインテントの発話とほぼ同じですね。これにより、尋ねられたこと以上の情報を話した場合の自由度が上がります。
注: ここでは「{drink}」だけの発話を定義していません。そうすることもできますが、Alexaが自動的にモデルに追加するので、これは任意です。
フロントエンドが正しく設定されたので、バックエンドのしくみを見てdrinkの値に応じてスロットを引き出しましょう。
フローチャートを使用して、前の答えに応じて次に引き出すスロットを説明したいところですが、スキルは電話連絡網ではありません。電話連絡網は定型的すぎるため、会話にはうまく適用できません。代わりに、状況に応じたデザインを使用して、すでに収集した情報に基づいてユーザーの答えに反応するスキルを作成します。フローチャートではなく式を使って、状況に応じてスキルが必要とする情報を説明しようとしているのはこのためです。ここで満たそうとしている式は (drink || coffeeRoast && teaType) です。スキルで必要な値はdrinkと、coffeeRoastまたはteaTypeのいずれかであり、それはdrink、つまり状況によって異なります。
drinkの値がコーヒーであれば、スキルはcoffeeRoastの値を必要とします。同様に、drinkの値がお茶であれば、スキルはteaTypeの値を必要とします。
drinkの値に基づいてスロットを引き出すには、4つのハンドラーを定義します。これらのハンドラーはスキルのさまざまな状況を表し、適切なスロットを引き出します。 ハンドラーは次のとおりです。
StartedInProgressOrderIntentHandler
このハンドラーは単純なものです。canHandle関数は次の場合にtrueを返します。
これはユーザーが「注文をお願い」と言ったときに発生します。
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.intent.name === "OrderIntent"
&& handlerInput.requestEnvelope.request.dialogState !== 'COMPLETED';
},
ハンドラー関数は単純にDialog.Delegateを返すため、Alexaは自動的に欠落している必須スロットを促します。
handle(handlerInput) {
return handlerInput.responseBuilder
.addDelegateDirective()
.getResponse();
}
以下に関数全体を示します。
const StartedInProgressOrderIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.intent.name === "OrderIntent"
&& handlerInput.requestEnvelope.request.dialogState !== 'COMPLETED';
},
handle(handlerInput) {
return handlerInput.responseBuilder
.addDelegateDirective()
.getResponse();
}
}
CoffeeGivenOrderIntentHandler
ここから少し面白くなってきます。このハンドラーはDialog.ElicitSlotを使用して、drinkスロットがコーヒーの場合にcoffeeRoastスロットを引き出します。
canHandle関数は次の場合にtrueを返します。
最後の条件がとても重要です。これがないとスキルはcoffeeRoastスロットを引き出し続けてしまいます。ロボットのように同じことを永遠に繰り返して聞き続けるとお客様は困ってしまいますね。
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.intent.name === "OrderIntent"
&& handlerInput.requestEnvelope.request.intent.slots.drink.value
&& handlerInput.requestEnvelope.request.intent.slots.drink.value === 'コーヒー'
&& !handlerInput.requestEnvelope.request.intent.slots.coffeeRoast.value
}
このハンドル関数は、まるで標準の音声ディレクティブを返しているかのように音声と再プロンプトを定義しますが、addElicitSlotDirectiveを呼び出すことで、Alexaは指定されたスロットを、たとえ音声モデルで必須と選択されていなくても、引き出します。
return handlerInput.responseBuilder
.speak('ローストはライト、ミディアム、ミディアムダーク、ダークのどれにしますか?')
.reprompt('ローストはライト、ミディアム、ミディアムダーク、ダークのどれにしますか?')
.addElicitSlotDirective('coffeeRoast')
.getResponse();
CoffeeGivenOrderIntentHandler全体では次のようになります。
const CoffeeGivenOrderIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.intent.name === "OrderIntent"
&& handlerInput.requestEnvelope.request.intent.slots.drink.value
&& handlerInput.requestEnvelope.request.intent.slots.drink.value === 'コーヒー'
&& !handlerInput.requestEnvelope.request.intent.slots.coffeeRoast.value
},
handle(handlerInput) {
return handlerInput.responseBuilder
.speak('ローストはライト、ミディアム、ミディアムダーク、ダークのどれにしますか?')
.reprompt('ローストはライト、ミディアム、ミディアムダーク、ダークのどれにしますか?')
.addElicitSlotDirective('coffeeRoast')
.getResponse();
}
}
TeaGivenOrderIntentHandler
ここまでで、TeaGivenOrderIntentHandlerの構築方法ももうおわかりでしょう。実はCoffeeGivenOrderIntentHandlerとまったく同じなのですが、drinkが茶であることと、teaTypeの値がないことをチェックします。
コードにすると次のようになります。
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.intent.name === "OrderIntent"
&& handlerInput.requestEnvelope.request.intent.slots.drink.value
&& handlerInput.requestEnvelope.request.intent.slots.drink.value === '茶'
&& !handlerInput.requestEnvelope.request.intent.slots.teaType.value
}
もうおわかりですね。CoffeeGivenOrderIntentHandlerではElicitSlotディレクティブを使用して次に引き出すスロットをAlexaに指示します。この場合はteaTypeスロットを引き出します。
return handlerInput.responseBuilder
.speak("紅茶、緑茶、ウーロン茶、白茶のどれにしますか?")
.reprompt("紅茶、緑茶、ウーロン茶、白茶のどれにしますか?")
.addElicitSlotDirective('teaType')
.getResponse();
}
上記の部分を組み合わせると、TeaGivenOrderIntentHandlerは次のようになります。
const TeaGivenOrderIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.intent.name === "OrderIntent"
&& handlerInput.requestEnvelope.request.intent.slots.drink.value
&& handlerInput.requestEnvelope.request.intent.slots.drink.value === '茶'
&& !handlerInput.requestEnvelope.request.intent.slots.teaType.value
},
handle(handlerInput) {
return handlerInput.responseBuilder
.speak("紅茶、緑茶、ウーロン茶、白茶のどれにしますか?")
.reprompt("紅茶、緑茶、ウーロン茶、白茶のどれにしますか?")
.addElicitSlotDirective('teaType')
.getResponse();
}
}
ご覧のとおり、これらのハンドラーはとてもよく似ています。タピオカティーなど別の飲み物を追加して、ユーザーがタピオカティーを注文したらbobaTypeスロットを引き出すのであれば、単純にdrinkの値チェックをタピオカティーに変更し、teaTypeの値があるかどうかのチェックの代わりにbobaTypeのチェックにして、ハンドル関数を更新してbobaTypeスロットを引き出すようにすればいいだけです。
CompletedOrderIntentHandler
必要なスロットをすべて収集したら、ユーザーの飲み物を注文する必要があります。このサンプルでは、単純にAlexaが注文を復唱します。この場合、ダイアログモデルでスロットの収集が完了したときに実行するハンドラーを作成する必要があります。
CompletedOrderIntentHandlerのcanHandle関数は、次の場合にtrueになります。
コードにすると次のようになります。
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.intent.name === "OrderIntent"
&& handlerInput.requestEnvelope.request.dialogState === "COMPLETED";
}
完了したら、スロットにアクセスして、ユーザーの飲み物の選択に応じて応答を作成します。
const drink = handlerInput.requestEnvelope.request.intent.slots.drink.value;
const type;
if (drink === 'コーヒー') {
type = handlerInput.requestEnvelope.request.intent.slots.coffeeRoast.value;
} else if (drink === '茶') {
type = handlerInput.requestEnvelope.request.intent.slots.teaType.value;
} else {
type = '水';
}
上記のコードでは、typeの値をユーザーの飲み物の選択に応じて設定しています。たとえば、ユーザーが「コーヒーをちょうだい」と言い、ローストの選択で「ミディアム」と回答したなら、typeがミディアムに設定されます。
drinkの値に基づいてtypeを設定できるようになっているので、応答を作成して返すだけです。
const speechText = `${type}${drink}でよろしいですか?`;
return handlerInput.responseBuilder
.speak(speechText)
.getResponse();
ここでは、単純にユーザーが注文したものを復唱する文字列を作成して、ResponseBuilderを使用して応答を返しています。
ここに挙げた手順に従うことで、ダイアログモデルを活用して、スロットの値に基づいてスキルが次に入力を促す選択肢を簡単に変更できます。ダイアログモデルでは、必須スロットに基づいて会話を続けますが、ElicitSlotがあるインテントに属する任意のスロットを、そのスロットが必須ではなくても、引き出すことができます。
この記事を最後まで読んでいただき、ありがとうございました。このテクニックを応用して、ご自分のスキルで使ってみてください。オンラインのディスカッションも歓迎します。 Twitterのアカウントは@Toshimin8です。