Alexa skill ki(ASK)を使って、Datadogからの回答が取れるか?


AWS re:Invent 2016出参加者にAWS Echo Dotが配られたのには、時代の変化の基調を感じざるを得ませんでした。会場で起きている光景を見て、Androidを配っていた時のGoogle appの状況をもいだしました。当時iphoneに大きく遅れていたandroid端末が、世界の主流になっている今日、Amazonが狙っていることを想像するに絶やし事ではないでしょうか。

はじめに

今後、このASKネタを使ってDatadogが新しく出したAPMの機能や、スクリプトの最終実行先のLambda関数からカスタムメトリクスをDatadogへ送信する方法などの数本のホスト(英語ポストにも挑戦した)にしていこうと思っています。そのシリーズ開始宣言として、今ポストでは、先ずcpuの値を拾ってくるところまでを書きたいと思います。

尚、ASKの基本的な知識は、国内の第一人者(僕が勝手に思っているだけかもしれませんが...)のJun Ichikawaのhatenaqiitaの記事に任せたいと思います。

ASKでプログラムするために僕が選んだ環境(2016/12/24現在)

開発環境:
- MacOS 10.12.2
- Python 2.7.10 (pyenv&virtualenvの環境下で動作しています)
- Flask-ask (askの開発をするためのpython framework)
- Datadogpy (datadog apiのpython lib)
- ngrok (Mac上で起動したflask アプリにトンネリングするためのソフト)

テスト環境:
- Amazon develpperサイトのAlexaの開発コンソールのtestページ
- Echosim (自作ハードなしで、国内で唯一公式使えるテスト環境)
海外で実験した実機と比較して、音声応答までの時間が長いのと操作が煩雑なのが難点ですが、十分使えるレベルにはあると思います。

なんで、こんな環境にしたの?

個人的に、Pythonが一番安心して触れるのでこの組み合わせにしました。更にdatadogpyは、Datadogが開発しており、更新が頻繁にされ、最も安定していることも選択の理由になりました。更に、これからシリーズのポストを書く上でも、DatdogのAPMが対応しているプログラミング言語とフレームワークという点でも、pythonとflaskの組み合わせは、ベストマッチングでした。

尚、pythonを使ったASKのframeworkには、PyAlexaというモノもあります。2016年のAWS summit Tokyoの頃に、僕が書いていたASKのプロジェクトにはPyAlexaを使っていました。当時は、それしか見つけられなかったのが選択の理由でした。Flask-askが公開された現在、どちらを選ぶべきかと聞かれると、以下のようにコメントすると思います。

  • AWS Lambdaを使って、手っ取り早く開発したいならPyAlexaかな〜。
  • Zappaなどのserverlessの知識を試してみたい、DataogのAPMも試してみたい、会話の回答を作る部分が複雑に成り手元で開発環境を実施してみたいならFlask-askかな〜。

僕のような週末プログラマーでなければ、どちらを使っても結果は大きく違わないし、スクラッチで書いても良いと思います。最後には、スクラッチで書けるほどの知識がframeworkを使っていても必要になるのは間違いないです。

作業開始

環境構築

pythonでの開発になるので、必要なモノはpipでインストールすることができます。

$ pip install flask-ask
$ pip install datadog

次に、ngrokのdownloadページからアプリをdownloadします。

その後インストラクション有るように展開し、ngrokが起動できるように準備しておきます。展開後、僕は、~/Appricatonsに移動しておきました。

$ cd /path/to/ngrok
$ unzip ngrok.zip
$ mv ngork ~/Applications/

次に、PC上の適当の場所に、開発用のディレクトリを作ります。

$ cd ~
$ mkdir ymabiko
$ cd mymabiko

僕は、次のようなファイルとディレクトリを準備しました。

├── dogfood.py
├── speech_assets
│   ├── IntentSchema.json
│   ├── SampleUtterances.txt
│   └── customSlotTypes
│       └── LIST_OF_METRIC
├── templates.yaml
└── yamabiko.py

作成したファイルやディレクトリの名称は、自由に決めてください。尚、利用目的は以下の様になります。

name type 概要
yamabiko.py file ASKのメインプログラムを記述していきます。
templates.yaml file 音声応答に必要な、文字列を生成するためのテンプレテーとを記述しています。
dogfood.py file Datadogへの問い合わせをするをするためのhelper関数を記述していこうと思います。
speech_assets dir Amazon DevelperサイトのASK SKillページに設定する項目を管理するディレクトリにします。
IntentSchema.json file IntentSchemaを記述しておきます。
SampleUtterances.txt file Utteranceを記述しておきます。
customSlotTypes dir IntentSchemaで使っているカスタムなSlotのサンプルを記述します。

既にAmazonによって多くのbuilt-in slot typeが準備されています。
LIST_OF_METRIC file custom slot typeのサンプルを記述します。

dogfood.py:
api_keyapp_keyは、自分の環境に交わせて変更してください。

#! /usr/bin/env python

from datadog import initialize, api
import time

options = {
    'api_key': '81df64ebed3a1ddf6ebf3b148e70b29e',
    'app_key': 'a18c26f402f1fd0901eb291e21db769c6648cb5a'
}

initialize(**options)


def cpu_idle_query(now):
    query = 'system.cpu.idle{*} by {*}'
    try:
        api_call_response = api.Metric.query(start=now - 60, end=now, query=query)
        metric_value = api_call_response['series'][-1]['pointlist'][-1][-1]
    except:
        metric_value = "not able to get the value"

    return metric_value


def get_metric_value(now, metric):

    if metric.lower() == 'cpu':
        response = cpu_idle_query(now)
    elif metric.lower() == 'hdd':
        response = 'not available'
    else:
        response = None

    return response


def main():
    now = int(time.time())
    metrics = ['CPU', 'HDD', 'Memory']

    for metric in metrics:
        print get_metric_value(now, metric)


if __name__ == '__main__':
    main()

yamabiko.py:

import logging

from flask import Flask, json, render_template
from flask_ask import Ask, request, session, question, statement
from dogfood import get_metric_value
import time

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


@ask.launch
def launch():
    card_title = render_template('card_title')
    question_text = render_template('welcome')
    reprompt_text = render_template('welcome_reprompt')
    return question(question_text).reprompt(reprompt_text).simple_card(card_title, question_text)


@ask.intent('WhatsTheValueIntent', mapping={'metric': 'Metric'})
def metric_is(metric):
    card_title = render_template('card_title')
    now = int(time.time())
    if metric is not None:
        metric_value = get_metric_value(now, metric)
        if metric_value is None:
            question_text = render_template('unknown_metric_reprompt')
            return question(question_text).reprompt(question_text).simple_card(card_title, question_text)
        if type(metric_value) is not str:
            metric_value = str(metric_value)
        statement_text = render_template('known_metric_bye', metric=metric, value=metric_value)
        return statement(statement_text).simple_card(card_title, statement_text)
    else:
        question_text = render_template('unknown_metric_reprompt')
        return question(question_text).reprompt(question_text).simple_card(card_title, question_text)


@ask.session_ended
def session_ended():
    return "", 200


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

templates.yaml:

card_title: Yamabiko

welcome: |
    Welcome to the Hachi sample. Please tell me what metric value you need by saying like, I want cpu value

welcome_reprompt: Please tell me what metric value you need by saying like , I want cpu value

known_metric_bye: Value for {{ metric }} is {{ value }}. Have a nice day.

unknown_metric_reprompt: |
    I'm not sure what you need |
    Please tell me what metric value you need by saying like , I want cpu value

IntentSchema.json:

{
  "intents": [
    {
      "intent": "WhatsTheValueIntent",
      "slots": [
        {
          "name": "Metric",
          "type": "LIST_OF_METRIC"
        }
      ]
    }
  ]
}

SampleUtterances.txt:

WhatsTheValueIntent I want to know {Metric}
WhatsTheValueIntent I want to know {Metric} value
WhatsTheValueIntent give me {Metric}
WhatsTheValueIntent give me {Metric} value
WhatsTheValueIntent I would like to know {Metric}
WhatsTheValueIntent I need to know {Metric}
WhatsTheValueIntent I need to know {Metric} value
WhatsTheValueIntent how about {Metric}

LIST_OF_METRIC:

cpu
hdd
network
memory

[Github repo]へのリンク

アプリの起動

flask-askの起動:

$ cd /path/to/yamabiko
$ python yamabiko.py
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger pin code: 128-893-675

起動後にファイルに変更を加えると、flask-askがファイルを変更を検知し、再起動をしなくても最新の状態でamazonからの問い合わせにかいとうしてくれるようになり、手元の環境でゴリゴリと開発を進めることができます。

ngorkの起動:
flask-askの起動時に表示された、プロトコルとport番号を付けてngrokをきどうします。http5000番。僕の環境では、~/Applicationsにexcutableのパスが設定されていないので以下の実行をしましす。

$ cd ~/Applications
$ ./ngrok http 5000

コマンドを実行すると、下記の様な画面がターミナル全面に表示されます。

ngrok by @inconshreveable                                       (Ctrl+C to quit)

Session Status                online
Version                       2.1.18
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://4c7c8258.ngrok.io -> localhost:5000
Forwarding                    https://4c7c8258.ngrok.io -> localhost:5000

Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00

ここで重要なのが、Forwardingの部分のhttps://4c7c8258.ngrok.ioなります。この情報を、Amazon developerサイトで入力することになります。又、この値は、ngrokを起動する度に更新されますので、Amazon developerサイトでその都度変更する必要があります。

Amazon Developerサイトで、Skill登録

Amazon Developer Consoleに移動し、Skillの登録をします。AWS Lambdaを使わなかった場合の設定に関して補足が必要な部分のみを書きます。(手抜きで申し訳ありません。)

Skill InformationInteraction Modelの設定までは、AWS Lambdaを使った場合と全く同じです。先に紹介したJun Ichikawa氏の記事に書かれている内容は、多くの優良情報を含んでいるので、こちらを一読するのが最も簡単で、分かりやすいと思います。

尚僕は、Invoation nameを、"Hachi"と設定しています。従って、Echoを起動するには、”Alexa ask hachi....”と、呼びかけることになります。

次のConfigurationの部分で、HTTPSNorth Americaを選択し、先に確認したngrokのURLを設定するようにします。

ngrokを使っているので、SSL Certificateの部分では、"a sub-domain of a domain that has a wildcard certificate"と書かれてる部分を選択します。

動作確認

先に紹介した方法のどちらかで、テストをしてみます。僕は、Echosim を使うことにします。

上記にamazon developerサイトで使っている認証情報を使ってアクセスします。その後、”Alexa ask hachi....”と、語りかけていきます。

尚、同等なことは、amazon developerサイトのTestページから文字列を入力し、実行結果のjsonを再生することで聴くことができます。個人的には、実機が使える環境に移動して作業を進めるのがのが最も効率でした。コードを変更しながら話しかけることで、煩雑な画面遷移が最小限に押さえられることは、とってもありがたい状況でした。

まとめ

どちらかと言うと、今回のポストはlask-askの使い方になってしまいましたね。ここまでの事が理解できていると、Datadogに関係していなくても何でも好きに新しいSkillがプログラミングできる準備ができているのでは無いかと思います。プログラムの中身的には、エラー処理がしていいない、音声の問い合わせにメトリクスの選択と回答の構築など、イケていないところは沢山ありますが、それらは今後に発展に任せたいと思います。

年明け以降に予定では、flask-askのそれぞれの機能を解説したり、もう少し色々頑張ってyamabiko(alexa上では、hachi)が色々なことができるように改善していきたいと思います。こんなことができたら良いねと言うアイディアがあれば、コメントして提案してもらえますと助かります。

又、2016年のAdvent Calenderを読んで、Datadog使ってみよう思ったら、Datadogの本家サイトから、GET STARTED FREEをクリック、Popup内で必要情報を入力すれば、直ちにトライアルが始められます。