Amazon Echo で暗算問題を出してもらうスキルをつくった

# はじめに
ulgeek アドベントカレンダーの 13日目です。

Amazon Echo Dot を今月はじめに購入できたので、Alexaスキルを作成しました。
ちなみに2017年12月12日時点で、Amazon Echo（Dot,Plus含む）は招待された人のみ購入可能です。
私の場合、2017年11月13日に招待リクエストを出し、約3週間後の2017年12月1日
に「Echo Dotの招待者に選ばれました」メールを受信し、購入に至ってます。

そんなAmazon Echoを使い、自身はじめての作成物として挑んだAlexaスキルは、
「Alexaが足し算の問題（例：87 足す 99 は？）を出題し、ユーザの回答に対して、Alexaが結果を返す」
というスキルです。
記録も兼ねて作成の手順も一通り記載していますので、ご覧ください！

# 前提条件
- awsアカウントが作成済み
- Amazon開発者ポータルアカウントが作成済み
- 動作環境
    - Lambda関数ランタイム：Python3.6
    - Alexa対応デバイス:Amazon Echo Dot

### その他
あらかじめAmazon Echo,Amazon Alexaの概要を知っておいたほうがいいです。以下のページがとても分かり易く参考になります。
        - [Amazon Alexaで音声サービスを開発しよう | Alexa Skills Kit | アレクサ](https://developer.amazon.com/ja/alexa-skills-kit)
        - [【Alexa初心者向け】Alexa Skill Kitを噛み砕いて解説してみる](https://dev.classmethod.jp/cloud/introduction-of-alexa-skill-kit/)

# 作成したAlexaスキル概要
Alexaが2項式の足し算問題を出題してくれます。
問題に回答すると、Alexaが答え合わせをしてくれます。

- 会話の例－その1（ユーザの回答が正解）
    - ユーザ「アレクサ、暗算ゲームはじめて。」
    - アレクサ「それでは問題です。77 足す 44 は？」
    - ユーザ「121」
    - アレクサ「スゴい！正解です！77 足す 44 の答えは 121 です。よくわかりましたね！」
- 会話の例－その2（ユーザの回答が不正解）
    - ユーザ「アレクサ、暗算ゲームはじめて。」
    - アレクサ「それでは問題です。77 足す 44 は？」
    - ユーザ「答えは131」
    - アレクサ「残念！77 足す 44 の答えは 121 です。」

# Alexaスキル作成～動作させるまでの流れ
1. Lambda関数を作成する
2. Alexaスキルを作成する
3. 作成したAlexaスキルを自分のAmazon Echoで動かす

# 1.Lambda関数を作成する
Alexaスキルから呼ばれるLambda関数を作成します。

1. AWSマネジメントコンソールにログイン後、Lambdaサービスの画面を開き、「関数の作成」をクリックする
2. 「関数の作成」画面にて以下のように入力する
![Lambda Management Console - Google Chrome 2017-12-11 12.56.50.png](https://qiita-image-store.s3.amazonaws.com/0/220814/0f02583f-6b97-26f9-89e4-795b9bf538a4.png)
    - **名前：** `myFunction`
    - **ランタイム：** `Python 3.6`
    - **ロール：** `既存のロール`
    - **既存のロール：** `lambda_basic_execution` ※「カスタムロール」を選択後、別画面で表示する画面内右下の許可をクリックする

3. 「関数の作成」画面にてトリガーに「Alexa Skill Kit」を選択し、画面右下の「追加」をクリック
![Lambda Management Console - Google Chrome 2017-12-11 13.18.27_1.png](https://qiita-image-store.s3.amazonaws.com/0/220814/9bf78733-d8bb-4357-5af7-c881f2457247.png)

4. Lambda関数のコードに、下に提示する **lambda_function.py** の内容を入力し、画面右上の「保存」をクリックする
![Lambda Management Console - Google Chrome 2017-12-11 13.23.23.png](https://qiita-image-store.s3.amazonaws.com/0/220814/740803b0-9b6b-a980-bebc-138be8add167.png)

```python3:lambda_function.py
import random
import logging

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

def lambda_handler(event, context):
    logging.debug(event)
    if event['request']['type'] == "LaunchRequest":
        return onLaunchRequest(event)
    elif event['request']['type'] == "IntentRequest":
        return onIntentRequest(event)
    else:
        # SessionEndedRequestのとき
        # コードによるセッションの終了以外の理由で、現在のスキルセッションが終了した場合
        return onSessionEndedRequest(event)

def onLaunchRequest(event):
    x = random.randint(1,100)
    y = random.randint(1,100)
    logging.debug('------- x='+ str(x) + ', y=' + str(y) + '----------')
    response = {
        'version': '1.0',
        'sessionAttributes': {'x': str(x), 'y': str(y)},
        'response': {
            'outputSpeech': {
                'type': 'PlainText',
                'text': '暗算ゲームを始めます。それでは問題です。' + str(x) + '足す' + str(y) + 'は？'
            },
            'reprompt': {
                'outputSpeech': {
                    'type': 'PlainText',
                    'text': '問題です。' + str(x) + '足す' + str(y) + 'は？'
                }
            },
            'shouldEndSession': False
        }
    }
    return response

def onIntentRequest(event):
    intent = event['request']['intent']
    if intent['name'] == "AnswerOnlyIntent" or intent['name'] == "AnswerIntent":
        intent = event['request']['intent']
        userAnswerValue = intent['slots']['Answer']['value']
        x = event['session']['attributes']['x']
        y = event['session']['attributes']['y']
        answer = int(x) + int(y)
        logging.debug('###### Alexaの問題 ' + x + ' 足す ' + y + 'は？(答え'+ str(answer) + ') ######')
        logging.debug('###### ユーザの答え ' + str(userAnswerValue) + ' ######')
        if int(userAnswerValue) == answer:
            outputSpeech = 'すごい！正解です！' + x + ' 足す ' + y + 'は ' + str(userAnswerValue) + 'です。よくわかりましたね！'
            response = buildResponse(outputSpeech, True)
            logging.debug(response)
            return response
        else:
            outputSpeech = '残念！' + x + ' 足す ' + y + 'は ' + str(userAnswerValue) + 'ではありません。正解は' + str(answer) + 'でした。'
            response = buildResponse(outputSpeech, True)
            logging.debug(response)
            return response
    else:
        # 想定外のIntentが送信された場合
        response = buildResponse('ごめんなさい。想定外の問題が発生しました。', True)
        return response

def onSessionEndedRequest(event):
    return  buildResponse('ごめんなさい。想定外の問題が発生しました。', True)

def buildResponse(outputSpeech, shouldEndSession, attributes={}):
    response = {
        'version': '1.0',
        'sessionAttributes': attributes,
        'response': {
            'outputSpeech': {
                'type': 'PlainText',
                'text': outputSpeech
            },
            'shouldEndSession': shouldEndSession
        }
    }
    return response
```

## セッションを引き継いで値の受け渡しする方法
**セッションを引き継いで値の受け渡しする方法の一つに、**
**「レスポンスの内の'sessionAttributes'属性」を使うことができます。**

今回のスキルでは、会話は以下のようにやりとりします。

```
【開始】ユーザのスキル開始要求：「アレクサ、暗算ゲームはじめて」
　　↓
　（Lambda関数処理1）：【発話1】のレスポンスを生成
　　↓
【発話1】Alexaからの問題出題：「暗算ゲームをはじめます。それでは問題です。20足す40は？」
　　↓
【返答】ユーザからの回答：「60」
　　↓
  （Lambda関数処理2）：【発話2】のレスポンスを生成
　　↓
【発話2】Alexaからの結果応答：「すごい！正解です。20足す40は60です。よくわかりましたね！」
```


ここで、【発話2】の内容は、【発話1】の内容と【返答】の内容を評価して決定します。
たとえば、【発話1】「20足す40は？」 に対して、【返答】「60」と回答されたとき、
【返答】「60」は答えとして正解か？判定するために（Lambda関数処理2）にて
【発話1】「20足す40は？」と言ったことを憶えておく必要があります。
さもないと、【返答】「60」が正しいか否かの判定ができなくなります。

今回はLambda関数からのレスポンスの要素'sessionAttributes' に値を格納して、
セッションを引き継いで値の受け渡しをおこなっています。
具体的には（Lambda関数処理1）で値の格納を行い、（Lambda関数処理2）で格納した値を取得しています。

**セッションに格納する**

```python
    # ↓↓↓↓ 一部抜粋 ↓↓↓↓
    response = {
        'version': '1.0',
        'sessionAttributes': {'x': str(x), 'y': str(y)}, # ★sessionAttributes 属性にdictionary形式で設定可能
        'response': {
            # 省略
            # ・
            # ・
            },
            'shouldEndSession': False # ★shouldEndSession 属性をFalseにすることで、ユーザのセッションが開いたまま維持する
        }
    }
　　return response
    # ↑↑↑↑ 一部抜粋 ↑↑↑↑
```

**セッションに格納された情報を取得**

```python
    # ↓↓↓↓ 一部抜粋 ↓↓↓↓
    intent = event['request']['intent']
    if intent['name'] == "AnswerOnlyIntent" or intent['name'] == "AnswerIntent":
        intent = event['request']['intent']
        userAnswerValue = intent['slots']['Answer']['value']
        # ★sessionAttributes 属性から値を取得するときは、属性名'attributes'で指定する
        x = event['session']['attributes']['x'] # ★sessionAttributes 属性に格納された値 x を取得 
        y = event['session']['attributes']['y'] # ★sessionAttributes 属性に格納された値 y を取得 
    # ↑↑↑↑ 一部抜粋 ↑↑↑↑
```

また、レスポンスの要素'response'属性に'shouldEndSession'という属性があります。
設定値によって、以下のように振る舞います。

 - True: ユーザーのセッションがクローズする
 - False: ユーザーのセッションがオープンのまま維持される（デフォルトFalse）

セッションを継続させ、値を引き継ぐためには、'response'属性内の'shouldEndSession'属性の値をFalseにする必要があります。

セッションを引き継いで値の受け渡しする方法について、
今回はレスポンスの要素'sessionAttributes' を使いましたが
ほかにもDynamoDB等にSessionIDと紐付かせて登録する等があるかと思います。
簡素な情報を受け渡しするには今回の方法で問題ないですが、
より複雑な情報だったり、セッションが終わった後も引き継いだ情報を記録しておいて後で参照したい等
ある場合は今回の方法は最善ではありません。受け渡しする値の性質、使われ方によって、
最善の方法を選択する必要があります。

# 2.Alexaスキルを作成する
Alexaスキルを作成します。スキルを作成する過程で、先に作成したLambda関数と紐付けたり、
以下のような対話に関する定義を設定します。
※対話に関する定義の参考ページ：[ユーザーによる発話](https://developer.amazon.com/ja/designing-for-voice/what-users-say/#開発前の設計)

- どんな言葉でAlexaスキルが起動するか？の定義
- 上記の言葉でAlexaスキルが起動したとき、どんなタイプのリクエストなのか？の定義

を設定します。

1. Amazon開発者ポータルサイト( https://developer.amazon.com/ja/ )にログインする
2. ダッシュボードを開き、Alexa Skill Kit の「始める」をクリック
![アマゾン アプリ 開発者ポータル - Google Chrome 2017-12-11 10.48.08.png](https://qiita-image-store.s3.amazonaws.com/0/220814/9eb6865c-74c8-30ba-7363-02ce44870607.png)
3. 「Alexa Skill Kit を使用して Alexa スキルを開発する」画面で「新しいスキルを追加する」をクリック
![アマゾン アプリ 開発者ポータル - Google Chrome 2017-12-11 10.50.52.png](https://qiita-image-store.s3.amazonaws.com/0/220814/0df429f7-712e-a1db-acd1-caf95bf4c45e.png)
4. 「スキル情報」設定画面にて以下を入力し、「次へ」をクリック
    - **スキル名：** `暗算ゲーム`
    - **呼び出し名：** `暗算ゲーム`
5. 「対話モデル」設定画面にて以下を入力
    - **インテントスキーマ**
    
    ```json:インテントスキーマ
    {
      "intents": [
        {
          "slots": [
            {
              "name": "Answer",
              "type": "AMAZON.NUMBER"
            }
          ],
          "intent": "AnswerIntent"
        },
        {
          "slots": [
            {
              "name": "Answer",
              "type": "AMAZON.NUMBER"
            }
          ],
          "intent": "AnswerOnlyIntent"
        }
      ]
    }
    ```
    - **サンプル発話**

    ```:サンプル発話
    AnswerIntent 答えは {Answer}
    AnswerOnlyIntent {Answer}
    ``` 
6. 「設定」画面にて、以下のように指定して「次へ」をクリック
    - **サービスエンドポイントのタイプ：** `AWS Lambda の ARN (Amazonリソースネーム）`
    - **デフォルト：** *作成したLambdaのANR(AWSマネジメントコンソールのLambda関数画面に表示)*  ※下記の図を参照
    - **その他の項目** デフォルト値のまま
![Lambda Management Console - Google Chrome 2017-12-11 16.17.45.png](https://qiita-image-store.s3.amazonaws.com/0/220814/562242a2-8479-18b7-f3e2-bc4009b17115.png)
7.  「テスト」画面にて なにも入力しないで「次へ」をクリック
8. 「公開情報」設定画面にて以下のようにして「次へ」をクリック
    - **カテゴリー：** `任意（例：Lifestyle）`
    - **サブカテゴリー：** `任意（例：Home Services）`
    - **テストの手順：** `任意の文字列（例：暗算ゲーム）`
    - **国と地域：** `「国と地域を選択する:」を選択し、「日本」をチェック`
    - **スキルの簡単な説明：** `任意の文字列（例：暗算ゲームをします）`
    - **スキルの詳細な説明：** `任意の文字列（例：アレクサが足し算の問題を出します。回答すると正解かどうかを答えてくれます。）`
    - **サンプルフレーズ1：** `暗算ゲームはじめて`
    - **サンプルフレーズ2：** `暗算ゲームを開いて`
    - **サンプルフレーズ3：** `（空欄）`
    - **キーワード：** `（空欄）`
    - **小アイコン：** `任意の108x108 ピクセルの PNG (透明可)または JPG の画像ファイル`
    - **大アイコン：** `任意の512x512 ピクセルの PNG (透明可)または JPG の画像ファイル`
9. 「プライバシーとコンプライアンス」設定画面にて、項目にすべて「いいえ」を選択し、「輸出コンプライアンス」の内容を確認の上、チェックをし、「保存」をクリック

# 3.作成したAlexaスキルを自分のAmazon Echoで動かす
1. Amazon開発者ポータルサイトで、作成したAlexaスキルを表示した状態のとき、画面左下部の「Skills Beta Testing」をクリックして、「テスターの管理」画面を表示する
2. 「テスターの管理」画面で、「テスターを追加」をクリックして、表示されるダイアログ画面に自分のAmazonアカウントのメールアドレスを入力する
![Skills Beta Testing for Alexa - Google Chrome 2017-12-11 16.37.38_1.png](https://qiita-image-store.s3.amazonaws.com/0/220814/8b91e9a5-83bf-64bb-cf65-a2b5317b9046.png)
3. 件名「You're invited to beta test a new Alexa skill」メールが送られてきたら、リンク「 <font color="Blue">Enable Alexa skill "暗算ゲーム"</font>」をクリックする。注）「JP customers: To get started, follow this link:」とあるほう
4. 「Alexa管理画面」にて、作成したスキルが表示されているはずなので、「スキルを有効にする」をクリックする
![Amazon Alexa - Google Chrome 2017-12-11 16.47.22.png](https://qiita-image-store.s3.amazonaws.com/0/220814/ebdcdbe4-6be2-5357-8cd8-178abec44b36.png)
5. Amazon Echoに「アレクサ、暗算ゲームはじめて」と話しかける。

# まとめ
Alexaスキル作る前は「面倒な手順、手続きが必要なのかなぁ・・」と、
少し身構えてましが、作成するのに最低限ブラウザさえあればよく、
今回程度の内容であれば1時間もあれば作成できてしまうくらい
とても簡単にAlexaスキルを作成できます！

また、今回サクッとスキルを作ろうと、Echo Dot上でスキルを動作させながらデバッグしていたのですが、
暗算が難しくて、自分で正解を回答することができず、正解ケースのデバックに非常に苦労しました:sweat_smile:
そのため、もとは3桁同士計算問題だったのですが、2桁同士の計算問題にしています・・・

今しりとりをするAlexaスキルも作っているので機会があれば紹介したいと思っています！