アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

「いこーよのおでかけナビ」の実装について

前々回、前回のエントリーでAlexaスキル「いこーよのおでかけナビ」の開発の経緯考えたことについて書きましたが、 今回は「いこーよのおでかけナビ」(以降いこナビと呼びます)をどの様に実装したかについて書きたいと思います。

いこーよのおでかけナビ

いこーよのおでかけナビ

いこナビの技術スタック

いこナビで使っている主な技術要素は次のとおりです。

まあまあ標準的な構成ではないかと思います。

以下、それぞれの要素について説明します。

Alexa と Lambda

Alexaスキルは大きく分けると次の2つの要素に分けられます。

  • ユーザの言葉を受けてバックエンドを呼び出す対話モデル
  • ユーザの言葉に対応する応答を作るバックエンド

対話モデルの部分はAlexaサービスを使うことになります。 ユーザの発言の"意図"をインテントとして定義して、どのような言葉がどのインテントを表すのかを決めていきます。

バックエンドはコードを書いて実装する部分です。Alexaが判定したインテントが呼ばれるので、それに対する受け答えを返す処理を実装します。 HTTPSでアクセスできて、Alexaが定義するJSONをやり取りできれば、Rails1でもなんでも構いません。AmazonではLambdaの利用を推奨しています。 Lambdaもいくつかの言語をサポートしていますが、SDK等の対応状況から見てJavaScript、すなわちNode.jsが優先されているようです。

いこナビの開発では、推奨に従ってLambdaを採用し、ランタイムにはNode.js(v8.10.0)を選択しました。
実装言語としてはTypescriptを使っています。
個人的にはJavaScriptに型なんか不要派だったのですが、食わず嫌いも良くないし、今回、個人的な新たな技術的な取り組みとして使ってみることにしました。
結果的にはTypescriptを採用して良かったです。
Amazonから提供される ASK SDK(AlexaのSDK)自体もTypescriptで実装されており、 型情報があるのでVSCodeでは便利なコード補完が使えるので効率的に実装を進めることができました。 自分で実装した関数も引数、戻り値の方をきちんと定義しておけば使い方を間違うこともなかったです。

Serverless + webpack

コードのトランスパイルおよびデプロイにはServerless Frameworkを使いました。
他の選択肢としては AWS SAM や ask-cliもあったのですが、もともと他のプロダクトで利用していて慣れていたのと、 Lambdaを実行するIAM Roleの管理等も一括してできそうなので Serverless Frameworkを利用しました。

LambdaはServerlessで管理していますが、Alexaの対話モデルの定義ファイルに関してはask-cliを使って管理しています2

AWS DynamoDB

ユーザの郵便番号や利用履歴などを保存するために利用しています。
Alexaにはセッションの概念があり、スキルを起動して終了するまでのやり取りが1つのセッションとして管理されます。 ワンセッションの中で保存したい情報はセッションに保存するだけで良いのですが、セッションを越えて保存したい情報は何らか永続化の仕組みが必要です。
ASK SDKで標準サポートされているデータベースはDynamoDBです。いこナビでもDynamoDBを採用しました。

Rails

いこーよのデータを取得するためのAPIをいこーよ内に実装しました。なのでRailsです。 今回は次の2種類のAPIを実装しました。

  • 郵便番号から市区町村を取得するAPI
  • 施設情報を取得するAPI

実装に際して注意したこと

プラットフォーム依存の分離

ASK SDKを利用したAlexaスキルの開発ではインテントを処理するためにリクエストハンドラと呼ばれるモジュールを実装します。

ざっくり分けると次のような処理を行います。

  1. canHandle関数でそのハンドラが処理すべきリクエストかどうかを判定する
  2. リクエストからSlotの値を取り出す
  3. セッション属性や永続化データの取得/保存を行う
  4. Slotの値や属性の値に応じた応答を返す

この内、1,2,3はAlexaプラットフォームに依存した処理となります。 4.についてはスキル固有のビジネスロジックであり、Alexaには本質的に依存する部分ではありません。

次の理由から Alexaに依存したコードとビジネスロジックは切り離して実装しました。

  • 今後のAlexaプロトコル(実態はJSONフォーマット)の変更の影響を最小化したい
  • 今後のAlexa SDKの変更の影響を最小化したい(実際、SDKv1とSDKv2の実装は別物となった)
  • テストしやすく実装したい
  • AoG等他のプラットフォームに対応する場合に、ビジネスロジックはできる限り再利用したい。

テストもしっかり実装する

まあ、当たり前の話ですが、開発期間に余裕がない中でもきちんとやろうと決めていました。 というのは次のような開発になると予測したためです。

  • スキル自体の動作を確認しながらの開発になる
  • モジュールの分割等の実装方針自体も試行錯誤しながらの開発になる

要は自分なりのベストプラクティスを模索しながらの実装になるので、大胆にリファクタしながら開発するには しっかりしたテストコードが必要と考えたのです。それに実機でじっくりテストを行う時間はなさそうでした。

はじめはAlexaに依存した部分をローカルで動かすのは難しそうだったので、 ビジネスロジックを分離しその部分だけでもしっかりテストをしようと思っていました。

しかし、Alexaとのやり取りへの理解不足もあり、スキルとして動かしてみるとエラーが多発しました。 仕方なく、Lambdaをデプロイしてはエミュレータで確認するということを繰り返していましたが、 実装が進むにつれデプロイにかかる時間のオーバヘッドが非効率で開発が思うように進まなくなりました。 また、デバッグログを仕込んで実行し、エラー箇所をClowdwatch Logsで確認するのも効率よくありません。

そこで Serverless Frameworkの invoke localコマンドを利用してローカルでLambdaを実行してテストでききるようにしました。 VSCodeのデバッガと組み合わせればステップ実行も可能なため、効率的にデバッグできるようになりました。

その後、bespoken/virtual-alexaというライブラリを導入し、 Jestでテストを書き始めてからは、とても効率的に開発を進められるようになりました。

実機でのテストも怠らない

バグ発見を目的とした実機テストは時間がなくて十分にはできないと開発当初から思っていましたが、実機での動作確認は重要と考えました。
前述の Jest + virtual-alexaでテストできるのは所詮はバックエンドのみ。 前段のAlexaがどの様に言葉を聞き取り、インテントをリクエストしてくるか、 スロットの値を設定するかは実機での確認が欠かせません。

Alexaに喋らせる台詞も、音声で聞いてみるとわかりにくかったりするので、 聞き取りやすい言い回しに修正する作業も実機で動かしながら行いました。

Alexaシミュレータでもある程度動作確認はできます。でも実機のマイクやスピーカの特性までは シミュレートしてくれるわけではないのですし、 Echo Spotなどのディスプレイ付きデバイスのディスプレイのタッチ動作はサポートしていません。

あと、実機での動作確認で困ったのは作業場所です。 オフィスだとどうしても周りの人の声を拾ってしまってテストになりません。 幸い、弊社はリモート勤務OKなので「今日は声出す開発するんで」と言って リモート勤務にして自宅で開発作業を行いました。

リリースはしましたが…

はじめてのスキル開発で手探り状態の中実装したので、コードには試行錯誤の跡がそのまま残っている箇所が散見されます。 モジュール分割の考え方が開発初期と終盤で変化してきたため、モジュールの役割分担がぶれている箇所もあります。 リファクタを進め、整理していきたいと思っていますが、 Typescriptによる型の支援がありますし、テストカバレッジも高く維持できているのでそれほど大変ではないと考えています。

また、最初に検討した構想のすべてをまだ実装したわけではありません。 特に、前回のエントリで触れた「愛される仕掛け」については これからです。

他のVUIプラットフォームへの対応も進めたいと思っています。

さいごに

「いこーよのおでかけナビ」の開発はまだ続いていきます。 VUIでユーザのよりよいお出かけ体験をサポートしてみたいエンジニアを待っています。

actindi.net


  1. damianFC/alexa-rubykit: Amazon Echo Alexa’s App Kit Ruby Implementationとかあるようです。

  2. 対話モデルの定義ファイルはJSON形式でダウンロードできますが、それもコードとしてGitでバージョン管理しています。