ask-utilsでAlexaスキル開発を楽にする

Alexaスキル作ってますか。量産してますか。 いっぱいスキル作ってると、同じコードのコピペが増えてきて面倒だなーって感じる時ありませんか。 10スキルいったあたりでめんどくさくなってきたので、よく使うfixture系を […]

広告ここから
広告ここまで

目次

    Alexaスキル作ってますか。量産してますか。

    いっぱいスキル作ってると、同じコードのコピペが増えてきて面倒だなーって感じる時ありませんか。

    10スキルいったあたりでめんどくさくなってきたので、よく使うfixture系を全部ライブラリにまとめています。

    npm i -S ask-utils でダウンロードできますので、楽したい方はぜひ試してみてください。

    ただ、これの使い方をちゃんと紹介してなかったので、あらためて紹介していきます。

    ask-utilsにある機能(2019/06時点)

    だいたい3つ以上のスキルで書いたコードはここに放り込まれていきます。なので機能はどんどん増えていきます。

    また、スキル課金やAmazon Payなど「使う人は使うし、使わない人は使わない」という機能は@ask-utils/isp のように別ライブラリにしています。

    現時点で用意している機能は以下のとおりです。

    • canHandleでのリクエストタイプ判定ヘルパー
    • attributeManager系のヘルパー
    • リクエストの値を取得するためのヘルパー
    • プログレッシブ応答のためのヘルパー
    • シンプルなリクエスト・レスポンスログ収集ヘルパー
    • DynamoDBクライアント
    • レスポンスビルダー(beta)

    [Tips] Typescript ready

    ask-utilsはTypescriptで実装しています。

    ask-sdk-model / ask-sdk-coreの型定義を利用するようにしていますので、Typescriptで実装されている方も気兼ねなく利用できます。

    なおこの記事では、hosted skillでの利用も想定して、ビルドなしで実行できる書き方でサンプルを記述します。

    canHandleでのリクエストタイプ判定ヘルパー

    ask-utilsを作ったきっかけ部分です。

    Alexaでは以下のようなオブジェクトを並べて、canHandleでtrueを返したオブジェクトのhandleを実行します。

    const { getRequestType } = require(‘ask-sdk’)
    const ExampleHandler = {
      canHandle(handlerInput) {
        return getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest'
      },
      handle(handlerInput) {
        const speechText = 'This is example skill'
        return handlerInput.responseBuilder
          .speak(speechText)
          .getResponse()
      }
    }

    getRequestTypeが登場したことでこれまでよりも煩雑さがなくなりましたが、これでもLaunchRequestを作るたびにこれを書くのはちょっと冗長です。

    ということでask-utilsでは、isXXXXというヘルパーを用意しています。先程の例では、isLaunchRequestという関数を使うことができます。

    const { isLaunchReques } = require(‘ask-utils')
    const LaunchRequestHandler = {
      canHandle (handlerInput) {
        return isLaunchRequest(handlerInput)
      },
      handle (handlerInput) {
        const speechText = 'Thi
        const speechText = 'This is example skill'
        return handlerInput.responseBuilder
          .speak(speechText)
          .getResponse()
      }
    }

    この他にも以下のようなヘルパーが用意されています。

    関数名 対応するIntent 戻り値
    isHelpIntent(handlerInput) AMAZON.HelpIntent boolean
    isYesIntent(handlerInput) AMAZON.YesIntent boolean
    isNoIntent(handlerInput) AMAZON.NoIntent boolean
    isStopIntent(handlerInput) AMAZON.StopIntent boolean
    isCancelIntent(handlerInput) AMAZON.CancelIntent boolean
    isMachedIntent(handlerInput, ‘ExampleIntent’) ExampleIntent (IntentRequest) boolean

    最後のisMatchedIntentはrequest.type + request.intent.nameで判定してくれますので、自分で追加したインテントのハンドラーを作る時に便利です。

    Dialogのstatusもサポート

    この他にもDialogのstatusについてもヘルパーがあります。

    関数名 対応するstate 戻り値
    isDialogStarted STARTED boolean
    isDialogInprogres IN_PROGRESS boolean
    isDialogCompleted COMPLETED boolean

    SessionAttributesManager系のヘルパー

    次はattributeManager系のヘルパーです。

    SessionAttributesManagerでは、値の更新・追加を行う時に以下のような書き方が必要です。

    const atts = handlerInput.attributesManager.getSessionAttributes()
    atts.newAtt = ‘hello’
    handlerInput.attributesManager.setSessionAttributes(atts)

    ただ、個人的にこの書き方があまり好きでないのと、毎回3行書くのも嫌だなぁと思ったので、以下のようなヘルパーを用意しました。

    const { updateSessionAttributes } = require(‘ask-utils')
    
    updateSessionAttributes(handlerInput, {newAtt: ‘hello’})

    追加・更新したい属性だけ書いたオブジェクトを第二引数に渡してやれば、内部でマージして保存してくれます。

    他にもattributes決め打ちで取得できるgetSessionAttribute(handlerInput, ‘name’)という関数も用意しています。

    リクエストの値を取得するためのヘルパー

    次はリクエストに含まれている値を取得する際のヘルパーです。

    以下のような値を一発で取得できます。

    // Synonymの値もフォローした状態でSlotの値を返す
    const name = getSlotValue(handlerInput, ‘name’) // string
    
    // Slotのオブジェクトをとってくる
    const slot = getSlot(handlerInput, ‘name’) // object

    あとはnullableになっていてasとか使わないといけなくなる系の値向けにTypeGuard的なものも用意しています。

    // テストなどでserviceClientが無い時を想定したTypeGuard
    const client = handlerInput.serviceClientFactory
    if (!hasServiceClientFactory(client)) throw new Error(‘no client’)
    // TypeGuardを通るので、undefinedでTypeScriptのエラーが出ない
    client.getUpsServiceClient()
    
    // 'Connections.Response'の時にrequestEnvelopeをinterfaces.connections.ConnectionsResponseにする
    isSkillConnectionResponse(handlerInput.requestEnvelope)
    
    // 'Connections.Request'の時にrequestEnvelopeをinterfaces.connections.ConnectionsRequestにする
    isSkillConnectionRequest(handlerInput.requestEnvelope)

    もともとはuserIdやdeviceIdなどもサポートしていたのですが、コアの方でUtil系の関数を増やす動きがあったので、そちらから使えるようにしてもらいました。 (その時のPR: https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs/pull/546)

    userIdやdeviceIdはask-sdkまたはask-sdk-coreにあるヘルパーを使って取得しましょう。

    プログレッシブ応答のためのヘルパー

    Alexaには、時間のかかる処理を実行する間に「つなぎの発話」をさせることが可能です。

    このための機能を「プログレッシブ応答」とよんでいます。

    スキルは、ユーザーのリクエストへの完全な応答を準備している間もユーザーの関心を引き続けられるようプログレッシブ応答を送信することができます。プログレッシブ応答は、スキルの完全な応答を待つ間にAlexaが再生する割り込みのSSMLコンテンツ(テキストの読み上げや短いオーディオ)です。

    https://developer.amazon.com/ja/docs/custom-skills/send-the-user-a-progressive-response.html

    で、これを実装するためにはAlexa側で提供されているAPIを非同期で呼出す必要があります。

        const { serviceClientFactory, requestEnvelope } = handlerInput
        const client = serviceClientFactory.getDirectiveServiceClient()
        const { requestId } = requestEnvelope.request
        const payload: services.directive.SendDirectiveRequest = {
            header: {
                requestId
            },
            directive: {
                type: 'VoicePlayer.Speak',
                speech
            }
        }
        client.enqueue(payload)

    これを毎回実装するのも煩雑なのと、プログレッシブ応答が失敗しても処理を中断させてはいけない問題がありますので、これもヘルパーを用意しました。

    await enqueueProgressiveResponse(handlerInput, 'Now your data processing')
    
    // エラー時のハンドルを追加できる(Promise<void>なのでレポーティングとかリトライくらいにしか使えないけど)
    await enqueueProgressiveResponse(handlerInput, 'Now your data processing', (error) => notifySlack(error))

    こちらでは、enqueueする際にtry ~ catchしてAPIがエラーになっても処理が継続するように作られています。

    また、ユニットテストなどでリクエストを送るために必要な値が足りないときも何もしない作りになっていますので、考慮事項を減らせるかなと思います。

    シンプルなリクエスト・レスポンスログ収集ヘルパー

    スキル開発において、「どのようなリクエストがきて、どのようなレスポンスを返したか」という記録はとても重要です。

    これも毎回Loggerを作るのが面倒だったので、簡単なヘルパーを用意しました。

    const Alexa = require('ask-sdk-core')
    const { RequestLogger, ResponseLogger } = require(‘ask-utils’)
    
    exports.handler = Alexa.SkillBuilders.custom()
      .addRequestHandlers(…)
      .addRequestInterceptors(RequestLogger)
      .addResponseInterceptors(ResponseLogger)
      .lamnda()

    これでCloudWatch Logsにリクエスト・レスポンスを記録してくれます。

    ただしまるごとconsole.logしているだけですので、銀行系などのセンシティブな情報を扱うスキルでは適宜マスクする形のロガーを作られることをおすすめします。

    DynamoDBクライアント

    完全なわがままなのですが、S3とDynamoDBどちらも利用したい派故につくりました。

    初回利用か否かや利用回数などの基本的な情報はコストを抑える目的でS3に保存しています。一方でクエリしたい情報がある場合はやはりDynamoDBに保存したいです。

    また、ask-sdkが用意するヘルパーでは、1つのキーにMap型で保存されるため、クエリし辛いというデメリットもありました。

    ということで独自に用意したのがこちらです。

    const { DBClient } = require(‘ask-sdk’)
    
    // 第二引数でclient / primary key / ログ設定変更可能
    const client = new DBClient(‘YOUR_TABLE_NAME’)
    
    const item = await client.get(‘ask.user.xxxx’)
    item.newAtts = ‘hello’
    await client.put(‘ask.user.xxx’, item)

    こちらのputでは、第二引数で渡したオブジェクトのkeyでそれぞれDynamoDBにキーを作ります。

    クエリしたい情報が複数ある場合などには使いやすいと思います。

    レスポンスビルダー(beta)

    これはβ版ですので、突然仕様が変わる可能性もあります。

    スキルを開発していると、状況によって「ちょっと一言」付け加えたい時などがあります。

    そういう場合にstringをどんどんaddしていくのは煩雑ですし、とはいえ配列でpushしていくのも若干面倒な時があります。

    ということで最近はレスポンスの値を作るためのビルダーを作って試してみています。

    const { ContentBuilder } = require(‘ask-utils’)
    const { getLocale } = require(‘ask-sdk’)
    
    handle(handlerInput) {
      const { requestEnvelope, responseBuilder } = handlerInput
      const builder = new ContentBuilder(getLocale(requestEnvelope), responseBuilder)
     
      builder.putSpeechText(‘hello’).putRepromptText(‘What do you want to do?’)
      builder.putSpeechText(‘how do you do?’)
      return builder.getResponse()
    }

    今の所、speech / repromptとaddDirectiveの3メソッドをサポートしています。

    おわりに

    この他にもAmazon Pay / ISP(スキル課金) / Proactive Event(通知)などに特化したヘルパーライブラリも作っています。

    おおよその情報はask-utils.dev から見れますので、こちらもぜひ。

    質問・バグレポートについて

    GitHubにIssueでお願いします。DMでのレポートや要望についてはかなりの確率でスルーしますのでご了承くださいませ。

    オープンソースなプロダクトなので、議論・要望などもオープンな場所でやりましょう。

    ちなみにPull Requestもらえるとめっちゃ喜びます。Amazonのほしいものリストで本とか送られるとさらに喜びます。

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