30分くらいでAlexa Skills Kit SDK for Python (Beta) を使ったAlexa スキルを作る！

Python ユーザー待望の[Alexa Skills Kit SDK for Python(Beta)](https://github.com/alexa-labs/alexa-skills-kit-sdk-for-python) （以下、SDK）が先日リリースされましたので、これを使ってスキルを作ってみたいと思います。

今回は、Serverless Framework（以下、Serverless）と組み合わせてみたので、Alexa スキルの開発者登録や、AWS などの初期設定ができていれば、30分くらいで開発できると思います。

テーマはご当地B級グルメ検索スキルとし、ユーザーが都道府県名を言うと、そのご当地のB級グルメを説明してくれるスキルを開発します。

AWS CLI の設定は以下の記事などを参考にしてください。
* [最短でAWS CLIをインストールして設定する方法](https://tomokazu-kozuma.com/procedure-for-installing-aws-cli/)

それでは始めていきましょう！

# Alexa コンソールでスキル作成

## スキルを新規作成

まずは、[Alexa 開発者コンソール](https://developer.amazon.com/alexa/console/ask) にログインして新しいスキルを作成します。
コンソール トップページで「スキルの作成」ボタンをクリックして、「新しいスキルを作成」ページでスキル名を入力、デフォルトの言語は日本語に設定します。
![新しいスキル](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/alexa_skill_python_sdk/01_alexaconsole.png)

「スキルに追加するモデルを選択」では、デフォルトのカスタムを選択して「スキルを作成」ボタンを押します。

## スキルの呼び出し名の設定

左側のメニューから「呼び出し名」を選択し、スキルの呼び出し名を入力します。
”ビーキュウグルメ”としました。
呼び出し名にはアルファベットを入力できないので、カタカナにしておきます。
![スキルの呼び出し名](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/alexa_skill_python_sdk/02_slillname.png)

## カスタムインテントの作成

左側のメニュー「インテント」の右側にある「＋追加」ボタンを押してカスタムインテントを作ります。
インテント名を入力して「カスタムインテントを作成」ボタンを押します。
ここでは”BudgetFriendlyGourmetIntent”としました。
![新しいカスタムインテント](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/alexa_skill_python_sdk/03_newcustomintent.png)

## スロットタイプの作成

スキルで都道府県名を認識するため、都道府県名用のスロットタイプを作成します。
左側のメニュー「スロットタイプ」の右側にある「＋追加」ボタンを押します。
スロットタイプ名を入力して「カスタムスロットタイプを作成」ボタンを押します。
ここでは”PREFECTURE_LIST”としました。
![新しいスロットタイプ](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/alexa_skill_python_sdk/04_newslottype.png)

## スロット値の登録

47ある都道府県を一つずつ登録するのは面倒なので、CSVを使って一括登録します。
![スロット値の登録](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/alexa_skill_python_sdk/06_addslotvalue.png)

「スロットのタイプ」画面で「一括編集」リンクを押して「スロットタイプの値を一括編集」ダイアログを表示し、CSVファイルを指定します。
![スロット値の登録](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/alexa_skill_python_sdk/07_importslotcsv.png)
CSV の値が表示されれば「送信」ボタンを押して登録します。

インポートするCSVの各列は左から下記のようになっています。
1. スロット値の名称
2. スロット値のID
3. 3列目以降はシノニム（同意語）で複数設定できます。


## インテントスロットの設定

先ほど作った都道府県名を認識するカスタムインテント（BudgetFriendlyGourmetIntent）に、都道府県名をパラメータとして取得できるようにインテントスロットを設定します。

左側のメニューで「BudgetFriendlyGourmetIntent」を選択してインテントページを開きます。
画面下のインテントスロットの表で、インテントスロット名を入力して「＋」ボタンを押します。
ここでは”Prefecture”とします。
![インテントスロットの設定](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/alexa_skill_python_sdk/08_addintentslot.png)

入力したインテントスロット名の右側にあるスロットタイプを、先ほど登録した”PREFECTURE_LIST”を指定します。

## サンプル発話の設定

カスタムインテントで都道府県名を受け付けるため、サンプル発話で都道府県名のスロットタイプを設定します。
インテントページ上部にあるサンプル発話のテキストボックスへ”{Prefecture}”と入力して、右側の「＋」ボタンを押し、都道府県のスロットタイプを受け付けるようにします。
![サンプル発話の登録](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/alexa_skill_python_sdk/09_addsampleconv.png)

ここまで終わったら、画面上部の「モデルを保存」ボタンを押して設定を保存しておきます。

# プログラム開発

## SDK の準備

開発環境でSDK をダウンロードします。
今回はMac OS High Sierra 環境で開発します。

### virtualenv の設定

適当なディレクトリを作成し、virtualenv を設定しておきます。

```shell
$ mkdir BudgetFriendlyGourmetSkill
$ cd BudgetFriendlyGourmetSkill/
$ virtualenv venv
Using base prefix '/Library/Frameworks/Python.framework/Versions/3.6'
New python executable in /Users/xxxxxxxx/projects/AlexaSkills/BudgetFriendlyGourmetSkill/venv/bin/python3.6
Also creating executable in /Users/xxxxxxxx/projects/AlexaSkills/BudgetFriendlyGourmetSkill/venv/bin/python
Installing setuptools, pip, wheel...done.
$ source venv/bin/activate
(venv) $ mkdir budget-friendly-gourmet-skill
(venv) $ cd budget-friendly-gourmet-skill
```

### SDK のダウンロード

SDK は開発ディレクトリ直下に展開します。

```shell
(venv) $ pip install ask-sdk -t .
Collecting ask-sdk
（中略）
Installing collected packages: docutils, jmespath, six, python-dateutil, botocore, s3transfer, boto3, ask-sdk-model, certifi, idna, chardet, urllib3, requests, ask-sdk-core, ask-sdk-dynamodb-persistence-adapter, ask-sdk
Successfully installed ask-sdk-0.1.2 ask-sdk-core-0.1.2 ask-sdk-dynamodb-persistence-adapter-0.1.2 ask-sdk-model-0.2.1 boto3-1.7.70 botocore-1.10.70 certifi-2018.4.16 chardet-3.0.4 docutils-0.14 idna-2.7 jmespath-0.9.3 python-dateutil-2.7.3 requests-2.19.1 s3transfer-0.1.13 six-1.11.0 urllib3-1.23
```

## Serverless でLambda テンプレートを展開する

Alexa スキルではLambda を使うので、Serverless を使ってLambda 開発用のテンプレートを展開して必要なファイルを自動生成します。

```shell
(venv) $ serverless create --template aws-python3
Serverless: Generating boilerplate...
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.29.2
 -------'

Serverless: Successfully generated boilerplate for template: "aws-python3"
Serverless: NOTE: Please update the "service" property in serverless.yml with your service name
```

## 「serverless.yml」の編集
自動生成された「serverless.yml」を編集して、Alexa Skill 用に変更します。

```yml:serverless.yml

service: BudgetFriendlyGourmetSkill

provider:
  name: aws
  runtime: python3.6
  stage: dev
  region: ap-northeast-1

functions:
  budget-friendly-gourmet:
    handler: handler.handler
    events: 
      - alexaSkill: amzn1.ask.skill.xxxx
```

最終行のalexaSkill には、先だって作ったAlexa スキルのスキルID を設定します。
スキルID は、エンドポイント画面で確認できます。
エンドポイント画面は、Alexa 開発者コンソール左側のメニューにある「エンドポイント」をクリックして表示します。
![スロット値の登録](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/alexa_skill_python_sdk/10_skillid.png)

## B級グルメ情報のインポート

B級グルメ情報は、DynamoDB に格納しておきます。
スロットタイプ（PREFECTURE_LIST）で設定したスロット値のID（都道府県名のアルファベット表記）をキーとして、名前とよみがな、料理の概要を格納しておきます。

B級グルメ情報のDBへの登録は、[こちら](https://github.com/sumihiro3/BudgetFriendlyGourmetSkill)に[グルメ情報のデータ](https://github.com/sumihiro3/BudgetFriendlyGourmetSkill/blob/master/budget-friendly-gourmet-skill/budget_friendly_gourmet.csv)と、[インポート用スクリプト](https://github.com/sumihiro3/BudgetFriendlyGourmetSkill/blob/master/budget-friendly-gourmet-skill/budget_friendly_gourmet_importer.py)を置いておきますので参考にしてください。

## 実装

いよいよ実装です。
Alexa Skills Kit SDK for Python(Beta) では、デコレータを使うか、リクエストハンドリング用のクラスを継承するかして、インテント判別後の処理を行います。

今回はコード量も多くないですので、見通しが良いようにデコレータを使って実装します。
コードは多くないとはいえ個別に解説すると長くなるので、カスタムインテント（BudgetFriendlyGourmetIntent）判定後の処理を行うメソッドのみ解説します。

ソースコードの全文は、[こちら](https://github.com/sumihiro3/BudgetFriendlyGourmetSkill)を参考にしてください。

```python:handler.py（抜粋）
@sb.request_handler(can_handle_func=is_intent_name('BudgetFriendlyGourmetIntent'))
def budget_friendly_gourmet_intent_handler(handler_input):
    logger.info('BudgetFriendlyGourmetIntent handler called!!')
    slots = handler_input.request_envelope.request.intent.slots
    slot_name = 'Prefecture'
    gourmet_info = None
    # 呼びかけられた都道府県名からB級グルメ情報を取得する
    if slot_name in slots:
        prefecture_slot = slots[slot_name]
        logger.info('Slot [{Prefecture}] found! Slot: %s', 
            prefecture_slot)
        # [A] インテントスロットに都道府県名が設定されているかをチェックする
        slot_resolution_list = prefecture_slot.resolutions.resolutions_per_authority
        slot_resolution = slot_resolution_list[0]
        slot_status = slot_resolution.status.code
        if StatusCode.ER_SUCCESS_MATCH == slot_status:
            # [B] インテントスロットからスロット値のIDを取得する
            prefecture_id = slot_resolution.values[0].value.id
            logger.info('Prefecture id: %s', prefecture_id)
            try:
                # [C] スロット値のID　を基にB級グルメ情報をDynamoDBから取得します
                gourmet_info = get_budget_friendly_gourmet_for(prefecture_id)
            except Exception:
                pass
    # [D] グルメ情報を基に読み上げるセリフを組み立てる
    if gourmet_info is not None:
        speech_text = '{}には{}というB級グルメがあります。{}です。'.format(
            prefecture_slot.value,
            gourmet_info['yomi'],
            gourmet_info['detail']
        )
        end_session = True  # [E] B級グルメ情報を返したらスキルのセッションは完了させる
    else:
        logger.info('No gourmet info found...')
        speech_text = 'もう一度、都道府県名を教えてください。'
        end_session = False # [F] 聞き返すのでスキルのセッションは継続させる
    # 読み上げるセリフを返す
    return handler_input.response_builder.speak(speech_text).set_card(
        SimpleCard('B級グルメ', speech_text)).set_should_end_session(
        end_session).response

```

1行目はデコレータを使ってカスタムインテント（BudgetFriendlyGourmetIntent）判別後に呼ばれるメソッドとして定義しています。

以下、コード中のコメントで **[A]** などとしている箇所を解説します。

### [A] インテントスロットに都道府県名が設定されているかをチェックする

インテントでインテントスロット（今回は都道府県名）が認識されると、StatusCode には”ER_SUCCESS_MATCH”というEnum 値が設定されますので、その判定をします。

```python
# [A] インテントスロットに都道府県名が設定されているかをチェックする
        slot_resolution_list = prefecture_slot.resolutions.resolutions_per_authority
        slot_resolution = slot_resolution_list[0]
        slot_status = slot_resolution.status.code
        if StatusCode.ER_SUCCESS_MATCH == slot_status:
```

StatusCode のEnum は*ask_sdk_model.slu.entityresolution.status_code.StatusCode* クラスで定義されているので、個別にインポートしておくと判定しやすいですね。

### [B] インテントスロットからスロット値のIDを取得し、B級グルメ情報を取得する

スロットタイプでスロット値のIDを定義してあれば、ここでIDを取得できます。
今回のように、ユーザーの入力を基にデータベースなどから情報を取得するのであれば、スロット値のIDは設定しておいたほうがよさそうですね。
ただ、階層が深すぎて取得するのに苦戦しました。SDK 本リリース時にはもう少し取得しやすくなっていて欲しいです。。。

あとは、取得したスロット値のID を使ってB級グルメ情報をDynamoDBから取得するメソッドを呼び出しています。

### [C] グルメ情報を基に読み上げるセリフを組み立てる

```python
    # [C] グルメ情報を基に読み上げるセリフを組み立てる
    if gourmet_info is not None:
        speech_text = '{}には{}というB級グルメがあります。{}です。'.format(
            prefecture_slot.value,
            gourmet_info['yomi'],
            gourmet_info['detail']
        )
        end_session = True  # B級グルメ情報を返す場合はスキルのセッションは完了させる
    else:
        logger.info('No gourmet info found...')
        speech_text = 'もう一度、都道府県名を教えてください。'
        end_session = False # 聞き返すのでスキルのセッションは継続させる
    # 読み上げるセリフを返す
    return handler_input.response_builder.speak(speech_text).set_card(
        SimpleCard('B級グルメ', speech_text)).set_should_end_session(
        end_session).response
```

DB より取得したB級グルメ情報を使って読み上げるセリフを組み立てます。
インテントスロットを認識されなかったり、B級グルメ情報を取得できなかった場合は、もう一度都道府県名を質問します。
スキルのセッションを修了させるかはレスポンスを生成するビルダクラスのメソッド引数にboolean 値を渡せるので、処理に応じて変更しています。

# デプロイ

実装が終わったら、AWS 上へデプロイします。

```shell
(venv) $ serverless deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (32.35 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.........
Serverless: Stack update finished...
Service Information
service: BudgetFriendlyGourmetSkill
stage: dev
region: ap-northeast-1
stack: BudgetFriendlyGourmetSkill-dev
api keys:
  None
endpoints:
  None
functions:
  budget-friendly-gourmet: BudgetFriendlyGourmetSkill-dev-budget-friendly-gourmet
```

コマンド一つでAWS へデプロイされました。ほんと、Serverless って便利ですね。
ここまでくれば、あと一息です。

# スキルとLambda の紐づけ設定

デプロイしたLambda と、先に作ったAlexa スキルを紐づけます。
Lambda からAlexa スキルへの紐づけは「serverless.yml」で設定したので、あとはAlexa スキルからLambda への紐づけです。

Alexa スキルからLambda への紐づけは、エンドポイント画面で設定します。
エンドポイント画面は、Alexa 開発者コンソール左側のメニューにある「エンドポイント」をクリックして表示します。
![スキルとLambda の紐づけ](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/alexa_skill_python_sdk/11_addlambdaarn.png)

「デフォルトの地域」欄にLambda のARN を入力します。
入力したら、画面上部の「エンドポイントを保存」ボタンを押して保存します。

## スキルのビルド

これで設定が終わりましたので、スキルをビルドして使える状態にします。
コンソール左側のメニュー「呼び出し名」を押して「呼び出し名」画面を表示させ、画面上部の「モデルを保存」を押して保存し、「モデルをビルド」ボタンを押してビルドを開始します。
![ビルド](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/alexa_skill_python_sdk/02_slillname.png)

少し時間がかかりますが、「ビルド成功」の通知がくれば設定完了です！
テストをしてみましょう。

# スキルのテスト

画面上部の「テスト」タブを押して、スキルのテスト画面を表示させます。
テストが有効になっていない場合は、画面左上のスイッチをON にしてテストを有効にします。
![スキルのテスト](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/alexa_skill_python_sdk/12_testskill.png)

それでは発話テストをしていきましょう。
左上の発話用のテキストボックスに冒頭で登録したスキルの呼び出し名を入力します。
”ビーキュウグルメを開いて”と入力してEnter キーを押します。
![スキルのテスト](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/alexa_skill_python_sdk/13_testskill02.png)

うまく稼働していると”どの都道府県のB級グルメが知りたいですか？”と問いかけてくるので、好きな都道府県名を入力してください。（例えば、秋田県なら”秋田”でも”秋田県”のどちらでも認識されます。）
ご当地B級グルメの情報を返してくれたら成功です！


# まとめ

どうだったでしょうか？
これくらいの簡単なスキルであれば、30分程度あれば開発できてしまいます。
とはいえ、文章でまとめてみると長くなりますね。時間的には実装2割、文章化8割といったところでしょうか。

Alexa Skills Kit SDK for Python (Beta) は、まだベータ版なのでAPI 仕様などは変わるのでしょうが、結構簡単に開発できるようになっています。

今回は苦戦したインテントスロットの判別可否や、スロット値などがもう少し簡単に取得できるようになれば使いやすくなりそうです。

Alexa スキルと言えば、[amazon alexa skill Awards](http://alexaskillawards.jp/) の締切まで3週間と迫ってきましたが、皆さんは申し込みされたでしょうか？
8月に大阪で開催予定の[「Alexaスキルアワード公式ハッカソン大阪」](https://alexaskillawards.connpass.com/event/92927/)に参加して、そこで作った作品で応募したいと思っています。

大阪のハッカソンに参加される方がいらっしゃいましたら、よろしくおねがいします。

また、感想や「こうした方が良いよ」などありましたらコメントなど頂けると嬉しいので、お気軽に！


# 今回使ったソースコードなど

今回使ったファイルはこ[こちら](https://github.com/sumihiro3/BudgetFriendlyGourmetSkill)に置いておきますので参考にしてください。


# 参考サイト

* [ASK SDK for Python (Beta)](https://github.com/alexa-labs/alexa-skills-kit-sdk-for-python)
* [Alexa Skills Kit SDK for Python (Beta) Document](https://alexa-skills-kit-python-sdk.readthedocs.io/en/latest/index.html)
* [カスタムスロットタイプの作成と編集](https://developer.amazon.com/ja/docs/custom-skills/create-and-edit-custom-slot-types.html#edit-or-upload-slot-type-values-in-bulk)
* [B級ご当地グルメ一覧](https://ja.wikipedia.org/wiki/B%E7%B4%9A%E3%81%94%E5%BD%93%E5%9C%B0%E3%82%B0%E3%83%AB%E3%83%A1%E4%B8%80%E8%A6%A7)
* [最短でAWS CLIをインストールして設定する方法](https://tomokazu-kozuma.com/procedure-for-installing-aws-cli/)
* [Splatoon2のステージ情報を調べるAlexaのカスタムスキルを作った](http://manatee-it.hatenadiary.jp/entry/2018/05/10/155813)