LoginSignup
5
1

More than 5 years have passed since last update.

TypeScript を使って Alexa Custom Skills を作ろう ASK-SDK for Node.js V2への移行

Last updated at Posted at 2018-05-21

はじめに

この投稿では、先日ソース公開したじゃらじゃら 符計算Alexa Skills Kit for Node.js V2へのマイグレーションについて書いていきます。

ゆるい前提

マイグレーション

まずは、パッケージ構成情報を変える

削除したもの

  • alexa-sdk
  • @types/alexa-sdk
  • alexa-conversation

alexa-conversationは、v1のテストフレームワークです。

新しく入れたもの

  • ask-sdk-core
  • ask-sdk-model
  • i18next
  • i18next-sprintf-postprocessor
  • util 標準であるそうです。そちら使うようにしようと思います!
  • alexa-conversation-model-assert

v2から型定義も合わせて提供されているのがいいですね!

また、v1ではi18nextによる多言語がサポートされていましたが、v2では廃止されました。
じゃらじゃらでは多言語対応する予定はありませんが、発話内容管理上引き続き利用することとしました。

diff

   "dependencies": {
-    "alexa-sdk": "^1.0.0",
-    "config": "^1.29.2",
-    "log4js": "^2.5.2"
+    "ask-sdk-core": "^2.0.3",
+    "ask-sdk-model": "^1.2.0",
+    "config": "^1.30.0",
+    "i18next": "^11.3.1",
+    "i18next-sprintf-postprocessor": "^0.2.2",
+    "log4js": "^2.5.2",
+    "util": "^0.10.3"
   },
   "devDependencies": {
-    "@types/alexa-sdk": "^1.1.1",
     "@types/aws-lambda": "0.0.34",
-    "@types/config": "0.0.33",
+    "@types/config": "0.0.34",
+    "@types/i18next": "^8.4.3",
+    "@types/i18next-sprintf-postprocessor": "0.0.29",
     "@types/mocha": "^5.0.0",
     "@types/node": "^9.6.2",
     "@types/power-assert": "^1.4.29",
     "@types/sinon": "^4.1.3",
-    "alexa-conversation": "^0.2.0",
+    "alexa-conversation-model-assert": "^1.2.0",

インテントハンドラーの作成

v1の頃から、TypeScript を使って Alexa Custom Skills を作ろう (1) 設計に合わせて、ある程度ファイルを分割して作成していたので行った作業は以下です。

  • インテントファイルの移動、名称変更
  • インポートモジュールの変更
  • Class定義からオブジェクトへ変更(canHanldle, handleの実装)

v1のコード

/src/intents/help-intent.ts
import * as Alexa from 'alexa-sdk';
import { HelpUtterance as Utterance } from '../utterances/help-utterance';
import { IntentBase } from './intent-base';

/**
 * ヘルプ インテントクラス
 */
export class HelpIntent extends IntentBase<Utterance> {
  /**
   * コンストラクタ
   */
  constructor(utterance: Utterance) {
    super(utterance);
  }

  /**
   * アクション
   * @param context ハンドラコンテキスト
   */
  public execute(context: Alexa.Handler<any>) {
    // 発話取得
    const result = this.utterance.respond(context);

    // レスポンス設定
    context.response
      .speak(result.speech)
      .listen(result.repromptSpeech);

    // レスポンス生成
    context.emit(':responseReady');
  }
}

v2のコード

/src/handlers/help-intent-handler.ts
import * as Ask from 'ask-sdk-core';
import { createUtterance } from '../factories/utterance-factory';
import { HelpUtterance as Utterance } from '../utterances/help-utterance';

/**
 * ヘルプ インテントハンドラ
 */
export const HelpIntentHandler: Ask.RequestHandler = {
  /**
   * 実行判定
   * @param handlerInput ハンドラ
   */
  canHandle(handlerInput) {
    return (
      handlerInput.requestEnvelope.request.type === 'IntentRequest' &&
      handlerInput.requestEnvelope.request.intent.name === 'AMAZON.HelpIntent'
    );
  },
  /**
   * ハンドラ実行
   * @param handlerInput ハンドラ
   */
  handle(handlerInput) {
    // 発話取得
    const speechOutput = createUtterance(Utterance).respond(handlerInput);

    // レスポンス
    return handlerInput.responseBuilder
      .speak(speechOutput.speech)
      .reprompt(speechOutput.repromptSpeech)
      .getResponse();
  }
};

diff

以下の理由で比較的簡単に移行できました。

  • 元々ラッパーを利用していなかったこと(ask, tell)
  • 発話内容は外部モジュールで実装していたこと(utteranceクラス)
  • ヘルプインテント自体がシンプルである
-import * as Alexa from 'alexa-sdk';
+import * as Ask from 'ask-sdk-core';
+import { createUtterance } from '../factories/utterance-factory';
 import { HelpUtterance as Utterance } from '../utterances/help-utterance';
-import { IntentBase } from './intent-base';

 /**
- * ヘルプ インテントクラス
+ * ヘルプ インテントハンドラ
  */
-export class HelpIntent extends IntentBase<Utterance> {
+export const HelpIntentHandler: Ask.RequestHandler = {
   /**
-   * コンストラクタ
+   * 実行判定
+   * @param handlerInput ハンドラ
    */
-  constructor(utterance: Utterance) {
-    super(utterance);
-  }
-
+  canHandle(handlerInput) {
+    return (
+      handlerInput.requestEnvelope.request.type === 'IntentRequest' &&
+      handlerInput.requestEnvelope.request.intent.name === 'AMAZON.HelpIntent'
+    );
+  },
   /**
-   * アクション
-   * @param context ハンドラコンテキスト
+   * ハンドラ実行
+   * @param handlerInput ハンドラ
    */
-  public execute(context: Alexa.Handler<any>) {
+  handle(handlerInput) {
     // 発話取得
-    const result = this.utterance.respond(context);
+    const speechOutput = createUtterance(Utterance).respond(handlerInput);

-    // レスポンス設定
-    context.response
-      .speak(result.speech)
-      .listen(result.repromptSpeech);
-
-    // レスポンス生成
-    context.emit(':responseReady');
-  }
+    // レスポンス
+    return handlerInput.responseBuilder
+      .speak(speechOutput.speech)
+      .reprompt(speechOutput.repromptSpeech)
+      .getResponse();
   }
+};

発話(Utterance)クラスの移行

行った作業は以下です。

  • インポートモジュールの変更
  • 引数の名称と型を変更

v1のコード

/src/utterances/help-utterance.ts
import * as Alexa from 'alexa-sdk';
import { IUtteranceResult } from '../domains/utterance-result';
import { UtteranceBase } from './utterance-base';

/**
 * ヘルプ 発話クラス
 */
export class HelpUtterance extends UtteranceBase {
  /**
   * コンストラクタ
   */
  constructor() {
    super();
  }

  /**
   * 発話内容取得
   * @param context ハンドラコンテキスト
   * @returns 発話結果
   */
  public respond(context: Alexa.Handler<any>): IUtteranceResult {
    return {
      speech: <any>context.t('HELP_MESSAGE') + <any>context.t('HELP_FU_NUMBER') + <any>context.t('HELP_HAN_NUMBER'),
      repromptSpeech: <any>context.t('HELP_MESSAGE')
    };
  }
}

v2のコード

/src/utterances/help-utterance.ts
import * as Ask from 'ask-sdk-core';
import { IHelpSpeechOutput as ISpeechOutput } from './domains/help-speech-output';
import { UtteranceBase } from './utterance-base';

/**
 * ヘルプ 発話クラス
 */
export class HelpUtterance extends UtteranceBase {
  /**
   * コンストラクタ
   */
  constructor() {
    super();
  }

  /**
   * 発話内容取得
   * @param handlerInput ハンドラコンテキスト
   * @returns 発話内容
   */
  public respond(handlerInput: Ask.HandlerInput): ISpeechOutput {
    const requestAttributes = handlerInput.attributesManager.getRequestAttributes();

    return {
      speech: requestAttributes.t('HELP_MESSAGE') + requestAttributes.t('HELP_FU_NUMBER') + requestAttributes.t('HELP_HAN_NUMBER'),
      repromptSpeech: requestAttributes.t('HELP_MESSAGE')
    };
  }
}

diff

-import * as Alexa from 'alexa-sdk';
-import { IUtteranceResult } from '../domains/utterance-result';
+import * as Ask from 'ask-sdk-core';
+import { IHelpSpeechOutput as ISpeechOutput } from './domains/help-speech-output';
 import { UtteranceBase } from './utterance-base';

   /**
    * 発話内容取得
-   * @param context ハンドラコンテキスト
-   * @returns 発話結果
+   * @param handlerInput ハンドラコンテキスト
+   * @returns 発話内容
    */
-  public respond(context: Alexa.Handler<any>): IUtteranceResult {
+  public respond(handlerInput: Ask.HandlerInput): ISpeechOutput {
+    const requestAttributes = handlerInput.attributesManager.getRequestAttributes();
+
     return {
-      speech: <any>context.t('HELP_MESSAGE') + <any>context.t('HELP_FU_NUMBER') + <any>context.t('HELP_HAN_NUMBER'),
-      repromptSpeech: <any>context.t('HELP_MESSAGE')
+      speech: requestAttributes.t('HELP_MESSAGE') + requestAttributes.t('HELP_FU_NUMBER') + requestAttributes.t('HELP_HAN_NUMBER'),
+      repromptSpeech: requestAttributes.t('HELP_MESSAGE')
     };
   }
 }

インテントハンドラ同様、比較的変更箇所も少なく移行できました。

多言語対応

次に廃止されたi18nextを組み込みます。

インターセプターの作成

以下のソースを参考にしています。

https://github.com/alexa/skill-sample-nodejs-howto/blob/master/lambda/custom/index.js#L214-L229

/src/interceptors/common-interceptor.ts
import * as Ask from 'ask-sdk-core';
import * as i18n from 'i18next';
import * as sprintf from 'i18next-sprintf-postprocessor';
import { languageStrings } from '../utterances/language-strings';

export const CommonInterceptor: Ask.RequestInterceptor = {
  process(handlerInput) {
    const localizationClient = i18n.use(sprintf).init({
      lng: handlerInput.requestEnvelope.request.locale,
      overloadTranslationOptionHandler: sprintf.overloadTranslationOptionHandler,
      resources: languageStrings,
      returnObjects: true
    });

    const attributes = handlerInput.attributesManager.getRequestAttributes();
    attributes.t = (key: string | string[], options?: i18n.TranslationOptions<object> | undefined) => {
      return localizationClient.t(key, options);
    };
  }
};

さらにローカライズファイルも変更します。
今までは、%sにより動的に補完していましたが、パラメータ毎に任意の名称を付けるよう変更しました。

こんな感じ。

  • pointParent: 親の点数
  • pointChildren: 子の点数
-      'ANSWER_CHILD_TSUMO': 'ツモは親が<break time="100ms"/>%s点、子が<break time="100ms"/>%s点です。',
+      'ANSWER_CHILD_TSUMO': 'ツモは親が<break time="100ms"/>{{pointParent}}点、子が<break time="100ms"/>{{pointChildren}}点です。',

Lambdaハンドラーの移行

最後にLambdaハンドラーの移行です。

  • インポートモジュールの変更
  • V2に合わせハンドラを追加
  • 環境変数をAPP_IDからALEXA_SKILL_IDへ変更(これは必須ではないです。解りやすくする為)

v1のコード

/src/index.ts
import * as Alexa from 'alexa-sdk';
import * as Handlers from './handlers';
import { languageStrings } from './utterances/language-strings';

/**
 * エントリポイント
 * @param event イベント
 * @param context コンテキスト
 * @param callback コールバック
 */
export const handler = (
  event: Alexa.RequestBody<any>,
  context: Alexa.Context,
  callback: (err: any, response: any) => void
) => {

  const alexa = Alexa.handler(event, context, callback);
  if (process.env.APP_ID) {
    alexa.appId = process.env.APP_ID;
  }
  alexa.resources = languageStrings;
  alexa.registerHandlers(Handlers.DefaultHandler);
  alexa.execute();
};

v2のコード

/src/index.ts
import * as Ask from 'ask-sdk-core';
import * as Handlers from './handlers';
import * as Interceptors from './interceptors';

/**
 * エントリポイント
 */
export const handler = Ask.SkillBuilders.custom()
.addRequestHandlers(
  Handlers.CalculatePointIntentHandler,
  Handlers.CancelAndStopIntentHandler,
  Handlers.HelpIntentHandler,
  Handlers.LaunchRequestHandler,
  Handlers.SessionEndedRequestHandler
)
.addRequestInterceptors(Interceptors.CommonInterceptor)
.addErrorHandlers(
  Handlers.ErrorHandler
)
.withSkillId(String(process.env.ALEXA_SKILL_ID))
.lambda();

diff

-import * as Alexa from 'alexa-sdk';
+import * as Ask from 'ask-sdk-core';
 import * as Handlers from './handlers';
-import { languageStrings } from './utterances/language-strings';
+import * as Interceptors from './interceptors';

 /**
  * エントリポイント
- * @param event イベント
- * @param context コンテキスト
- * @param callback コールバック
  */
-export const handler = (
-  event: Alexa.RequestBody<any>,
-  context: Alexa.Context,
-  callback: (err: any, response: any) => void
-) => {
-
-  const alexa = Alexa.handler(event, context, callback);
-  if (process.env.APP_ID) {
-    alexa.appId = process.env.APP_ID;
-  }
-  alexa.resources = languageStrings;
-  alexa.registerHandlers(Handlers.DefaultHandler);
-  alexa.execute();
-};
+export const handler = Ask.SkillBuilders.custom()
+.addRequestHandlers(
+  Handlers.CalculatePointIntentHandler,
+  Handlers.CancelAndStopIntentHandler,
+  Handlers.HelpIntentHandler,
+  Handlers.LaunchRequestHandler,
+  Handlers.SessionEndedRequestHandler
+)
+.addRequestInterceptors(Interceptors.CommonInterceptor)
+.addErrorHandlers(
+  Handlers.ErrorHandler
+)
+.lambda();

じゃらじゃらスキルは機能がシンプルなので、指定方法が変わったくらいなのであまり特筆することが無いのです。。。

強いて言うなら、
今まで状態毎に作成していたハンドラは、定義されたインテントをaddRequestHandlersに登録していくことになります。
状態遷移が多いスキルでは、この部分が大きく移行コストへ影響すると思います。

例えば、以下のようにセッションの状態などをCanHandle関数へ細かく定義していくことになります。

  /**
   * 実行判定
   * @param handlerInput ハンドラ
   */
  canHandle(handlerInput) {
    const session = getSession(handlerInput);

    return (
      handlerInput.requestEnvelope.request.type === 'IntentRequest' &&
      handlerInput.requestEnvelope.request.intent.name === 'AMAZON.NextIntent' &&
      session.state === '_STATUS' &&
      session.xxxxxx !== undefined
    );
  },

ついでにランタイムバージョンも変更してみた

AWS Lambdaのランタイムバージョンをv6.10からv8.10へ変更しました。

  • ターゲットを変更

ターゲットの変更

tsconfig.json
   "compilerOptions": {
-    "target": "ES2015",
+    "target": "ES2017",

まとめ

ASK SDK for Node.js V2の移行について、どのように移行していったかを簡単に纏めました。

じゃらじゃらスキルは状態を持っていないので、比較的簡単に移行ができました。
肝の点数計算インテントはソース公開しているのでそちらをご覧ください。
移行について何かあればまた別記事で共有したいと思います。

v2のソースコードは以下にあります。

じゃらじゃら 符計算 v2 - GitHub

i18next使わないバージョン

お知らせ(宣伝)

手前味噌ではありますが、v2用のテストフレームワークを作りましたので良ければお使いください。

ASK SDK v2 for Node.jsのテストフレームワークが欲しい(作ってみた) - Qiita
alexa-conversation-model-assert - npm
alexa-conversation-model-assert - GitHub

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1