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作った話」にサンプルなどを載せていますのでぜひ。

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark