Alexaスキル「あかちゃんきろく」リリースしました!
初アレクサスキル開発で、色々学びながらなんとかかんとかリリースできました。
そんな学習で得た知識・メモでっす。ASK SDK(v2 Node.js)を噛み砕いて知りたいとこまで調べたメモになっとります。
スキル「あかちゃんきろく」は↓で紹介しています。操作動画もあります。
Alexaスキル「あかちゃんきろく」~声でもタッチでも!手がふさがっててもしっかり育児記録&確認~
「あかちゃんきろく」はAPLによる画面表示対応しており、APLメモも別途まとめてます↓。
Alexaスキル開発メモ – APL(Alexa Presentation Language)を噛み砕く編
- 30日間無料で聴き放題
- 12万冊以上が聴き放題
- Alexaからサクッと読書
Alexaスキル開発の基本知識
基礎知識
- 現在、Node.js、Java、Pythonで開発可能(ASK SDKs)
- Node.jsが主流の開発環境で、開発情報もNode.js関連が早い・多い
Alexa Skills Kit SDK v2 for Node.js を使ったスキル開発
Node.jsで開発するにはAlexa Skills Kit SDK v2 for Node.js(以下ASK SDK)を使う。
以下はこのASK SDK for Node.jsの内容を噛み砕いてメモしていったもの。
主にSDK提供のAPIや、フレームワーク、機能について取り上げる。SDK環境セットアップ方法等は他を参照。
参照ドキュメント
- ドキュメント : Alexa Skills Kit ルート
- ドキュメント : Alexa Skills Kit SDK for Node.js
- リファレンス : ASK SDK for Node.js
- TypeDoc : ASK SDK for Node.js
- Githubリポジトリ : ASK SDK Node.js
- Alexaブログ : スキル開発トレーニング
- Classmethodの記事
大枠
- SkillBuilderでスキルコンフィギュレーション設定・スキル処理関数を作成
- RequestHandlerを作成してSkillBuilderに設定、そこでユーザー発話リクエストを処理
- リクエスト入力には、インテントやスロットなどの情報が内包
- インテント=ユーザーの発話意図、スロット=発話に含まれる変数(数字やタイプなど)
- AttributesManagerを用いて、セッション情報や、永続保存情報管理
Lambda作成等の基本プログラムフロー:「Alexaスキル開発トレーニングシリーズ 第2回 対話モデルとAlexa SDK」。
とりあえずこれ見れば、ざっくりとはすぐ作れる。
skillBuilderでスキルを作成
Skillのコンフィギュレーションメソッドを備えたSkillBuilderが提供されている。lambda向けの関数ハンドラを返すメソッドがある(lambda()
)ので、Lambda関数本体をこれで設定する。
サンプルの最後の方に書いてあるやつがそれ↓。
const skillBuilder = Alexa.SkillBuilders.custom(); exports.handler = skillBuilder .addRequestHandlers( HelpHandler, HoroscopeIntentHandler, LuckyColorIntentHandler ) .lambda();
custom()メソッドにより、CustomSkillBuilderが使われている。
この他にstandard()でStandardSkillBuilderを使うことも可能。
SkillBuilderは、Base, Custom, Standardがあるが、それぞれまとめたインターフェース一覧は以下。
(Base)
addRequestHandler(matcher : ((handlerInput : HandlerInput) => Promise<boolean> | boolean) | string, executor : (handlerInput : HandlerInput) => Promise<Response> | Response) : this;
addRequestHandlers(...requestHandlers : RequestHandler[]) : this;
addRequestInterceptors(...executors : Array<RequestInterceptor | ((handlerInput : HandlerInput) => Promise<void> | void)>) : this;
addResponseInterceptors(...executors : Array<ResponseInterceptor | ((handlerInput : HandlerInput, response? : Response) => Promise<void> | void)>) : this;
addErrorHandler(matcher : (handlerInput : HandlerInput, error : Error) => Promise<boolean> | boolean, executor : (handlerInput : HandlerInput, error : Error) => Promise<Response> | Response) : this;
addErrorHandlers(...errorHandlers : ErrorHandler[]) : this;
withCustomUserAgent(customUserAgent : string) : this;
withSkillId(skillId : string) : this;
getSkillConfiguration() : SkillConfiguration;
create() : Skill;
lambda() : LambdaHandler;
(Custom専用)
withPersistenceAdapter(persistenceAdapter : PersistenceAdapter) : this;
withApiClient(apiClient : ApiClient) : this;
(Standard専用)
withTableName(tableName : string) : this;
withAutoCreateTable(autoCreateTable : boolean) : this;
withPartitionKeyGenerator(partitionKeyGenerator : PartitionKeyGenerator) : this;
withDynamoDbClient(customDynamoDBClient : DynamoDB) : this;
最初のうちは、サンプルのテンプレ通りに作るならあまり意識する必要はない。
少し凝ったスキルで、DynamoDBへの接続や、Progressive Response、所在地情報、等を使い出すとコンフィギュレーション設定が必要。
その場合は、PersistenceAdapterやApiClientなどを参照しながらカスタマイズした機能をもつスキルとする。
基本はStandardSkillBuilderを使っておけば大体事足りそう。
デフォルトでDynamoDbPersistenceAdapterとDefaultApiClientを使用し、DynamoDbPersistenceAdapterの設定に役立つ便利なメソッドを提供します。
InterceptorsやErrorHandlerを使うとRequestHandler全てに必要な処理や例外処理を統一的に扱えて便利。
userIdとrequest内容のログ出力とかを1つの処理にまとめられるとか。
- addRequestInterceptors : RequestHandlersの前処理を設定
- addResponseInterceptors : RequestHandlersの後処理を設定
- addErrorHandlers : 例外処理のハンドラ設定
handlerInpputとは
まず、ベースとなるのが、サンプルの各Handlerに渡されているhandlerInput。
handlerInputのプロパティとして提供されているのが以下。
RequestEnvelope:スキルの送信されるリクエスト本文全体を含みます。
AttributesManager:リクエスト、セッション、永続アトリビュートへのアクセスを提供します。
ServiceClientFactory:Alexa APIの呼び出しが可能なサービスクライアントを構築します。
ResponseBuilder:応答を作成するヘルパー関数を含みます。
Context:ホストコンテナが渡すオプションのcontextオブジェクトを提供します。たとえば、AWS Lambdaで実行されるスキルの場合は、AWS Lambda関数のcontextオブジェクトになります。
実コードのTypeDocは以下で参照可能
Interface HandlerInput
どれも重要でそれぞれ以降で詳細化。
リクエスト本文
上のRequestEnvelopeに当たる中身。JSONオブジェクトで、JSではそれぞれプロパティとしてアクセス。
よく使うのは以下。
- request : インテントやスロット等の解決
- context.user.userId : ユーザーID取得
- context.System.device.supportedInterfaces : 画面操作対象かの判定など
Request内情報、Intent、Slotは以下で取得(request = handlerInput.requestEnvelope.request)
- request.type で 発話Intentなのか、APL操作なのか等を分岐
- request.intent.name で Intent名
- request.intent.slots配列からスロット値参照
以下が実際のリクエスト本文例。
{
"version": "1.0",
"session": {
"new": true,
"sessionId": "amzn1.echo-api.session.[unique-value-here]",
"application": {
"applicationId": "amzn1.ask.skill.[unique-value-here]"
},
"attributes": {
"key": "string value"
},
"user": {
"userId": "amzn1.ask.account.[unique-value-here]",
"accessToken": "Atza|AAAAAAAA...",
"permissions": {
"consentToken": "ZZZZZZZ..."
}
}
},
"context": {
"System": {
"device": {
"deviceId": "string",
"supportedInterfaces": {
"AudioPlayer": {}
}
},
"application": {
"applicationId": "amzn1.ask.skill.[unique-value-here]"
},
"user": {
"userId": "amzn1.ask.account.[unique-value-here]",
"accessToken": "Atza|AAAAAAAA...",
"permissions": {
"consentToken": "ZZZZZZZ..."
}
},
"apiEndpoint": "https://api.amazonalexa.com",
"apiAccessToken": "AxThk..."
},
"AudioPlayer": {
"playerActivity": "PLAYING",
"token": "audioplayer-token",
"offsetInMilliseconds": 0
}
},
"request": {}
}
セッション中保持や永続保存等、様々な情報の管理(アトリビュート管理)
上記handlerInputからとれるAttributesManagerで、寿命に応じた情報(=アトリビュート)の管理ができる。
寿命のタイプは以下の通り
- Requestごと : 会話1ターン単位で揮発
- Sessionごと : セッション終了まで保持
- 永続保存 : DBに自動的に保存、スキル再起動時も参照可能
セッション情報管理(SessionAttribute)
- セッションアトリビュートに情報を保存することで、会話またぎの情報を保持し続けられる
- 利用サンプル:Alexaスキル開発トレーニングシリーズ 第3回 音声ユーザーインターフェースの設計
保存例(上記サンプルから抜粋)
let attributes = handlerInput.attributesManager.getSessionAttributes(); attributes.city = "東京"; handlerInput.attributesManager.setSessionAttributes(attributes);
参照例
let attributes = handlerInput.attributesManager.getSessionAttributes(); var city = attributes.city;
永続保存情報管理(DynamoDBと自動連携したデータ保存)
Alexa SDKの提供する AttributesManager には永続アトリビュートの取得と保存を簡単に行える3つのメソッドが用意されています。
setPersistentAttribute
保存したいアトリビュートを設定します。処理高速化のため、このメソッドを実行した時点ではアトリビュートはローカルのみにキャッシュされていて、永続ストレージには保存されていないことに注意が必要です。
savePersistentAttribute
ローカルにキャッシュされているアトリビュートを永続ストレージに保存します。非同期処理なので、async/await 構文で使う必要があります。
getPersistentAttributes
保存してあるアトリビュートを取得します。非同期処理なので、async/await構文で使う必要があります。
Alexaスキル開発トレーニングシリーズ 第4回 データの保存
- 永続アトリビュート(DB保存データ)を利用可能。ASKはデフォルトでDynamoDBへ簡単に接続できるようになってる
- LambdaにDynamoDBへの権限さえ設定してあればOK(Hosted Skillならデフォルトで使えると思う)
Responseの構築
応答本文
{
"version": "string",
"sessionAttributes": {
"key": "value"
},
"response": {
"outputSpeech": {
"type": "PlainText",
"text": "話すプレーンテキストの文字列",
"ssml": "<speak>話すSSML文字列</speak>",
"playBehavior": "REPLACE_ENQUEUED"
},
"card": {
"type": "Standard",
"title": "カードのタイトル",
"content": "Simpleカードの内容",
"text": "Standardカードのテキストの内容",
"image": {
"smallImageUrl": "https://url-to-small-card-image...",
"largeImageUrl": "https://url-to-large-card-image..."
}
},
"reprompt": {
"outputSpeech": {
"type": "PlainText",
"text": "話すプレーンテキストの文字列",
"ssml": "<speak>話すSSML文字列</speak>",
"playBehavior": "REPLACE_ENQUEUED"
}
},
"directives": [
{
"type": "InterfaceName.Directive"
(...ディレクティブの タイプに 応じた プロパティ )
}
],
"shouldEndSession": true
}
}
ResponseBuilder
上記の応答本文を構築するヘルパークラスがResponseBuilder。以下のようなメソッドがある。
speak(speechOutput: string): this;
reprompt(repromptSpeechOutput: string): this;
withSimpleCard(cardTitle: string, cardContent: string): this;
withStandardCard(cardTitle: string, cardContent: string, smallImageUrl?: string, largeImageUrl?: string): this;
withLinkAccountCard(): this;
withAskForPermissionsConsentCard(permissionArray: string[]): this;
withCanFulfillIntent(canFulfillIntent : CanFulfillIntent) : this;
addDelegateDirective(updatedIntent?: Intent): this;
addElicitSlotDirective(slotToElicit: string, updatedIntent?: Intent): this;
addConfirmSlotDirective(slotToConfirm: string, updatedIntent?: Intent): this;
addConfirmIntentDirective(updatedIntent?: Intent): this;
addAudioPlayerPlayDirective(playBehavior: interfaces.audioplayer.PlayBehavior, url: string, token: string, offsetInMilliseconds: number, expectedPreviousToken?: string, audioItemMetadata? : AudioItemMetadata): this;
addAudioPlayerStopDirective(): this;
addAudioPlayerClearQueueDirective(clearBehavior: interfaces.audioplayer.ClearBehavior): this;
addRenderTemplateDirective(template: interfaces.display.Template): this;
addHintDirective(text: string): this;
addVideoAppLaunchDirective(source: string, title?: string, subtitle?: string): this;
withShouldEndSession(val: boolean): this;
addDirective(directive: Directive): this;
getResponse(): Response;
よく使いそうなのは以下。
- speak : 発話内容
- withStandardCard : アクティビティに表示するカード情報設定(SimpleCardもある)
- reprompt : ユーザー応答がない場合に再度発話して操作提示を行う
- addDirective : 追加の命令情報設定。画面表示や音楽再生命令等(下記で詳細化)
- withShouldEndSession : セッション継続するかの設定
これらの必要なものを組み合わせて使ったのがいつもの↓となる。
const response = handlerInput.responseBuilder .speak('foo') .reprompt('bar') .withSimpleCard('title', 'cardText') .getResponse();
Directiveで専用処理
ResponseにDirectiveを適切に設定することで専用の特殊処理が可能。例えば以下。
- 画面付き端末への画面描画
- オーディオ再生処理
- ビデオ再生処理
特定のインターフェース(音声ストリーミング用のAudioPlayerインターフェースなど)を使用してデバイスレベルで実行するアクションを指定する、ディレクティブの配列です。
ResponseBuilderのaddDirectiveにDirectiveオブジェクトを設定。
Directiveのタイプ定義は以下。
// Directive定義 export declare type Directive = interfaces.audioplayer.StopDirective | dialog.ConfirmSlotDirective | interfaces.audioplayer.PlayDirective | interfaces.alexa.presentation.apl.ExecuteCommandsDirective | interfaces.connections.SendRequestDirective | interfaces.display.RenderTemplateDirective | interfaces.gadgetController.SetLightDirective | dialog.DelegateDirective | interfaces.display.HintDirective | dialog.ConfirmIntentDirective | interfaces.gameEngine.StartInputHandlerDirective | interfaces.videoapp.LaunchDirective | interfaces.gameEngine.StopInputHandlerDirective | interfaces.alexa.presentation.apl.RenderDocumentDirective | interfaces.connections.SendResponseDirective | dialog.ElicitSlotDirective | interfaces.audioplayer.ClearQueueDirective; // APL向けRenderDocumentDirectiveインターフェースの定義例 export declare namespace interfaces.alexa.presentation.apl { interface RenderDocumentDirective { 'type': 'Alexa.Presentation.APL.RenderDocument'; 'token'?: string; 'document'?: { [key: string]: any; }; 'datasources'?: { [key: string]: any; }; 'packages'?: Array; } } // ResponseBuilderで利用 builder.addDirective({ type : 'Alexa.Presentation.APL.RenderDocument', version: '1.0', document: require('./homepage.json'), datasources: require('./data.json') })
(node_modules/ask-sdk-model/index.d.tsを参照)
他オーディオやビデオの詳細は以下のリンク
セッション維持・終了の設定
セッションを維持する場合は、上記responseBuilderで以下の対応どちらかを行えば良い。
- reprompt でユーザーへのプロンプト再提示
- withShouldEndSession を false
逆にセッションを終了する場合は
- withShouldEndSession を true
withShouldEndSessionは画面付き端末処理をするかどうかで等でデフォルト設定値が変化する。
なのでreprompt指定がない場合は基本的に明示的に設定するべき。(スキル審査でもひっかかる案件)
userIdの取扱い
AmazonアカウントのユーザーIDをキーにするが、スキルを有効化し直すと変化するらしいので、スキルごとのユニークキーっぽい
永続アトリビュートは、永続ストレージ上でユーザーIDをプライマリキーとして保存されます。ユーザーIDはAlexaアプリのAmazonアカウントごとに一意ですが、ユーザーが一度スキルを無効化してから再び有効化すると変化するので注意が必要です。
個人的に今後の発展性で考えているのが、UserIdのスキル間連携。なので、スキルごとにユニークではなく、Amazonアカウント単位でユニークに取れる手法がないか模索。
ちょっと意図は違うけど、ASK SDKのuserId取得箇所をいじったスキルをshowさんが作成しているのは、何かのとっかかりにはなるかも↓。
三番目に公開したAlexaスキル「IT業界の深い闇」でSDKをなぜいじったか
アクティビティのカード情報
- 上記ResponseBuilderのCard系のメソッドで設定
- スキルの応答にカードを追加する
ダイアログモデル
- 必須スロットの要求
- スロット内容の確認
- インテント内容の確認
などを設定に基づいて自動で組み込める。
インテントで必須スロット情報なしで解釈されることもあるので、組み込んでおくとより安定しやすいかも。
確認ダイアログは、確実性があがるけどやりとりが増えるので使い所を見極めたいところかも。
注意・メモ
タイムゾーンについて
- AWSのタイムゾーンは、UTCのままで Lambda のdateとかも日本時間とずれる。
- Lambdaの環境変数設定すれば、変わる https://qiita.com/nullian/items/39ecf1f6d0194b72e8e6
一歩上の上のスキル開発
Database連携(DynamoDB)
ちょっと凝った便利系スキルを作ろうとすると案件にあがるのがDatabaseを利用したデータ保存。
AWSのサービスであるDynamoDBが最も手近でやりやすいDB(な気がする)。
AWS SDKをインストールしてれば以下のような形で利用可能
var AWS = require('aws-sdk'); AWS.config.update({ region: "ap-northeast-1", endpoint: "https://dynamodb.ap-northeast-1.amazonaws.com" }); var docClient = new AWS.DynamoDB.DocumentClient(); let params = { TableName: "TableName", KeyConditionExpression: "UserId = :u", ExpressionAttributeValues: { ":u": userId } }; let result = false; await docClient.query(params).promise(). then( data => { console.log("queryUser succeed."); func(data); result = true; }, error => { console.log("queryUser error."); } );
DynamoDBの要点・メモ
- NoSQL型のDB
- プライマリキーに対して、柔軟かつ任意の形式のデータを保存可能
- UpdateExpression等を適切に書くのがポイント
- query, updateメソッドなどは、requestまで飛ぶかどうかは、callback引数の有無で変わる
Called when a response from the service is returned. If a callback is not supplied, you must call AWS.Request.send() on the returned request object to initiate the request.
画面操作対応スキルの開発
- Displayテンプレートでテンプレに基づく画面表示が可能
- APL(Amazon Presentation Language)を使うと自分で画面表示カスタマイズ可能
- APLはイメージはHTML/CSSのAlexa版
APLについては別途まとめてます。
Progressive Responseを使った通知等
タイマー計測をしたいので、そこで、経過時間をいわせるのにはこれかな?
リマインダーAPI
↑タイマーの経過時間発話に使おうと思えばいけないこともなさそう。
リスト(やることリスト・買い物リスト)と連携
リスト操作が可能で、スマホ側で表示等の拡張した対応も。
SSML(独自の音声合成設定)
- SSMLで 発話の音声調整ができる
https://developer.amazon.com/ja/blogs/alexa/post/ff4f8bf0-4c86-4c5f-9c50-467816b0a017/jptraining6plus
開発環境
Nodejsでの開発環境構築についてのメモ。Java, PythonでもASK SDKは用意されており、同様に開発は可能。
ASK SDK Nodejs v2
上でもろもろメモしてるAlexaスキルを作るためのNode.jsのSDK。
TypeScriptで作成されており、Type情報等もあるので、TypeScriptで開発してもよし。
以下の記事でわかりやすくSDKの特徴が解説されている。特徴を引用させていただくと以下の通り。
Alexa SDKの特徴は、次のように列挙できます。
- NPMパッケージとして提供される
- コア機能のみのパッケージも利用可能 (Ver2)
- 各種のリクエストをハンドラとして定義できる
- ハンドラは、柔軟にグループ化や共通処理が可能(Ver2)
- セッションごとやセッションを跨ぐデータの永続化が可能 (Ver2)
- DynamoDB等のDBが使用可能(Ver2)
- すべての音声出力は、SSMLでラップされる
- 全てのLambdaイベントとコンテキストにアクセスが可能
- デバイスアドレスなどのAlexaサービスをラップし、簡単に利用可能 (Ver2)
- すべてのSDKツールが、handInputオブジェクトに用意され単体テストが可能 (Ver2)
- TypeScript定義ファイルと、.d.tsファイルの読み取りが可能 (Ver2)
- async/awaitが利用可能 (Ver2)
ASK CLI
スキルの新規作成、クローン、デプロイ等が行えるCLI。
やっぱりデプロイが楽ちんになるので、基本入れる。
Alexa Hosted Skill開発
AWSのアカウントや設定なしで、Lambda、DynamoDB、S3などが無料枠で勝手に使える超開発しやすくなるサービス。
スキルの作成段階でHosted Skill開発を選択する。
無料枠を勝手に超えることもないし、明示的にAWSアカウント移行も可能なので、これからは基本 Hosted Skill で作っとくといいかも。
ただ、DBがっつり使ったり、すでにAWSをしっかり使ってるならそっちのほうがいいか。
Alexa-hostedスキルを使用してスキルをエンドツーエンドで作成する
NOID
ノードエディタでプログラミング不要でスキル開発ができるサービス。
無料枠を超えると月1万円~でちょっとつらそう。
Alexa開発をお試しするにはいいのかも。
今後のAlexa SDK
Baby Activity Support
アメリカではじまった、赤ちゃんの行動記録系のサポートライブラリ。
Understand the Baby Activity Skill API
Enable Hands-Free Logging for Your Baby Care Apps and Devices with the Baby Activity Skill API
コメント