Flask-AskでAlexaのカスタムスキルを作成

はじめに

昨年末にAmazon Echo Dotを購入したので、カスタムスキルを作ろうと思い、その方法を備忘としてまとめます。

Alexaのカスタムスキルは以前作ったことがあり、そのときに使用したFlask-Askを使用して実装したいと思います。

Flask-Askをインストールする際の注意点はこちらの記事にまとめています。

kittagon.hateblo.jp

環境構築

DockerでPython環境の構築

今回は作ったプログラムと環境をHerokuにデプロイするので、コンテナをそのままデプロイ出来るようDockerで環境を作ります。

まずは簡単に以下のようなDockerfileを作ります。

FROM python:3.7.1-alpine3.8
ADD . /app
WORKDIR /app
RUN apk update && apk add git vim

以下コマンドでDockerコンテナを起動します。

$ docker build . -t alexa_env
$ docker run -it -v $PWD:/app -p 8000:8000 alexa_env /bin/ash

今回はalpineのDocker imageを使用しているので、/bin/bashではなくbin/ashであることに注意。

qiita.com

docs.docker.jp

Dockerコンテナに入ったら入ったら、flaskgunicornインストールします。

/app $ pip install flask gunicorn

動作確認のためのapp.pyを作成します。

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run()

gunicornを起動します。

/app $ gunicorn app:app -b 0.0.0.0:8000

ブラウザからhttp://localhost:8000を開いて"Hello World!"が表示されればOKです。

Herokuにデプロイ

続いてDockerfileを編集してHerokuにデプロイ出来る環境を作っていきます。
事前にコンテナ内でpip freeze > requirements.txtを実行しrequirement.txtを作成しておきます。
また前回の記事にて実施したFlask-Askのパッケージのローカルへのダウンロードと編集したパッケージを/src配下に準備するとともに、build-base,libffi-dev,openssl-devのインストールを追記しておきます。

最終的なDockerfileは以下の通り。

FROM python:3.7.2-alpine3.8
ADD . /app
WORKDIR /app
RUN apk update && \
    apk add git vim build-base libffi-dev openssl-dev && \
    pip install --upgrade pip && \
    pip install -r requirements.txt && \
    pip install src/Flask-Ask-0.9.8.tar.gz
EXPOSE 8000
ENV PORT 8000
CMD gunicorn app:app -b 0.0.0.0:$PORT

以前の記事に書いたように、以下コマンドでherokuにデプロイします。

$ heroku login # herokuにログイン
$ heroku container:login # Heroku 上の Container Registry へログイン
$ heroku create alexacustomskil # Heorkuアプリの作成
$ heroku container:push web --app yourappname
$ heroku container:release web --app yourappname

これでHerokuにデプロイされ、デプロイされたURLにアクセスし先ほどと同様"hello World!"が表示されればOKです。

スキル作成

いよいよここからはカスタムスキルを作成していきます。
基本的にはこちらの開発ブログに掲載されているチュートリアルに従って実装していきます。

Flask-Ask: A New Python Framework for Rapid Alexa Skills Kit Development : Alexa Blogs

こちらのチュートリアルでは、数字が3つ読み上げられるのでその数字を覚えて逆から読むというゲームを作っています。

Alexa Developer Console上での設定と、Flask-Askでのサーバ側の処理実装が必要になります。

Amazon Developper Consoleで設定

チュートリアルとは少し画面構成が違いますが、Console上で設定するものは以下のものです。

  • スキルの作成
    • スキルの名前を設定 「アレクサ、〇〇を開いて」のときの名前
    • スキルのインテントを設定 インテントとはユーザーがアレクサに対して応答する言葉のカテゴリのようなもの そのユーザー応答が何を意味するのかを定義します。
      今回登録するのは以下の2つ
      • YesIntent ゲーム開始時に「始めてもいいですか?」の問に対して、ユーザーがOKを場合の発話を定義
      • AnswerIntent 出された問題に対してユーザーが解答する場合の発話を定義
    • それぞれのインテントのスロットを設定 インテントにて定義する発話の中で使用する、変数の器のようなもの 今回のAnswerIntentでは発話の中で数字を読み上げるので、固定値で発話パターンを定義出来ないので、変数としてスロットを定義したうえで発話を定義します。

まずはAlexa Developer ConsoleにAmazonアカウントでログインします。
開発者アカウントにするために追加で情報入力が必要な際は入力します。

https://developer.amazon.com/alexa/console/ask

開発者コンソールが開いたらスキルの作成をクリックします。

f:id:ti_taka:20190203184434p:plain

新しいスキルの登録画面で、カスタムスキルを作成します。

f:id:ti_taka:20190203184439p:plain f:id:ti_taka:20190203213541p:plain

必要情報を入力すると、スキル編集画面にきます。

f:id:ti_taka:20190203184452p:plain

まずはインテントを登録します。

f:id:ti_taka:20190203184452p:plain

今回は上で説明したとおり、以下の2つを登録します。

  • YesIntent ゲーム開始時に「始めてもいいですか?」の問に対して、ユーザーがOKを場合の発話を定義
  • AnswerIntent 出された問題に対してユーザーが解答する場合の発話を定義

まずはYesIntentを登録します。

f:id:ti_taka:20190203184457p:plain

また、YesIntentのサンプルを登録し、このIntentがどういった発話パターンを取りうるのかAlexaに知ってもらいます。

f:id:ti_taka:20190203184502p:plain

次にAnswerIntentを登録します。

f:id:ti_taka:20190203184509p:plain

AnserIntentについては、発話パターンのサンプルを登録する前に、発話パターンの中で使用されるスロット(変数、器のようなもの)を登録します。

f:id:ti_taka:20190203184515p:plain

今回は3つの数字のスロットを使用しますので、それぞれfirst、second、thirdの3個を登録します。 スロットのタイプはAMAZON.numberとします。

f:id:ti_taka:20190203184522p:plain

次にこのスロットを使用してAnswerIntentの発話パターンを登録します。 今回は以下の2パターンを登録しました。

f:id:ti_taka:20190203184526p:plain

次に、このスキルを呼び出した際にそれを処理するサーバーのURLを設定します。 先程デプロイしたherokuのURLを設定します。

f:id:ti_taka:20190203184530p:plain

最後に今回のスキルを呼び出すためのスキル名を設定します。 設定を保存し、ビルドします。

f:id:ti_taka:20190203184536p:plain

これで開発コンソールでの設定は完了です。

左メニューのJSONエディター上は以下のような表示になります。
このJSONをそのまま編集してもOKです。

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "メモリーゲーム",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "YesIntent",
                    "slots": [],
                    "samples": [
                        "うん",
                        "はい",
                        "もちろん",
                        "OKです",
                        "OK",
                        "了解"
                    ]
                },
                {
                    "name": "AnswerIntent",
                    "slots": [
                        {
                            "name": "first",
                            "type": "AMAZON.NUMBER"
                        },
                        {
                            "name": "second",
                            "type": "AMAZON.NUMBER"
                        },
                        {
                            "name": "third",
                            "type": "AMAZON.NUMBER"
                        }
                    ],
                    "samples": [
                        "{first} と {second} と {third}",
                        "{first} {second} {third}"
                    ]
                }
            ],
            "types": []
        }
    }
}

Flask-ASKで実装

サーバ側の処理はFlask-Askで実装していきます。

Welcome to Flask-Ask — Flask-Ask documentation

チュートリアルの通り、app.pyを以下のように書き換えます。

import logging
from random import randint
from flask import Flask, render_template
from flask_ask import Ask, statement, question, session

app = Flask(__name__)
ask = Ask(app, "/")
logging.getLogger("flask_ask").setLevel(logging.DEBUG)

@ask.launch
``def new_game():
    welcome_msg = render_template('welcome')
    return question(welcome_msg)

@ask.intent("YesIntent")
def next_round():
    numbers = [randint(0, 9) for _ in range(3)]
    round_msg = render_template('round', numbers=numbers)
    session.attributes['numbers'] = numbers[::-1]  # reverse
    return question(round_msg)

@ask.intent("AnswerIntent", convert={'first': int, 'second': int, 'third': int})
def answer(first, second, third):
    winning_numbers = session.attributes['numbers']
    if [first, second, third] == winning_numbers:
        msg = render_template('win')
    else:
        msg = render_template('lose')
    return statement(msg)

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

templateファイルの中にはAlexaが話すセリフを記載します。

welcome: メモリーゲームへようこそ! これから5つの数字を読み上げるので、アタナはその数字を逆の順番で答えてください。準備はいいですか?

round: 今回の数字は {{ numbers|join(", ") }} です。反対の順番で読み上げてください。

win: 正解です!

lose: 不正解です。

サーバー側の処理はこれでOKなので、再度Herokuにデプロイします。

そして開発用アカウントに紐づけたEcho端末や、アレクサシミュレーターで「アレクサ、メモリーゲームを開いて」と話しかけてゲームがスタートすれば成功です。

が、返事が返ってきません。

Herokuのログを確認すると以下のようなエラーが出ていました。

2019-01-24T15:02:16.836079+00:00 app[web.1]: File "/usr/local/lib/python3.7/site-packages/OpenSSL/crypto.py", line 740, in _subjectAltNameString
2019-01-24T15:02:16.836081+00:00 app[web.1]: method = _lib.X509V3_EXT_get(self._extension)
2019-01-24T15:02:16.836082+00:00 app[web.1]: AttributeError: module 'lib' has no attribute 'X509V3_EXT_get'

調べてみるとpyOpenSSLを再インストールする必要があるようです。

electricsheep.hatenadiary.jp

記事を参考にDockerfileを以下のように修正します。

FROM python:3.7.2-alpine3.8
ADD . /app
WORKDIR /app
RUN apk update && \
    apk add git vim build-base libffi-dev openssl-dev && \
    pip install --upgrade pip && \
    pip install -r requirements.txt && \
    pip install src/Flask-Ask-0.9.8.tar.gz && \
    pip uninstall --yes pyOpenSSL && \
    yes | pip install pyOpenSSL
EXPOSE 8000
ENV PORT 8000
CMD gunicorn app:app -b 0.0.0.0:$PORT

再度Herokuにデプロイすると無事動作するようになりました。

まとめ

前回の続きで、Flask-Askを使用してAlexaのカスタムスキルを作成してみました。
Flaskを使って簡単に実装できるので、アイデア次第でいろんなスキルが作れそうです。