Alexa Skills Kit でシンプルなカスタムスキルを実装

 前回 Amazon Echo Dot の初期設定と既存のスキルを使ってみるところだけやりましたが、今回は自作のスキル(カスタムスキル)を実装してみます。Alexa では対話形式で複雑な処理を行うスキルも実装できますが、まず今回はシンプルに、スキルを起動したら結果を返して終了するという最もシンプルなパターンのカスタムスキルを実装してみます。

 うちには小学生の子どもが二人いるのですが、ゲームの順番などでしょっちゅう喧嘩しているので、 Alexa に順番を決めてもらうスキルを作成してみます。(教育的には話し合って解決できるようにするべきという点は一旦置いておきます。)

 ひとまず今回はシミュレータで動作確認ができるところまでをやってみたいと思います。

開発の流れ

 カスタムスキルを作成するには Alexa Skills Kit(ASK)を利用します。Alexa Skills Kit での開発については下記で公式のドキュメントが公開されています。

developer.amazon.com

 スキル開発の流れとしては、上記ドキュメントで下記画像のように紹介されています。

f:id:akanuma-hiroaki:20171118132920p:plain

 また、公式ドキュメントの一部として、クラスメソッドが作成しているトレーニングドキュメントもあるようです。

developer.amazon.com

 開発工程での主な流れとしては下記のようになるかと思います。

  1. 対話モデルの作成

  2. Lambda でバックエンドの処理を作成して対話モデルと紐付け

  3. シミュレータ or 実機でテスト

 Amazon Developer Account や AWS Account の作成方法は割愛させていただきますので、アカウント登録が終わっている前提で、それぞれ進めてみたいと思います。

対話モデルの作成

 まずはスキルを作成して対話モデルを作成していきます。下記リンク先にアクセスして開発者コンソールのダッシュボードを開きます。

Amazon Developer Sign In

 開発者コンソールのダッシュボードは下記のような画面になります。

f:id:akanuma-hiroaki:20171119231851p:plain

 画面上部の ALEXA メニューをクリックして Alexa のダッシュボードを開きます。

f:id:akanuma-hiroaki:20171119232448p:plain

 Alexa の要素としては Alexa Skills Kit(ASK) と Alexa Voice Service(AVS) があり、今回は Alexa Skills Kit を選択します。ちなみに Alexa Voice Service は音声を認識してテキストに変換したり、テキストの内容を音声に変換して発話するために利用するもので、自前でハードウェアに Alexa を組み込んでスマートスピーカーを作る際などに利用します。

f:id:akanuma-hiroaki:20171119232709p:plain

 初めてスキルを作成する場合はスキル一覧には何もありませんが、作成済みのスキルがある場合は上記のように一覧に表示されます。今回は新たにスキルを作るために 新しいスキルを追加する をクリックします。

f:id:akanuma-hiroaki:20171119233414p:plain

 スキルの作成画面に遷移しますので、今回は下記の内容で入力・選択します。

  • スキルの種類:カスタム対話モデル

  • 言語:Japanese

  • スキル名:順番決め

  • 呼び出し名:順番決め

 その他の項目は今回はデフォルトのままにしておきます。入力できたら画面下部の 保存 ボタンをクリックします。

f:id:akanuma-hiroaki:20171122223615j:plain

 するとスキルが作成されますので、 次へ ボタンをクリックして対話モデル作成画面へ進みます。

f:id:akanuma-hiroaki:20171122223840j:plain

 上記は対話モデル作成画面での入力後の状態です。

 まずインテントスキーマは Alexa からバックエンドの Lambda ファンクションへ送られるインテントの種類を定義します。今回は下記のように定義します。

{ "intents": [
    { "intent": "DecideOrderIntent" },
    { "intent": "AMAZON.HelpIntent" },
    { "intent": "AMAZON.StopIntent" },
    { "intent": "AMAZON.CancelIntent" }
]}

 今回実装するカスタムスキル用のインテントとして DecideOrderIntent を定義しています。また、それ以外にビルトインインテントを3種類使用します。

 今回のカスタムスキルでは特にユーザから情報を提供する必要はないので、カスタムスロットタイプはブランクのままにしておきます。将来的にはユーザから順番決めの候補者名を提供するようにしたいので、その際にはカスタムスロットタイプを定義することになるかと思います。

 また、サンプル発話は下記のように定義します。

DecideOrderIntent 順番決め
DecideOrderIntent 順番決めて
DecideOrderIntent 順番を決めて
DecideOrderIntent 順番決めを開いて
DecideOrderIntent 誰の順番
DecideOrderIntent 誰の番
DecideOrderIntent 誰が先

 サンプル発話にはユーザがこのスキルを呼び出したいときに使う可能性のある発話内容をリストアップしておきます。

 ここまで入力したら 次へ ボタンをクリックしてエンドポイント等の設定画面へ進みます。ここでスキルのコンソールは一旦置いておいて、 Lambda ファンクションの実装に移ります。

エンドポイントの Lambda 実装

 Alexa Skills Kit では、入力された音声の内容に応じて処理を行うロジックとして、 AWS Lambda のファンクションを使うか、 Web API を呼び出すかを指定できます。標準は Lambda なので、今回も Lambda のファンクションとして実装します。Alexa Skills Kit のエンドポイントとしての Lambda 実装についてのリファレンスは下記になります。

developer.amazon.com

 Alexa Skills Kit でのエンドポイントとのやりとりは JSON で行われますが、その内容についてのリファレンスは下記になります。

developer.amazon.com

 まずは AWS のコンソールにログインして Lambda のコンソールを表示します。

f:id:akanuma-hiroaki:20171120000635p:plain

 新たにファンクションを作成するため、 関数の作成 をクリックします。

f:id:akanuma-hiroaki:20171120000801p:plain

 ベースとなるサンプルを選択します。もちろんファンクションを一から作成しても良いのですが、 Alexa のエンドポイント用の実装サンプルがいくつか公開されていますので、それを変更する形で実装したいと思います。今回は Python での実装サンプルである、「alexa-skills-kit-color-expert-python」を利用します。

f:id:akanuma-hiroaki:20171120001523p:plain

 基本的な情報としてファンクション名とロールを設定します。私のケースでは既存のロールがあったので 既存のロールを選択 を選択してそれを利用していますが、初めて作成する場合には テンプレートから新しいロールを作成 もしくは カスタムロールの作成 を選択して新しくロールを作成します。

 上記以外はひとまずそのままで、画面下部の 関数の作成 ボタンをクリックするとファンクションが作成されます。

f:id:akanuma-hiroaki:20171122224059j:plain

 私が実行したところ、作成後の画面で上記のようなエラーが表示されました。Alexa Skills Kit のトリガーの作成に失敗したということのようですが、トリガーとしては追加されているようなので、ひとまずこのまま進みます。

 一旦 Lambda コンソールは置いておいて、作成した Lambda ファンクションをスキルに紐づけるために、スキルのコンソールに戻ります。

f:id:akanuma-hiroaki:20171122224320j:plain

 サービスエンドポイントのタイプで「AWS Lambda の ARN」を指定し、Lambda ファンクション作成後の画面の右上に表示されていた ARN をデフォルトに設定します。「エンドポイントの地理的リージョンを設定しますか?」の項目には「いいえ」を指定し、その他はデフォルトのままで 次へ をクリックします。

f:id:akanuma-hiroaki:20171122224612j:plain

 シミュレーターが表示されますので、意図した通りにカスタムスキルを呼び出せるか確認します。エンドポイントにもリクエストが送信されますので、先ほど作成した Lambda ファンクションへも実際にリクエストが送信されることになります。

 サービスシミュレーターのテキストに、カスタムスキルのサンプル発話に登録したものの一つを入力して 順番決めを呼び出す をクリックします。するとリクエストとレスポンスの JSON が下に表示されます。レスポンスの内容は Lambda ファンクションの処理結果になりますが、先ほど作成したファンクションはまだサンプルの内容を変更していませんので、サンプルのレスポンスがそのまま返ってきています。

 それでは Lambda ファンクションを今回のカスタムスキルの意図に沿ったものに変更していきますが、まずはファンクションのテストのためにどんな JSON がリクエストされるのかを確認しておきます。シミュレータの実行結果ではリクエストの JSON は下記のようになっていました。あくまで私が実行した時の例なので、ID等の値は変わってくるかと思いますし一部マスクしていますが、構成は同様になるかと思います。

{
  "session": {
    "new": true,
    "sessionId": "SessionId.7caeec82-30ad-4629-9c96-xxxxxxxxxxxx",
    "application": {
      "applicationId": "amzn1.ask.skill.1f313097-0cb4-4be1-a4d8-xxxxxxxxxxxx"
    },
    "attributes": {},
    "user": {
      "userId": "amzn1.ask.account.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    }
  },
  "request": {
    "type": "LaunchRequest",
    "requestId": "EdwRequestId.252e3464-1623-422f-9bf3-xxxxxxxxxxxx",
    "locale": "ja-JP",
    "timestamp": "2017-11-19T22:25:21Z"
  },
  "context": {
    "AudioPlayer": {
      "playerActivity": "IDLE"
    },
    "System": {
      "application": {
        "applicationId": "amzn1.ask.skill.1f313097-0cb4-4be1-a4d8-xxxxxxxxxxxx"
      },
      "user": {
        "userId": "amzn1.ask.account.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
      },
      "device": {
        "supportedInterfaces": {}
      }
    }
  },
  "version": "1.0"
}

 この JSON をそのまま Lambda コンソールからテストイベントとして設定しておきます。

f:id:akanuma-hiroaki:20171122224939j:plain

 そして今回のファンクションの内容は下記のようにしました。とりあえず今回はユーザからの候補者名の入力は受け付けず、固定の二人の名前を実行時に毎回シャッフルして順番を返す単純なものです。

# -*- coding: utf-8 -*-
from __future__ import print_function
from random import shuffle

# --------------- 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 decide_order():
    candidates = ['太郎君', '花子さん']
    shuffle(candidates)
    return "今回は、" + '、'.join(candidates) + "の順番です。"

def get_welcome_response():
    session_attributes = {}
    card_title = "順番決め"
    speech_output = decide_order()

    should_end_session = True
    return build_response(session_attributes, build_speechlet_response(
        card_title, speech_output, None, should_end_session))

def handle_session_end_request():
    card_title = "順番決め終了"
    speech_output = "良い一日を!"

    should_end_session = True
    return build_response({}, build_speechlet_response(
        card_title, speech_output, None, should_end_session))

def get_order(intent, session):
    session_attributes = {}
    reprompt_text = None

    speech_output = decide_order()
    should_end_session = True

    return build_response(session_attributes, build_speechlet_response(
        intent['name'], 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):
    """ Called when the user specifies an intent for this skill """

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

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

    if intent_name == "DecideOrderIntent":
        return get_order(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):
    print("event.session.application.applicationId=" +
          event['session']['application']['applicationId'])

    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'])

 lambda_handler() メソッドに渡している event には、先ほど確認したリクエストの内容が入ってきます。

 また、リクエストの種別としては下記3種類を想定していて、処理内容を切り分けています

  • LaunchRequest: スキル起動時

  • IntentRequest: セッション継続中 もしくはスキル起動時に追加情報と一緒に起動した場合

  • SessionEndedRequest: スキル終了時

 今回はユーザからの追加情報は受け付けないので、 LaunchRequest と IntentRequest で基本的には同じ処理をしています。 IntentRequest の処理の内容はインテント名でさらに切り分けて AMAZON.HelpIntent 等についての処理もサンプルにあったものをそのまま残していますが、今回の内容ではスキルの起動以外にユーザの入力を待ち受けることがないため、 DecideOrderIntent 以外の処理は実行されないかと思います。

 ファンクションを保存して再度シミュレータからリクエストを実行すると、下記のようにレスポンスが返るようになります。

f:id:akanuma-hiroaki:20171122225220j:plain

まとめ

 今回はシンプルなカスタムスキル実装ということで、ユーザからはスキル起動の発話だけ受け取って結果を返すという単純なものでしたので、入力値のハンドリング等も特に必要なく、簡単に実装できました。 Lambda からはさらに他のAWSサービスや外部サービスへも連携できますので、アイディア次第で色々なことができそうです。一方でユーザからの入力内容によって処理を変えられるようにもしたいので、次回以降で入力値のハンドリングも行うスキルを実装してみたいと思います。