LoginSignup
43
29

More than 5 years have passed since last update.

Alexa Skills Kit SDK for Node.js ざっくり訳

Last updated at Posted at 2018-02-17

このポストは alexa-skills-kit-sdk-for-nodejsについてのものだが、V2がリリースされ、すでに過去のバージョンについてのものになった。バージョンが上がり、SDKの機能も実装スタイルも変わったので、リポジトリのV2のドキュメントの日本語訳を参考にされるのがよいと思う。

v1についてのドキュメントを参照するならば、
2018年3月、リポジトリに日本語版が追加されたのでそちらを参照いただきたい。


以下は私がalexa-skills-kit-sdk-for-nodejs/Readme.mdをざっくり訳したもの。v1のものである。

Overview

Alexa SDKチームは新しいAlexa Node.js SDKをリリースする。これは開発者による開発者のためのオープンソースのSDKだ。

Alexa Skill Kit、Node.jsそしてLambdaの組み合わせは最も人気あるAlexaスキル開発のソフトウェアスタックだ。
イベントドリブンでノンブロッキングなI/Oを持つNode.jsはAlexaスキルによく合っており、Node.jsはオープンソースライブラリの巨大なエコシステムでもある。加えて、AWS Lambdaは100万リクエスト/月が無料でほとんどの開発者にとって十分な無料枠だ。また、Alexaトリガーは信頼できるので、LambdaのSSL証明書の管理もいらない。

AWS Lambdaと Node.js、Alexa Skills Kitを使ったAlexaスキルのセットアップはシンプルだ。しかもコードも少なくて済む。Alexa SDKチームはAlexa Skills Kit SDK specifically for Node.js を共通の問題を簡単に実装し、スキルのロジックに注力できるように作った。

新しいalexa-sdkを使えば、早くそして、不要な複雑さを避けた開発ができる。
SDKは次の特徴がある。

  • npmパッケージなのでどんなNode.js環境にも容易にデプロイできる
  • 組み込みイベントを使ってAlexaのレスポンスを生成できる
  • 新規セッションと未処理イベントのためのキャッチオールイベント
  • インテント処理ベースのインベントマシーンを作るヘルパ関数
  • 属性をDynamoDBに永続化できるシンプルな設定
  • すべての音声出力は自動的にSSMLでラップされる
  • this.eventthis.contextを通じて、Lambdaのイベントとコンテキストも利用できる
  • 組み込み関数をオーバーライドして状態管理やレスポンスの生成を柔軟に変更できる。たとえば、S3に状態を保存することも可能だ。

Setup Guide

alexa-sdkはGithubからすぐに利用できる。次のコマンドでインストールできる。

npm install --save alexa-sdk

Getting Started: Writing a Hello World Skill

Basic Project Structure

HelloWorldスキルでは次が必要。

  • スキルのエントリーポイント。
    • すべての必要なパッケージ、イベントの受信、appIdの設定、dynamoDBテーブルの設定、ハンドラの登録などが含まれる。
  • それぞれのリクエストを扱うハンドラ関数

Set Entry Point

上記のため、次の内容でindex.jsを作る

index.js
const Alexa = require('alexa-sdk');

exports.handler = function(event, context, callback) {
    const alexa = Alexa.handler(event, context, callback);
    alexa.appId = APP_ID // APP_ID は Amazon developer consoleでスキルを作ると発行されるskill id
    alexa.execute();
};

これで、alexa-sdkをインポートして、Alexaオブジェクトを生成できる。

Implement Handler Functions

次に必要なのは、スキルのイベントとインテントを処理すること。Alexa-sdkを使えばインテントを簡単に扱える。
ハンドラ関数をindex.jsに実装すればよい。あるいは別のファイルに実装してインポートしても良い。
例えば、HelloWorldIntent ハンドラを作るには2つの実装方法がある。

const handlers = {
    'HelloWorldIntent' : function() {
        //emit response directly
        this.emit(':tell', 'Hello World!');
    }
};

あるいは次の様にも書ける。

const handlers = {
    'HelloWorldIntent' : function() {
        //build response first using responseBuilder and then emit
        this.response.speak('Hello World!');
        this.emit(':responseReady');
    }
};

Alexa-sdk では responseBuilderspeak/listen で生成する発話レスポンスオブジェクトを tell/askを使って実装することもできる。

this.emit(':tell', 'Hello World!'); 
this.emit(':ask', 'What would you like to do?', 'Please say that again?');

これは次と同じだ。

this.response.speak('Hello World!');
this.emit(':responseReady');

this.response.speak('What would you like to do?')
             .listen('Please say that again?');
this.emit(':responseReady');

:ask/listen:tell/speakの違いは :tell/speakがユーザのインプットの待受をやめてセッションを終了するということだ。
次節でレスポンスオブジェクトを作る場合の2つの方法の違いについて比較する。

ハンドラは互いにリクエストを送り合うことができる。ハンドラのチェーンも可能だ。
次の例では LaunchRequestHelloWorldIntent が同じ'Hello World'を返す。

const handlers = {
    'LaunchRequest': function () {
        this.emit('HelloWorldIntent');
    },

    'HelloWorldIntent': function () {
        this.emit(':tell', 'Hello World!');
    }
};

イベントハンドラーを作ったら、それを registerHandlers でAlexaオブジェクトに登録する必要がある。
index.js に次のように追加する

index.js
const Alexa = require('alexa-sdk');

exports.handler = function(event, context, callback) {
    const alexa = Alexa.handler(event, context, callback);
    alexa.registerHandlers(handlers); //<= ここを追加
    alexa.execute();
};

複数のハンドラを一度に登録することもできる。

alexa.registerHandlers(handlers, handlers2, handlers3, ...);

ここまでのステップを終えれば、スキルはデバイスで動作する。

Response vs ResponseBuilder

Node.js SDK ではresponse objectsを生成する2つの方法がある。

一つ目の方法は this.emit(:${action}, 'responseContent') だ。
以下に一般的なスキルのレスポンスの完全なリストの例を示す。

Response Syntax Description
this.emit(':tell',speechOutput); Tell with speechOutput
this.emit(':ask', speechOutput, repromptSpeech); Ask with speechOutput and repromptSpeech
this.emit(':tellWithCard', speechOutput, cardTitle, cardContent, imageObj); Tell with speechOutput and standard card
this.emit(':askWithCard', speechOutput, repromptSpeech, cardTitle, cardContent, imageObj); Ask with speechOutput, repromptSpeech and standard card
this.emit(':tellWithLinkAccountCard', speechOutput); Tell with linkAccount card, for more information, click here
this.emit(':askWithLinkAccountCard', speechOutput); Ask with linkAccount card, for more information, click here
this.emit(':tellWithPermissionCard', speechOutput, permissionArray); Tell with permission card, for more information, click here
this.emit(':askWithPermissionCard', speechOutput, repromptSpeech, permissionArray) Ask with permission card, for more information, click here
this.emit(':delegate', updatedIntent); Response with delegate directive in dialog model
this.emit(':elicitSlot', slotToElicit, speechOutput, repromptSpeech, updatedIntent); Response with elicitSlot directive in dialog model
this.emit(':elicitSlotWithCard', slotToElicit, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj); Response with card and elicitSlot directive in dialog model
this.emit(':confirmSlot', slotToConfirm, speechOutput, repromptSpeech, updatedIntent); Response with confirmSlot directive in dialog model
this.emit(':confirmSlotWithCard', slotToConfirm, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj); Response with card and confirmSlot directive in dialog model
this.emit(':confirmIntent', speechOutput, repromptSpeech, updatedIntent); Response with confirmIntent directive in dialog model
this.emit(':confirmIntentWithCard', speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj); Reponse with card and confirmIntent directive in dialog model
this.emit(':responseReady'); レスポンスを作る前でなく、後に呼ぶこと。これはAlexaサービスに処理を返す。また、saveStateを呼ぶ。オーバーライド可能
this.emit(':saveState', false); this.attributesの内容と現在のハンドラの状態をDynamoDBに保存する。前もって作ったレスポンスをAlexaサービスに送る。もし別の永続化を使いたければオーバーライド可能。第2引数は任意で、trueにすると強制保存する
this.emit(':saveStateError'); 状態保存時にエラーが発生すれば呼ばれる。エラー処理をオーバーライドできる

もし、自分でレスポンス生成を実装するなら、this.responseを使う。this.response
レスポンスの属性をそれぞれ設定する関数群を持っている。
Alexa Skills Kit組み込みの音声プレイヤー、動画ブレイヤーをサポートする時に便利だ。
レスポンスを作ったら、this.emit(':responseReady')を実行すれば、Alexaに送信される。
this.responseの関数はチェーン可能なので、1行に実装もできる。

次は responseBuilder を使う場合のレスポンス作成例のリストだ。

Response Syntax Description
this.response.speak(speechOutput); 最初の発話出力を speechOutputに設定する
this.response.listen(repromptSpeech); 聞き返しの発話出力を repromptSpeechにセットし shouldEndSessionfalseにする。この関数が呼ばれなければ, this.response の shouldEndSession は trueとなる.
this.response.cardRenderer(cardTitle, cardContent, cardImage); レスポンスに standard cardを追加する。 cardTitle, cardContent and cardImage で構成される。
this.response.linkAccountCard(); レスポンスに linkAccount card を追加する。 こちらを参照のこと
this.response.askForPermissionsConsentCard(permissions); perimissionを求めるカードをレスポンスに追加する。 こちらを参照のこと
this.response.audioPlayer(directiveType, behavior, url, token, expectedPreviousToken, offsetInMilliseconds);(Deprecated) AudioPlayer directive をパラメータとともにレスポンスに追加する
this.response.audioPlayerPlay(behavior, url, token, expectedPreviousToken, offsetInMilliseconds); AudioPlayer directive をパラメータとともに追加し、AudioPlayer.Playをセットする
this.response.audioPlayerStop(); AudioPlayer.Stop directiveを追加する
this.response.audioPlayerClearQueue(clearBehavior); AudioPlayer.ClearQueue directiveを追加し、 directiveのclearBehaviorをセットする
this.response.renderTemplate(template); Display.RenderTemplate directive をレスポンスに追加する
this.response.hint(hintText, hintType); Hint directive をレスポンスに追加する
this.response.playVideo(videoSource, metadata); VideoApp.Play directiveをレスポンスに追加する
this.response.shouldEndSession(bool); shouldEndSession を追加する

レスポンスを生成したら、this.emit(':responseReady')を呼ぶだけで送信される。
次の2つの例はレスポンスオブジェクトを生成する。

//Example 1
this.response.speak(speechOutput)
            .listen(repromptSpeech);
this.emit(':responseReady');
//Example 2
this.response.speak(speechOutput)
            .cardRenderer(cardTitle, cardContent, cardImage)
            .renderTemplate(template)
            .hint(hintText, hintType);
this.emit(':responseReady');

responseBuilder はレスポンスオブジェクトを生成する柔軟な方法なので、利用をおすすめする。

Tips

  • レスポンスイベントが:ask, :tell, :askWithCardなどを発行するとき、Lambda関数はcontext.succeed()を実行する。callback関数を渡していなければ、バックグラウンドのタスクの処理がすぐに停止する。終了していない非同期のジョブは完了せず、その後のコードも実行されない。:saveStateのような応答のないイベントは除く。
  • リクエストをある状態から他の状態に「送る」ことをインテントのフォワーディングといい、this.handler.state に送り先の状態をセットする。そして、もしターゲットの状態が””なら、this.emit("TargetHandlerName")を実行する。他の状態なら this.emitWithState("TargetHandlerName") を実行する。

  • prompt と reprompt の値はSSMLのタグで囲われる。XMLとして意味を持つ文字はエスケープしておく必要がある。例えば、this.emit(":ask", "I like M&M's")を実行するなら、&&amp;にエスケープしないとエラーになる。<&lt;, >&gt;とエスケープするのも同様。

Standard Request and Response

Alexa は HTTPS でスキルサービスと通信する。ユーザがAlexaスキルを実行すると スキルサービスは HTTP POSTでJSONを受け取る。リクエストにはロジックを動かし、JSONのレスポンスを返すのに必要なパラメータが含まれる。Node.jsがネイティブにJSONを処理できるのでAlexa Node.js SDKではJSONのシリアライゼーション処理が不要となっている。Alexaがユーザの要求に応えられるよう適切なレスポンスを返さねばならない。リクエストのJSONの仕様はこちらを参照.

レスポンスには次の属性を含む必要がある。
- OutputSpeech
- Reprompt
- Card
- List of Directives
- shouldEndSession

次は発話とカード両方を含む例だ

const speechOutput = 'Hello world!';
const repromptSpeech = 'Hello again!';
const cardTitle = 'Hello World Card';
const cardContent = 'This text will be displayed in the companion app card.';
const imageObj = {
    smallImageUrl: 'https://imgs.xkcd.com/comics/standards.png',
    largeImageUrl: 'https://imgs.xkcd.com/comics/standards.png'
      };
this.response.speak(speechOutput)
             .listen(repromptSpeech)
             .cardRenderer(cardTitle, cardContent, imageObj);
this.emit(':responseReady');

Interfaces

AudioPlayer Interface

スキルのレスポンスに次のディレクティブをそれぞれ含めることができる。

  • PlayDirective
  • StopDirective
  • ClearQueueDirective

次の例はPlayDirective で音声を再生する

const handlers = {
    'LaunchRequest' : function() {
        const speechOutput = 'Hello world!';
        const behavior = 'REPLACE_ALL';
        const url = 'https://url/to/audiosource';
        const token = 'myMusic';
        const expectedPreviousToken = 'expectedPreviousStream';
        const offsetInMilliseconds = 10000;
        this.response.speak(speechOutput)
                     .audioPlayerPlay(behavior, url, token, expectedPreviousToken, offsetInMilliseconds);
        this.emit(':responseReady');
    }
};

上の例では最初にspeechOutputを発話してから音声再生を試みる。

AudioPlayerインターフェースを使ったスキルを開発するとき、再生状態を変更するために playbackリクエストを送信する。それぞれのイベントに対応したハンドラ関数を実装することができる。

const handlers = {
    'PlaybackStarted' : function() {
        console.log('Alexa begins playing the audio stream');
    },
    'PlaybackFinished' : function() {
        console.log('The stream comes to an end');
    },
    'PlaybackStopped' : function() {
        console.log('Alexa stops playing the audio stream');
    },
    'PlaybackNearlyFinished' : function() {
        console.log('The currently playing stream is nearly complate and the device is ready to receive a new stream');
    },
    'PlaybackFailed' : function() {
        console.log('Alexa encounters an error when attempting to play a stream');
    }
};

AudioPlayerインターフェースのドキュメントはこちら

注意: imgObjに関する仕様はこちら

Dialog Interface

DialogインターフェースはAlexaとユーザのマルチターン会話を管理するディレクティブを提供する。ユーザのリクエストに応えるための情報を尋ねるのに使える。ダイアログディレクティブについて詳しくはDialog InterfaceSkill Editorを参照のこと。

現在のdialogStateを見るにはthis.event.request.dialogStateを参照する。

Delegate Directive

ユーザーとのダイアログで次のターンを処理するコマンドをAlexaに送信する。スキルがダイアログモデルを実装しており、ダイアログの状態(dialogState)がSTARTEDIN_PROGRESSなら、このディレクティブが使える。もし dialogStateCOMPLETEDならこのディレクティブを発行できない。

this.emit(':delegate')を使うとディレクティブの応答をAlexaに任せることができる

const handlers = {
    'BookFlightIntent': function () {
        if (this.event.request.dialogState === 'STARTED') {
            let updatedIntent = this.event.request.intent;
            // Pre-fill slots: update the intent object with slot values for which
            // you have defaults, then emit :delegate with this updated intent.
            updatedIntent.slots.SlotName.value = 'DefaultValue';
            this.emit(':delegate', updatedIntent);
        } else if (this.event.request.dialogState !== 'COMPLETED'){
            this.emit(':delegate');
        } else {
            // All the slots are filled (And confirmed if you choose to confirm slot/intent)
            handlePlanMyTripIntent();
        }
    }
};

Elicit Slot Directive

あるスロットの値をユーザから聞き出すようをAlexaに指示する。slotToElicitで聞き出すスロットの名前を指定する。スロットの値を聞き出すためのフレーズをspeechOutputに指定する。

this.emit(':elicitSlot', slotToElicit, speechOutput, repromptSpeech, updatedIntent)あるいはthis.emit(':elicitSlotWithCard', slotToElicit, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj)を使ってスロットを聞き出す指示を送る。

this.emit(':elicitSlotWithCard', slotToElicit, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj)を使う場合、updatedIntentimageObjは任意で指定する(nullを渡したり、設定しなくても良い)。

const handlers = {
    'BookFlightIntent': function () {
        const intentObj = this.event.request.intent;
        if (!intentObj.slots.Source.value) {
            const slotToElicit = 'Source';
            const speechOutput = 'Where would you like to fly from?';
            const repromptSpeech = speechOutput;
            this.emit(':elicitSlot', slotToElicit, speechOutput, repromptSpeech);
        } else if (!intentObj.slots.Destination.value) {
            const slotToElicit = 'Destination';
            const speechOutput = 'Where would you like to fly to?';
            const repromptSpeech = speechOutput;
            const cardContent = 'What is the destination?';
            const cardTitle = 'Destination';
            const updatedIntent = intentObj;
            // An intent object representing the intent sent to your skill.
            // You can use this property set or change slot values and confirmation status if necessary.
            const imageObj = {
                smallImageUrl: 'https://imgs.xkcd.com/comics/standards.png',
                largeImageUrl: 'https://imgs.xkcd.com/comics/standards.png'
            };
            this.emit(':elicitSlotWithCard', slotToElicit, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj);
        } else {
            handlePlanMyTripIntentAllSlotsAreFilled();
        }
    }
};

Confirm Slot Directive

これはAlexaにあるスロットの値の値を確認してからダイアログを続行するように指示する。slotToConfirmで確認するスロットの名前を指定する。ユーザに確認する際の台詞はspeechOutputで指定する。

this.emit(':confirmSlot', slotToConfirm, speechOutput, repromptSpeech, updatedIntent) または this.emit(':confirmSlotWithCard', slotToConfirm, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj)を使って、スロットを確認するように指示する。

this.emit(':confirmSlotWithCard', slotToConfirm, speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj)を使う場合、updatedIntentimageObjは任意だ。nullを渡すか、何も渡さなければ指定しないことになる。

const handlers = {
    'BookFlightIntent': function () {
        const intentObj = this.event.request.intent;
        if (intentObj.slots.Source.confirmationStatus !== 'CONFIRMED') {
            if (intentObj.slots.Source.confirmationStatus !== 'DENIED') {
                // Slot value is not confirmed
                const slotToConfirm = 'Source';
                const speechOutput = 'You want to fly from ' + intentObj.slots.Source.value + ', is that correct?';
                const repromptSpeech = speechOutput;
                this.emit(':confirmSlot', slotToConfirm, speechOutput, repromptSpeech);
            } else {
                // Users denies the confirmation of slot value
                const slotToElicit = 'Source';
                const speechOutput = 'Okay, Where would you like to fly from?';
                this.emit(':elicitSlot', slotToElicit, speechOutput, speechOutput);
            }
        } else if (intentObj.slots.Destination.confirmationStatus !== 'CONFIRMED') {
            if (intentObj.slots.Destination.confirmationStatus !== 'DENIED') {
                const slotToConfirm = 'Destination';
                const speechOutput = 'You would like to fly to ' + intentObj.slots.Destination.value + ', is that correct?';
                const repromptSpeech = speechOutput;
                const cardContent = speechOutput;
                const cardTitle = 'Confirm Destination';
                this.emit(':confirmSlotWithCard', slotToConfirm, speechOutput, repromptSpeech, cardTitle, cardContent);
            } else {
                const slotToElicit = 'Destination';
                const speechOutput = 'Okay, Where would you like to fly to?';
                const repromptSpeech = speechOutput;
                this.emit(':elicitSlot', slotToElicit, speechOutput, repromptSpeech);
            }
        } else {
            handlePlanMyTripIntentAllSlotsAreConfirmed();
        }
    }
};

Confirm Intent Directive

これはAlexaにスキルの処理実行前にすべての情報を確認するためのものだ。speechOutputでユーザへの問いかけを指定する。ユーザは問いかけのすべての値を確認する必要がある。

this.emit(':confirmIntent', speechOutput, repromptSpeech, updatedIntent)
または
this.emit(':confirmIntentWithCard', speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj)
を使う。

this.emit(':confirmIntentWithCard', speechOutput, repromptSpeech, cardTitle, cardContent, updatedIntent, imageObj)では、updatedIntent and imageObjは任意で指定する。

const handlers = {
    'BookFlightIntent': function () {
        const intentObj = this.event.request.intent;
        if (intentObj.confirmationStatus !== 'CONFIRMED') {
            if (intentObj.confirmationStatus !== 'DENIED') {
                // Intent is not confirmed
                const speechOutput = 'You would like to book flight from ' + intentObj.slots.Source.value + ' to ' +
                intentObj.slots.Destination.value + ', is that correct?';
                const cardTitle = 'Booking Summary';
                const repromptSpeech = speechOutput;
                const cardContent = speechOutput;
                this.emit(':confirmIntentWithCard', speechOutput, repromptSpeech, cardTitle, cardContent);
            } else {
                // Users denies the confirmation of intent. May be value of the slots are not correct.
                handleIntentConfimationDenial();
            }
        } else {
            handlePlanMyTripIntentAllSlotsAndIntentAreConfirmed();
        }
    }
};

Dialogインターフェースについてのドキュメントはこちら

Display Interface

Alexa provides several Display templates to support a wide range of presentations. Currently, there are two categories of Display templates:

Alexaは、さまざまなプレゼンテーションをサポートするため、いくつかのDisplay templatesを提供する。現状、Display templatesには2つのカテゴリーがある。

  • BodyTemplate はテキストを画像を表示する。それらは選択的には表示できない。現状、4つのオプションがある。
    • BodyTemplate1
    • BodyTemplate2
    • BodyTemplate3
    • BodyTemplate6
    • BodyTemplate7
  • ListTemplateはスクロールできるリストを表示する。それぞれ、テキストと任意で画像を表示できる。現状、2つのオプションがある。
    • ListTemplate1
    • ListTemplate2

Developers must include Display.RenderTemplate directive in their skill responses.
Template Builders are now included in alexa-sdk in the templateBuilders namespace. These provide a set of helper methods to build the JSON template for the Display.RenderTemplate directive. In the example below we use the BodyTemplate1Builder to build the Body template.

Display.RenderTemplateディレクティブをレスポンスに含める必要がある。
alexa-sdk ではtemplateBuildersにテンプレートビルダーが含まれている。Display.RenderTemplateディレクトのためにJSONのテンプレートを作るヘルパーメソッドがある。
次の例はBodyTemplate1BuilderBody templateを作るものだ。

const Alexa = require('alexa-sdk');
// utility methods for creating Image and TextField objects
const makePlainText = Alexa.utils.TextUtils.makePlainText;
const makeImage = Alexa.utils.ImageUtils.makeImage;

// ...
'LaunchRequest' : function() {
    const builder = new Alexa.templateBuilders.BodyTemplate1Builder();

    const template = builder.setTitle('My BodyTemplate1')
                            .setBackgroundImage(makeImage('http://url/to/my/img.png'))
                            .setTextContent(makePlainText('Text content'))
                            .build();

    this.response.speak('Rendering a body template!')
                .renderTemplate(template);
    this.emit(':responseReady');
}

画像とテキストフィールドオブジェクトを作るためのユーティリティメソッドもあり、Alexa.utilsに含まれている。

const ImageUtils = require('alexa-sdk').utils.ImageUtils;

// Outputs an image with a single source
ImageUtils.makeImage(url, widthPixels, heightPixels, size, description);
/**
Outputs {
    contentDescription : '<description>'
    sources : [
        {
            url : '<url>',
            widthPixels : '<widthPixels>',
            heightPixels : '<heightPixels>',
            size : '<size>'
        }
    ]
}
*/

ImageUtils.makeImages(imgArr, description);
/**
Outputs {
    contentDescription : '<description>'
    sources : <imgArr> // array of {url, size, widthPixels, heightPixels}
}
*/


const TextUtils = require('alexa-sdk').utils.TextUtils;

TextUtils.makePlainText('my plain text field');
/**
Outputs {
    text : 'my plain text field',
    type : 'PlainText'
}
*/

TextUtils.makeRichText('my rich text field');
/**
Outputs {
    text : 'my rich text field',
    type : 'RichText'
}
*/

次の例では、 ListTemplate1BuilderListItemBuilder で ListTemplate1 を作っている。

const Alexa = require('alexa-sdk');
const makePlainText = Alexa.utils.TextUtils.makePlainText;
const makeImage = Alexa.utils.ImageUtils.makeImage;
// ...
'LaunchRequest' : function() {
    const itemImage = makeImage('https://url/to/imageResource', imageWidth, imageHeight);
    const listItemBuilder = new Alexa.templateBuilders.ListItemBuilder();
    const listTemplateBuilder = new Alexa.templateBuilders.ListTemplate1Builder();
    listItemBuilder.addItem(itemImage, 'listItemToken1', makePlainText('List Item 1'));
    listItemBuilder.addItem(itemImage, 'listItemToken2', makePlainText('List Item 2'));
    listItemBuilder.addItem(itemImage, 'listItemToken3', makePlainText('List Item 3'));
    listItemBuilder.addItem(itemImage, 'listItemToken4', makePlainText('List Item 4'));
    const listItems = listItemBuilder.build();
    const listTemplate = listTemplateBuilder.setToken('listToken')
                                            .setTitle('listTemplate1')
                                            .setListItems(listItems)
                                            .build();
    this.response.speak('Rendering a list template!')
                .renderTemplate(listTemplate);
    this.emit(':responseReady');
}

Display.RenderTemplateディレクティブをEchoのようなディスプレイのないデバイスに送るとinvalid directive error が発生する。どのディレクティブをサポートするかはデバイスのsupportedInterfaces属性を確認すればよい。

const handler = {
    'LaunchRequest' : function() {

    this.response.speak('Hello there');

    // Display.RenderTemplate directives can be added to the response
    if (this.event.context.System.device.supportedInterfaces.Display) {
        //... build mytemplate using TemplateBuilder
        this.response.renderTemplate(myTemplate);
    }

    this.emit(':responseReady');
    }
};

同様に動画は VideoAppがデバイスでサポートされているかどうかチェックすれば良い。

const handler = {
    'PlayVideoIntent' : function() {

    // VideoApp.Play directives can be added to the response
    if (this.event.context.System.device.supportedInterfaces.VideoApp) {
        this.response.playVideo('http://path/to/my/video.mp4');
    } else {
        this.response.speak("The video cannot be played on your device. " +
        "To watch this video, try launching the skill from your echo show device.");
    }

        this.emit(':responseReady');
    }
};

Displayインターフェースについてのドキュメントはこちらを参照のこと。

Playback Controller Interface

PlaybackControllerインターフェースはユーザがデバイス上のボタンやリモコンを操作した際にリクエストを送信できるようにする。これらは「Alexa, 次の曲」のような音声コマンドとは異なる。PlaybackControllerリクエストを扱えるようにするためにPlaybackControllerインターフェースをAlexa Node.js SDKを使って実装する必要がある。

const handlers = {
    'NextCommandIssued' : function() {
        //Your skill can respond to NextCommandIssued with any AudioPlayer directive.
    },
    'PauseCommandIssued' : function() {
        //Your skill can respond to PauseCommandIssued with any AudioPlayer directive.
    },
    'PlayCommandIssued' : function() {
        //Your skill can respond to PlayCommandIssued with any AudioPlayer directive.
    },
    'PreviousCommandIssued' : function() {
        //Your skill can respond to PreviousCommandIssued with any AudioPlayer directive.
    },
    'System.ExceptionEncountered' : function() {
        //Your skill cannot return a response to System.ExceptionEncountered.
    }
};

PlaybackControllerインターフェースに関するドキュメントはこちらを参照のこと。

VideoApp Interface

Echo showで動画を見るためにVideoApp.Launchディレクティブを送る。Alexa Node.js SDKではresponseBuilder でJSONレスポンスを生成できる。

次は動画を再生する例だ。

//...
'LaunchRequest' : function() {
    const videoSource = 'https://url/to/videosource';
    const metadata = {
        'title': 'Title for Sample Video',
        'subtitle': 'Secondary Title for Sample Video'
    };
    this.response.playVideo(videoSource, metadata);
    this.emit(':responseReady');
}

VideoAppインターフェースのドキュメントはこちら

Skill and List Events

スキル開発者はAlexaスキルイベントを直接組み込むことができる。もし、スキルがイベントを購読するとイベント発生時に通知される。

スキルサービスでイベントを利用するためには、Add Events to Your Skill With SMAPIにかかれているとおり、Alexa Skill Management API (SMAPI)へのアクセスを設定する必要がある。

Skill and List Events come out of session. Once your skill has been set up to receive these events. You can specify behaviour by adding the event names to your default event handler.

Skill and List Eventsはセッションを終了させる。一旦、スキルがイベントを受けるよう設定されると、デフォルトのイベントハンドラにイベント名を追加するとその振る舞いを定義できる。

const handlers = {
    'AlexaSkillEvent.SkillEnabled' : function() {
        const userId = this.event.context.System.user.userId;
        console.log(`skill was enabled for user: ${userId}`);
    },
    'AlexaHouseholdListEvent.ItemsCreated' : function() {
        const listId = this.event.request.body.listId;
        const listItemIds = this.event.request.body.listItemIds;
        console.log(`The items: ${JSON.stringify(listItemIds)} were added to list ${listId}`);
    },
    'AlexaHouseholdListEvent.ListCreated' : function() {
        const listId = this.event.request.body.listId;
        console.log(`The new list: ${JSON.stringify(listId)} was created`);
    }
    //...
};

exports.handler = function(event, context, callback) {
    const alexa = Alexa.handler(event, context, callback);
    alexa.registerHandlers(handlers);
    alexa.execute();
};

sample skill and walk-throughをスキルイベントの参考にしてほしい

Services

Device Address Service

Alexa NodeJS SDKはDeviceAddressServiceヘルパークラスを提供している。これはユーザのデバイスの住所情報を取得するためのDevice Address APIユーティリティだ。現在、次のメソッドが用意されている1

getFullAddress(deviceId, apiEndpoint, token)
getCountryAndPostalCode(deviceId, apiEndpoint, token)

apiEndpointtokenはリクエストのthis.event.context.System.apiEndpointthis.event.context.System.user.permissions.consentTokenから取得できる。
deviceId はリクエストのthis.event.context.System.device.deviceIdから取得できる。

const Alexa = require('alexa-sdk');

'DeviceAddressIntent': function () {
    if (this.event.context.System.user.permissions) {
        const token = this.event.context.System.user.permissions.consentToken;
        const apiEndpoint = this.event.context.System.apiEndpoint;
        const deviceId = this.event.context.System.device.deviceId;

        const das = new Alexa.services.DeviceAddressService();
        das.getFullAddress(deviceId, apiEndpoint, token)
        .then((data) => {
            this.response.speak('<address information>');
            console.log('Address get: ' + JSON.stringify(data));
            this.emit(':responseReady');
        })
        .catch((error) => {
            this.response.speak('I\'m sorry. Something went wrong.');
            this.emit(':responseReady');
            console.log(error.message);
        });
    } else {
        this.response.speak('Please grant skill permissions to access your device address.');
        this.emit(':responseReady');
    }
}

List Management Service

Alexa ユーザは 2つのデフォルトリストが利用できる。Alexa ToDoリストと Alexa ショッピングリストだ。
加えてスキルからカスタムリストを作って管理できる。

Alexa NodeJS SDKはListManagementServiceヘルパクラスをデフォルトリストやカスタムリストを扱いやすくするために用意している。
現在、次のメソッドがある。

getListsMetadata(token)
createList(listObject, token)
getList(listId, itemStatus, token)
updateList(listId, listObject, token)
deleteList(listId, token)
createListItem(listId, listItemObject, token)
getListItem(listId, itemId, token)
updateListItem(listId, itemId, listItemObject, token)
deleteListItem(listId, itemId, token)

tokenはリクエストのthis.event.context.System.user.permissions.consentTokenから取得できる
listIdGetListsMetadataを使って、itemIdGetListを使ってそれぞれ取得できる。

const Alexa = require('alexa-sdk');

function getListsMetadata(token) {
    const lms = new Alexa.services.ListManagementService();
    lms.getListsMetadata(token)
    .then((data) => {
        console.log('List retrieved: ' + JSON.stringify(data));
        this.context.succeed();
    })
    .catch((error) => {
        console.log(error.message);
    });
};

Directive Service

enqueue(directive, endpoint, token)

これはスキル実行中に非同期にAlexaデバイスにディレクティブを返す。現在、発話ディレクティブのみサポートしている。
SSML (MP3 audioを含む)とテキスト形式をサポートしている。スキルが有効な時にディレクティブは元のデバイスに返される。apiEndpointtokenパラメータはそれぞれリクエストのthis.event.context.System.apiEndpointthis.event.context.System.apiAccessTokenから取得できる。

  • レスポンスの発話は600文字まで。
  • SSML内の音声ファイルは30秒まで
  • ディレクティブの数は制限がない。必要なら、複数のリクエストを送信しても良い。
  • ディレクティブサービスには重複処理が含まれていない。それで同じディレクティブを複数回受信する可能性があるため、リトライ処理はお勧めしない。
const Alexa = require('alexa-sdk');

const handlers = {
    'SearchIntent' : function() {
        const requestId = this.event.request.requestId;
        const token = this.event.context.System.apiAccessToken;
        const endpoint = this.event.context.System.apiEndpoint;
        const ds = new Alexa.services.DirectiveService();

        const directive = new Alexa.directives.VoicePlayerSpeakDirective(requestId, "Please wait...");
        const progressiveResponse = ds.enqueue(directive, endpoint, token)
        .catch((err) => {
            // catch API errors so skill processing an continue
        });
        const serviceCall = callMyService();

        Promise.all([progressiveResponse, serviceCall])
        .then(() => {
            this.response.speak('I found the following results');
            this.emit(':responseReady');
        });
    }
};

Extend Features

Skill State Management

Alexa-sdk は受信したインテントを適切なハンドラに渡すためにステートマネージャを使う。セッション属性に文字列として保存された状態が、スキルの現在の状態を示す。組み込みのインテントルーティングでも、インテント名に状態文字列を付加することで同様の実装が可能だが、alexa-sdkのステートマネージャを使って実装することができる。

highlowgameをは状態管理の例だ。このスキルではユーザが数を予想し、Alexaが大きいか小さいかや、ユーザが何回プレイしたかを答える。
'start' と 'guess'の2つの状態を持っている。

const states = {
    GUESSMODE: '_GUESSMODE', // User is trying to guess the number.
    STARTMODE: '_STARTMODE' // Prompt the user to start or restart the game.
};

newSessionHandlersのNewSessionハンドラは入力インテントや起動リクエストのショートカットで、ルーティングを行っている。

const newSessionHandlers = {
    'NewSession': function() {
        if(Object.keys(this.attributes).length === 0) { // Check if it's the first time the skill has been invoked
            this.attributes['endedSessionCount'] = 0;
            this.attributes['gamesPlayed'] = 0;
        }
        this.handler.state = states.STARTMODE;
        this.response.speak('Welcome to High Low guessing game. You have played '
                        + this.attributes['gamesPlayed'].toString() + ' times. Would you like to play?')
                    .listen('Say yes to start the game or no to quit.');
        this.emit(':responseReady');
    }
};

新規セッションの場合、スキルの状態がthis.handler.stateによって、簡単にSTARTMODEに設定されていることに注意。スキルの状態は自動的にセッション属性として保存される。もし、DynamoDBを使えばセッションを超えて保存することもできる。

NewSessionはキャッチオールな振る舞いで、よいエントリーポイントだが、必須ではないことに注意しよう。NewSessionはその名前を持つハンドラが定義されている場合にのみ呼び出される。それぞれの状態はもし、組み込みの永続化を利用するなら呼び出されるそれぞれのNewSessionハンドラを定義することができる。上の例では異なるNewSessionの振る舞いをstates.STARTMODEstates.GUESSMODEに実装でき、柔軟性を得ることができる。

スキルのさまざまな状態に対応するインテントを定義するには、 Alexa.CreateStateHandler関数を使う。
スキルが特定の状態にあるときだけ動くインテントハンドラが定義でき、これは大きな柔軟性を与える。

例えば、上記で定義したGUESSMODE状態では、質問に回答するユーザを扱う。次の様にStateHandlersを使う。

const guessModeHandlers = Alexa.CreateStateHandler(states.GUESSMODE, {

'NewSession': function () {
    this.handler.state = '';
    this.emitWithState('NewSession'); // Equivalent to the Start Mode NewSession handler
},

'NumberGuessIntent': function() {
    const guessNum = parseInt(this.event.request.intent.slots.number.value);
    const targetNum = this.attributes['guessNumber'];

    console.log('user guessed: ' + guessNum);

    if(guessNum > targetNum){
        this.emit('TooHigh', guessNum);
    } else if( guessNum < targetNum){
        this.emit('TooLow', guessNum);
    } else if (guessNum === targetNum){
        // With a callback, use the arrow function to preserve the correct 'this' context
        this.emit('JustRight', () => {
            this.response.speak(guessNum.toString() + 'is correct! Would you like to play a new game?')
                        .listen('Say yes to start a new game, or no to end the game.');
            this.emit(':responseReady');
        });
    } else {
        this.emit('NotANum');
    }
},

'AMAZON.HelpIntent': function() {
    this.response.speak('I am thinking of a number between zero and one hundred, try to guess and I will tell you' +
    ' if it is higher or lower.')
                .listen('Try saying a number.');
    this.emit(':responseReady');
},

'SessionEndedRequest': function () {
    console.log('session ended!');
    this.attributes['endedSessionCount'] += 1;
    this.emit(':saveState', true); // Be sure to call :saveState to persist your session attributes in DynamoDB
},

'Unhandled': function() {
    this.response.speak('Sorry, I didn\'t get that. Try saying a number.')
                .listen('Try saying a number.');
    this.emit(':responseReady');
}
});

一方、STARTMODEStateHandlersを次の様に定義する。

const startGameHandlers = Alexa.CreateStateHandler(states.STARTMODE, {

    'NewSession': function () {
        this.emit('NewSession'); // Uses the handler in newSessionHandlers
    },

    'AMAZON.HelpIntent': function() {
        const message = 'I will think of a number between zero and one hundred, try to guess and I will tell you if it' +
        ' is higher or lower. Do you want to start the game?';
        this.response.speak(message)
                    .listen(message);
        this.emit(':responseReady');
    },

    'AMAZON.YesIntent': function() {
        this.attributes['guessNumber'] = Math.floor(Math.random() * 100);
        this.handler.state = states.GUESSMODE;
        this.response.speak('Great! ' + 'Try saying a number to start the game.')
                    .listen('Try saying a number.');
        this.emit(':responseReady');
    },

    'AMAZON.NoIntent': function() {
        this.response.speak('Ok, see you next time!');
        this.emit(':responseReady');
    },

    'SessionEndedRequest': function () {
        console.log('session ended!');
        this.attributes['endedSessionCount'] += 1;
        this.emit(':saveState', true);
    },

    'Unhandled': function() {
        const message = 'Say yes to continue, or no to end the game.';
        this.response.speak(message)
                    .listen(message);
        this.emit(':responseReady');
    }
});

AMAZON.YesIntentAMAZON.NoIntentguessModeHandlers では定義されていない。それは、この状態では'yes' や 'no'の返答には反応しなくていいからだ。それらのインテントはUnhandledハンドラで受けられる。

また、NewSessionUnhandledハンドラの振る舞いがこのゲームの両方の状態で異なっているのにも気付いたか?
このゲームでは、 newSessionHandlersオブジェクトで定義されたNewSessionハンドラを呼び出して状態をリセットする。 それを定義することなく、alexa-sdkは現在の状態のインテントハンドラを呼び出す。
alexa.execute()を呼ぶ前に各状態のハンドラを登録するを忘れないように。見つからないよ。

セッションを終了するとき、属性は自動的に保存される。しかし、ユーザがセッションを終了するなら、強制的に保存するように:saveStateイベントを発行する(this.emit(':saveState', true))必要がある。ユーザによるセッション「終了」やタイムアウト時に呼ばれるSessionEndedRequestハンドラでそれを実装する。上の例を確認してほしい。

もし明確に状態をリセットするなら、次のコードでできる。

this.handler.state = '' // delete this.handler.state は参照エラーの原因になる。
delete this.attributes['STATE'];

Persisting Skill Attributes through DynamoDB

後で使うためにセッション属性値をストレージに保存したいと思うことも多い。Alexa-sdk では Amazon DynamoDB(NoSQL データベース・サービス)に1行のコードで保存できるように実装されている。

alexa.executeを実行する前に、DynamoDBのテーブル名をalexaオブジェクトにセットするだけだ。

exports.handler = function (event, context, callback) {
    const alexa = Alexa.handler(event, context, callback);
    alexa.appId = appId;
    alexa.dynamoDBTableName = 'YourTableName'; // That's it!
    alexa.registerHandlers(State1Handlers, State2Handlers);
    alexa.execute();
};

そして、後必要なのは、alexaオブジェクトの属性プロパティに値をセットすることだけだ。putgetにも分かれていない。

this.attributes['yourAttribute'] = 'value';

あらかじめマニュアルでテーブルを作る ことができる。あるいは Lambda 関数に DynamoDBの テーブル作成権限を与えておいても良い。その場合、自動的に作成される。最初の呼び出し時にテーブルの作成に1分かそこらかかることを忘れないように。もし、テーブルをマニュアルで作るなら、プライマリキーは文字列で"userId"をしておくこと。

注意:lambdaでスキルをホストして、スキルの属性をDynamoDBに保存するなら、lambdaの実行ロールがDynamoDBへのアクセス権を持っていることを確認すること。

Adding Multi-Language Support for Skill

Hello World のサンプルを示す。すべてのユーザに示す言語の文字列を次のフォーマットで定義する。

const languageStrings = {
    'en-GB': {
        'translation': {
            'SAY_HELLO_MESSAGE' : 'Hello World!'
        }
    },
    'en-US': {
        'translation': {
            'SAY_HELLO_MESSAGE' : 'Hello World!'
        }
    },
    'de-DE': {
        'translation': {
            'SAY_HELLO_MESSAGE' : 'Hallo Welt!'
        }
    }
};

Alexa-sdkで国際化機能を有効にするために、上で作ったオブジェクトをリソースにセットすること。

exports.handler = function(event, context, callback) {
    const alexa = Alexa.handler(event, context);
    alexa.appId = appId;
    // To enable string internationalization (i18n) features, set a resources object.
    alexa.resources = languageStrings;
    alexa.registerHandlers(handlers);
    alexa.execute();
};

一旦、各言語の文字列を定義して有効にすると、this.t()関数でそれらの文字列にアクセスできる。受け付けたリクエストのロケールにあった言語で文字列が取得できる。

const handlers = {
    'LaunchRequest': function () {
        this.emit('SayHello');
    },
    'HelloWorldIntent': function () {
        this.emit('SayHello');
    },
    'SayHello': function () {
        this.response.speak(this.t('SAY_HELLO_MESSAGE'));
        this.emit(':responseReady');
    }
};

マルチリンガルなスキルの開発とデプロイについてこちらを参照のこと。

Device ID Support

ユーザがAlexaスキルを有効にすると、スキルはユーザに許可を求めることができる。許可されれば、Alexaデバイスに紐付いたアドレス情報が得られる。
このアドレス情報をスキルの機能やユーザ体験の向上に使うことができる。

deviceIdは各リクエストのcontextオブジェクトに含まれ、this.event.context.System.device.deviceIdでインテントハンドラからアクセス可能。スキル内でどうやってdeviceIdとAddress APIを使うかはAddress API sample skillを参照。

Speechcons (Interjections)

SpeechconsはAlexaの発生をより自然にするための単語やフレーズだ。それを使うためにSSMLマークアップをテキストに含めて発行すれば良い。

  • this.emit(':tell', 'Sometimes when I look at the Alexa skills you have all taught me, I just have to say, <say-as interpret-as="interjection">Bazinga.</say-as> ');
  • this.emit(':tell', '<say-as interpret-as="interjection">Oh boy</say-as><break time="1s"/> this is just an example.');

Speechcons(感嘆符)は英語(US/UK/India)とドイツ語でサポートされている

Setting up your development environment

Alexa Skills Kitを始めるためのさらなる情報は 次の資料を参照のこと


  1. getFullAddress(deviceId, apiEndpoint, token)のレスポンスは次の通り。見ての通り,端末に登録されていない項目はnullが返ってくるのでそのハンドリングも必要になる。サービスシミュレータではちゃんと動かないので注意。 

    {
    "addressLine1": null,
    "addressLine2": null,
    "addressLine3": null,
    "districtOrCounty": null,
    "stateOrRegion": null,
    "city": null,
    "countryCode": "JP",
    "postalCode": "XXX-XXXX"
    }
    
43
29
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
29