[日本語Alexa] Alexa SDK V1のコードをV2に移植した。そして、ついでにTypeScriptにしてみた。

2018.06.11

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 はじめに

4月の初めに公開した下記のスキルですが、当時、Alexa SDKのバージョン2がまだ公開されていなかったため、当然ですが、バージョン1で書きました。


ビンゴ大会

そして、だいぶバージョン2にも慣れてきたので、新しいバージョンへ移植してみました。今回は、その時に行った作業や、気づいた事項について紹介させてください。

2 主な作業

バージョン1から2への移植にあたっての主な作業は、以下の3つでした。言い換えれば、2つのバージョンでの大きな違いは、下記ということになるかも知れません。

  • ハンドラ
  • ステート
  • アトリビュート

(1) ハンドラ

バージョン1は、ハンドラがインテント名とステートに直接結びついていました。Alexaから送られてくるインテント名とセッションに保持されたステート(状態)に基づいてSDK内部でルーティングする仕様になっていました。

しかし、バージョン2になって、このSDK内でのルーティングの仕組み無くなりました。ステートについても専用のものはなくなり、セッション属性を使用して独自に設計するようになりました。ルーティングが行われないため、各ハンドラの入り口(canHanle())で、処理対象のインテントなどを選別する仕様になっています。

具体的には、下記のような感じです。(InitIntentを処理するハンドラ)

バージョン1

const handlers = {
    'InitIntent' : function() {
        // 処理の内容
    },

バージョン2

const InitIntentHandler = {
    canHandle(handlerInput) {
        // インテント名がInitIntentの時に処理する
        return handlerInput.requestEnvelope.request.type === 'IntentRequest'
            && handlerInput.requestEnvelope.request.intent.name === 'InitIntent';
    },
    handle(handlerInput) {
        // 処理の内容
    }
};

このような書換え作業は、全ハンドラについて必要となり、この作業が、最も書換の量が多くなる部分でしょう。

しかし、利点として、バージョン2では、ステートの処理が自前であるため、ステートごとに記述していた同一インテント名に対するハンドラをまとめ記述できたり、インテント名依存しないので、設計に素直な処理分けなどが可能となります。

今回は、ステートごとに書いていた、AMAZON.StopIntentAMAZON.CancelIntentAMAZON.HelpIntentなどは、まとめることで、コード量がだいぶ減りました。


参考:[日本語Alexa] Alexa SDK for Node.js Ver2入門(その2)ハンドラの登録

(2) ステート

バージョン2でステート(状態)に応じた処理を記述するには、ステート自体を作成する必要があります。

この作業は、スキルの内容(ステートをどれだけ使っているか)によって大きく変わるところかも知れません。

今回作業した「ビンゴ大会」では、ステートは、AMAZON.YesIntentの処理の区別にしか使用していませんでした。

Alexa:「次の数字は、99です! 今までの数字を読み上げますか?」
ユーザ:「はい」<=この時は、今までに出た数字を読み上げる


Alexa:「初期化しますか?」
ユーザ:「はい」<=この時は、今まで出た数字を全部削除する

そこで、全体に渡るステートは排除して、Alexaから「初期化しますか?」の質問をした時だけ、フラグを立てるようなステートを実装しました。

具体的には、次のとおりです。InitIntentで、セッションアトリビュートに'INIT'を書き込み、AMAZON.YesIntentでは、それに応じて動作を気に変えています。なお、InitIntentの直後以外は、このステートを無効にするため、RequestInterceptor(すべてのハンドラの前処理)で、AMAZON.YesIntent以外が来た時に、このフラグを無効化するようにしました。

const InitIntentHandler = {
    canHandle(handlerInput) {
        // InitIntentを処理する
    },
    handle(handlerInput) {
        // セッション情報にINITを書き込む
        sessionAttributes.state = 'INIT';
        const speechText = '初期化して宜しいですか?';
    }
};

const RequestInterceptor = {
    process(handlerInput) {
      // AMAZON.YesIntent以外のとき、ステートを削除する
    }
};

const YesIntentHandler = {
    canHandle(handlerInput) {
        // AMAZON.YesIntentを処理する
    },
    handle(handlerInput) {
      if (isInit) { // ステートがINITの時
        // 初期化する処理
      } else {
        // いままで出た数字を読み上げる処理
      }
    }
};

(3) アトリビュート

セッション情報の記述は、バージョン1と2で大きく変わりました。バージョン1では、this.sessionAttributes["KEY"]に値を代入しておけば、後は、SDKが結構うまくやってくれていたのですが、バージョン2では、属性の生存期間ごと記述要領が変わったため、ここは、全面的に書き換えとなります。

注意深く、移植が必要なところかも知れません。

今回は、ステートはセッション情報とし、その他は永続化情報としました。また永続化の方は保存と取得のヘルパー関数を定義し、使用の前後に挟みました。(これにより、永続化データを触る処理では、Lambdaの起動ごとにDBのへの書き換えが発生しますので、実装要領の良否は一考の必要があるかも知れません)

// 永続化情報の取得
async function getAttrbutes(handlerInput: Alexa.HandlerInput):Promise<{[key: string]: any}> {
    return await handlerInput.attributesManager.getPersistentAttributes();
}
// 永続化情報の保存
async function setAttrbutes(handlerInput: Alexa.HandlerInput, attributes:{[key: string]: any}): Promise<void> {
    handlerInput.attributesManager.setPersistentAttributes(attributes);
    await handlerInput.attributesManager.savePersistentAttributes();
}

永続化情報の更新の例

let attributes = await getAttrbutes(handlerInput); // 取得
let index = attributes.index;
index += 1;
attributes.index = index;
await setAttrbutes(handlerInput, attributes); // 保存


参考:[日本語Alexa] Alexa-SDK Ver2(その4) スキル属性

3 TypeScript

なんだかんだ言っても、結構全面的な書換となるため、ついでと言っては横暴かも知れませんが・・・TypeScriptに変更しました。


参考:[日本語Alexa] Alexa-SDK Ver2 をTypeScriptで使う
参考:[日本語Alexa] Alexa-SDK Ver2をTypeScriptで書く時のタグ付き共用体の取扱要領について

TypeScriptでの作業要領は上記に譲るとして、今回作業して感じたことを、少し紹介させてください。

(1) 変数・関数名などの間違い

移植中にありがちな、変数名や関数名のタイプミスは、全部、TypeScriptでは、警告が出るので、簡単に排除できます。前バージョンのコードをそのままコピーするだけのような単純作業では、文字の誤挿入やコピーミスなどへの注意が必要なくなるので、作業の効率は非常に高くなると感じました。

(2) 未使用のワーニング

TypeScriptでは、未使用の変数や、関数にワーニングが出せます。 これにより、移植作業の漏れなどが、すぐに分かるので、こちらも強力かも知れません。

(3) 型の不一致

型の不一致がエラーとなるので、数値扱いの間違いなど、すぐに分かります。

どれも、型言語であるTypeScriptでは当然の動作ですが、TypeScriptにして初めて気づいた問題もあった感じです。(もともとバグってた・・・)

全般を通して、JavaScriptからJavaScriptへの移植よりもスムーズであり、安全に作業できたように感じています。

4 最後に

移植には、全く関係ないですが、今回、ちょっとだけ機能を追加しました。「もう一回言って」とか「最後に出た数字は?」と問いかけると、最後の番号を再確認できます。ビンゴ会場が騒がしくて聞き逃してしまった時などにこの機能が活躍すると嬉しいです。

ってことで、審査への提出も終わりです。

今回実装したコードは、下記に置きました。何かの参考になれば幸いです。

バージョン1
github [GitHub] https://github.com/furuya02/Bingo

バージョン2
github [GitHub] https://github.com/furuya02/Bingo/tree/future/v2