ダイアログモデルを用いたAlexaスキルを開発してみた

# はじめに
2019年2月に、日本語環境での「発話プロファイラー」を用いたダイアログモデルの簡易テストが利用可能になりました。

簡易テストの実施手順については、クラスメソッド様のブログ記事「[日本語スキルでも発話プロファイラーを使った対話モデルの確認ができるようになりました](https://dev.classmethod.jp/voice-assistant/amazon-alexa/testing-your-dialog-with-utterance-profiler-in-jp/)」が参考になるかと思います。

今回は、クラスメソッド様のブログ記事に取り上げられた「発話プロファイラー」の簡易テストを自分の環境で検証してみるために、Alexaスキル「ホテルの予約」をダイアログモデルとAlexa-hostedを用いて作成してみましたので、その内容をお伝えしようと思います。

（初めてのダイアログモデルを用いたAlexaスキル開発となります。あやまり等がございましたら、コメント欄にてご指摘のほどお待ちしております。）

# 参考文献
- [発話をテストして対話モデルを改善する](https://developer.amazon.com/ja/docs/custom-skills/test-utterances-and-improve-your-interaction-model.html)
- [日本語スキルでも発話プロファイラーを使った対話モデルの確認ができるようになりました](https://dev.classmethod.jp/voice-assistant/amazon-alexa/testing-your-dialog-with-utterance-profiler-in-jp/)
- [発話プロファイラーを使ってコードを実装する前に会話フローをテストする](https://dev.classmethod.jp/voice-assistant/amazon-alexa/testing-your-dialog-with-utterance-profiler/)
- [ダイアログモデルでAMAZON.YesIntentとAMAZON.NoIntentが利用可能になりました](https://dev.classmethod.jp/cloud/alexa-dialog-model-yes-no-intent/)

# 今回作成するAlexaスキル
- スキル名：ホテルの予約

※ダイアログモデルとAlexa-hostedを用いたAlexaスキルの完成をめざします！

```text:トークシナリオ
ユーザー：「アレクサ、ホテルの予約を開いて」
アレクサ：「ホテルの予約スキルへようこそ。どちらの市区町村のホテルをお探しでしょうか？」
ユーザー：「渋谷」
アレクサ：「何月何日のご予約でしょうか？」
ユーザー：「1月1日」
アレクサ：「何泊されますか？」
ユーザー：「1泊」
アレクサ：「お部屋にはシングル、ダブル、ツイン、スイートがございます。ご希望のルームサイズを教えて下さい。」
ユーザー：「ダブル」
アレクサ：「2020-01-01 渋谷 ダブル 1 泊のご宿泊でよろしいですか？」
ユーザー：「はい」
アレクサ：「ご予約をお受けいたしました。ご利用ありがとうございます。」

```
---

# スキルの作成
1. [Amazon Developer Portal](https://developer.amazon.com/ja)にログインする。
2. 「スキルの作成」ボタンをクリックする。

![no1-1.png](https://qiita-image-store.s3.amazonaws.com/0/240522/b99f4ca5-4bba-96fd-ca1f-e27211aea0fd.png)

---

# 新しいスキルを作成
1. スキル名に「ホテルの予約」と入力する。
2. 「カスタム」スキルを選択する。
3. 「Alexaがホスト」を、選択する。
4. 「スキルを作成」ボタンをクリックする。

![no2-2.png](https://qiita-image-store.s3.amazonaws.com/0/240522/d2d505e6-afb9-b28a-ee95-f5d911d5cc44.png)

---

# ハローワールドインテントの削除
1. 「HelloWorldIntent」の「削除アイコン」をクリックし、ハローワールドインテントを削除する。

※ハローワールドインテントは利用しないため削除します。

![no3-2.png](https://qiita-image-store.s3.amazonaws.com/0/240522/ab6678e2-0c2a-adaa-2b74-e53aec1b4e8b.png)

---

# JSONエディターの編集＆保存＆ビルド
1. 右メニューから「JSONエディター」をクリックする。
2. エディターの編集領域に、「ja-JP.json」ファイルの内容をコピー＆ペーストする。
3. 「モデルを保存」ボタンをクリックする。
4. 「モデルをビルド」ボタンをクリックする。

※「モデルをビルド」には1～2分程度時間がかかります。

![no4-2.png](https://qiita-image-store.s3.amazonaws.com/0/240522/575b70b4-939f-c04c-b980-bc882b145c3d.png)

---

# ja-JP.json

```json:ja-JP.json
{
    "interactionModel": {
        "languageModel": {
            "invocationName": "ホテルの予約",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "BookingIntent",
                    "slots": [
                        {
                            "name": "Location",
                            "type": "AMAZON.City",
                            "samples": [
                                "{Location}",
                                "{Location} で",
                                "{Location} です"
                            ]
                        },
                        {
                            "name": "CheckInDate",
                            "type": "AMAZON.DATE",
                            "samples": [
                                "{CheckInDate}",
                                "{CheckInDate} で"
                            ]
                        },
                        {
                            "name": "Nights",
                            "type": "AMAZON.NUMBER",
                            "samples": [
                                "{Nights}",
                                "{Nights} で",
                                "{Nights} 泊"
                            ]
                        },
                        {
                            "name": "RoomType",
                            "type": "RoomTypeSlots",
                            "samples": [
                                "{RoomType}",
                                "{RoomType} がいいです"
                            ]
                        }
                    ],
                    "samples": [
                        "{Location}",
                        "{Location} です",
                        "{Location} で {Nights} 泊",
                        "{CheckInDate}",
                        "{CheckInDate} で",
                        "{Nights} ",
                        "{Nights} 泊",
                        "{Nights} 泊でお願いします",
                        "{RoomType}",
                        "{RoomType} がいいです",
                        "予約したい",
                        "ホテルを探して"
                    ]
                }
            ],
            "types": [
                {
                    "name": "RoomTypeSlots",
                    "values": [
                        {
                            "id": "4",
                            "name": {
                                "value": "スイート"
                            }
                        },
                        {
                            "id": "3",
                            "name": {
                                "value": "ツイン"
                            }
                        },
                        {
                            "id": "2",
                            "name": {
                                "value": "ダブル"
                            }
                        },
                        {
                            "id": "1",
                            "name": {
                                "value": "シングル"
                            }
                        }
                    ]
                }
            ]
        },
        "dialog": {
            "intents": [
                {
                    "name": "BookingIntent",
                    "delegationStrategy": "ALWAYS",
                    "confirmationRequired": true,
                    "prompts": {
                        "confirmation": "Confirm.Intent.806228297399"
                    },
                    "slots": [
                        {
                            "name": "Location",
                            "type": "AMAZON.City",
                            "confirmationRequired": false,
                            "elicitationRequired": true,
                            "prompts": {
                                "elicitation": "Elicit.Slot.806228297399.72450908030"
                            }
                        },
                        {
                            "name": "CheckInDate",
                            "type": "AMAZON.DATE",
                            "confirmationRequired": false,
                            "elicitationRequired": true,
                            "prompts": {
                                "elicitation": "Elicit.Slot.806228297399.701866593031"
                            }
                        },
                        {
                            "name": "Nights",
                            "type": "AMAZON.NUMBER",
                            "confirmationRequired": false,
                            "elicitationRequired": true,
                            "prompts": {
                                "elicitation": "Elicit.Slot.806228297399.1070627167385"
                            }
                        },
                        {
                            "name": "RoomType",
                            "type": "RoomTypeSlots",
                            "confirmationRequired": false,
                            "elicitationRequired": true,
                            "prompts": {
                                "elicitation": "Elicit.Slot.806228297399.1442535180474"
                            }
                        }
                    ]
                }
            ],
            "delegationStrategy": "ALWAYS"
        },
        "prompts": [
            {
                "id": "Elicit.Slot.806228297399.72450908030",
                "variations": [
                    {
                        "type": "PlainText",
                        "value": "どちらの市区町村のホテルをお探しでしょうか"
                    }
                ]
            },
            {
                "id": "Elicit.Slot.806228297399.701866593031",
                "variations": [
                    {
                        "type": "PlainText",
                        "value": "何月何日のご予約でしょうか？"
                    }
                ]
            },
            {
                "id": "Elicit.Slot.806228297399.1070627167385",
                "variations": [
                    {
                        "type": "PlainText",
                        "value": "何泊されますか？"
                    }
                ]
            },
            {
                "id": "Elicit.Slot.806228297399.1442535180474",
                "variations": [
                    {
                        "type": "PlainText",
                        "value": "お部屋にはシングル、ダブル、ツイン、スイートがございます。ご希望のルームサイズを教えて下さい。"
                    }
                ]
            },
            {
                "id": "Confirm.Intent.806228297399",
                "variations": [
                    {
                        "type": "PlainText",
                        "value": "{CheckInDate} {Location} {RoomType} {Nights} 泊のご宿泊でよろしいですか？"
                    }
                ]
            },
            {
                "id": "Confirm.Slot.997859247350.524480625934",
                "variations": [
                    {
                        "type": "PlainText",
                        "value": "{Location} でよろしいですか？"
                    }
                ]
            }
        ]
    }
}

```

---

# コードエディターの編集＆Save＆Deploy
1. 上メニューから「コードエディター」をクリックする。
2. 右メニューから「index.js」ファイルをクリックする。
2. エディターの編集領域に、「index.js」ファイルの内容をコピー＆ペーストする。
3. 「Save」ボタンをクリックする。
4. 「Deploy」ボタンをクリックする。

※「Deploy」には2～3分程度時間がかかります。

![no5-2.png](https://qiita-image-store.s3.amazonaws.com/0/240522/a4e18aa4-415f-01aa-a987-e72a0b94cca6.png)

---

```javascript:index.js
/* eslint-disable  func-names */
/* eslint-disable  no-console */

// 参考サイト：https://dev.classmethod.jp/voice-assistant/amazon-alexa/testing-your-dialog-with-utterance-profiler-in-jp/

/**
 * モジュール参照
 */
const Alexa = require('ask-sdk-core');


/**
 * LaunchRequestHandler
 * ユーザ発話：「＜スキル名＞を開いて」
 */
const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'LaunchRequest';
    },
    handle(handlerInput) {
        const speechText = 'ホテルの予約スキルへようこそ。どちらの市区町村のホテルをお探しでしょうか？';
        return handlerInput.responseBuilder
            .speak(speechText)
            .reprompt(speechText)
            .getResponse();
    }
};


/**
 * OrderIntentHandlerHandler
 * ユーザ発話：「＜渋谷＞／予約したい／ホテルを探して」
 */
const OrderIntentHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest' &&
             handlerInput.requestEnvelope.request.intent.name === 'BookingIntent';
    },
    handle(handlerInput) {
        const dialogState = handlerInput.requestEnvelope.request.dialogState;
        const confirmationStatus = handlerInput.requestEnvelope.request.intent.confirmationStatus;

        // ダイアログモデルのスロット質問中の場合
        if (dialogState !== 'COMPLETED') {
            return handlerInput.responseBuilder
                .addDelegateDirective()
                .getResponse();
            
        // ダイアログモデルのスロットが全て埋まった場合
        } else {
            // 予約確認Alexa応答に対して「いいえ」発話時
            if (confirmationStatus !== 'CONFIRMED') {
                // 予約内容変更受付をしたい場合は、こちらのロジックのコメントを外して利用してください
                // let updatedIntent = handlerInput.requestEnvelope.request.intent;
                // updatedIntent.confirmationStatus = "IN_PROGRESS";
                // return handlerInput.responseBuilder
                //     .speak('どちらの市区町村のホテルをお探しでしょうか？')
                //     .addElicitSlotDirective('Location', updatedIntent)
                //     .getResponse();

                // 予約中断受付
                return handlerInput.responseBuilder
                    .speak('ご予約の中断をお受けいたしました。またのご利用をお待ちしてます。')
                    .withShouldEndSession(true)
                    .getResponse();

            }
            
            // 予約確認Alexa応答に対して「はい」発話時
            return handlerInput.responseBuilder
                .speak('ご予約をお受けいたしました。ご利用ありがとうございます。')
                .withShouldEndSession(true)
                .getResponse();
        }
 
    }
};


/**
 * HelpIntentHandler
 * ユーザ発話：「ヘルプ」
 */
const HelpIntentHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest'
            && handlerInput.requestEnvelope.request.intent.name === 'AMAZON.HelpIntent';
    },
    handle(handlerInput) {
        const speechText = 'ホテルの予約スキルです。ご希望のご宿泊内容をおっしゃってください。';

        return handlerInput.responseBuilder
            .speak(speechText)
            .reprompt(speechText)
            .getResponse();
    }
};


/**
 * CancelAndStopIntentHandler
 * ユーザ発話：「キャンセル」、「止めて」
 */
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 = 'ご利用の中断をお受けいたしました。またのご利用をおまちしてます。';
        return handlerInput.responseBuilder
            .speak(speechText)
            .withShouldEndSession(true)
            .getResponse();
    }
};


/**
 * CancelAndStopIntentHandler
 * ユーザ発話：「終了」
 */
const SessionEndedRequestHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'SessionEndedRequest';
    },
    handle(handlerInput) {
        // Any cleanup logic goes here.
        return handlerInput.responseBuilder.getResponse();
    }
};


/**
 * IntentReflectorHandler
 * ユーザ発話：「＜カスタムインテントのサンプル発話（デバッグ用）＞」
 */
const IntentReflectorHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest';
    },
    handle(handlerInput) {
        const intentName = handlerInput.requestEnvelope.request.intent.name;
        const speechText = `デバッグモードです。${intentName}が呼び出されました。`;

        return handlerInput.responseBuilder
            .speak(speechText)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};


/**
 * ErrorHandler
 * エラーハンドラ
 */
const ErrorHandler = {
    canHandle() {
        return true;
    },
    handle(handlerInput, error) {
        console.log(`~~~~ Error handled: ${error.message}`);
        const speechText = `もう一度、別の言い方でおっしゃっていただけますか？`;

        return handlerInput.responseBuilder
            .speak(speechText)
            .reprompt(speechText)
            .getResponse();
    }
};


/**
 * exports.handler
 * メイン処理
 */
exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        OrderIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler
    )
    .addErrorHandlers(
        ErrorHandler)
    .lambda();

```

---

# テストの起動
1. 上メニューから「テスト」をクリックする。
2. テストのステージから「開発中」を選択する。

![no6-2.png](https://qiita-image-store.s3.amazonaws.com/0/240522/943ab27c-4f7e-ad1f-fe43-1ff15fd96e5c.png)

---

# テストの実施
1. 「ホテルの予約をひらいて」と入力してEnterキーを押下する。

※パソコンおよびブラウザのマイクが有効の場合は、「マイクアイコン」をクリックし続けることで音声発話でのテスト実施も可能です。

![no7-2.png](https://qiita-image-store.s3.amazonaws.com/0/240522/9f3f201f-dd13-3107-c103-407bfaff97a4.png)

---

# テスト結果の確認
- ブラウザを利用したテストシミュレータにてAlexaスキルの動作確認がおこなえます。

![no8-2.png](https://qiita-image-store.s3.amazonaws.com/0/240522/29c8c92d-1080-db13-397d-6ab13f227acb.png)

---

# おわりに
みなさん、いかがでしたでしょうか？Alexaスキル「ホテルの予約」は完成しましたでしょうか。
「ja-JP.json」ファイルを保存することで「BookingIntent」が作成されています。「BookingIntent」の設定内容などを見ていただけると、ダイアログモデルでのインテント設定やスロット設定方法が理解できると思います。
「index.js」ファイルについては、「OrderIntentHandler」処理内容を見ていただけると、ダイアログモデルでのプログラミング方法が理解できると思います。
記事に誤り、不備等がございましたら、コメントにてご指摘いただけると幸いです。

2019/02/10 TAKAHIRO NISHIZONO

---

# 追記：2019/02/10
- ダイアログモデルおよびAlexa-hostedを用いたAlexaスキル開発をおこなった所感として、簡単にAlexaスキルが開発できる反面、細かいトークフローの制御や不具合発生時のデバッグ調査が困難であり、ビジネスで開発する音声スキル開発には向いていないのかなと感じました。ダイアログモデルおよびAlexa-hostedの利用は、音声モックアップの作成や、ビジネス打ち合わせの際の即興プロトタイプ作成、小規模なプライベート開発までにとどめ、大規模なビジネス開発はダイアログモデルを用いず、AWS Lambdaの利用を前提とした方が好ましいかもしれません。（あくまで個人の感想です。）

---

# 補足資料
- インテントの確認設定
![インテントの確認.PNG](https://qiita-image-store.s3.amazonaws.com/0/240522/a2c37c68-d1da-5b92-cd75-d6c419c9adff.png)

- スロット値取得の必須化設定
![スロットの確認.PNG](https://qiita-image-store.s3.amazonaws.com/0/240522/3ff53f2d-ecb8-8642-2d5c-71777b0bb056.png)

- スロットの確認設定
![スロットの個別確認.PNG](https://qiita-image-store.s3.amazonaws.com/0/240522/406ab561-c3b6-3b94-519d-8160202786f5.png)

---