Alexa スキルを Serverless + Typescriptで開発する

## はじめに

自分もAlexaスキルを作ってみようとちょっといじってみた。
Alexaスキルのバックエンドには *AWS Lambda* を *Node.js* ランタイムで利用するのが手っ取り早い。
どうせなら、この機会に *Typescript* も使ってみようと思って *Typescript* と *VSCode* にも入門した。

また、*Lambda*のデプロイ等の操作には今の所、 *AWS SAM* よりも 手間が少ないと感じる *Serverless Framework* を利用している。

*Serverless Framework* にはGithubなどで公開されている既存のアプリケーションを元にアプリケーションの雛形を作る機能(`serverless install`)があるので、ベースの部分をテンプレートとして公開してみた。

[HeRoMo/serverless\-aws\-alexa\-ts](https://github.com/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](https://serverless.com/framework/docs/providers/aws/guide/installation/)
- AWSのアカウント
    - Lambdaのデプロイ、実行とIAMのRoleの作成等が許可されているもの。
- Alexaスキルのモデル
    - Alexaスキルのwebコンソールで作るやつ。スキルIDを先に用意しておきたいだけなので保存ができる程度の最低限の設定のみでOK。

### Serverlessのセットアップ

まずはServerless Frameworkのプロジェクトとしてのセットアップを行う。
次のコマンドを実行する。 `your-app-name`の部分はご自由に（変更したら後の説明も適宜読み替えよろしく）。

```bash
$ 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](https://serverless.com/framework/docs/providers/aws/cli-reference/config-credentials/)を見ながら設定すれば良い。
すでに *AWS CLI* がセットアップしている環境では、その認証情報を使うので何もしなくても良い[^1]。

[^1]: 複数のプロファイルを設定している場合は後述のコマンドは対応していないので、 `sls deploy --aws-profile <AWSアカウントのプロファイル名>` を利用してほしい。

認証設定ができれば次のコマンドでデプロイできる。
TypescriptがコンパイルされてLambda関数としてデプロイされる。

```bash
npm run sls:deploy
```

最初にデプロイしたときに Lambda関数のARN（関数を識別するためのID）が決まるので、AWSコンソールにログインして、デプロイした関数を探す。
米国東部 (バージニア北部)リージョンに *<your-app-name>-develop-handler* という名前でデプロイされているはず。見つけたらそのARN（Lambdaのコンソールの右上に表示されている `arn:aws:lambda`から始まる長い文字列）をメモして Alexaのコンソールのエンドポイントのデフォルトとして設定する。

### 発話モデルの管理

次に発話モデルの管理をできるように設定する。
Webコンソールで作成した発話モデル（インテントやスロット）はJSON形式で出力することができる。
発話モデルあってのスキルなのでちゃんと作ろうとすると、そのJSONもコードの一部としてリポジトリで管理したほうが良いと思う。

このテンプレートでは *ask-cli* を含んでおり、それを利用して発話モデルのデータのアップロード、ダウンロードをサポートしている。

次のようにコマンドを実行してまずは認証設定をおこなう。
*ask-cli*はLambdaのデプロイまでサポートしているが、その機能は使わないので、`Skip AWS credential for ask-cli.`を選択して良い。

```bash
$ /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.

認証設定ができれば、次のコマンドで発話モデルをアップロードできる。
<font color="Red">※すでにwebコンソール上で発話モデルを作り込んでいる場合、それを上書きで消してしまうので注意。</font>

```bash
$ npm run ask:update-model
```

逆にwebコンソールで設定した発話モデルを取得するには次のコマンドを実行する。

```bash
$ npm run ask:get-model
```

### 動作確認
- Lambda関数をデプロイ
- そのARNをスキルのエンドポイントに設定
- 発話モデルのアップロード

上記3点ができた時点でスキルを起動できるようになる。

Alexaのwebコンソールでテストを有効にして、Alexaシミュレータで**「サンプルスキルを開いて」**と入力すれば、**「Welcome to the Alexa Skills Kit, you can say hello!」** と返ってくるはず[^2]。

[^2]: ブラウザから音が出るので注意

## インテントハンドラーの実装
インテントを発話モデルで作成するとそれを受けるためのインテントハンドラーを実装する必要がある。
このテンプレートでは スキル起動時に呼ばれる `LaunchRequestHandler` とデフォルトでスキルに設定される3つのビルトインインテントを処理するハンドラ、そしてサンプル実装として `HelloIntentHandler`が *src/handlers/*以下においている。

それらを元に自分のスキルに合うように実装していけばいいようにしている。

### SSML
ハンドラーの主な仕事はユーザの発話に合わせた返答のセリフを生成することである。
単にテキストとして実装して返すこともできるが、それではイントネーションや数字やアルファベットの読み方が思ったとおりにならない場合がある[^3]。また、声を大きくしたり、速く喋ったり、ゆっくり話したりしたい箇所もあるだろう。

[^3]: 「123」と書いて「ひゃくにじゅさん」ではなく「いちにいさん」と読ませたいときもあると思う。

そのような読み方を指定するマークアップとしてSSMLというものがある。Alexaでもサポートされており、スキルの応答で利用できる[^4]。ただ、HTMLっぽいマークアップなので開始タグとそれに対応した閉じタグを書く必要があったりと少々生で書くのは面倒くさい。

[^4]: [音声合成マークアップ言語（SSML）のリファレンス \| Custom Skills](https://developer.amazon.com/ja/docs/custom-skills/speech-synthesis-markup-language-ssml-reference.html)

それを多少楽にしてくれる [ssml-builder](https://github.com/mandnyc/ssml-builder) というライブラリがある。このテンプレートには型定義ファイルとともに含めているのですぐに使い始めることができる。VSCodeだとコード補完も利用できるようになっている。

## テスト
さて、開発中には実際にコードを動かしてテストを行うことになるが、いちいちLambdaにデプロイしてそれをAlexaコンソールから動かして確認するのを繰り返すのは結構面倒。ローカルで動かして確認しながら実装したい。

### ローカル実行
Serverless Frameworkのサブコマンドに `invoke local`があり、それを使用してAlexaスキル用のLambda関数もローカル実行できる。
それには次のようにコマンドを実行すれば良い。

```bash
$ serverless invoke local -f handler -p /__tests__/fixtures/LaunchRequest.json
```

`LaunchRequest.json`はテンプレートに含めているもので AlexaコンソールのAlexaシミュレータ画面に表示されるLaunchRequestのJSON入力を単にコピーしてJSONファイルとして保存したものである。

このコマンドを毎回入力するのも面倒なので、次のような設定をVSCodeの`launch.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](https://github.com/bespoken/virtual-alexa)を利用している。

テストコードのサンプル実装として次も含んでいる。
*virtual-alexa* では発話モデルも読み込んで発話に対する応答をテストするので発話モデルも含めたテストが可能になっている。

```typescript:__tests__/integrations/alexa.spec.ts
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のテンプレートを公開したのでそれを紹介した。

テストについては詳しく書くと長くなりすぎると思ったので、サラリと流したが別のポストを作るかもしれない。