kun432's blog

Alexaなどスマートスピーカーの話題中心に、Voiceflowの日本語情報を発信してます。たまにAWSやkubernetesなど。

Interceptorを使ったアトリビュートの管理をざっくり理解する

Alexa道場の最新エピソードで、Alexa Champion伊東さんによる「シチュエーショナルデザイン」の話を見ました。「シチュエーショナルデザイン」についてはVoiceflowでどうするか?というのをまとめたいと思いつつ、今回注目したのはライブコーディングの方です。他の人が書いたコードを見るのってとてもいい勉強になりますよね。普段はVoiceflowをよく使うのでコードを書くことはそれほど多くないし、仕事で触ってきたのは専らPerlだったりなので、JSのコーディング力ももっと高めていきたい!

ということで、ちょっとポイントを絞って、Interceptorを使ったセッション情報の管理を少し追いかけてみたいと思います。あんまり関係ないですが、Alexa-hostedで試してます。

Request Interceptorを使った永続アトリビュートの読み出し

動画の中にもありましたが、Request Interceptorは、各handlerに渡される前にかならず実行される処理です。なので共通的に常に行うような処理、例えばhandlerInputの中身をログに残しておく、永続セッションの読み出しを行うなどに向いています。Request Interceptorを使用しない場合、永続セッションの情報を必要とするすべてのハンドラに読み出し処理を書かないといけないので、積極的に活用していきたい機能です。

「妄想トリップ」のサンプルコードでは、永続セッションのストレージをS3で行っていますが、バケット名が指定されていますね。

const storage = new S3PersistenceAdapter({
  bucketName: 'alexa-skill-trip-trip'
})

普通にLambdaを使う場合はこれでいいのですが、Alexa-hostedの場合、S3バケット名は自動生成されて決められているので、自分で指定せずに環境変数から取る形になります。あと、小さなところだと公式で紹介されている書き方とは少し違うので、ちょっと公式に習った書き方に直してみたいと思います。

公式の記載通り、skillBuilderハンドラに .withPersistenceAdapterを追加します。そして、.addRequestInterceptors(RequestInterceptor)でRequest Interceptorを追加します。

exports.handler = skillBuilder
  .addRequestHandlers(
    LaunchRequestHandler,
    DestinationCityIntentHandler,
    DestinationCountryIntentHandler,
    HelpHandler,
    ExitHandler,
    FallbackHandler,
    SessionEndedRequestHandler,
  )
  .addRequestInterceptors(RequestInterceptor)           // ★ココ
  .addErrorHandlers(ErrorHandler)
  .withPersistenceAdapter(                              // ★ココ
    new persistenceAdapter.S3PersistenceAdapter({       // ★ココ
      bucketName: process.env.S3_PERSISTENCE_BUCKET,    // ★ココ
      s3Client: new AWS.S3({                            // ★ココ
        apiVersion: 'latest',                           // ★ココ
        region: process.env.S3_PERSISTENCE_REGION})     // ★ココ
      })                                                // ★ココ
    )                                                   // ★ココ
  .lambda();

Request Interceptorです。元のサンプルではpersisteceAdapterオブジェクトのgetAttributesメソッドでhandlerInput.requestEnvelopeを指定する形にしてありますが、公式に習ってAttributesManagerを使う形に書き換えました。

const RequestInterceptor = {
  async process(handlerInput) {
    console.log(handlerInput);
   

    // get memory from S3
    try {
      const { attributesManager } = handlerInput;                        // ★ココ
      let context = await attributesManager.getPersistentAttributes();   // ★ココ
      if (Object.keys(context).length > 0) {
        userContext = context;
      }
      console.log(userContext);
    } catch(e) {
      console.log(e);
    }
  }
}

永続セッションに書き込むところは個別に行われていますね。こちらもAttributesManagerを使って書き換えました。

const DestinationCityIntentHandler = {
  canHandle(handlerInput) {
    const request = handlerInput.requestEnvelope.request;
    return (request.type === 'IntentRequest' && request.intent.name === 'DestinationCityIntent');
  },
  async handle(handlerInput) {
    const city = Alexa.getSlotValue(handlerInput.requestEnvelope, 'city')
    console.log(city)
    const result = await getFromWikipedia(city)
    console.log(result)

    userContext.visit = userContext.visit + 1
    userContext.date = Date.now()
    userContext.lastVisitCityDetail = result
    userContext.lastVisitCity = city
    const { attributesManager } = handlerInput;               // ★ココ
    attributesManager.setPersistentAttributes(userContext);   // ★ココ
    await attributesManager.savePersistentAttributes();       // ★ココ
    const speech = `楽しんできてくださいね。<audio src="soundbank://soundlibrary/airport/airport_01" />いってらっしゃい。
    `
    console.log(speech)
    return handlerInput.responseBuilder
      .speak(speech)
      .withSimpleCard(SKILL_NAME, 'test')
      .getResponse();
  },
};

Persistent Attributes へのセットをattributesManagerで行う場合、setPersistentAttributesとsavePersistentAttributesの2つのメソッドがあります。set〜だとキャッシュへの保存となり、saveだと永続ストレージへの保存となります。ドキュメントに記載があるのですが、パフォーマンスを考慮するようなケースのため、みたいですね。普段はsetでローカルにキャッシュして、必要なときだけsaveでS3に保存する、という感じで使えば、session attributes的な使い方ができるということかもしれません。ただ、その場合はsaveがされないというケースも考慮が必要かもです。

ちなみに、Sessioin AttributesはsetSessionAttributesだけです。

Response Interceptorを使ったセッションアトリビュート・永続アトリビュートの保存

Request Interceptorが前処理なら、Response Interceptorが後処理になります。例えば、Request InterceptorとResponse Interceptorの両方でhandlerInputをログに残すことで、飛んできたインテントとリクエストとそれに対するバックエンド側の応答を両方を記録することができるので、デバッグに役立ちますね。

今度は、「朝のコーヒーで性格診断」のサンプルコードを見てみます。skillBuilderハンドラの.withPersistenceAdapterのところだけ書き換えれば、hostedでも動くはずです(package.jsonとかrequireで読み込んでいるところはよしなに。)

まず Requset Interceptor。アトリビュートは、スキルが起動したそのセッション中だけ有効なsession attributesと、スキル終了後も継続するpersistent attributesがありますが、request interceptor内で、それぞれ読み出し・初期化する感じですね。

const RequestInterceptor = {
  async process(handlerInput) {
    const { attributesManager } = handlerInput;
    storage = await attributesManager.getPersistentAttributes() || {};
    session = attributesManager.getSessionAttributes();

    try {
      if (Object.keys(session).length === 0) {
        attributesManager.setSessionAttributes(session)      
      }
    } catch (error) {
      console.log(error)
      attributesManager.setSessionAttributes(session)  
    }
    console.log('storage:', storage)
    console.log('session:', session)
  }
};

こちらがResponse Interceptor。storageがPersistent Attributeの方で、ここで+1してあります。"visit"とありますが、Response Interceptorはリクエストがあるたびに実行されるので、スキル起動回数ではなく、発話回数という感じですね。

const ResponseInterceptor = {
  async process(handlerInput) {
    storage.visit += 1
    const { attributesManager } = handlerInput;
    await attributesManager.savePersistentAttributes(storage);
    attributesManager.setSessionAttributes(session);
  }
};

で、実際のこの部分を使うのは、以下のところになります。

const LaunchRequest = {
  canHandle(handlerInput) {
    return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
  },
  async handle(handlerInput) {
    return talk.launch(handlerInput.responseBuilder, storage)
  },
};

LaunchRequestで起動時にtalkというライブラリのlaunchメソッドに渡してあります。talk.jsの方も見てみます。

module.exports = {
    launch: (responseBuilder, storage) => {
        if (Object.keys(storage).length === 0) {
            return response(responseBuilder, {
                speak: 'こんにちは。ご来店ありがとうございます。あなたのお好みのコーヒーから、あなたの性格を判定します。あなたの好きなコーヒーはなんですか?',
                reprompt: 'あなたが好きなコーヒーは何ですか?',
                shouldEndSession: false
            })
        } else {
            return response(responseBuilder, {
                speak: 'こんにちは。ご来店ありがとうございます。今日はどんなコーヒーを飲みますか?',
                reprompt: 'あなたが好きなコーヒーは何ですか?',
                shouldEndSession: false
            })        
        }

    },
...snip...

Persistent Atributesのキーがなにも定義されていない場合は、初回起動、ということでスキルの説明的なメッセージが流れます。2回目以降であればstorage.visitが定義されているので、スキルの説明なくメッセージが流れるというわけですね。

次にsession attributesを実際に読んだり書いたりしているところも見てみます。まずセッションの構造はこんな感じで、

  • 珈琲の種類
  • ミルクと砂糖の有無

という感じになってます。

// セッションに保存するデータ構造の定義
let session = {
  diagnosisAttributes: {
    coffee: '',
    withMilk: '',
    withSugar: ''
  }
}

DiagnosisRequestIntentのcanHandleで、&& !session.diagnosisAttributes || !session.diagnosisAttributes.coffeeでdiagnosisAttributesというオブジェクトそのものがあるかないか?チェックしてます。コーヒーの種類をユーザから受け取るので、この時点ではsession.diagnosisAttributes.coffeeはない、ということですね。

そしてhandleで、ユーザの発話からスロットcoffee, withMilk, withSugarを抜き出してsession.diagnosisAttributesに入れてます。

const = {
  canHandle(handlerInput) {
    return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' 
      && Alexa.getIntentName(handlerInput.requestEnvelope) === 'DiagnosisRequestIntent'

      // [シチュエーション] コーヒーがオーダーされてない 
      && !session.diagnosisAttributes || !session.diagnosisAttributes.coffee
  },
  async handle(handlerInput) {
    session.diagnosisAttributes = getSynonymValues(handlerInput, ['coffee', 'withMilk', 'withSugar'])
    console.log('slot values:', session.diagnosisAttributes)
    return talk.diagnosisRequest(handlerInput.responseBuilder, session.diagnosisAttributes)
  }
};

ただこの時点ではあくまでも変数に入れただけで、session attributesとしてはresponse interceptorの中で保存するというわけですね。

const ResponseInterceptor = {
  async process(handlerInput) {
    storage.visit += 1
    const { attributesManager } = handlerInput;
    await attributesManager.savePersistentAttributes(storage);
    attributesManager.setSessionAttributes(session);
  }
};

実はこのあたりでいくつか疑問があって、少し自分で書き換えて試してみたりしてたら盛大にハマったりもしたので、それについては別でまとめたいと思います。

まとめ

ということで、Interceptorを使ったアトリビュートの管理を「ざっくり」理解してみようという回でした。上で書いたように少しハマったりもしたので、次回は「きちんと」理解するために、もう少し追いかけてみます。