はじめてのAlexa

## はじめに
Alexaスキルを試行錯誤で作成したので、調べたことなどを[クイズサンプル](https://github.com/alexa/skill-sample-nodejs-trivia)を解説する形でまとめておきます。

##Alexaのイメージ
まずはAlexaって何よ？ってところですが、簡単な流れとしてはAmazonEcho(EchoDotsやEchoShowなど)に語りかけると、Alexaスキルが起動し、
何らかの応答をするという流れになっています。 
裏側の処理はlambdaとしていますが、別のサーバでももちろん可能です。(Amazonとしてはlambdaの利用を推奨しているようです）

![フロー](https://s3-ap-northeast-1.amazonaws.com/qiita-img-store/AlexaFlow.png)  


## Trivia Skill

では本題のクイズサンプルを見ていきましょう。

### 環境の作成
まぁ、英語でビルド手順が書いてあるので、迷わないと思いますが手順としては以下になります。詳しい手順は省略します。  
1. [Amazon開発者コンソール](https://developer.amazon.com/edw/home.html)からスキルを作成する
2. [ここ](https://github.com/alexa/skill-sample-nodejs-trivia)から`git clone`して`npm install --alexa-sdk`してzipに固める
3. AWS上でAlexaスキルをトリガーとしたlambda関数を作る(リージョンはバージニア北部）
4. 2.で作成したzipを3.で作成した関数へアップロード
5. 1.で作成したAlexaスキルのConfigurationから3.で作成した関数を繋げる
6. AlexaスキルのInteractionModelを設定して終了

### ユーザの発声を理解する
Alexaがユーザの言葉を理解するには適切なInteraction Modelの設計が必要になります。
Triviaスキルでは以下の設定をしています。  

#### インテント
ユーザが何をしたいのかという_意思_をintentとして登録します。  

```IntentSchema.json
{
  "intents": [
    {
      "intent": "AnswerIntent",
      "slots": [
        {
          "name": "Answer",
          "type": "AMAZON.NUMBER"
        }
      ]
    },
    {
      "intent": "DontKnowIntent"
    },
    {
      "intent": "AMAZON.StartOverIntent"
    },
    {
      "intent": "AMAZON.RepeatIntent"
    },
    {
      "intent": "AMAZON.HelpIntent"
    },
    {
      "intent": "AMAZON.YesIntent"
    },
    {
      "intent": "AMAZON.NoIntent"
    },
    {
      "intent": "AMAZON.StopIntent"
    },
    {
      "intent": "AMAZON.CancelIntent"
    }
  ]
}
```

intent内で`AMAZON.`が付いているintentはビルトインのもので、既にAMAZONによって[定義](https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/built-in-intent-ref/built-in-intent-library)されています。

#### アテランス
ユーザが何を言っているのかという内容をutteranceとして設定しています。utteranceで設定したユーザ言わんとしている内容をintentへつなげています。  

```SampleUtterances_en_US.txt
AnswerIntent the answer is {Answer}
AnswerIntent my answer is {Answer}
AnswerIntent is it {Answer}
AnswerIntent {Answer} is my answer
AnswerIntent {Answer}

AMAZON.StartOverIntent start game
AMAZON.StartOverIntent new game
AMAZON.StartOverIntent start
AMAZON.StartOverIntent start new game

DontKnowIntent i don't know
DontKnowIntent don't know
DontKnowIntent skip
DontKnowIntent i don't know that
DontKnowIntent who knows
DontKnowIntent i don't know this question
DontKnowIntent i don't know that one
DontKnowIntent dunno
```

上記のutteranceではユーザが「i don't know」と言えばDontKnowIntentとして解釈され、lambdaへはDontKnowIntentが来たよと通知されます。

### lambdaコード
では、lambdaへintentが渡ってきた場合の処理についてですが、このサンプルでは、Alexaの会話状態をステートとして管理しており、そのステート内でintentをどう処理するのかを記述しています。  

```(一部)index.js
var newSessionHandlers = {
    "LaunchRequest": function () {
        this.handler.state = GAME_STATES.START;
        this.emitWithState("StartGame", true);
    },
    "AMAZON.StartOverIntent": function() {
        this.handler.state = GAME_STATES.START;
        this.emitWithState("StartGame", true);
    },
    "AMAZON.HelpIntent": function() {
        this.handler.state = GAME_STATES.HELP;
        this.emitWithState("helpTheUser", true);
    },
    "Unhandled": function () {
        var speechOutput = this.t("START_UNHANDLED");
        this.emit(":ask", speechOutput, speechOutput);
    }
};
```
上記はステートがない、つまり初期状態のintentの振り分けを定義しています。
LaunchRequestはAlexaがInvocationName(起動ワード)で呼ばれたときに呼ばれるintentになります。
Trivia SkillでInvocation Nameを"trivia master"としていた場合、「Alexa, trivia master」と呼びかけるとLaunchRequestが呼ばれ、ステートがSTARTに設定され、`this.emitWithState("StartGame", true);`により、STARTステート内の処理が実行されます。

```(一部)index.js
var startStateHandlers = Alexa.CreateStateHandler(GAME_STATES.START, {
    "StartGame": function (newGame) {
        var speechOutput = newGame ? this.t("NEW_GAME_MESSAGE", this.t("GAME_NAME")) + this.t("WELCOME_MESSAGE", GAME_LENGTH.toString()) : "";
        // Select GAME_LENGTH questions for the game
        var translatedQuestions = this.t("QUESTIONS");
        var gameQuestions = populateGameQuestions(translatedQuestions);
        // Generate a random index for the correct answer, from 0 to 3
        var correctAnswerIndex = Math.floor(Math.random() * (ANSWER_COUNT));
        // Select and shuffle the answers for each question
        var roundAnswers = populateRoundAnswers(gameQuestions, 0, correctAnswerIndex, translatedQuestions);
        var currentQuestionIndex = 0;
        var spokenQuestion = Object.keys(translatedQuestions[gameQuestions[currentQuestionIndex]])[0];
        var repromptText = this.t("TELL_QUESTION_MESSAGE", "1", spokenQuestion);

        for (var i = 0; i < ANSWER_COUNT; i++) {
            repromptText += (i+1).toString() + ". " + roundAnswers[i] + ". ";
        }

        speechOutput += repromptText;

        Object.assign(this.attributes, {
            "speechOutput": repromptText,
            "repromptText": repromptText,
            "currentQuestionIndex": currentQuestionIndex,
            "correctAnswerIndex": correctAnswerIndex + 1,
            "questions": gameQuestions,
            "score": 0,
            "correctAnswerText": translatedQuestions[gameQuestions[currentQuestionIndex]][Object.keys(translatedQuestions[gameQuestions[currentQuestionIndex]])[0]][0]
        });

        // Set the current state to trivia mode. The skill will now use handlers defined in triviaStateHandlers
        this.handler.state = GAME_STATES.TRIVIA;
        this.emit(":askWithCard", speechOutput, repromptText, this.t("GAME_NAME"), repromptText);
    }
});
```

STARTステート内では音声出力用の文字列`speechOutput`などの出力内容の作成とセッション情報の保存を行っています。
alexa-sdkでは`this.attributes`のプロパティに情報を保管しています。
`this.attributes`に保管した情報はAlexaがセッションを保持している、つまり、会話中(スキルが動作している最中)はセッション情報として、リクエストに保管した情報を投げてくれます。
ステート以外に保持したい情報は`this.attributes`に持たせることでセッション管理ができます。

### セッション保持のタイミング
では、どんなタイミングでセッションが保持されているのでしょうか。
`:saveState`と`:saveStateError`以外で`emit`するタイミングで保持されます。まあ想像通りですね。

### DynamoDBへの保管
セッション情報はalexaスキルが終了した時点で破棄されます。
次にスキルを呼び出したときに前回の情報を保持しておきたい場合はDynamoDBにセッション情報を保管しておくことができます。

```(一部)index.js
exports.handler = function(event, context, callback) {
    var alexa = Alexa.handler(event, context);
    // この１行を追加するだけでemitするたびにDynamoDBに
    // ステートを含めたセッション情報が保管されます
    alexa.dynamoDBTableName = 'セッション保管用のテーブル名';
    
    alexa.appId = APP_ID;
    // To enable string internationalization (i18n) features, set a resources object.
    alexa.resources = languageString;
    alexa.registerHandlers(newSessionHandlers, startStateHandlers, triviaStateHandlers, helpStateHandlers);
    alexa.execute();
};
```

DynamoDBにセッションを保管する際にはlambda関数にDynamoDBへのアクセス権限を付与しておく必要があります。

## 参考文献

+ https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs
+ http://dev.classmethod.jp/cloud/aws/amazon-alexa-basic-knowledge/
+ http://qiita.com/ToshiakiEnami/items/170f1890fb0064151ab8
+ http://qiita.com/ysak/items/de3e10a00cce19c48076
+ https://www.youtube.com/watch?v=S7HU_TO2dlE&t=1022s