ASK CLIとServerless FrameworkでTypeScriptをつかったAlexaスキル開発
この記事はAlexa Advent Calendar 2018およびAlexa Skills Kit SDK Advent Calendar 2018 18日目の記事です。 プロジェクトの立ち上げ まずはASK […]
目次
この記事はAlexa Advent Calendar 2018およびAlexa Skills Kit SDK Advent Calendar 2018 18日目の記事です。
プロジェクトの立ち上げ
まずはASK CLIでプロジェクトを作りましょう。
$ ask new -n alexa-typescript
$ cd alexa-typescript
続いてLambdaのソースコードをServerless Frameworkベースに置き換えます。
$ cd lambda/custom
$ rm -rf ./*
$ sls create -t aws-alexa-typescript
$ yarn install
こんな感じのディレクトリ構成になっていればOKです。
$ tree -I node_modules
.
├── lambda
│ └── custom
│ ├── handler.ts
│ ├── package.json
│ ├── serverless.yml
│ ├── source-map-install.js
│ ├── tsconfig.json
│ ├── webpack.config.js
│ └── yarn.lock
├── models
│ └── en-US.json
└── skill.json
3 directories, 8 files
Lambdaの準備
続いてLambdaのデプロイとAlexaへのつなぎ込みをやっていきます。
TypeScriptはこのままでも問題ありませんが、compilerOptions.strict: true
にしておくとより厳密な型チェックが可能です。
また、serverless.ymlを見ると、ここだけでスキルセットアップが完結するようになっています。
service:
name: aws-alexa-typescript
plugins:
- serverless-webpack
- serverless-alexa-skills
provider:
name: aws
runtime: nodejs8.10
custom:
alexa:
# Step 1: Run `sls alexa auth` to authenticate
# Step 2: Run `sls alexa create --name "Serverless Alexa Typescript" --locale en-GB --type custom` to create a new skill
skills:
# Step 3: Paste the skill id returned by the create command here:
- id: amzn1.ask.skill.xxxx-xxxx-xxxx-xxxx-xxxx
manifest:
publishingInformation:
locales:
en-GB:
name: Serverless Alexa Typescript
apis:
custom:
endpoint:
# Step 4: Do your first deploy of your Serverless stack
# Step 5: Paste the ARN of your lambda here:
uri: arn:aws:lambda:[region]:[account-id]:function:[function-name]
# Step 6: Run `sls alexa update` to deploy the skill manifest
# Step 7: Run `sls alexa build` to build the skill interaction model
# Step 8: Enable the skill in the Alexa app to start testing.
manifestVersion: '1.0'
models:
en-GB:
interactionModel:
languageModel:
invocationName: serverless typescript
intents:
- name: HelloIntent
samples:
- 'hello'
functions:
alexa:
handler: handler.alexa
events:
- alexaSkill: ${self:custom.alexa.skills.0.id}
ですが今回はすでにASK CLI経由で公開済みのスキルをTypeScriptに載せ換えることをメインにしたいと思いますので、照井さんごめんなさいという気持ちを持ちつつ以下のように書き換えます。
service:
name: aws-alexa-typescript
plugins:
- serverless-webpack
provider:
name: aws
runtime: nodejs8.10
functions:
alexa:
handler: handler.alexa
events:
- alexaSkill
あとはデプロイするだけです。
$ sls deploy
Service Information
service: aws-alexa-typescript
stage: dev
region: us-east-1
stack: aws-alexa-typescript-dev
api keys:
None
endpoints:
None
functions:
alexa: aws-alexa-typescript-dev-alexa
アップしたLambdaのARNをAWS CLIで取得しておきましょう。lambdaではなく、CloudFormationの方から引くこともできますので、そこはお好みでどうぞ。
$ aws lambda list-functions | grep aws-alexa-typescript-dev-alexa
"FunctionName": "aws-alexa-typescript-dev-alexa",
"FunctionArn": "arn:aws:lambda:us-east-1:99999999:function:aws-alexa-typescript-dev-alexa",
skill.jsonの更新
ここからはAlexaへの組み込みです。まずはskill.jsonを書き換えましょう。
"apis": {
"custom": {
"endpoint": {
"uri": "arn:aws:lambda:us-east-1:99999999:function:aws-alexa-typescript-dev-alexa"
}
}
余談ですが、skill.jsonやmodelのアップデートはVS Codeがおすすめです。コードスニペットが公式から出ていますので、入力補完が効いて便利です。
.ask/configの更新
続いてconfigの更新も行います。こちらの更新を忘れるとデプロイに失敗しますので要注意です。
{
"deploy_settings": {
"default": {
"skill_id": "",
"was_cloned": false,
"merge": {
"manifest": {
"apis": {
"custom": {
"endpoint": {
"uri": "arn:aws:lambda:us-east-1:999999:function:aws-alexa-typescript-dev-alexa"
}
}
}
}
}
}
}
}
これでデプロイ準備ができました。
$ ask deploy
[Info]: Could not find hooks folder, creating a new hooks folder and downloading scripts.
Profile for the deployment: [default]
-------------------- Create Skill Project --------------------
Skill Id: amzn1.ask.skill.xxxx-xxxx-xxxx-xxx
Skill deployment finished.
Model deployment finished.
[Info]: No lambda functions need to be deployed.
[Info]: No in-skill product to be deployed.
Your skill is now deployed and enabled in the development stage. Try simulate your Alexa skill skill using "ask dialog" command.
Skill Idが発行されています。最後にこれをserverless.yaml
にセットしておきましょう。
service:
name: aws-alexa-typescript
plugins:
- serverless-webpack
provider:
name: aws
runtime: nodejs8.10
functions:
alexa:
handler: handler.alexa
events:
- alexaSkill: amzn1.ask.skill.xxxx-xxxx-xxxx-xxx
テストする
実際につながっているかのテストは、以下のコマンドでやるのが便利です。
*US以外はmodels/以下を書き換えてください。
*defaultのprofile限定です。
$ ask simulate -s $(cat .ask/config | jq -r .deploy_settings.default.skill_id) -l en-US -t "open $(cat models/en-US.json | jq -r .interactionModel.languageModel.invocationName)"
これでレスポンスのJSONが返って来ればOKです。
TypeScriptでインテントを作る
これだけだとTypeScript関係ないので、ちょっとだけやってみましょう。
型をインストールする
ASK SDKの型情報はask-sdk-modelを入れることで使えます。
$ yarn add ask-sdk-model
インテントを作る
デフォルトで書かれているコードを書き直すとこんな感じになります。
import * as Ask from 'ask-sdk';
import { Response } from 'ask-sdk-model';
import HandlerInput = Ask.HandlerInput
import RequestHandler = Ask.RequestHandler
const NewHandler: RequestHandler = {
canHandle(handlerInput: HandlerInput): boolean {
return true
},
handle(handlerInput: HandlerInput): Response {
return handlerInput.responseBuilder
.speak('Hello world!')
.getResponse()
}
}
export const alexa = Ask.SkillBuilders.custom()
.addRequestHandlers(
NewHandler
)
.lambda();
レスポンスについてはビルダーを使うので、基本的にはあまり気にすることもなさそうですね。addDirective
やAPL系のレスポンスを入れるときに変な構造になっていないかチェックするくらいでしょうか。
// intent nameの比較だけする関数
const isMatchedIntentName = (request: IntentRequest, intentName: string): boolean => {
return request.intent.name === intentName
}
// 意図したIntentRequestかを確認する関数
const isMatchedIntent = (handlerInput: HandlerInput, intentName: string): boolean => {
if (handlerInput.requestEnvelope.request.type !== 'IntentRequest') return false
return isMatchedIntentName(handlerInput.requestEnvelope.request, intentName)
}
// Displayに対応しているかどうかを確認する関数
const isSupportedDisplay = (system: SystemState): boolean => {
if (!system.device) return false
return !!system.device.supportedInterfaces.Display
}
// BodyTemplate1のディレクティブだけ返す関数
const getBodyTemplate1 = (title: string, content: string): interfaces.display.BodyTemplate1=> {
const textContent = new Ask.RichTextContentHelper()
.withPrimaryText(content)
.getTextContent()
return {
type: 'BodyTemplate1',
backButton: 'VISIBLE',
title,
textContent,
token: 'token'
}
}
const DisplayHandler: RequestHandler = {
canHandle(handlerInput: HandlerInput): boolean {
return isMatchedIntent(handlerInput, 'DisplayIntent')
},
handle(handlerInput: HandlerInput): Response {
const response = handlerInput.responseBuilder
.speak('Hello world!')
if (isSupportedDisplay(handlerInput.requestEnvelope.context.System)) {
const displayDirective = getBodyTemplate1('Hello', 'world')
response.addRenderTemplateDirective(displayDirective)
}
return response.getResponse()
}
}
おわりに
Alexaはディスプレイ対応・非対応どちらにも対応したレスポンスを作る必要があります。そのためhandle()
メソッドの戻り値をStrictにするのはちょっと辛そうです。
どちらかというと上記のサンプルのように、directiveやAPLのテンプレートといった構文に不安の残る実装については別関数かクラスメソッドにして、そちらで型をチェックするようにするとよいでのはないかと思います。
あとは型定義しておくと、VS CodeやJetBrains系のIDEが入力補完やコードジャンプなどをやってくれるのが便利ですね。初めて使うDirectiveでも、interfaceと公式のドキュメントみればだいたいどういう値を入れれば良いかわかるのでとても良いです。
そしてこれを応用してディレクティブビルダーを1つ作ってみました。こちらについては、「AlexaでのAmazon Pay連携実装が大変そうだったのでSDK作った話」にサンプルなどを載せていますのでぜひ。