そういう、モデルなんです。

ビジネスモデル、設計図、模型などの現状と動向を考察、関連書籍の紹介

アレクサスキル Alexa Skill 向けローカル開発環境

アレクサ開発者コンソール Alexa Developer Console 上で直接、Lambda 関数(node.js 実装)を編集するというスタイルで開発していたが、障害の発生時に切り分けが難しいので、一部をローカル開発環境構築することとした。

ちょうど

Amazon Alexaプログラミング入門 (impress top gear)

Amazon Alexaプログラミング入門 (impress top gear)

 

に手順の記載があったので、参考にしてローカル開発環境構築。

node.js のプロジェクトを1つ作成

Node.js command prompt より、対話形式でプロジェクト作成。

npm init

This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

( 20行くらい中略)
Is this OK? (yes)

package.json というファイルが1つだけできた。

dir

ドライブ E のボリューム ラベルは データ です
ボリューム シリアル番号は 1A04-81C2 です

e:\Users\tombi\Projects\diceroller のディレクト

2019/04/14 07:26 <DIR> .
2019/04/14 07:26 <DIR> ..
2019/04/14 07:26 219 package.json
1 個のファイル 219 バイト
2 個のディレクトリ 3,969,808,773,120 バイトの空き領域

出来たファイルは package.json だけ。これだけかい。

全く問題なし

node.js のテストプログラムで動作確認

index.js に console.log ('Hello World'); だけ書いて、

node index.js

Hello World

全く問題なし

node.js 環境にAlexa Skills Kit SDK を追加 

上記の書籍よりも新しい SDK があるかもしれないので、念のため 何を開発できるか知る | Alexa Skills Kit | アレクサ のサイトでチェック。

コードサンプルとNode.js SDK

のリンク先へたどっていくと、GitHub に着いた。

github.com

 がそれらしい。でも、何をどうやって npm で取得すればいいのか分からん。

README.ja.md

という、いかにも日本人向けの README を見つけたので開くと Package と NPM の対応関係が表になっており、上記書籍と同じく ask-sdk で問題ないらしいと分かった。

Eドライブに作っていたプロジェクトに追加しようとしたら、

npm install --save ask-sdk

npm ERR! code UNKNOWN、npm ERR! errno -4094、npm ERR! syscall fsync

のようなエラーが出て、ドはまりした。 

fsync?OSレベルのエラー?

本題と関係ないが、その後の顛末は:

tombi-aburage.hatenablog.jp

Dドライブにプロジェクトを複写して再試行したら何故かうまくいった。

npm install ask-sdk

npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN diceroller@1.0.0 No description
npm WARN diceroller@1.0.0 No repository field.

+ ask-sdk@2.5.1
added 19 packages from 69 contributors and audited 22 packages in 3.509s
found 0 vulnerabilities

 

Lambdaをローカルで実行するツール lambda-local を導入

とりあえず Alexa Skill Kit SDK までは進んだので、

Amazon Alexaプログラミング入門 (impress top gear)

Amazon Alexaプログラミング入門 (impress top gear)

 

 を参照しつつ、次の作業へ。

npm install -g lambda-local

C:\Users\tombi\AppData\Roaming\npm\lambda-local -> C:\Users\tombi\AppData\Roaming\npm\node_modules\lambda-local\bin\lambda-local
+ lambda-local@1.5.2
added 31 packages from 83 contributors in 4.522

全く問題なし

いよいよ index.js を本物(Alexa スキルの主処理)に差し替える

ここから先は、単なる自作のアプリケーション開発なので、もう上記の書籍は当てにできない世界となる。

数日前に、アレクサ開発者コンソール Alexa Developer Console 上で作成した自分のプログラム(複数個のサイコロを振らせるというスキル)

サイコロ係

サイコロ係

 

f:id:tombi-aburage:20190418112157p:plain

ソースコードの中身を、node.js のローカル環境構築のさいにテストプログラム代わりにしていた index.js に上書きコピーした。

本当はファイルをダウンロードしたかったのだが、Alexa Developer Console のファイルをダウンロードする方法が分からなかったので、仕方なくコピー&ペースト…

方法はダサいが、ともかくも index.js はローカル開発環境に移行された。
サーバ環境(アレクサ開発者コンソール )で実際に動作確認が取れているコードなので、これをローカル開発環境でも動かせるようにするのが今からの作業ということ。

テストデータは Alexa Developer Console のスキルI/Oを元に作成

ローカル開発環境でテストをするためには、その入力となるデータの準備が必要なのだが、手作業で作るのは面倒なので、実際に動作しているサーバ側のスキルサービスの出力をパクることにした。

スキルI/Oをオンにして、テストを実行。表示されたJSON入力の内容をコピーして、ローカル開発環境の test サブフォルダ配下に LaunchRequest.JSON という名前で保存した。

f:id:tombi-aburage:20190414181304p:plain

lambda-local で index.js を実行する

lambda-local -l index.js -h handler -e test\LaunchRequest.json 

D:\Users\tombi\Projects\diceroller>
info: START RequestId: 7711489c-0c23-b7ef-89dc-d3fc5d2ae50a
info: End - Message
info: ------
(中略、ここに応答内容が表示される)

info: ------
info: Lambda successfully executed in 69ms.

スキルの起動要求 LaunchRequest に成功したようだ。

全く問題なし

起動要求のテストができただけでは、大して嬉しくもない。

同じように、一番テストを行いたい主処理のインテントについても、同じ手口で Alexa Developer Console に JSON 入力ファイルを作成させ、ローカル開発環境の test サブフォルダ配下に RollDiceIntent.json という名前で保存した。

lambda-local -l index.js -h handler -e test\RollDiceIntent.JSON 

info: START RequestId: f7c5ae17-873e-f05a-2b02-4c6ebafdaa40
D:\Users\tombi\Projects\diceroller\test\RollDiceIntent.JSON:2
"version": "1.0",
^

SyntaxError: Unexpected token :

(中略、ここに激しいスタックトレースのダンプ)

error: End - Error
error: ------
error: {
"errorMessage": "Unexpected token :",
"errorType": "SyntaxError",
"stackTrace": [
"rsion\": \"1.0\",",
"",
"",
"taxError: Unexpected token :",
"new Script (vm.js:80:7)",
"createScript (vm.js:274:10)",
"Object.runInThisContext (vm.js:326:10)",
"Module._compile (internal/modules/cjs/loader.js:664:28)",
"Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)",
"Module.load (internal/modules/cjs/loader.js:600:32)",
"tryModuleLoad (internal/modules/cjs/loader.js:539:12)",
"Function.Module._load (internal/modules/cjs/loader.js:531:3)",
"Module.require (internal/modules/cjs/loader.js:637:17)",
"require (internal/modules/cjs/helpers.js:22:18)"
]
}
error: ------
error: Lambda failed in 63ms.

あれ?何故失敗するんだ?

結論としては、JSON入力ファイル名の拡張子を誤って大文字(.JSON)で指定していたことが原因だった。Windows の分際で大文字・小文字を区別するとは生意気な!

こんなに激しく責めなくてもいいだろ!
しかもスタックトレース、全然ヒントになってないし!

拡張子を小文字で指定し直して実行すると、うまくいった。

lambda-local -l index.js -h handler -e test\RollDiceIntent.json 

info: START RequestId: ccef6fd6-7858-5c52-eb0a-ad5f668df2d5
info: End - Message
info: ------
info: {
(中略、ここに応答内容の前半が表示される)
"ssml": "<speak>6面体を3個振ります。1,<break time=\"0.5s\"/>5,<break time=\"0.5s\"/>1が出ました 。合計は7です。</speak>"
(中略、ここに応答内容が後半が表示される)

info: ------
info: Lambda successfully executed in 51ms.

おお!全く問題なし

引き続き Visual Studio Codeデバッグするための設定

Amazon Alexaプログラミング入門 (impress top gear)

Amazon Alexaプログラミング入門 (impress top gear)

 

 を参照しつつ、やろうとしたが、Launch.json が何のためのものなのか知らないし、作り方も知らなかった(打ち込むのも面倒なので嫌)のでネットで調査した。

www.atmarkit.co.jp

なるほど、VS Codeデバッグ画面からひな形を作成できるのか・・・

ここまでのところ npm のプロジェクトまでは作ってあったが、Visual Studio Code (VS Code) のプロジェクト(ワークスペース)は作っていなかったので、まずこれを作った。

作ったといっても npm のフォルダを丸ごと、VS Code ワークスペースに追加しただけのことだが。

その後、デバッグ画面で歯車アイコンをポチったらファイルが作成された。

起動するプログラム program と引数 args を、先ほど Node.js command prompt で実行した lambda-local のコマンドと全く同じ意味になるように変更した。

まずはデバッガを起動せず、たんに実行してみる。

実は VS Code は初めて使うので、ブレークポイントの指定の仕方どころか、プログラムの実行のさせ方すらも知らない。

とりあえず

  • デバッグせずに開始 [ Debug ] - [ Start Without Debugging ]

を実行したら、デバッグコンソール DEBUG CONSOLE で

C:\Program Files\nodejs\node.exe C:\Users\tombi\AppData\Roaming\npm\node_modules\lambda-local\bin\lambda-local -l index.js -h handler -e test/RollDiceIntent.json

が実行されて、出力結果も期待どおりに出力された。

f:id:tombi-aburage:20190414202232p:plain

通常の実行は、問題なし

デバッグ開始 Start Debugging して実行してみる。

今回もブレークポイントは指定せずに、

を実行したら、デバッグコンソール DEBUG CONSOLE に

Waiting for the debugger to disconnet...

と表示され、それ以上、進まなくなった。

f:id:tombi-aburage:20190414203219p:plain

ブレークポイントはないのだから、通常の実行と同じように、そのまま進行して呼び出し終了するものと思ったのだが・・・

調べてみたら以下の記事に同じような表示例があって、この記事の筆者は全然気にしていないようだったので、とりあえず置いておくことにした。

blog.hiroppy.me

ブレークポイントを指定し、デバッグ開始 Start Debugging して実行してみる。

VS Code のインタフェースをつつきまわしていたら、

というメニューを見つけたので、これを適当な行に設定してから実行をしてみた。

f:id:tombi-aburage:20190414204417p:plain

おお!止まっている!

デバッグコンソールを見てみると、先ほどとは表示が違っている。

Debugger attached.

どうやら、先ほどの

Waiting for the debugger to disconnet...

というメッセージは「実行が全て終了したので、デバッグのセッションも終わらせておきましたよ。俺って親切でしょう?」という意味だったらしい。

・・・紛らわしいぞ。

ブレークポイントで配列内の値が見えることを確認した後、実行を再開させると、さきほどと同じように、

Waiting for the debugger to disconnet...

となった。

デバッグ実行も、問題なし

これでデバッガも使えるようになった。素晴らしい!

但しデバッグ開始 Start Debugging による実行では、デバッグコンソールには lamda 関数の処理結果の JSON出力は出力されない点は、ちょっと違和感がある。

標準出力をリダイレクトするとかしないとダメなのかもしれない。

動くようになったので、リファクタリング着手

多言語対応をするつもりは無いのだが、スキルサービスからの応答発話のリテラルをソース本体から追い出したいので、How to Localize Your Alexa Skills : Alexa Blogs を参考にしながら、国際化対応のパッケージをローカル開発環境に追加した。

国際化対応のパッケージを追加

npm i -save i18next i18next-sprintf-postprocessor 

npm WARN diceroller@1.0.0 No description
npm WARN diceroller@1.0.0 No repository field.

+ i18next@15.0.9
+ i18next-sprintf-postprocessor@0.2.2
added 5 packages from 40 contributors and audited 27 packages in 2.07s
found 0 vulnerabilities 

全く問題なし

引き続き How to Localize Your Alexa Skills : Alexa Blogs を参考にしながら、ソースコードを全面的に変更。index.js にベタ書きしていた日本語応答発話のリテラルを、新たに作成した言語リソースファイル  ja.js の方に移すことにした。

自作のスキルサービスは

f:id:tombi-aburage:20190418112157p:plain

というシーケンスになっているのだが、スキルインタフェース(図略)からのバウンダリとなっている handler を収容している index.js に日本語の発話がどんどん増えてきて、見通しが悪くなってきていた。 ・・・全部追い出すぞ。

// ja.js
module.exports = {
translation : {
'SKILL_NAME' : 'サイコロ係' // <- can either be a string...
,'ASK_COMMAND' : '何面体を何個、振りましょうか?'
,'HELP_USAGE' : 'あなたの代わりにサイコロを振ります。 <break time="1s"/>\
「何面体を」<break time="1s"/>「何個」<break time="1s"/>「振って」<break time="1s"/>のように指示してください。'
,'TOO_MANY_DICE' : '8面体以上の場合、同時に振れるのは8個までなんです。すみません。'
,'CANNOT_HEAR_YOU' : 'すみません、よく聞き取れませんでした。'
,'REPEAT_COMMAND' : [
'%s面体を%s個振ります。','%s面体のサイコロを%s個振ります。','%s面体ダイスを%s個振ります。'
]
,'EXCITED' : [ // <- or an array of strings.
'','それっ!','うりゃ!','むん!','よっと!'
]
,'SURPRISED' : [
'おっと!','あれっ?','おやっ?','ほほう!'
]
,'SAY_AGAIN_RESULT' : '先ほどの結果を、もう一度お伝えします。'
,'BYE' : 'さようなら。'
}
}

\ (*´Д`) / いい感じの定数名を考えるのが、けっこうストレスだった。そのうち命名規則でも考えることにしよう。

リテラルを追い出す以外にも、幾つかの発話候補の中から無作為にどれか1つを選んで応答させることが簡単になったので、やる価値はあるかと。

もう、文言修正ごときで主処理 index.js には手を入れないで済む…

これでスキルサービスからの応答の文言修正については、主処理 index.js を変更する必要がなくなり、言語リソースファイル ja.js を変更するだけで良くなった。

しかし主処理から呼び出している自作のユーティリティ関数 DiceRoller.js の内部での応答文言組み立てについては、この関数に handlerInput への参照を引き渡してしまうとサブルーチンとしてのモジュールの独立性が保てないので後回しにした。

このユーティリティ関数専用の言語リソースファイルを別に用意して、i18next を直接使う方法がよさそうだが、リテラルがほとんどないので手間のほうが上回るので、今はやらない。

サーバ環境側でのテストのさいにハマる

ローカル開発環境でのエラーは出なくなったので、アレクサ開発者コンソール Alexa Developer Console の方へ反映することにした。

  • index.js をコピペ
  • i18n サブフォルダを作成し、その配下に ja.js ファイルを作成し、コピペ
  • 自作のユーティリティ関数 DiceRoller.js もコピペ
    ファイルアップロードでの入れ替え方を未だに理解していないため、未だにコピペなんです orz

コンソールでテストしてみたら、起動リクエスト LaunchRequest すらも動かないという致命的なエラー。[ テスト] - [ デバイスのログ] で全部のエラーメッセージを見ると

SKILL_ENDPOINT_ERROR

とかいうエラーが起動リクエスト発行のさいに出ていた。

門前払いかよ!

Cloud Watch の詳細ログを見ると

Unable to import module 'index'

とのこと。そもそも index.js をモジュール(ソース)として読み込むことすらもできていない。いろいろ調べると、依存するモジュールを読み込めていないときに出ることが多いとのことだった。

つまり、つい先ほど追加したライブラリである i18next i18next-sprintf-postprocessor  が最も怪しい。

結論としては:

  • 国際化対応のパッケージを追加したさいにプロジェクトの package.json も変わっていたのに、アレクサ開発者コンソールの方へ反映するのを忘れていた

というポカミスだった。

ローカル開発環境の最新の package.json

{
"name": "diceroller",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "tombi.aburage",
"license": "ISC",
"dependencies": {
"ask-sdk": "^2.5.1",
"i18next": "^15.0.9",
"i18next-sprintf-postprocessor": "^0.2.2"
},
"devDependencies": {
"@types/node": "^11.13.4"
}
}

のようになっており、 i18nextなんちゃらの2つが追記されていたようだ。
ファイルの中身を丸ごとコピペ w したら動くようになった。

コンソールで動作確認後、実機 Echo でも無事動作確認がとれた。

しかし、そろそろ手でコピペではなく、自動反映するようにでもしないとダメかねぇ。