30分くらいでClova Extension Kit SDK for Python を使ったClova スキルを作る！（後編：実装編）

[前編](https://qiita.com/sumihiro3/private/3ca0a2f849a116b509ff)からの続きで、Clova Extension Kit SDK for Python（以下、PythonSDK）を使ってClova skill を作っていきます。
この記事は後編の実装編となります。

前編はこちら（↓）

* [30分くらいでClova Extension Kit SDK for Python を使ったClova スキルを作る！（前編：スキル設定編）](https://qiita.com/sumihiro3/private/3ca0a2f849a116b509ff)

Flask ベースのAWS 向けサーバーレス開発ツールである [Zappa](https://github.com/Miserlou/Zappa)を使って、Lambda + API Gateway + DynamoDB 環境で開発します。
いろんな環境を使うのは設定が面倒など難しいイメージがありますが、Zappa を使うことで煩わしい設定もなく環境を立ち上げることが出来ます。

スキルのテーマは、[前回のAlexa Skill 編](https://qiita.com/sumihiro3/items/0a7b56671ed3f1864b89)と同じご当地グルメ検索です。
各都道府県にあるご当地グルメを確認して、それぞれのご当地グルメの情報を確認できるようにします。
このスキルでできることは次の2種類です。

* 都道府県名からご当地グルメを調べる：北海道のご当地グルメは
* 個別のご当地グルメの詳細を調べる：ザンギのことを教えて

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

# 開発環境構築

## PythonSDK などパッケージの準備

開発に必要となるパッケージを揃えていきます。
今回使うzappa はpython の仮想環境仕様が前提なので、まずは仮想環境の準備です。

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

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

上記の方法でうまくいかない場合（python2.7 環境となってしまう）は、[こちら](https://qiita.com/tackey/items/a5e1ab29c8152fcc5da6)の記事を参考にしてpython3 環境を作ってください。

### zappa のインストール

pip を使って開発ツールのzappa をインストールします。

```shell
(venv) $  pip install zappa
Collecting zappa
Successfully installed PyYAML-3.12 Unidecode-1.0.22 Werkzeug-0.14.1 argcomplete-1.9.3 base58-1.0.0 boto3-1.7.72 botocore-1.10.72 certifi-2018.4.16 cfn-flip-1.0.3 chardet-3.0.4 click-6.7 docutils-0.14 durationpy-0.5 future-0.16.0 hjson-3.0.1 idna-2.7 jmespath-0.9.3 kappa-0.6.0 lambda-packages-0.20.0 pip-10.0.1 placebo-0.8.1 python-dateutil-2.6.1 python-slugify-1.2.4 requests-2.19.1 s3transfer-0.1.13 six-1.11.0 toml-0.9.4 tqdm-4.19.1 troposphere-2.3.1 urllib3-1.23 wsgi-request-logger-0.4.6 zappa-0.46.2
(venv) $ 
```

### PythonSDK などのインストール

こちらもpip を使って必要なパッケージをインストールしていきます。

まずはPythonSDKです。

```shell
(venv) $ pip install clova-cek-sdk
Collecting clova-cek-sdk
（中略）
Successfully installed asn1crypto-0.24.0 cffi-1.11.5 clova-cek-sdk-1.0.0 cryptography-2.3 pycparser-2.18
(venv) $ 
```

Flask もインストールしておきます。

```shell
(venv) $ pip install flask
Collecting flask
（中略）
Successfully installed Jinja2-2.10 MarkupSafe-1.0 flask-1.0.2 itsdangerous-0.24
(venv) $ 
```

## Zappa でアプリを新規作成

zappa init コマンドを使ってアプリを作成＆セットアップしていきます。

```shell
(venv) $ mkdir local-gourmet-skill
(venv) $ cd local-gourmet-skill/
(venv) $ zappa init

███████╗ █████╗ ██████╗ ██████╗  █████╗
╚══███╔╝██╔══██╗██╔══██╗██╔══██╗██╔══██╗
  ███╔╝ ███████║██████╔╝██████╔╝███████║
 ███╔╝  ██╔══██║██╔═══╝ ██╔═══╝ ██╔══██║
███████╗██║  ██║██║     ██║     ██║  ██║
╚══════╝╚═╝  ╚═╝╚═╝     ╚═╝     ╚═╝  ╚═╝

Welcome to Zappa!

Zappa is a system for running server-less Python web applications on AWS Lambda and AWS API Gateway.
This `init` command will help you create and configure your new Zappa deployment.
Let's get started!
```

### 実行環境名の設定
開発環境と本番環境など環境名を分けて登録することができます。ここではデフォルトのdev として登録します。

```shell
Your Zappa configuration can support multiple production stages, like 'dev', 'staging', and 'production'.
What do you want to call this environment (default 'dev'): dev
```

### デプロイ時に使用するS3バケット名の設定

zappa でデプロイする際に使用するS3バケット名を指定します。
特にこだわりがなければ、デフォルトを使用します。

```shell
AWS Lambda and API Gateway are only available in certain regions. Let's check to make sure you have a profile set up in one that will work.
Okay, using profile default!

Your Zappa deployments will need to be uploaded to a private S3 bucket.
If you don't have a bucket yet, we'll create one for you too.
What do you want to call your bucket? (default 'zappa-xxxxxxxxx'): 
```

### モジュールパスの指定

Lambda 実行時に呼ばれるモジュール名を指定します。
ここでは”main.app”としました。

```shell
What's the modular path to your app's function?
This will likely be something like 'your_module.app'.
Where is your app's function?: main.app
```

### Lambda 関数のグローバル化

デプロイするLambda を全リージョンに展開するか、と聞かれますが、ここはAWS-CLI で指定したデフォルトのリージョンのみにデプロイするように"n" を指定します。

```shell
You can optionally deploy to all available regions in order to provide fast global service.
If you are using Zappa for the first time, you probably don't want to do this!
Would you like to deploy this application globally? (default 'n') [y/n/(p)rimary]: n
```

### 設定ファイルの確認

ここまでの設定内容が設定ファイルに書き込まれます。
内容を確認して、これで初期設定は完了です！

```shell
Okay, heres your zappa_settings.json:

{
    "dev": {
        "app_function": "main.app",
        "aws_region": "ap-northeast-1",
        "profile_name": "default",
        "project_name": "local-gourmet-s",
        "runtime": "python3.6",
        "s3_bucket": "zappa-xxxxxxxx"
    }
}

Does this look okay? (default 'y') [y/n]: y

Done! Now you can deploy your Zappa application by executing:

	$ zappa deploy dev

After that, you can update your application code with:

	$ zappa update dev

To learn more, check out our project page on GitHub here: https://github.com/Miserlou/Zappa
and stop by our Slack channel here: https://slack.zappa.io

Enjoy!,
 ~ Team Zappa!
(venv) $ 
```

### 稼働確認用コードの追加

モジュールパスの設定に応じてpython ファイルを作成します。
先ほどのモジュールパス設定では”main.app" としたので、コードを書くファイルの名前は”main.py”とします。
”main.py” に下記のコードを書いてください。

```python:main.py
import logging
from flask import Flask

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

# Flask
app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def lambda_handler(event=None, context=None):
    logger.info('Lambda function invoked index()')

    return 'hello from Flask!'

if __name__ == '__main__':
    app.run(debug=True)
```

### ローカル環境での実行テスト

AWS へのアップロードする前にローカル環境でも動作確認ができます。
main.py を実行して、http://localhost:5000 にアクセスします。

```shell
$ python main.py
```

別ターミナルやブラウザなどでhttp://localhost:5000 にアクセスして、”hello from Flask!”という文字列が表示されていればOKです。

```shell
$ curl http://localhost:5000
hello from Flask!
```

### アプリのデプロイ

ここまで出来たらAWS へデプロイします。
zappa deploy コマンドだけでAWS（Lambda + API Gateway）環境へデプロイできます！

```shell
(venv) $ zappa deploy
Calling deploy for stage dev..
（中略）
Deploying API Gateway..
Deployment complete!: https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev
(venv) $
```
最後の行に表示されるURL がデプロイしたLambda ファンクションを実行先となります。
ローカル環境での確認と同じように、ブラウザなどで、表示されたURL へアクセスして可動しているか確認しましょう。

```shell
(venv) $ curl https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev
hello from Flask!
```

先ほどと同じように ”hello from Flask!” という文字列が表示されていればOKです。

躓かなければ、環境設定からここまで10分もあれば出来てしまうと思います。

# ご当地グルメ情報のDBへのインポート

ご当地グルメ情報は、DynamoDB に格納しておきます。
DynamoDB で”GourmetInfo” というテーブルを作り、そこにご当地グルメ情報をインポートします。

テーブル作成とデータインポートのスクリプトをこちらに置いておきますので参考にしてください。

# 実装

いよいよサーバー側アプリの実装です。main.py ファイルにコードを書いていきます。
今回使ったソースコードなどは、[こちら](https://github.com/sumihiro3/LocalGourmetSkillForClova)に置いておきますので参考にしてください。

## Clova プラットフォーム側から呼び出されるメソッドの実装

まずはClova デバイス（Clova Friends など）からのスキル呼び出しや実行時に呼ばれるサーバー側アプリのメソッドを実装します。

```python:main.py（抜粋）

@app.route('/clova', methods=['POST'])
def clova_service():
    resp = clova.route(request.data, request.headers)
    resp = jsonify(resp)
    # make sure we have correct Content-Type that CEK expects
    resp.headers['Content-Type'] = 'application/json;charset-UTF-8'
    return resp
```

このメソッドはそのまま書いてしまってください。Clova プラットフォーム側から受けたPOST リクエストの中身をそのままClova クラスの `route` メソッドに渡し、返り値をレスポンスとして返しているだけです。

## インテント判別時に実行されるメソッドの実装（FindGourmetByPrefectureIntent）

とある都道府県のご当地グルメを調べたいときに呼び出されるインテント（FindGourmetByPrefectureIntent）に合致した場合に実行されるメソッドを実装します。

```python:main.py（抜粋）
@clova.handle.intent('FindGourmetByPrefectureIntent')
def find_gourmet_by_prefecture_intent_handler(clova_request):
    '''
    都道府県に応じたご当地グルメ情報メッセージを返すインテント
    （FindGourmetByPrefectureIntent）判別時に呼ばれるメソッド

    Parameters
    ----------
    clova_request : Request
        Intent 判別時のリクエスト
    
    Returns
    -------
    builder : Response
        都道府県に応じたご当地グルメ情報メッセージを含めたResponse
    '''
    logger.info('find_gourmet_by_prefecture_intent_handler method called!!')
    prefecture = clova_request.slot_value('prefecture')
    logger.info('Prefecture: %s', prefecture)
    response = None
    if prefecture is not None:
        try:
            # 都道府県名を判別できた場合
            response = make_gourmet_info_message_by_prefecture(prefecture)
        except Exception as e:
            # 処理中に例外が発生した場合は、最初からやり直してもらう
            logger.error('Exception at make_gourmet_info_message_for: %s', e)
            text = '処理中にエラーが発生しました。もう一度はじめからお願いします。'
            response = response_builder.simple_speech_text(text)
    else:
        # 都道府県名を判別できなかった場合
        text = 'もう一度、ご当地グルメを調べたい都道府県名を教えてください。'
        response = response_builder.simple_speech_text(text)
        response_builder.add_reprompt(response, 
            'ご当地グルメを調べたい都道府県名を教えてください。')
    # retrun
    return response
```

### デコレータの設定

先頭行は、インテント判別時に実行されるメソッドを定義するデコレータです。
`@clova.handle.intent` デコレータの引数にインテント名（ここでは、FindGourmetByPrefectureIntent）を渡しています。

### スロット値の取得

Clova プラットフォームからのリクエストからFindGourmetByPrefectureIntent に設定したスロット（prefecture: 都道府県名）を受け取ります。

```python:main.py（抜粋）
    prefecture = clova_request.slot_value('prefecture')
    logger.info('Prefecture: %s', prefecture)
    response = None
    if prefecture is not None:
        try:
            # 都道府県名を判別できた場合
（中略）
    else:
        # 都道府県名を判別できなかった場合
（後略）
```

スロット値は、Clova プラットフォームからのリクエスト オブジェクトに対して `slot_value` メソッドを呼び出すだけで取得できます。とても簡単ですね。
しかも、このメソッドで取得できるのは、ユーザーが喋った内容ではなく、スロットタイプの代表語が取得できます。

例えば、ユーザーが下記のように喋ったとします。

* **東京**のご当地グルメを教えて
* **東京都**のご当地グルメって何？

ユーザー発話として”東京”と”東京都”で異なるのですが、ユーザーが上記どちらのように喋っても、スロットタイプ「CLOVA.JP_ADDRESS_KEN」に登録された辞書レコードの代表語、この場合は”東京都”が取得できます。
そのため、スキル開発側ではユーザー発話による表記ゆれは気にせずに、データベースへの検索などの処理を実装できます。

ユーザー発話そのものを取りたい場合もあるので良し悪しはありますが、スキル開発を優先したAPI 設計にしているのかなぁ、と思っています。

### スロット値として判別できているかどうか

Clova プラットフォーム側でスロット値として（この場合は都道府県名として）判別できているかどうかは、先ほどの `slot_value` メソッドの返り値がNone かどうかで判別できます。
こちらも簡単ですね。

```python:main.py（抜粋）
    prefecture = clova_request.slot_value('prefecture')
（中略）
    if prefecture is not None:
        try:
            # 都道府県名を判別できた場合
（中略）
    else:
        # 都道府県名を判別できなかった場合
（後略）
```

## ご当地グルメを調べたい都道府県名を判別できた時の処理

調べたい都道府県名が分かれば、先ほどDynamoDB へインポートしたご当地グルメ情報を取得して、Clova デバイスに喋らせるセリフを組み立てて返すだけです。

ユーザーが発話した都道府県に登録されているご当地グルメが複数ある場合は、どのご当地グルメを詳しく聞きたいかを聞き返してユーザーの発話を待ちます。一方、一つだけ登録されている場合は、そのご当地グルメ情報を返してスキルを修了させています。

```python:main.py（抜粋）

def make_gourmet_info_message_by_prefecture(prefecture):
    '''
    都道府県に応じたご当地グルメ情報メッセージを生成する

    Parameters
    ----------
    prefecture : str
        都道府県名
    
    Returns
    -------
    builder : Response
        都道府県に応じたご当地グルメ情報メッセージを含めたResponse
    '''
    logger.info('make_gourmet_info_message_by_prefecture method called!!')
    try:
        gourmet_info_list = inquiry_gourmet_info_list_for(prefecture)
        message = ''
        reprompt = None
        end_session = False
        if gourmet_info_list is None:
            # ご当地グルメ情報が登録されていない場合
            message = '{} にはご当地グルメ情報が登録されていませんでした。他の都道府県で試してください。'.format(
                prefecture
            )
            reprompt = 'ご当地グルメを調べたい都道府県名を教えてください。'
        elif len(gourmet_info_list) == 1:
            # ご当地グルメ情報が1件だけ登録されている場合
            gourmet_info = gourmet_info_list[0]
            gourmet_info_detail = gourmet_info['detail']
            if gourmet_info_detail.endswith('。') == False:
                gourmet_info_detail += 'です。'
            message = '{} のご当地グルメは {} です。{}'.format(
                prefecture,
                gourmet_info['yomi'],
                gourmet_info_detail
            )
            # ご当地グルメ情報を返してスキルのセッションを完了させる
            end_session = True
        else:
            # ご当地グルメ情報が複数件登録されている場合
            gourmet_names = ''
            for info in gourmet_info_list:
                gourmet_names += info['yomi'] + '、'
            message = '{} には、{} 件のご当地グルメが登録されています。'.format(
                prefecture,
                len(gourmet_info_list)
            )
            message += '詳しく知りたいご当地グルメがあればお調べしますので、ご当地グルメ名をお知らせください。'
            message += '登録されているご当地グルメは、{} です。'.format(gourmet_names)
        # build response
        response = response_builder.simple_speech_text(message, end_session=end_session)
        if reprompt is not None:
            response = response_builder.add_reprompt(response, reprompt)
        return response
    except Exception as e:
        logger.error('Exception at make_gourmet_info_message_by_prefecture: %s', e)
        raise e
```

もう一つのインテント「FindGourmetByNameIntent」に対する処理も同じように実装します。
詳しい内容は[ソースコード](https://github.com/sumihiro3/LocalGourmetSkillForClova)をご覧ください。

## アプリの設定

Zappa アプリの設定は以下のようになります。
以下の設定項目は、皆さんの環境に応じて読み替えてください

* s3_bucket
    * アプリデプロイ時に使用するS3 バケット名
* CLOVA_APPLICATION_ID
    * Clova スキルのExtension ID
* TABLE_GOURMET_INFO
    * ご当地グルメ情報を格納するDynamoDB のテーブル名

```json:zappa_settings.json
{
    "dev": {
        "app_function": "main.app",
        "aws_region": "ap-northeast-1",
        "profile_name": "default",
        "project_name": "local-gourmet-skill",
        "runtime": "python3.6",
        "s3_bucket": "zappa-xxxxxxxx",
        "log_level": "INFO",
        "environment_variables": {
            "TZ": "Asia/Tokyo",
            "CLOVA_APPLICATION_ID": "com.hoge.foo.barskill",
            "TABLE_GOURMET_INFO": "GourmetInfo"
        }
    }
}
```

# 実装したアプリのデプロイ

サーバー側アプリを実装できたら `zappa update` コマンドでデプロイします。
最初のデプロイ時は `zappa deploy` でしたが、更新時は `zappa update` コマンドですので注意してください。

```shell
(venv) $ zappa update
Calling update for stage dev..
Downloading and installing dependencies..
（中略）
Your updated Zappa deployment is live!: https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev
(venv) $ 
```

# スキルのサーバー設定変更

[前編](https://qiita.com/sumihiro3/private/3ca0a2f849a116b509ff)で後回しにしたスキルのサーバー設定を変更します。

[Clova Developer Center のスキル設定画面](https://clova-developers.line.me/cek/#/list)を開き、スキル一覧にある基本情報の「修正」リンクをクリック、変更を実施します。
![サーバー設定](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/clova_skill_python_sdk/26_EditServerSetting_01.png)

「サーバー設定」画面まで進み、「ExtensionサーバーのURL」欄に先ほどデプロイしたアプリのURL + /clova を入力して「保存」ボタンをクリック設定更新します。
![サーバー設定](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/clova_skill_python_sdk/27_EditServerSetting_02.png)

これで設定完了です！

# スキルのテスト

次は対話モデルのテスト画面で、思ったとおりに動くかテストしてみましょう。
[Clova Developer Center のスキル設定画面](https://clova-developers.line.me/cek/#/list)を開き、スキル一覧にある対話モデルの「修正」リンクをクリック、対話モデル画面を開きます。
![サーバー設定](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/clova_skill_python_sdk/28_TestSkill_01.png)

対話モデル画面左側メニュの「テスト」ボタンをクリックし、「ユーザーのサンプル発話をテスト」欄にテストしたい発話内容を入力して「テスト」ボタンをクリックするとスキルのテストを実施できます。
ここでは”東京のご当地グルメを教えて”と入力します。
![サーバー設定](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/clova_skill_python_sdk/29_TestSkill_02.png)

テスト結果は画面下に表示されるので、想定通りのインテントやスロット値が判別されているか、応答のセリフを確認できます。
今回は想定通り、インテントは「FindGourmetByPrefectureIntent」、スロットprefecture の値は「東京都」、応答のセリフはご当地グルメが複数登録されている時のセリフが返されていることが確認できます。
![サーバー設定](https://s3-ap-northeast-1.amazonaws.com/my-qiita-images/clova_skill_python_sdk/30_TestSkill_03.png)

また、テスト結果の下部にはClova プラットフォームからサーバー側アプリへ送られるリクエスト内容（JSON）も表示されているので、単体テストにも使えますね！

# 実機テスト

実機で動かしてみると、こんな感じになります。
実機でテストするには、Clova アプリでスキルストアを有効にしておく必要があります。少し手間取りました :-)
<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">試しに作ったご当地グルメ検索 スキルの実機テスト。<br>Python SDK を使ってみた。結構簡単にできましたよ。<br>ちょっと、説明のセリフが長すぎたかなぁ <a href="https://twitter.com/hashtag/Clova_CEK?src=hash&amp;ref_src=twsrc%5Etfw">#Clova_CEK</a> <a href="https://twitter.com/hashtag/LINE_API?src=hash&amp;ref_src=twsrc%5Etfw">#LINE_API</a> <a href="https://t.co/2UyHFQGffV">pic.twitter.com/2UyHFQGffV</a></p>&mdash; sumihiro3 (@sumihiro3) <a href="https://twitter.com/sumihiro3/status/1028098363739103232?ref_src=twsrc%5Etfw">August 11, 2018</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 最後に

どうでしょうか？３０分位で出来たでしょうか？
文章にすると長くなりましたが、スキル開発の流れを掴めるとスムーズに開発できると思います。

それではみなさん、グランプリ賞金1000万円の「[LINE BOOT AWARDS 2018](https://www.line-community.me/awards/)」に向けてがんばりましょう！

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

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

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

# 参考サイト

* [clova-cek-sdk-python :GitHub](https://github.com/line/clova-cek-sdk-python)
* [Clova Extension Kit SDK for Python Document](https://clova-cek-sdk-python.readthedocs.io/en/stable/index.html)
* [Clova CEKでのスキル開発の始め方〜Node.jsで開発スタート編〜](https://dotstud.io/blog/clova-cek-nodejs-tutorial/)