はじめに
自分もAlexaスキルを作ってみようとちょっといじってみた。
Alexaスキルのバックエンドには AWS Lambda を Node.js ランタイムで利用するのが手っ取り早い。
どうせなら、この機会に Typescript も使ってみようと思って Typescript と VSCode にも入門した。
また、Lambdaのデプロイ等の操作には今の所、 AWS SAM よりも 手間が少ないと感じる Serverless Framework を利用している。
Serverless Framework にはGithubなどで公開されている既存のアプリケーションを元にアプリケーションの雛形を作る機能(serverless install
)があるので、ベースの部分をテンプレートとして公開してみた。
HeRoMo/serverless-aws-alexa-ts
READMEの日本語版がわりに、このテンプレートについて書いてみる。
このテンプレートが含んでいる主な要素
- Serverless framework によるビルド、デプロイ、ローカル実行
- Tyepscript で実装できる環境(tslint、いくつかの独自型定義ファイル)
- ask-cli による 発話モデルの管理(ダウンロードとアップロード)
- Jestによるユニットテスト
- Visual Studio Code によるデバッグ実行
- とりあえず、Alexaスキルの起動確認ができる程度の発話モデルとLambdaの実装
セットアップからスキルの起動確認まで
Alexaスキルは大きく分けて2つのパートがある。
一つは 主にAlexaコンソールで作る発話モデル。
もう一つは、発話モデルからのリクエストを受けて返事を生成するバックエンド処理。
このServerlessのテンプレートはAlexaスキルの起動確認ができる程度の発話モデルとLambdaの実装を含んでいるのでそれを動かすまでの手順を説明する。
事前準備
次を準備しておく。
- Serverless Framework
- AWSのアカウント
- Lambdaのデプロイ、実行とIAMのRoleの作成等が許可されているもの。
- Alexaスキルのモデル
- Alexaスキルのwebコンソールで作るやつ。スキルIDを先に用意しておきたいだけなので保存ができる程度の最低限の設定のみでOK。
Serverlessのセットアップ
まずはServerless Frameworkのプロジェクトとしてのセットアップを行う。
次のコマンドを実行する。 your-app-name
の部分はご自由に(変更したら後の説明も適宜読み替えよろしく)。
$ serverless install --url https://github.com/HeRoMo/serverless-aws-alexa-ts --name your-app-name
$ cd your-app-name
$ npm install
次にコンフィグファイルを用意する。
config/config.yml.template に雛形を用意しているので、これを config.yml にリネームして編集する。
ソースに書きづらい項目はLambdaでは環境変数として与えるのがよいが、それらの値 serverless.yml に直接書くと、やはりリポジトリにコミットするのは憚れる場合があろうとコンフィグファイルを読み込む形にしている。
自分のAlexaスキルのID( amzn1.ask.skill.xxxxxx...
から始まるアレ)を設定する。
Lambda のデプロイ
デプロイに先立ってAWSにデプロイするための認証設定を行っておく必要がある。
Serverless Framework Commands - AWS Lambda - Config Credentialsを見ながら設定すれば良い。
すでに AWS CLI がセットアップしている環境では、その認証情報を使うので何もしなくても良い1。
認証設定ができれば次のコマンドでデプロイできる。
TypescriptがコンパイルされてLambda関数としてデプロイされる。
npm run sls:deploy
最初にデプロイしたときに Lambda関数のARN(関数を識別するためのID)が決まるので、AWSコンソールにログインして、デプロイした関数を探す。
米国東部 (バージニア北部)リージョンに -develop-handler という名前でデプロイされているはず。見つけたらそのARN(Lambdaのコンソールの右上に表示されている arn:aws:lambda
から始まる長い文字列)をメモして Alexaのコンソールのエンドポイントのデフォルトとして設定する。
発話モデルの管理
次に発話モデルの管理をできるように設定する。
Webコンソールで作成した発話モデル(インテントやスロット)はJSON形式で出力することができる。
発話モデルあってのスキルなのでちゃんと作ろうとすると、そのJSONもコードの一部としてリポジトリで管理したほうが良いと思う。
このテンプレートでは ask-cli を含んでおり、それを利用して発話モデルのデータのアップロード、ダウンロードをサポートしている。
次のようにコマンドを実行してまずは認証設定をおこなう。
ask-cliはLambdaのデプロイまでサポートしているが、その機能は使わないので、Skip AWS credential for ask-cli.
を選択して良い。
$ /node_modules/.bin/ask init
-------------------- Initialize CLI --------------------
Setting up ask profile: [default]
? Please choose one from the following AWS profiles for skill's Lambda function deployment.
(Use arrow keys)
❯ default
──────────────
Skip AWS credential for ask-cli.
Use the AWS environment variables.
──────────────
続いてブラウザが開いて、amazon developer のログインを促される。
ログインして次のメッセージがブラウザに表示されれば認証成功。
Sign in was successful. Close this browser and return to the command line interface.
認証設定ができれば、次のコマンドで発話モデルをアップロードできる。
※すでにwebコンソール上で発話モデルを作り込んでいる場合、それを上書きで消してしまうので注意。
$ npm run ask:update-model
逆にwebコンソールで設定した発話モデルを取得するには次のコマンドを実行する。
$ npm run ask:get-model
動作確認
- Lambda関数をデプロイ
- そのARNをスキルのエンドポイントに設定
- 発話モデルのアップロード
上記3点ができた時点でスキルを起動できるようになる。
Alexaのwebコンソールでテストを有効にして、Alexaシミュレータで**「サンプルスキルを開いて」と入力すれば、「Welcome to the Alexa Skills Kit, you can say hello!」** と返ってくるはず2。
インテントハンドラーの実装
インテントを発話モデルで作成するとそれを受けるためのインテントハンドラーを実装する必要がある。
このテンプレートでは スキル起動時に呼ばれる LaunchRequestHandler
とデフォルトでスキルに設定される3つのビルトインインテントを処理するハンドラ、そしてサンプル実装として HelloIntentHandler
が *src/handlers/*以下においている。
それらを元に自分のスキルに合うように実装していけばいいようにしている。
SSML
ハンドラーの主な仕事はユーザの発話に合わせた返答のセリフを生成することである。
単にテキストとして実装して返すこともできるが、それではイントネーションや数字やアルファベットの読み方が思ったとおりにならない場合がある3。また、声を大きくしたり、速く喋ったり、ゆっくり話したりしたい箇所もあるだろう。
そのような読み方を指定するマークアップとしてSSMLというものがある。Alexaでもサポートされており、スキルの応答で利用できる4。ただ、HTMLっぽいマークアップなので開始タグとそれに対応した閉じタグを書く必要があったりと少々生で書くのは面倒くさい。
それを多少楽にしてくれる ssml-builder というライブラリがある。このテンプレートには型定義ファイルとともに含めているのですぐに使い始めることができる。VSCodeだとコード補完も利用できるようになっている。
テスト
さて、開発中には実際にコードを動かしてテストを行うことになるが、いちいちLambdaにデプロイしてそれをAlexaコンソールから動かして確認するのを繰り返すのは結構面倒。ローカルで動かして確認しながら実装したい。
ローカル実行
Serverless Frameworkのサブコマンドに invoke local
があり、それを使用してAlexaスキル用のLambda関数もローカル実行できる。
それには次のようにコマンドを実行すれば良い。
$ serverless invoke local -f handler -p /__tests__/fixtures/LaunchRequest.json
LaunchRequest.json
はテンプレートに含めているもので AlexaコンソールのAlexaシミュレータ画面に表示されるLaunchRequestのJSON入力を単にコピーしてJSONファイルとして保存したものである。
このコマンドを毎回入力するのも面倒なので、次のような設定をVSCodeのlaunch.json
に追加している。
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "LaunchRequest",
"program": "${workspaceRoot}/node_modules/.bin/sls",
"cwd": "${workspaceRoot}",
"args": ["invoke", "local", "-f", "handler", "-p", "./__tests__/fixtures/LaunchRequest.json"],
"outFiles": ["${workspaceRoot}/.webpack/service/*.js"],
"sourceMaps": true
}
]
}
これにより、VSCodeのデバッグ機能から実行すること事ができる。
コンパイル時にソースマップも出力しているのでブレークポイントを仕掛けてステップ実行も可能となっている。
Jest + virtual-alexa によるテスト
上記は単にローカルで実行できるだけで、確認は目視なので繰り返すと面倒になってくる。CIにも組み込めない。
やはりテスティングフレームワークによるテストをしたくなる。
テンプレートではテスティングフレームワークとしてJestを入れている。また、Alexaスキルを実行するのに virtual-alexaを利用している。
テストコードのサンプル実装として次も含んでいる。
virtual-alexa では発話モデルも読み込んで発話に対する応答をテストするので発話モデルも含めたテストが可能になっている。
import {SkillResponse, VirtualAlexa} from "virtual-alexa";
const alexa = VirtualAlexa.Builder()
.handler("./.webpack/service/index.handler")
.interactionModelFile("./interaction_models/model_ja-JP.json")
.create();
describe("launchResponse", async () => {
it("response launch message", async () => {
const launchResponse = await alexa.launch();
const responseSSML = launchResponse.response.outputSpeech.ssml;
expect(responseSSML).toBe("<speak>Welcome to the Alexa Skills Kit, you can say hello!</speak>");
});
});
describe("HelloIntentHandler", () => {
it("response Hello!", async () => {
const response = await alexa.utter("こんにちは") as SkillResponse;
const responseSSML = response.response.outputSpeech.ssml;
expect(responseSSML).toBe("<speak>Hello!</speak>");
});
});
DynamoDB
Alexaのボイスモデルと属性をやりとし、それをセッションを超えて覚えておくようなスキルを書く場合、何らかのデータベースとの連携が必要になるが、最も楽なのはDynamoDBだろう。
このテンプレートでは一応、そのためのコードも含めている。ただし、コメントアウトしているので serverless.yml と index.tsの該当部分をアンコメントしてして利用いただきたい。
おわりに
TypescriptでAlexaスキルを開発するときに、デプロイからテストまで一通り必要な項目を含むServerless Frameworkのテンプレートを公開したのでそれを紹介した。
テストについては詳しく書くと長くなりすぎると思ったので、サラリと流したが別のポストを作るかもしれない。