ASK SDK v2でのAlexa Presentation Languageの使用



ASK SDK v2でのAlexa Presentation Languageの使用

ASK SDK v2は、Alexa Presentation Language(APL)のディレクティブとリクエストをサポートしています。ASK SDKはNode、Java、Pythonで使用できます。このトピックでは、ASK SDK v2でスキルをビルドする際にAPLのディレクティブとリクエストを使用する方法を概説し、いくつかサンプルを示します。

SDKとAPLで開発を始める

このトピックを理解するには以下が必要です。

後出のサンプルコードは、NodeJavaPythonの「ハローワールド」サンプルを基にしています。以下のセクションでは、このサンプルを修正し、元の「ハローワールド」の応答にAPLを追加する方法を示します。

APLインターフェースに対応するようスキルを設定する

コードにAPLディレクティブを追加する前に、Alexa.Presentation.APLインターフェースに対応するようスキルを設定する必要があります。

Alexa.Presentation.APLインターフェースが有効になっていない場合、送信したAPLディレクティブはすべてエラーになります。

表示するAPLドキュメントを作成する

画面にコンテンツを表示するには、APLドキュメントをJSON形式で作成し、コードで使用できるようにする必要があります。

APLドキュメントをJSONファイルとして保存する

APLドキュメント構造に従うJSONファイルとしてドキュメントを作成します。たとえば、以下をhelloworldDocument.jsonという名前のファイルに保存します。

クリップボードにコピーされました。

{
  "type": "APL",
  "version": "1.2",
  "description": "シンプルなハローワールドAPLドキュメント。",
  "settings": {},
  "theme": "dark",
  "import": [],
  "resources": [],
  "styles": {},
  "onMount": [],
  "graphics": {},
  "commands": {},
  "layouts": {},
  "mainTemplate": {
    "parameters": [
      "payload"
    ],
    "items": [
      {
        "type": "Text",
        "height": "100vh",
        "textAlign": "center",
        "textAlignVertical": "center",
        "text": "ハローワールド"
      }
    ]
  }
}

これはとてもシンプルなドキュメントで、単一のTextコンポーネントを画面上に配置します。実際に表示されるテキストは、textプロパティで定義されている 「ハローワールド」です。このテキストは、textAlignプロパティとtextAlignVerticalプロパティによりviewportの中央に配置されます。

コードがアクセスできる場所にドキュメントを配置する

スキルのインテントハンドラーが、ドキュメントを含むJSONファイルをRenderDocumentディレクティブの一部として送信できる必要があります。そのため、このファイルはアクセスできる場所に配置してください。

クリップボードにコピーされました。

ドキュメントのJSONファイルをindex.jsファイルと同じフォルダに保存します。たとえば、Node.jsの「ハローワールド」スキルのディレクトリ構造では、helloworldDocument.jsonドキュメントがindex.jsと同じくlambdaフォルダに配置されています。

|   skill.json
|   
+---.ask
|       config
|       
+---hooks
|       pre_deploy_hook.ps1
|       
+---lambda
|       helloworldDocument.json
|       index.js
|       package.json
|       util.js
|       
\---models
        ja-JP.json

ドキュメントファイルがこの場所にあることで、ハンドラーはrequire()関数を使用してファイルを読み込むことができます。読み込みについては後述します。

クリップボードにコピーされました。

ドキュメントのJSONファイルをlambda_function.pyと同じレベルに保存します。たとえば、Pythonの「ハローワールド」スキルのディレクトリ構造では、helloworldDocument.jsonドキュメントがlambda_function.pyと同じくlambda/pyフォルダに配置されています。

|   skill.json
|
+---.ask
|       config
|
+---hooks
|       pre_deploy_hook.ps1
|
+---lambda
|   +---py
|   |   |   lambda_function.py
|   |   |   requirements.txt
|   |   |   helloworldDocument.json
\---models
        ja-JP.json

このように配置することで、ハンドラーはPythonのJSONクラスのloadメソッドと、ファイルI/Oのopenメソッドを使用して、ドキュメントファイルを読み込むことができます。読み込みについては後述します。

クリップボードにコピーされました。

ドキュメントJSONファイルをsrcと同じレベルのresourcesフォルダに保存し、pom.xmlファイルを更新して新しいresourcesフォルダを含めます。

たとえば、Javaの「ハローワールド」スキルのディレクトリ構造において、helloworldDocument.jsonドキュメントはresourcesフォルダにあります。

|   pom.xml
|   README.md
+---models
|       ja-JP.json
|       
+---resources
|       helloworldDocument.json
|       
+---src
|   |   HelloWorldStreamHandler.java
|   |   
|   +---handlers
|   |       CancelandStopIntentHandler.java
|   |       FallbackIntentHandler.java
|   |       HelloWorldIntentHandler.java
|   |       HelpIntentHandler.java
|   |       LaunchRequestHandler.java
|   |       SessionEndedRequestHandler.java
|   |       StartOverIntentHandler.java
|   |       
|   \---util
|           Constants.java
|           HelperMethods.java

これを基に、pom.xmlファイルの<build>セクションにresourcesフォルダを<resources>項目として指定します。

<build>
    <sourceDirectory>src</sourceDirectory>
    <resources>
      <resource>
        <directory>resources</directory>
      </resource>
    </resources>
</build>

このように配置することで、ハンドラーはJavaのFileクラスを使用してドキュメントファイルを読み込むことができます。読み込みについては後述します。

ユーザーのデバイスがAPLに対応していることを確認する

ユーザーは各種のAlexa搭載デバイスでスキルを呼び出すことができますが、画面付きと画面なしのデバイスがあります。応答にAPLディレクティブを含める前に、リクエストに含まれている対応インターフェースを必ずチェックし、デバイスでコンテンツを表示できることを確認してください。また、通常の音声出力がユーザーのデバイスに適していることも確認してください。たとえば、ユーザーのデバイスに画面がないのに「画面のXYZをご覧ください」などと発話することがないようにします。

デバイスがどのインターフェースに対応しているかは、すべてのリクエストに含まれているcontext.System.device.supportedInterfacesオブジェクトで確認できます。ユーザーのデバイスがAPLに対応している場合、このオブジェクトにはAlexa.Presentation.APLオブジェクトが含まれています。

クリップボードにコピーされました。

このサンプルコードはAlexa Skills Kit SDK for Node.js(v2)を使用しています。

Node.js SDKにはgetSupportedInterfaces関数が用意されており、ユーザーのデバイスが対応している全インターフェースのリストを取得できます。Alexa.Presentation.APLインターフェース対応であることをこのリストで確認してから、APLディレクティブを送信してください。

このインターフェースの参照に必要な構文にご注意ください。名前にドット(.)文字が含まれています。

// SDKのグローバル定数
const Alexa = require('ask-sdk-core');

//...

const HelloWorldIntentHandler = {
    canHandle(handlerInput) {
        // このハンドラーを使用するかどうかを判断するロジック       
    },
    handle(handlerInput) {
        // ...すべてのデバイスで使用されるその他のコード

        // ユーザーのデバイスがAPL対応かどうかを確認
        if (Alexa.getSupportedInterfaces(handlerInput.requestEnvelope)['Alexa.Presentation.APL']){
            // APLディレクティブの送信コードをここに配置
        }
    }

クリップボードにコピーされました。

このサンプルコードは、Alexa Skills Kit SDK for Pythonを使用しています。

utilsモジュールのget_supported_interfaces()メソッドを使用して、デバイスが対応している全インターフェースのリストを含むSupportedInterfacesオブジェクトを取得します。APL対応はalexa_presentation_aplアトリビュートの値で確認します。デバイスがAPL対応ではない場合、このアトリビュートには値としてNoneが格納されています。

from ask_sdk_core.dispatch_components import AbstractRequestHandler
from ask_sdk_core.handler_input import HandlerInput
from ask_sdk_model import Response
 
# リクエストutilsを使用
from ask_sdk_core.utils import get_supported_interfaces
 
# その他のインポートはここで
 
class HelloWorldIntentHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        # このハンドラーを使用するかどうかを判断するロジック
 
    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        if get_supported_interfaces(
                handler_input).alexa_presentation_apl is not None:
            # APLディレクティブの送信コードをここに配置

クリップボードにコピーされました。

このサンプルコードはAlexa Skills Kit SDK for Javaを使用しています。

RequestHelperクラスのgetSupportedInterfaces()メソッドを使用して、デバイスが対応している全インターフェースのリストを含むSupportedInterfacesオブジェクトを取得します。getAlexaPresentationAPL()を呼び出して、APL対応かどうかを確認します。デバイスがAPL対応ではない場合、このメソッドはnullを返します。

// RequestHelperクラスを使用
import com.amazon.ask.request.RequestHelper;
// (その他のインポートはここで)

public class HelloWorldIntentHandler implements RequestHandler {

    @Override
    public boolean canHandle(HandlerInput input) {
        // このハンドラーを使用するかどうかを判断するロジック
    }

    @Override
    public Optional<Response> handle(HandlerInput input) {
        // ...すべてのデバイスで使用されるその他のコード

        // ユーザーのデバイスがAPL対応かどうかを確認
        if (RequestHelper.forHandlerInput(input)
                .getSupportedInterfaces()
                .getAlexaPresentationAPL() != null) {

            // APLディレクティブの送信コードをここに配置
        }
    }
}

Alexa.Presentation.APLオブジェクトがcontext.System.device.supportedInterfacesに含まれているのは、以下がどちらも成立している場合です。

  • スキルがAPLに対応するよう設定されている。
  • ユーザーのデバイスがAPLに対応している。

APL対応のはずのデバイス(Echo Showなど)でスキルを呼び出したのに、上記のif文がfalseを返す場合は、スキルがAPL対応として設定されていません。どちらもAPLに対応するようスキルを設定するに戻ってください。

リクエストハンドラーの応答でRenderDocumentを返す

受信したリクエストに対処するリクエストハンドラーをスキルコードに作成します。このハンドラーは、Alexaが発話するためのテキストを含む応答と、その他の指示を含むディレクティブを返します。APLコンテンツを表示するには、Alexa.Presentation.APL.RenderDocumentディレクティブを含めます。このディレクティブのペイロードは、表示するAPLドキュメントです。

RenderDocumentに関する以下のガイドラインを参考にしてください。

  • RenderDocumentは、Alexa.Presentation.APLインターフェース対応のデバイスにのみ返します。前述の説明に従って対応状況を確認してください。
  • RenderDocumentディレクティブをビルドする際、このディレクティブのdocumentプロパティに渡すことができる変数にドキュメントJSONファイルを読み込む必要があります。
  • RenderDocumentディレクティブには必須のtokenプロパティがあります。ドキュメントを指定する文字列をここに設定してください。このトークンを使用して、ほかのディレクティブ(ExecuteCommandsなど)のドキュメントを特定したり、viewportに現在表示されているドキュメントを確認したりできます。以下のサンプルコードでは、トークンが「helloworldToken」に設定されています。

以下のサンプルは、前述のドキュメントを送信するシンプルなHelloWorldIntentHandlerのコードです。ユーザーのデバイスがAPL対応かどうかで、応答に含まれる発話が異なることにご注意ください。

クリップボードにコピーされました。

このサンプルコードはAlexa Skills Kit SDK for Node.js(v2)を使用しています。

このハンドラーは、スキルがHelloWorldIntent向けのIntentRequestを受信すると実行されます。helloworldDocument.jsonファイルを変数に読み込むには、require()関数を使用します。

const Alexa = require('ask-sdk-core');

// ハンドラーで使用するAPLドキュメントを読み込む
const helloworldDocument = require('./helloworldDocument.json');

// APLディレクティブの送信時に使用するトークン
const HELLO_WORLD_TOKEN = 'helloworldToken';

const HelloWorldIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldIntent';
    },
    handle(handlerInput) {
        let speakOutput = 'ハローワールド';
        let responseBuilder = handlerInput.responseBuilder;
        if (Alexa.getSupportedInterfaces(handlerInput.requestEnvelope)['Alexa.Presentation.APL']){
            
            // RenderDocumentディレクティブをresponseBuilderに追加する
            responseBuilder.addDirective({
                type: 'Alexa.Presentation.APL.RenderDocument',
                token: HELLO_WORLD_TOKEN,
                document: helloworldDocument
            });
            
            // 発話を画面付きデバイス向けにする
            speakOutput += "これでご挨拶が画面でも表示されるようになりました。"
        } else {
            // ユーザーのデバイスがAPL非対応であるため、発話を状況に合わせる
            speakOutput += "このサンプルをEcho ShowやFire TVのような画面付きデバイスで実行すると、もっと面白いことが起こります。";
        }
        return responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};

クリップボードにコピーされました。

このサンプルコードは、Alexa Skills Kit SDK for Pythonを使用しています。

このハンドラーは、スキルがHelloWorldIntent向けのIntentRequestを受信すると実行されます。helloworldDocument.jsonファイルを変数に読み込むには、openメソッドを使用してファイルを開き、json.loadメソッドを使用して内容をロードします。このサンプルで使用するユーティリティ関数_load_apl_documentは、ファイルパスを受け取り、ディレクティブで使用できるjsonドキュメントを返します。

ASK Python SDKには、RenderDocumentディレクティブを作成して応答に追加するためのモデルクラスが用意されています。

import json
 
from ask_sdk_core.utils import (
    is_intent_name, get_supported_interfaces)
from ask_sdk_core.handler_input import HandlerInput
from ask_sdk_core.dispatch_components import AbstractRequestHandler
 
from ask_sdk_model import Response
from ask_sdk_model.interfaces.alexa.presentation.apl import (
    RenderDocumentDirective)
 
from typing import Dict, Any
 
# ハンドラーで使用するAPLドキュメントファイルのパス
hello_world_doc_path = "helloworldDocument.json"
 
# APLディレクティブの送信時に使用するトークン
HELLO_WORLD_TOKEN = "helloworldToken"
 
 
def _load_apl_document(file_path):
    # type: (str) -> Dict[str, Any]
    """Load the apl json document at the path into a dict object."""
    with open(file_path) as f:
        return json.load(f)
 
 
class HelloWorldIntentHandler(AbstractRequestHandler):
    """ハローワールドインテント用ハンドラー。"""
 
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        return is_intent_name("HelloWorldIntent")(handler_input)
 
    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        speak_output = "ハローワールド"
        response_builder = handler_input.response_builder
 
        if get_supported_interfaces(
                handler_input).alexa_presentation_apl is not None:
            response_builder.add_directive(
                RenderDocumentDirective(
                    token=HELLO_WORLD_TOKEN,
                    document=_load_apl_document(hello_world_doc_path)
                )
            )
            # 発話を画面付きデバイス向けにする
            speak_output += ("これでご挨拶が画面でも表示されるように"
                             "なりました。")
        else:
            # ユーザーのデバイスがAPL非対応であるため、
            # 発話を状況に合わせる
            speak_output += ("このサンプルをEcho ShowやFire TVのような"
                             "画面付きデバイスで実行すると、もっと面白い"
                             "ことが起こります。")
 
        return response_builder.speak(speak_output).response

クリップボードにコピーされました。

このサンプルコードはAlexa Skills Kit SDK for Javaを使用しています。

このハンドラーは、スキルがHelloWorldIntent向けのIntentRequestを受信すると実行されます。helloworldDocument.jsonファイルを変数に読み込むには、JSONをパースしてStringObjectのマップ(HashMap<String, Object>)に変換します。このサンプルでは、Fileクラスを使用してファイルを読み込み、ObjectMapperクラスを使用してJSONをパースしてマップに変換します。

ASK Java SDKには、RenderDocumentディレクティブを作成して応答に追加するためのビルダーメソッドが用意されています。

package handlers;

import static com.amazon.ask.request.Predicates.intentName;
import static util.Constants.HELLO_WORLD_TOKEN; // String "helloworldToken"

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import com.amazon.ask.dispatcher.request.handler.HandlerInput;
import com.amazon.ask.dispatcher.request.handler.RequestHandler;
import com.amazon.ask.exception.AskSdkException;
import com.amazon.ask.model.Response;
import com.amazon.ask.model.interfaces.alexa.presentation.apl.RenderDocumentDirective;
import com.amazon.ask.request.RequestHelper;
import com.amazon.ask.response.ResponseBuilder;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

public class HelloWorldIntentHandler implements RequestHandler {

    @Override
    public boolean canHandle(HandlerInput input) {
        return input.matches(intentName("HelloWorldIntent"));
    }

    @Override
    public Optional<Response> handle(HandlerInput input) {
        String speechText = "ハローワールド";
        ResponseBuilder responseBuilder = input.getResponseBuilder();

        if (RequestHelper.forHandlerInput(input)
                .getSupportedInterfaces()
                .getAlexaPresentationAPL() != null) {
            try {
                // JSONドキュメントを取得し、文字列とオブジェクトをマッピングにする
                ObjectMapper mapper = new ObjectMapper();
                TypeReference<HashMap<String, Object>> documentMapType = 
                    new TypeReference<HashMap<String, Object>>() {};

                Map<String, Object> document = mapper.readValue(
                    new File("helloworldDocument.json"), 
                    documentMapType);

                // SDKのビルダーメソッドを使用してディレクティブを作成する
                RenderDocumentDirective renderDocumentDirective = RenderDocumentDirective.builder()
                        .withToken(HELLO_WORLD_TOKEN)
                        .withDocument(document)
                        .build();

                // ディレクティブをresponseBuilderに追加する
                responseBuilder.addDirective(renderDocumentDirective);

                // 発話を画面付きデバイス向けにする
                speechText += "これでご挨拶が画面でも表示されるようになりました。";

            } catch (IOException e) {
                throw new AskSdkException("ハローワールドドキュメントの読み込み、または逆シリアル化に失敗しました", e);
            }
        } else {
            // デバイスに画面がないため、発話出力を変更する
            speechText += "このサンプルをEcho ShowやFire TVのような画面付きデバイスで実行すると、もっと面白いことが起こります。";
        }

        // 発話を追加して応答を返す

        return responseBuilder
            .withSpeech(speechText)
            .withSimpleCard("APLを使用したハローワールド", speechText)
            .build();
    }

}


UserEventリクエストを処理する

スキルでAlexa.Presentation.APL.UserEventリクエストを使用すると、ユーザーがデバイス上で実行したアクションに対するメッセージを受信できます。これを行うには、APLドキュメントでSendEventコマンドを使用します。このコマンドは、スキルにUserEventリクエストを送信します。このリクエストに対するアクションを起こすには、スキルで標準リクエストハンドラーを使用します。

SendEventUserEventは、一般にボタンなどのUI要素向けに使用されます。ユーザーがボタンを操作したとき、SendEventコマンドが実行されるようボタンを定義します。こうしておくと、スキルがUserEventを受信し、ボタン操作に対応できます。

以降のセクションでは、これらについて説明します。

  • 前出のhelloworldDocument.jsonドキュメントを変更し、SendEventコマンドを追加する。
  • 新しいSendEventコマンドで生成されたUserEventを処理するハンドラーを追加する。

SendEventコマンドを実行するボタンのあるドキュメントを作成する

このサンプルドキュメントは、テキストの表示後、AlexaButtonレスポンシブ対応コンポーネントを表示します。AlexaButtonコンポーネントのprimaryActionプロパティには、ユーザーのボタン操作に対して実行するコマンドを定義します。ここでは、ボタン操作に対して次の2つのコマンドを順に実行します。 まず、AnimateItemを実行し、Textコンポーネントの不透明度を変えてテキストをフェードさせます。次に、SendEventを実行し、UserEventリクエストをスキルに送信します。

このドキュメントを、"helloworldWithButtonDocument.json"として前述の手順に従って保存します。

クリップボードにコピーされました。

{
  "type": "APL",
  "version": "1.0",
  "description": "このAPLドキュメントはテキストを画面に表示するとともに、操作されるとスキルにメッセージを送信するボタンを用意します。ボタンは、alexa-layoutsパッケージに用意されている定義済みのレスポンシブ対応コンポーネントです。",
  "import": [
    {
      "name": "alexa-layouts",
      "version": "1.1.0"
    }
  ],
  "mainTemplate": {
    "parameters": [
      "payload"
    ],
    "items": [
      {
        "type": "Container",
        "height": "100vh",
        "width": "100vw",
        "items": [
          {
            "type": "Text",
            "id": "helloTextComponent",
            "height": "75%",
            "text": "ハローワールド。 このAPLドキュメントにはalexa-layoutsパッケージのボタンが含まれています。ボタンを押して動作を確認してください。",
            "textAlign": "center",
            "textAlignVertical": "center",
            "paddingLeft": "@spacingSmall",
            "paddingRight": "@spacingSmall",
            "paddingTop": "@spacingXLarge",
            "style": "textStyleBody"
          },
          {
            "type": "AlexaButton",
            "alignSelf": "center",
            "id": "fadeHelloTextButton",
            "buttonText": "これはボタンです",
            "primaryAction": [
              {
                "type": "AnimateItem",
                "duration": 3000,
                "componentId": "helloTextComponent",
                "value": {
                  "property": "opacity",
                  "to": 0
                }
              },
              {
                "type": "SendEvent",
                "arguments": [
                  "ユーザーがボタンを押しました"
                ]
              }
            ]
          }
        ]
      }
    ]
  }
}

このドキュメントを表示するには、インテントハンドラーからRenderDocumentを返します。このサンプルで表示されるドキュメントはhelloworldWithButtonDocument.jsonトークンhelloworldWithButtonTokenです。

クリップボードにコピーされました。

このサンプルコードはAlexa Skills Kit SDK for Node.js(v2)を使用しています。

const Alexa = require('ask-sdk-core');

// ハンドラーで使用するAPLドキュメントを読み込む
const helloworldDocument = require('./helloworldDocument.json');
const helloworldWithButtonDocument = require('./helloworldWithButtonDocument.json');

// APLディレクティブの送信時に使用するトークン
const HELLO_WORLD_TOKEN = 'helloworldToken';
const HELLO_WORLD_WITH_BUTTON_TOKEN = 'helloworldWithButtonToken';

const HelloWorldWithButtonIntentHander = {
    canHandle(handlerInput){
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'HelloWorldWithButtonIntent';
    },
    handle(handlerInput){
        let speakOutput = "ハローワールド";
        let responseBuilder = handlerInput.responseBuilder;
        if (Alexa.getSupportedInterfaces(handlerInput.requestEnvelope)['Alexa.Presentation.APL']){
            
            // RenderDocumentディレクティブをresponseBuilderに追加する
            responseBuilder.addDirective({
                type: 'Alexa.Presentation.APL.RenderDocument',
                token: HELLO_WORLD_WITH_BUTTON_TOKEN,
                document: helloworldWithButtonDocument,
            });
            
            // 発話を画面付きデバイス向けにする
            speakOutput += "Alexa Presentation Languageへようこそ。ボタンを押して動作を確認してください。"
        } else {
            speakOutput += "このサンプルをEcho ShowやFire TVのような画面付きデバイスで実行すると、もっと面白いことが起こります。"
        }
        return responseBuilder
            .speak(speakOutput)
            //.reprompt('ユーザーが応答できるようセッションを開いたままにする場合は再プロンプトを追加してください')
            .getResponse();
    }
}

クリップボードにコピーされました。

このサンプルコードは、Alexa Skills Kit SDK for Pythonを使用しています。

import json
 
from ask_sdk_core.utils import (
    is_intent_name, get_supported_interfaces)
from ask_sdk_core.handler_input import HandlerInput
from ask_sdk_core.dispatch_components import AbstractRequestHandler
 
from ask_sdk_model import Response
from ask_sdk_model.interfaces.alexa.presentation.apl import (
    RenderDocumentDirective)
 
from typing import Dict, Any
 
# ハンドラーで使用するAPLドキュメントファイルのパス
hello_world_doc_path = "helloworldDocument.json"
hello_world_button_doc_path = "helloworldWithButtonDocument.json"
 
# APLディレクティブの送信時に使用するトークン
HELLO_WORLD_TOKEN = "helloworldToken"
HELLO_WORLD_WITH_BUTTON_TOKEN = "helloworldWithButtonToken"
 
 
def _load_apl_document(file_path):
    # type: (str) -> Dict[str, Any]
    """Load the apl json document at the path into a dict object."""
    with open(file_path) as f:
        return json.load(f)
 
 
class HelloWorldWithButtonIntentHandler(AbstractRequestHandler):
    """ハローワールドインテント用ハンドラー。"""
 
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        return is_intent_name("HelloWorldWithButtonIntent")(handler_input)
 
    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        speak_output = "ハローワールド"
        response_builder = handler_input.response_builder
 
        if get_supported_interfaces(
                handler_input).alexa_presentation_apl is not None:
            response_builder.add_directive(
                RenderDocumentDirective(
                    token=HELLO_WORLD_WITH_BUTTON_TOKEN,
                    document=_load_apl_document(hello_world_button_doc_path)
                )
            )
            # 発話を画面付きデバイス向けにする
            speak_output += ("Alexa Presentation Languageへようこそ。"
                             "ボタンを押して動作を確認してください。")
        else:
            # ユーザーのデバイスがAPL非対応であるため、
            # 発話を状況に合わせる
            speak_output += ("このサンプルをEcho ShowやFire TVのような"
                             "画面付きデバイスで実行すると、もっと面白い"
                             "ことが起こります。")
 
        return response_builder.speak(speak_output).response

クリップボードにコピーされました。

このサンプルコードはAlexa Skills Kit SDK for Javaを使用しています。

package handlers;

import static com.amazon.ask.request.Predicates.intentName;
import static util.Constants.HELLO_WORLD_WITH_BUTTON_TOKEN; // String "helloworldWithButtonToken"

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import com.amazon.ask.dispatcher.request.handler.HandlerInput;
import com.amazon.ask.dispatcher.request.handler.RequestHandler;
import com.amazon.ask.exception.AskSdkException;
import com.amazon.ask.model.Response;
import com.amazon.ask.model.interfaces.alexa.presentation.apl.RenderDocumentDirective;
import com.amazon.ask.request.RequestHelper;
import com.amazon.ask.response.ResponseBuilder;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

public class HelloWorldWithButtonIntentHandler implements RequestHandler {

    @Override
    public boolean canHandle(HandlerInput input) {
        return input.matches(intentName("HelloWorldWithButtonIntent"));
    }

    @Override
    public Optional<Response> handle(HandlerInput input) {
        String speechText = "ハローワールド";
        ResponseBuilder responseBuilder = input.getResponseBuilder();

        if (RequestHelper.forHandlerInput(input)
                .getSupportedInterfaces()
                .getAlexaPresentationAPL() != null) {
            try {
                // ハローワールドAPLドキュメントを含むJSONファイルを
                //「resources」フォルダから取得し、
                //  ディレクティブに追加する
                ObjectMapper mapper = new ObjectMapper();
                TypeReference<HashMap<String, Object>> documentMapType = new TypeReference<HashMap<String, Object>>() {
                };
                Map<String, Object> document = mapper.readValue(new File("helloworldWithButtonDocument.json"), documentMapType);

                // ディレクティブをビルドする
                RenderDocumentDirective renderDocumentDirective = RenderDocumentDirective.builder()
                    .withToken(HELLO_WORLD_WITH_BUTTON_TOKEN)
                    .withDocument(document)
                    .build();
                
                responseBuilder.addDirective(renderDocumentDirective);
                            
                // 発話を更新して画面について言及する
                speechText += "Alexa Presentation Languageへようこそ。ボタンを押して動作を確認してください。";

            } catch (IOException e) {
                throw new AskSdkException("ハローワールドドキュメントの読み込み、または逆シリアル化に失敗しました", e);
            }

        } else {
            speechText += "このサンプルをEcho ShowやFire TVのような画面付きデバイスで実行すると、もっと面白いことが起こります。";
        }

        return responseBuilder
            .withSpeech(speechText)
            .withSimpleCard("APLを使用したハローワールド", speechText)
            .build();
    }
}

UserEventリクエストハンドラーを追加する

前出のサンプルでユーザーがボタンを操作すると、画面に表示されていたテキストがフェードアウトします(AnimateItemコマンド)。続いて、AlexaがスキルにUserEventリクエストを送信します(SendEventコマンド)。スキルはそれに応答して、Alexaの標準的な発話や新しいAPLディレクティブなどのアクションを起こすことができます。

一般に、UserEventハンドラーはイベントのトリガー元コンポーネントを特定する必要があります。これは、スキルに複数のドキュメントがあってボタンなどの各種要素を使用しており、そうした要素がリクエストをスキルに送信できる、という可能性があるからです。このようなリクエストの型は、すべてAlexa.Presentation.APL.UserEventです。イベントの識別には、リクエストのsourceプロパティまたはargumentsプロパティを使用できます。

たとえば、前出のドキュメントに定義されているボタン操作があると、次のリクエストが送信されます。

{
  "request": {
    "type": "Alexa.Presentation.APL.UserEvent",
    "requestId": "amzn1.echo-api.request.1",
    "timestamp": "2019-10-04T18:48:22Z",
    "locale": "ja-JP",
    "arguments": [
      "ユーザーがボタンを押しました"
    ],
    "components": {},
    "source": {
      "type": "TouchWrapper",
      "handler": "Press",
      "id": "fadeHelloTextButton",
      "value": false
    },
    "token": "helloworldWithButtonToken"
  }
}

argumentsプロパティには、SendEventコマンドのargumentsプロパティに定義されている引数の配列が含まれます。sourceプロパティには、イベントをトリガーしたコンポーネントに関する情報としてコンポーネントのIDなどが含まれます。

したがって、ハンドラーでは、ドキュメントに定義されているコンポーネントのIDと同じrequest.source.idを持つ任意のUserEventリクエストに対処できます。このハンドラーは、ユーザーによるボタン操作「fadeHelloTextButton」に対し、ユーザーがボタンを押したことに触れる発話で対処します。

クリップボードにコピーされました。

このサンプルコードはAlexa Skills Kit SDK for Node.js(v2)を使用しています。

const HelloWorldButtonEventHandler = {
    canHandle(handlerInput){
        // APLスキルにはUserEventを生成するボタンが複数存在することがあるため、
        // このイベントをトリガーしたボタン操作をイベントソースIDで判断し、
        // 適切なハンドラーを使用します。このサンプルでドキュメントのAlexaButtonに
        // 設定されているIDの文字列は「fadeHelloTextButton」です。
        
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'Alexa.Presentation.APL.UserEvent'
            && handlerInput.requestEnvelope.request.source.id === 'fadeHelloTextButton';
    },
    handle(handlerInput){
        const speakOutput = "ボタン操作をありがとうございます。 もうお気づきかと思いますが、テキストが消えました。「最初から始めて」と命令すると再び表示されます。";
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt("テキストを再び表示するには、「最初から始めて」と言います。または、もう一度「こんにちは」と言ってください。")
            .getResponse();
    }
}

クリップボードにコピーされました。

このサンプルコードは、Alexa Skills Kit SDK for Pythonを使用しています。

from ask_sdk_core.utils import (
    is_request_type, is_intent_name, get_supported_interfaces)
from ask_sdk_core.handler_input import HandlerInput
from ask_sdk_core.dispatch_components import AbstractRequestHandler
from ask_sdk_model import Response
from ask_sdk_model.interfaces.alexa.presentation.apl import UserEvent
 
 
class HelloWorldButtonEventHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        # APLスキルにはUserEventを生成するボタンが複数存在することがあるため、
        # このイベントをトリガーしたボタン操作をイベントソースIDで判断し、
        # 適切なハンドラーを使用します。
        # このサンプルでドキュメントのAlexaButtonに設定されているIDの
        # 文字列は「fadeHelloTextButton」です。
 
        # user_event.sourceはdictオブジェクトです。idの取得には
        # dictionaryのgetメソッドを使用します。
        if is_request_type("Alexa.Presentation.APL.UserEvent")(handler_input):
            user_event = handler_input.request_envelope.request  # type: UserEvent
            return user_event.source.get("id") == "fadeHelloTextButton"
        else:
            return False
 
    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        speech_text = ("ボタン操作をありがとうございます。 もうお気づき"
                       "かと思いますが、テキストが消えました。「最初から"
                       "始めて」と命令すると再び表示されます。")
 
        return handler_input.response_builder.speak(speech_text).ask(
            "テキストを再び表示するには、「最初から始めて」と言います。"
            "または、もう一度「こんにちは」と言ってください。").response

クリップボードにコピーされました。

このサンプルコードはAlexa Skills Kit SDK for Javaを使用しています。

package handlers;

import java.util.Map;
import java.util.Optional;

import com.amazon.ask.dispatcher.request.handler.HandlerInput;
import com.amazon.ask.dispatcher.request.handler.impl.UserEventHandler;
import com.amazon.ask.model.Response;
import com.amazon.ask.model.interfaces.alexa.presentation.apl.UserEvent;

public class HelloWorldButtonEventHandler implements UserEventHandler {

    @Override
    public boolean canHandle(HandlerInput input, UserEvent userEvent) {      
        // これは型指定のあるハンドラーであるため、UserEvent専用として実行されます。
        // APLスキルにはUserEventを生成するボタンが複数存在することがあるため、
        // このイベントをトリガーしたボタン操作をイベントソースIDで判断し、
        // 適切なハンドラーを使用します。このサンプルで
        // 設定されているIDの文字列は「fadeHelloTextButton」です。

        // userEvent.getSource()メソッドはObjectを返します。Mapにキャストすることで
        // idを取得できます。
        Map<String,Object> eventSourceObject = (Map<String,Object>) userEvent.getSource();
        String eventSourceId = (String) eventSourceObject.get("id");
        return eventSourceId.equals("fadeHelloTextButton");
    }

    @Override
    public Optional<Response> handle(HandlerInput input, UserEvent userEvent) {

        String speechText = "ボタン操作をありがとうございます。 もうお気づきかと思いますが、テキストが消えました。「最初から始めて」と命令すると再び表示されます。";
        
        return input.getResponseBuilder()
            .withSpeech(speechText)
            .withReprompt("テキストを再び表示するには、「最初から始めて」と言います。または、もう一度「こんにちは」と言ってください。")
            .build();
    }
    
}

上記のコードは次のように発話するようAlexaに命令します。「ボタン操作をありがとうございます。 もうお気づきかと思いますが、テキストが消えました。『最初から始めて』と命令すると再び表示されます」

リクエストハンドラーの応答でExecuteCommandsを返す

スキルでは、Alexa.Presentation.APL.ExecuteCommandsディレクティブを使用するAPLコマンドをトリガーできます。これにより、ユーザーとの対話への応答として、発話などのコマンドを実行できます。

ExecuteCommandsディレクティブには、必須のtokenプロパティがあります。これが、現在表示されているドキュメントのRenderDocumentディレクティブに指定されているtokenと一致する必要があります。トークンが一致しない場合、デバイスはコマンドを実行しません。ExecuteCommandsディレクティブを送信して、以前のディレクティブでレンダリングされたドキュメントに対してこのディレクティブを実行するには、事前にリクエストのcontextをチェックして、想定ドキュメントが表示されることを確認してください。

ExecuteCommandsディレクティブは、RenderDocumentディレクティブと同じ応答に含めることもできます。この場合も、tokenが両ディレクティブで一致する必要があります。

このサンプルコードではStartOverIntentHandlerを追加します。ユーザーが「最初から始めて」と命令すると、スキルは現在の表示コンテキストをチェックし、デバイスに表示されているテキストのトークンが「helloworldWithButtonToken」であるかどうかを確認します。確認できると、スキルはAnimateItemコマンドを含むExecuteCommandsを送信します。このコマンドでは、Text要素の不透明度を3秒かけて0から1に戻してフェードイン効果を実現します。

クリップボードにコピーされました。

このサンプルコードはAlexa Skills Kit SDK for Node.js(v2)を使用しています。

const Alexa = require('ask-sdk-core');

// ハンドラーで使用するAPLドキュメントを読み込む
const helloworldDocument = require('./helloworldDocument.json');
const helloworldWithButtonDocument = require('./helloworldWithButtonDocument.json');

// APLディレクティブの送信時に使用するトークン
const HELLO_WORLD_TOKEN = 'helloworldToken';
const HELLO_WORLD_WITH_BUTTON_TOKEN = 'helloworldWithButtonToken';

const StartOverIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StartOverIntent';
    },
    handle(handlerInput) {
        let speakOutput = '';
        let responseBuilder = handlerInput.responseBuilder;
        
        if (Alexa.getSupportedInterfaces(handlerInput.requestEnvelope)['Alexa.Presentation.APL']) {
            speakOutput = "";
            
            // APLの視覚コンテキスト情報を取得し、デバイスがトークン
            // HELLO_WORLD_WITH_BUTTON_TOKENを持つドキュメントを表示しているかどうかを判断します。
            const contextApl = handlerInput.requestEnvelope.context['Alexa.Presentation.APL'];
            if (contextApl && contextApl.token === HELLO_WORLD_WITH_BUTTON_TOKEN){
                // ExecuteCommandsディレクティブをビルドします。この中で、Textコンポーネントの
                // 不透明度を1に戻して再び表示されるようにします。ディレクティブに含まれるトークンが、
                // 表示されているドキュメントのトークンと一致する必要があることに注意してください。
                
                speakOutput = "では、テキストを再び表示してみましょう。";
                
                // コンポーネントの不透明度を3秒かけて1に戻す
                // APLコマンドを作成します
                const animateItemCommand = {
                    type: "AnimateItem",
                    componentId: "helloTextComponent",
                    duration: 3000,
                    value: [
                        {
                            property: "opacity",
                            to: 1
                        }
                    ]
                }
                
                // ExecuteCommandsディレクティブにコマンドを追加し、それを
                // 応答に追加します。
                responseBuilder.addDirective({
                    type: 'Alexa.Presentation.APL.ExecuteCommands',
                    token: HELLO_WORLD_WITH_BUTTON_TOKEN,
                    commands: [animateItemCommand]
                })

                
            } else {
                // デバイスが想定したドキュメントを表示していないため、
                // 状況に応じた出力音声を用意します。
                speakOutput = "おや、リセット対象が何もありません。「ボタンインテントを使用するハローワールド」を呼び出してからボタンを押して、動作を確認してください。";
            }
        } else {
            speakOutput = "こんにちは。このサンプルは画面付きデバイスを使うと、もっと面白いことが起こります。Echo Show、Echo Spot、Fire TVのいずれかのデバイスで試してください。";
        }
        
        return responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};

クリップボードにコピーされました。

このサンプルコードは、Alexa Skills Kit SDK for Pythonを使用しています。

import json
 
from ask_sdk_core.utils import (
    is_request_type, is_intent_name, get_supported_interfaces)
from ask_sdk_core.handler_input import HandlerInput
from ask_sdk_core.dispatch_components import AbstractRequestHandler
 
from ask_sdk_model import Response
from ask_sdk_model.interfaces.alexa.presentation.apl import (
    RenderDocumentDirective, AnimatedOpacityProperty, AnimateItemCommand,
    ExecuteCommandsDirective, UserEvent)
from typing import Dict, Any
 
 
# ハンドラーで使用するAPLドキュメントファイルのパス
hello_world_doc_path = "helloworldDocument.json"
hello_world_button_doc_path = "helloworldWithButtonDocument.json"
 
# APLディレクティブの送信時に使用するトークン
HELLO_WORLD_TOKEN = "helloworldToken"
HELLO_WORLD_WITH_BUTTON_TOKEN = "helloworldWithButtonToken"
 
 
def _load_apl_document(file_path):
    # type: (str) -> Dict[str, Any]
    """Load the apl json document at the path into a dict object."""
    with open(file_path) as f:
        return json.load(f)
 
 
class StartOverIntentHandler(AbstractRequestHandler):
    """StartOverIntentのハンドラーです。"""
 
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool
        return is_intent_name("AMAZON.StartOverIntent")(handler_input)
 
    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        speak_output = None
        response_builder = handler_input.response_builder
 
        if get_supported_interfaces(
                handler_input).alexa_presentation_apl is not None:
            # JSONリクエストのAPL視覚コンテンツ情報を取得します。
            # 現在表示されているドキュメントのIDが
            # HELLO_WORLD_WITH_BUTTON_TOKENトークン("helloworldWithButtonToken")
            # であることを確認します。
            context_apl = handler_input.request_envelope.context.alexa_presentation_apl
            if (context_apl is not None and
                    context_apl.token == HELLO_WORLD_WITH_BUTTON_TOKEN):
                speak_output = ("では、テキストを再び表示"
                                "してみましょう。")
                animate_item_command = AnimateItemCommand(
                    component_id="helloTextComponent",
                    duration=3000,
                    value=[AnimatedOpacityProperty(to=1.0)]
                )
                response_builder.add_directive(
                    ExecuteCommandsDirective(
                        token=HELLO_WORLD_WITH_BUTTON_TOKEN,
                        commands=[animate_item_command]
                    )
                )
            else:
                # デバイスが想定したドキュメントを表示していないため、
                # 状況に応じた出力音声を用意します。
                speak_output = ("おや、リセット対象が何もありません。 "
                                "「ボタンインテントを使用するハローワールド」を"
                                "呼び出してからボタンを押して、動作を"
                                "確認してください。")
        else:
            # ユーザーのデバイスがAPL非対応であるため、
            # 発話を状況に合わせる
            speak_output += ("このサンプルを画面付きデバイスで実行すると"
                             "もっと面白いことが起こります。Echo Show、Echo Spot、"
                             "Fire TVのいずれかのデバイスで試してください")
 
        return response_builder.speak(speak_output).response
 

クリップボードにコピーされました。

このサンプルコードはAlexa Skills Kit SDK for Javaを使用しています。

package handlers;

import static com.amazon.ask.request.Predicates.intentName;
import static util.Constants.HELLO_WORLD_WITH_BUTTON_TOKEN; // String "helloworldWithButtonToken"

import java.util.Optional;

import com.amazon.ask.dispatcher.request.handler.HandlerInput;
import com.amazon.ask.dispatcher.request.handler.RequestHandler;
import com.amazon.ask.model.Response;
import com.amazon.ask.model.interfaces.alexa.presentation.apl.AnimateItemCommand;
import com.amazon.ask.model.interfaces.alexa.presentation.apl.AnimatedOpacityProperty;
import com.amazon.ask.model.interfaces.alexa.presentation.apl.ExecuteCommandsDirective;
import com.amazon.ask.request.RequestHelper;
import com.amazon.ask.response.ResponseBuilder;
import com.fasterxml.jackson.databind.JsonNode;

public class StartOverIntentHandler implements RequestHandler {
    @Override
    public boolean canHandle(HandlerInput input) {
        return input.matches(intentName("AMAZON.StartOverIntent"));
    }

    @Override
    public Optional<Response> handle(HandlerInput input) {
        String speechText;
        ResponseBuilder responseBuilder = input.getResponseBuilder();        
        
        if (RequestHelper.forHandlerInput(input)
            .getSupportedInterfaces()
            .getAlexaPresentationAPL() != null) {
            // JSONリクエストのAPL視覚コンテンツ情報を取得します。
            // 現在表示されているドキュメントのIDが
            // HELLO_WORLD_WITH_BUTTON_TOKENトークン("helloworldWithButtonToken")
            // であることを確認します。
            JsonNode context = input.getRequestEnvelopeJson().get("context");
            if (context.has("Alexa.Presentation.APL") && 
                (context.get("Alexa.Presentation.APL")
                    .get("token")
                    .asText()
                    .equals(HELLO_WORLD_WITH_BUTTON_TOKEN))){

                speechText = "では、テキストを再び表示してみましょう。";

                AnimatedOpacityProperty animatedOpacityProperty = AnimatedOpacityProperty.builder()
                    .withTo(1.0)
                    .build();
    
                AnimateItemCommand animateItemCommand = AnimateItemCommand.builder()
                    .withComponentId("helloTextComponent")
                    .withDuration(3000)
                    .addValueItem(animatedOpacityProperty)
                    .build();

                    // ExecuteCommandsのトークンが
                    // もともとドキュメントを表示していたRenderDocumentディレクティブに
                    // 指定されているトークンと一致することが必要です
                    ExecuteCommandsDirective executeCommandsDirective = ExecuteCommandsDirective.builder()
                        .withToken(HELLO_WORLD_WITH_BUTTON_TOKEN)
                        .addCommandsItem(animateItemCommand)
                        .build();
                
                    responseBuilder.addDirective(executeCommandsDirective);
            } else {
                // デバイスが想定したドキュメントを表示していないため、
                // 状況に応じた出力音声を用意します。
                speechText = "おや、リセット対象が何もありません。「ボタンインテントを使用するハローワールド」を呼び出してからボタンを押して、動作を確認してください。";
            }
            
        } else {
            speechText = "こんにちは。このサンプルは画面付きデバイスを使うと、もっと面白いことが起こります。Echo Show、Echo Spot、Fire TVのいずれかのデバイスで試してください。";
        }
     
        return responseBuilder
                .withSpeech(speechText)
                .build();
    }
}