はじめに
Alexaのプログラミングで、Alexaがユーザーに何かを尋ねて答えてもらう場合、期待しないインテントで処理してしまうことがあります。
例えば、「食べ物はどうなさいますか?」、「飲み物はどうなさいますか?」と尋ねる場合、ユーザーは、「ケーキ」や「コーヒー」といったことを回答しますが、この時使うのが、多くの場合カスタムスロットで、その名前をリストアップしたものを使います。
そんなときに飲み物のはずが、食べ物のスロットのインテントとして処理してしまうことがあります。
それをどうやって対応するかというのが、この記事の内容です。
今回実施する内容
二つのインテントにマッチする発話があった場合、それを期待するインテントで処理できるようにプログラミングする。
環境
OS:Windows 10 JP
Alexaスキル言語:Node.js
Editor:Visual Studio Code
Alexa developer console
参考
特になし
用語
特になし
前提条件
特になし
いくつかの対応方法を考える
今回のケースでの対策する方法をいくつか考えてみます。
色々やり方はあると思いますが、思いついたものを列挙します。
- 1つの発話で、二つのことを尋ねる
- 1つのハンドラーで複数のインテントを受けて、ハンドラー処理内でわける
- ハンドラーをわけて、ステート管理して、ステートで処理するハンドラーを決めて、そのハンドラーに応じた処理する
1は、食べ物と飲み物のように、「食べ物と飲み物はどうしますか?」と一度に尋ねて、「サンドイッチとコーヒー」と答えてもらうイメージで、こういった一度に尋ねたほうが効率よく処理できたりしますが、同時に尋ねないものはあって、例えば「店内で食べますか?それともテイクアウトですか?」と言ったようなものもあります。今回の主旨ではないので対象外にします。
2は、食べ物の回答と飲み物の回答をうけるハンドラーをひとつ作成してそこで処理をするイメージで、これはありですが、1つのハンドラーで分岐が多くなるため、ソースの視認性は下がります。
3は、食べ物用のハンドラーと飲み物用のハンドラーを準備しますが、現在がどちらの問い合わせをしているかを示すステートを作成して、そのステートの状態を確認して、適切なハンドラーで処理するというものです。
Alexaの公式のサンプルプログラムを見るとこれと同様のことをしていますので、これがまあいいんだろうなとは思います。
結局は2と同じなのですが、ソースの視認性はあがりますから。
ということで、今回は3について説明したいと思います。
二つのインテントにマッチする発話の実証
対策の前に、問題となる動作を確認したいと思います。
Alexa developer console上での作業
今回の試験用のスキルを作ります。
- スキル名も呼び出し名も何でもよいですが、「ステートサンプル」とします。
- カスタムスロットとして、食べ物のリスト
FoodList
と飲み物のリストDrinkList
を用意します。 食べ物リストには、「サンドウィッチ」、「ケーキ」を追加します。 飲み物リストには、「コーヒー」、「紅茶」を追加します。 - 食べ物
FoodIntent
と飲み物DrinkIntent
のインテントを二つ用意します。 食べ物インテントには、FoodName
のスロットを作り、FoodList
を割り当てます。 飲み物インテントには、DrinkName
のスロットを作り、DrinkList
を割り当てます。 サンプル発話は、それぞれ作成したスロットのみにします。
その他のスキルのソースコードとのリンクや、ビルドなどは割愛します。
ソースコードの作成
- 最初に最低限のindex.jsを作成する。 この部分は説明を割愛します。
-
FoodIntentHandler
とDrinkIntentHandler
を以下の通り追加する。 単にインテントで受けたスロットを取得して、それをAlexaに発話させるだけの処理です。
...
const FoodIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& handlerInput.requestEnvelope.request.intent.name === 'FoodIntent';
},
handle(handlerInput) {
const intent = handlerInput.requestEnvelope.request.intent;
const foodName = intent.slots.FoodName.value;
const speechText = `FoodIntentHandlerです。${foodName}ですね。`;
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(speechText)
.withSimpleCard("FoodIntentHandler", speechText)
.getResponse();
},
};
const DrinkIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& handlerInput.requestEnvelope.request.intent.name === 'DrinkIntent';
},
handle(handlerInput) {
const intent = handlerInput.requestEnvelope.request.intent;
const drinkName = intent.slots.DrinkName.value;
const speechText = `DrinkIntentHandlerです。${drinkName}ですね。`;
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(speechText)
.withSimpleCard("DrinkIntentHandler", speechText)
.getResponse();
},
};
...
const skillBuilder = Alexa.SkillBuilders.standard();
exports.handler = skillBuilder
.addRequestHandlers(
LaunchRequestHandler,
FoodIntentHandler,
DrinkIntentHandler,
HelpIntentHandler,
CancelAndStopIntentHandler,
SessionEndedRequestHandler
)
.addErrorHandlers(ErrorHandler)
.lambda();
動作の確認
これをAlexaのテストシミュレーターで実施すると、設定したカスタムスロットにある食べ物や飲み物は、期待するハンドラーで処理されます。
そこからさらにほかの食べ物や飲み物を話してみると、以下のようになりました。
寿司はDrinkIntentHandlerに、
かつ丼はDrinkIntentHandlerに、
オレンジジュースはFoodIntentHandlerになりました。
このように、発話した内容が期待するスロットに入るかというとそうでないことも多く、これがプログラムを作成するうえで思わぬ落とし穴におちてデバッグが難航することがありますし、期待しない値がスロットに入る場合はすぐに処理を中断するというのもせっかくのユーザーとの会話を終わらせるのももったいないなと思うので、その状況に応じた対応をしたいとも思います。
ということでこれをどのように対応するかを説明します。
ステート管理して処理する方法
それでは、ステートを追加してそれに応じて処理する方法を説明します。
ソースコードの編集
-
ステート管理用のオブジェクト変数を定義する。
const STATE = { FOOD_REQ : 0, DRINK_REQ : 1, };
ステート管理用にSTATEといいうオブジェクト変数を上記の通り定義します。
FOOD_REQは、食べ物を尋ねるときにそのステートに変更し、
DRINK_REQは、飲み物を尋ねるときにそのステートに変更し、使用します。 -
Launch用のハンドラー
LaunchRequestHandler
で、食べ物を問い合わせるようにステートを設定する。以下のようにセッションアトリビュートを取得し、
state
としてSTATE.FOOD_REQ
を設定し、セッションアトリビュートを設定する。その後、Launch用のハンドラーを返す。
Alexaに、「食べ物はどうしますか?」と尋ねるようにもする。const LaunchRequestHandler = { canHandle(handlerInput) { return handlerInput.requestEnvelope.request.type === 'LaunchRequest'; }, handle(handlerInput) { const attributes = handlerInput.attributesManager.getSessionAttributes(); attributes.state = STATE.FOOD_REQ; handlerInput.attributesManager.setSessionAttributes(attributes); return handlerInput.responseBuilder .speak("ステートサンプルへようこそ。食べ物はどうしますか?") .reprompt("食べ物はどうしますか?") .getResponse(); }, };
-
FoodIntentHandler
のcanHandle
内で、FoodIntent、DrinkIntentのどちらも受けられるようにしつつ、ステートがSTATE.FOOD_REQか判定する。以下のように、
handlerInput.requestEnvelope.request.intent.name
の値がFoodIntent
、またはDrinkIntent
のどちらも許容するようにする。
また、attributes.state
の値がSTATE.FOOD_REQ
となっていることを確認する。const FoodIntentHandler = { canHandle(handlerInput) { const attributes = handlerInput.attributesManager.getSessionAttributes(); return handlerInput.requestEnvelope.request.type === 'IntentRequest' && (handlerInput.requestEnvelope.request.intent.name === 'FoodIntent' || handlerInput.requestEnvelope.request.intent.name === 'DrinkIntent') && attributes.state === STATE.FOOD_REQ; },
-
FoodIntentHandler
のhandle(handlerInput)
内で、drinkName
の値をスロットの値から取得する。ここで、FoodIntent
を受けた場合は、FoodName
のスロットの値を取得し、DrinkIntent
を受けた場合は、DrinkName
のスロットの値を取得することになるため、それぞれのスロットが存在するかを確認するif文を書いたうえで値を取得する。
また、今回の例では、食べ物の後、飲み物を尋ねるように、ステート変更とそれに対する問い合わせをAlexaにさせる。handle(handlerInput) { const intent = handlerInput.requestEnvelope.request.intent; let foodName; if (intent.slots.FoodName) { foodName = intent.slots.FoodName.value; } else if (intent.slots.DrinkName) { foodName = intent.slots.DrinkName.value; } else { foodName = ""; } const attributes = handlerInput.attributesManager.getSessionAttributes(); attributes.state = STATE.DRINK_REQ; handlerInput.attributesManager.setSessionAttributes(attributes); const speechText = `FoodIntentHandlerです。${foodName}ですね。飲み物はどうしますか?`; return handlerInput.responseBuilder .speak(speechText) .reprompt(speechText) .withSimpleCard("FoodIntentHandler", speechText) .getResponse(); },
-
3と4の同様の内容を
DrinkIntentHandler
にも実施する。また、例であるため再度食べ物を問い合わせるようにする。const DrinkIntentHandler = { canHandle(handlerInput) { const attributes = handlerInput.attributesManager.getSessionAttributes(); return handlerInput.requestEnvelope.request.type === 'IntentRequest' && (handlerInput.requestEnvelope.request.intent.name === 'FoodIntent' || handlerInput.requestEnvelope.request.intent.name === 'DrinkIntent') && attributes.state === STATE.DRINK_REQ; }, handle(handlerInput) { const intent = handlerInput.requestEnvelope.request.intent; let drinkName; if (intent.slots.FoodName) { drinkName = intent.slots.FoodName.value; } else if (intent.slots.DrinkName) { drinkName = intent.slots.DrinkName.value; } else { drinkName = ""; } const attributes = handlerInput.attributesManager.getSessionAttributes(); attributes.state = STATE.FOOD_REQ; handlerInput.attributesManager.setSessionAttributes(attributes); const speechText = `DrinkIntentHandlerです。${drinkName}ですね。食べ物はどうしますか?`; return handlerInput.responseBuilder .speak(speechText) .reprompt(speechText) .withSimpleCard("DrinkIntentHandler", speechText) .getResponse(); }, };
動作の確認
上記の通りソースコードを変更したうえで、最初のソースコードで、DrinkIntentHandler
で受けていた「かつ丼」とFoodIntentHandler
で受けていた「オレンジジュース」を試すと期待通り、「かつ丼」はFoodIntentHandler
で、「オレンジジュース」はDrinkIntentHandler
で受けるようになりました。
JSONとソースコード
今回使ったAlexa developer consoleのJSONと、Alexaのソースコードを載せます。
上記で説明した以外のHelpIntentHandler
、CancelAndStopIntentHandler
、およびSessionEndedRequestHandler
は、もともと持っていたサンプルのものですので、今回の記事とは関係のない記載が見受けれらますが、無視ください。
{
"interactionModel": {
"languageModel": {
"invocationName": "ステートサンプル",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "FoodIntent",
"slots": [
{
"name": "FoodName",
"type": "FoodList"
}
],
"samples": [
"{FoodName}"
]
},
{
"name": "DrinkIntent",
"slots": [
{
"name": "DrinkName",
"type": "DrinkList"
}
],
"samples": [
"{DrinkName}"
]
}
],
"types": [
{
"name": "FoodList",
"values": [
{
"name": {
"value": "ケーキ"
}
},
{
"name": {
"value": "サンドウィッチ"
}
}
]
},
{
"name": "DrinkList",
"values": [
{
"name": {
"value": "紅茶"
}
},
{
"name": {
"value": "コーヒー"
}
}
]
}
]
}
}
}
const Alexa = require('ask-sdk-core');
const STATE = {
FOOD_REQ : 0,
DRINK_REQ : 1,
};
const LaunchRequestHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
},
handle(handlerInput) {
const attributes = handlerInput.attributesManager.getSessionAttributes();
attributes.state = STATE.FOOD_REQ;
handlerInput.attributesManager.setSessionAttributes(attributes);
return handlerInput.responseBuilder
.speak("ステートサンプルへようこそ。食べ物はどうしますか?")
.reprompt("食べ物はどうしますか?")
.getResponse();
},
};
const FoodIntentHandler = {
canHandle(handlerInput) {
const attributes = handlerInput.attributesManager.getSessionAttributes();
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& (handlerInput.requestEnvelope.request.intent.name === 'FoodIntent'
|| handlerInput.requestEnvelope.request.intent.name === 'DrinkIntent')
&& attributes.state === STATE.FOOD_REQ;
},
handle(handlerInput) {
const intent = handlerInput.requestEnvelope.request.intent;
let foodName;
if (intent.slots.FoodName) {
foodName = intent.slots.FoodName.value;
} else if (intent.slots.DrinkName) {
foodName = intent.slots.DrinkName.value;
} else {
foodName = "";
}
const attributes = handlerInput.attributesManager.getSessionAttributes();
attributes.state = STATE.DRINK_REQ;
handlerInput.attributesManager.setSessionAttributes(attributes);
const speechText = `FoodIntentHandlerです。${foodName}ですね。飲み物はどうしますか?`;
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(speechText)
.withSimpleCard("FoodIntentHandler", speechText)
.getResponse();
},
};
const DrinkIntentHandler = {
canHandle(handlerInput) {
const attributes = handlerInput.attributesManager.getSessionAttributes();
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& (handlerInput.requestEnvelope.request.intent.name === 'FoodIntent'
|| handlerInput.requestEnvelope.request.intent.name === 'DrinkIntent')
&& attributes.state === STATE.DRINK_REQ;
},
handle(handlerInput) {
const intent = handlerInput.requestEnvelope.request.intent;
let drinkName;
if (intent.slots.FoodName) {
drinkName = intent.slots.FoodName.value;
} else if (intent.slots.DrinkName) {
drinkName = intent.slots.DrinkName.value;
} else {
drinkName = "";
}
const attributes = handlerInput.attributesManager.getSessionAttributes();
attributes.state = STATE.FOOD_REQ;
handlerInput.attributesManager.setSessionAttributes(attributes);
const speechText = `DrinkIntentHandlerです。${drinkName}ですね。食べ物はどうしますか?`;
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(speechText)
.withSimpleCard("DrinkIntentHandler", speechText)
.getResponse();
},
};
const HelpIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& handlerInput.requestEnvelope.request.intent.name === 'AMAZON.HelpIntent';
},
handle(handlerInput) {
const speechText = 'You can say hello to me!';
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(speechText)
.withSimpleCard('Hello World', speechText)
.getResponse();
},
};
const CancelAndStopIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'IntentRequest'
&& (handlerInput.requestEnvelope.request.intent.name === 'AMAZON.CancelIntent'
|| handlerInput.requestEnvelope.request.intent.name === 'AMAZON.StopIntent');
},
handle(handlerInput) {
const speechText = 'Goodbye!';
return handlerInput.responseBuilder
.speak(speechText)
.withSimpleCard('Hello World', speechText)
.getResponse();
},
};
const SessionEndedRequestHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === 'SessionEndedRequest';
},
handle(handlerInput) {
console.log(`Session ended with reason: ${handlerInput.requestEnvelope.request.reason}`);
return handlerInput.responseBuilder
.speck("session end")
.getResponse();
},
};
const ErrorHandler = {
canHandle() {
return true;
},
handle(handlerInput, error) {
console.log(`Error handled: ${error.message}`);
return handlerInput.responseBuilder
.speak('エラーが発生しました')
.reprompt('エラーが発生しました')
.getResponse();
},
};
const skillBuilder = Alexa.SkillBuilders.standard();
exports.handler = skillBuilder
.addRequestHandlers(
LaunchRequestHandler,
FoodIntentHandler,
DrinkIntentHandler,
HelpIntentHandler,
CancelAndStopIntentHandler,
SessionEndedRequestHandler
)
.addErrorHandlers(ErrorHandler)
.lambda();
次の課題は?
ステート管理を入れることで、ユーザーとAlexaの会話の状態を管理でき、それに応じた処理をしやすくなり、期待しないハンドラーへ飛んで、思わぬ不具合を事前に防ぎやすくなります。
しかし、今回のようにIntentのサンプル発話として、カスタムスロットだけを設定するようなことを実施すると、「かつ丼」や「オレンジジュース」のような場合はよいのですが、その他の言葉もまるごと取得してしまうため、例えば、「持ち帰りにして」などといったように言った言葉も入ってしまうことがあります。それはそれで困りますので、サンプル発話がカスタムスロットだけというのは、気を付けて使わないといけないと思います。
また、どうしてもサンプル発話にカスタムスロットだけをいれたいような場合は、addRequestHandlers
で、外套の処理ハンドラーを一番最後のほうにもっていくと多少は期待しない処理に落ち込まないようになります。
Alexaのプログラムは、このaddRequestHandlers
の上から順番に処理するため、上のほうに記載したハンドラーでマッチすれば、カスタムスロットしかないハンドラーにまで処理がくることがないためです。
終わりに
今回は、二つのインテントにマッチする発話のソースコードでの対応方法を説明しました。
サンプル発話がカスタムスロットだけといった状況でなくても、期待しないIntentになることはありますので、ステート管理をして、おかしなところへ処理が飛ばないような工夫は必要と思います。
ステート管理して処理するのは、一番わかりやすいのは、YesIntentやNoIntentですね。これこそ、ステート管理しないと何に対する返事かわかりませんから。
「次の課題は?」に書いたように、それでも人が全く違うことを尋ねてくることもありますので、それに対してはまたどうするかは別課題ですが。私の場合は、そのまえの会話をセッションアトリビュートに設定しておいて、処理できない言葉が発現された場合は、「わかりませんでした。」の後にその前の会話を再度Alexaに話させるような処理を入れたりしています。
それから、なんでも入るカスタムスロットを応用して、会話をすべて取得してみるようなプログラミングもできて、それを応用したスキルもあるみたいです。それはそれで面白いとは思いますね。