Alexa for Businessのスキルを作成してみた

Alexa for Businessのスキルを作成してみました。
リージョンを指定するとそのリージョンで発生中のCloudwatchAlarmを答えるスキルです。

# 必要なもの
* AWSアカウント
* Amazon Developerアカウント
* Alexa Skills Kit Command Line Interface (ASK CLI)

# 作業概要
1. スキル用のLambdaの作成
2. スキルの作成とテスト
3. プライベートスキルの設定
4. 公開情報とプライバシーとコンプライアンスの設定
5. 申請とスキルの有効化

# 1. スキル用のLambdaの作成

以下AWSマネジメントコンソールにログインして作業します。


**1. Lambda用のIAMロールを作成する**

以下ポリシーを持ったIAMロールを作成します。
Lambda用のベースのポリシーにCloudwatchアラームへのアクセス許可を追加しています。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": "cloudwatch:DescribeAlarms",
            "Resource": "*"
        }
    ]
}
```
　
**2. blueprintsで[alexa-skills-kit-color-expert-python]を選択・編集しLambdaを作成する**

blueprintはpython2.7用ですが、python3.6用にしています。

```python
# -*- coding: utf-8 -*-
import boto3


# --------------- Helpers that build all of the responses ----------------------

def build_speechlet_response(title, output, reprompt_text, should_end_session):
    return {
        'outputSpeech': {
            'type': 'PlainText',
            'text': output
        },
        'card': {
            'type': 'Simple',
            'title': "SessionSpeechlet - " + title,
            'content': "SessionSpeechlet - " + output
        },
        'reprompt': {
            'outputSpeech': {
                'type': 'PlainText',
                'text': reprompt_text
            }
        },
        'shouldEndSession': should_end_session
    }


def build_response(session_attributes, speechlet_response):
    return {
        'version': '1.0',
        'sessionAttributes': session_attributes,
        'response': speechlet_response
    }


# --------------- Functions that control the skill's behavior ------------------

def get_welcome_response():
    session_attributes = {}
    card_title = "Welcome"
    speech_output = "こんにちは。AWSのアラート状況をお知らせするスキルです。 " \
                    "例えば次のように質問して下さい。 " \
                    "東京リージョンの状況は"
    reprompt_text = "例えば次のように質問して下さい。 " \
                    "東京リージョンの状況は"
    should_end_session = False
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, reprompt_text, should_end_session))


def handle_session_end_request():
    card_title = "Session Ended"
    speech_output = "Thank you for trying the Alexa Skills Kit sample. " \
                    "Have a nice day! "
    # Setting this to true ends the session and exits the skill.
    should_end_session = True
    return build_response({}, build_speechlet_response(
        card_title, speech_output, None, should_end_session))


def get_alarms(region):
    cloudwatch = boto3.client('cloudwatch', region_name=region)
    try:
        response = cloudwatch.describe_alarms(StateValue='ALARM')
        alarms = [alarm['AlarmName'] for alarm in response['MetricAlarms']]
        print(alarms)
        return alarms
    except Exception as e:
        print(e)
        raise e


def response_alarms(intent, session):
    card_title = intent['name']
    session_attributes = {}
    should_end_session = False
    region_dic = {
        '東京':'ap-northeast-1',
        'バージニア':'us-east-1',
        'シドニー':'ap-southeast-2'
    }

    if 'Region' in intent['slots'] \
    and intent['slots']['Region']['value'] in region_dic:
        region_jp = intent['slots']['Region']['value']
        region = region_dic[region_jp]
        alarms = get_alarms(region)
        if len(alarms) == 0:
            speech_output = "現在" + \
                            region_jp + \
                            "リージョンにアラートはありません。 "
        else:
            speech_output = "現在" + \
                            region_jp + \
                            "リージョンに" + \
                            str(len(alarms)) + \
                            "件アラートが発生しています。" + \
                            "アラート名は" + \
                            "と".join(alarms) + \
                            "です。"
        reprompt_text = None
        should_end_session = True
    else:
        speech_output = "リージョン名がわかりませんでした。" \
                        "もう一度お願いします。"
        reprompt_text = "リージョン名がわかりませんでした。 " \
                        "もう一度お願いします。"
        should_end_session = False
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, reprompt_text, should_end_session))


# --------------- Events ------------------

def on_session_started(session_started_request, session):
    print("on_session_started requestId=" + session_started_request['requestId']
          + ", sessionId=" + session['sessionId'])


def on_launch(launch_request, session):
    print("on_launch requestId=" + launch_request['requestId'] +
          ", sessionId=" + session['sessionId'])
    return get_welcome_response()


def on_intent(intent_request, session):
    print("on_intent requestId=" + intent_request['requestId'] +
          ", sessionId=" + session['sessionId'])

    intent = intent_request['intent']
    intent_name = intent_request['intent']['name']

    if intent_name == "MyAwsAlarmsIntent":
        return response_alarms(intent, session)
    elif intent_name == "AMAZON.HelpIntent":
        return get_welcome_response()
    elif intent_name == "AMAZON.CancelIntent" or intent_name == "AMAZON.StopIntent":
        return handle_session_end_request()
    else:
        raise ValueError("Invalid intent")


def on_session_ended(session_ended_request, session):
    print("on_session_ended requestId=" + session_ended_request['requestId'] +
          ", sessionId=" + session['sessionId'])



# --------------- Main handler ------------------

def lambda_handler(event, context):
    """ Route the incoming request based on type (LaunchRequest, IntentRequest,
    etc.) The JSON body of the request is provided in the event parameter.
    """
    print("event.session.application.applicationId=" +
          event['session']['application']['applicationId'])

    """
    Uncomment this if statement and populate with your skill's application ID to
    prevent someone else from configuring a skill that sends requests to this
    function.
    """
    # if (event['session']['application']['applicationId'] !=
    #         "amzn1.echo-sdk-ams.app.[unique-value-here]"):
    #     raise ValueError("Invalid Application ID")

    if event['session']['new']:
        on_session_started({'requestId': event['request']['requestId']},
                           event['session'])

    if event['request']['type'] == "LaunchRequest":
        return on_launch(event['request'], event['session'])
    elif event['request']['type'] == "IntentRequest":
        return on_intent(event['request'], event['session'])
    elif event['request']['type'] == "SessionEndedRequest":
        return on_session_ended(event['request'], event['session'])
```

# 2. スキルの作成とテスト

以下Amazon開発者コンソールにログインして作業します。

**1. スキル情報を設定する**

- スキルの種類：Custom
- 言語：Japanese
- スキル名：AWSアラート状況
- 呼び出し名：awsアラート状況

他はデフォルト
　
**2. 対話モデルを設定する**

- インテントスキーマ

```json
{
  "intents": [
    {
      "slots": [
        {
          "name": "Region",
          "type": "LIST_OF_REGIONS"
        }
      ],
      "intent": "MyAwsAlarmsIntent"
    },
    {
      "intent": "AMAZON.HelpIntent"
    },
    {
      "intent": "AMAZON.StopIntent"
    },
    {
      "intent": "AMAZON.CancelIntent"
    }
  ]
}
```

- カスタムスロットタイプ
    - タイプ：LIST_OF_REGIONS
    - 値：
        - 東京
        - バージニア
        - シドニー

- サンプル発話
    - MyAwsAlarmsIntent {Region} は
    - MyAwsAlarmsIntent {Region} の状況は
    - MyAwsAlarmsIntent {Region} のアラート状況は

　
**3. 「設定」を設定する**

- サービスエンドポイントのタイプ：AWS Lambda の ARN
    - 作成したLambdaのARNを指定する

他はデフォルト

　
**4. テストする**

4-1. あらかじめ、バージニアリージョンに3件Cloudwatchアラームが発生している状態にしておきます。

4-2. 発話を入力してください欄に「バージニアは」と入力してテストすると

4-3. サービスレスポンスに下記が返ってきます。

```json
{
  "version": "1.0",
  "response": {
    "outputSpeech": {
      "text": "現在バージニアリージョンに3件アラートが発生しています。アラート名はalarm_1とalarm_2とalarm_3です。",
      "type": "PlainText"
    },
    "card": {
      "content": "SessionSpeechlet - 現在バージニアリージョンに3件アラートが発生しています。アラート名はalarm_1とalarm_2とalarm_3です。",
      "title": "SessionSpeechlet - MyAwsAlarmsIntent"
    },
    "reprompt": {
      "outputSpeech": {
        "type": "PlainText"
      }
    },
    "speechletResponse": {
      "outputSpeech": {
        "text": "現在バージニアリージョンに3件アラートが発生しています。アラート名はalarm_1とalarm_2とalarm_3です。"
      },
      "card": {
        "content": "SessionSpeechlet - 現在バージニアリージョンに3件アラートが発生しています。アラート名はalarm_1とalarm_2とalarm_3です。",
        "title": "SessionSpeechlet - MyAwsAlarmsIntent"
      },
      "reprompt": {
        "outputSpeech": {}
      },
      "shouldEndSession": true
    }
  },
  "sessionAttributes": {}
}
```

# 3. プライベートスキルの設定

**1. alexa Skills Kit Command Line Interface (ASK CLI)をインストールする**

下記URLを参考にインストールします。
https://developer.amazon.com/docs/smapi/quick-start-alexa-skills-kit-command-line-interface.html
　
**2. プライベートスキルの設定をする**

下記URLを参考にプライベートスキルの設定をします。
http://docs.aws.amazon.com/a4b/latest/ag/private-skills.html

スキルのIDは、スキル情報に表示されます。

```bash
$ skill_id=<スキルのID>
$ ask api get-skill -s ${skill_id} > skill.json
$ vi skill.json
```

"distributionMode": "PRIVATE"を1行追加する

```json:skill.json
{
  "skillManifest": {
    "publishingInformation": {
      "locales": {
        "ja-JP": {
          "name": "AWSアラート状況"
        }
      },
      "isAvailableWorldwide": true,
      "distributionMode": "PRIVATE"
    },
    "apis": {
      "custom": {
        "endpoint": {
          "uri": "<LambdaのARN>"
        }
      }
    },
    "manifestVersion": "1.0"
  }
}
```

```bash
$ ask api update-skill -s ${skill_id} -f skill.json
```

# 4. 公開情報とプライバシーとコンプライアンスの設定

以下Amazon開発者コンソールで作業します。

ちょっと面倒ですが、小アイコン(108x108)と大アイコン(512x512)を用意してアップロードし
他記入できるところを全て記入します。

# 5. 申請とスキルの有効化

**1. 申請する**

```bash
$ ask api submit -s ${skill_id}
```
　
**2. ステータスがLiveになるまで、数時間待つ**
　
**3. Alexa for Businessオーガナイゼーションに配布する**

```bash
$ account_id=arn:aws:iam::<AWSアカウントID>:root
$ ask api add-private-distribution-account -s ${skill_id} --stage live --account-id ${account_id}
```
　
**4. スキルを有効にする**

AWSマネジメントコンソール：Alexa for Businessのprivate skillsの一覧にスキルが表示されます。

<img width="1403" alt="skill.png" src="https://qiita-image-store.s3.amazonaws.com/0/192584/b3a7681d-efe2-0cec-355e-d6a4f6740637.png">


[Review]をクリックして[Enable]を選択しスキルを有効にします。
　
**5. ここまで**
東京リージョンではまだ利用できないので、残念ながらここまでです。
2017/12/5追記 英語でよければ、利用できるそうです。


# 雑感
Alexa for Businessの何でもできちゃいそう感がすごい。
Re:Inventのキーノートを見たら「ちょっといい会議システム」みたいなものと勘違いしちゃいそうですが、もっと全然奥が深そう。
自分の中では2017年のRe:Inventの２番目の目玉です。
早く東京リージョンにこないかな。