ClojureScriptでAlexaスキルを書いてみた


みなさま、おはようございます。
人生初のコミュニティのアドベントカレンダー（= 会社のじゃないやつ）でドギマギしている [toku345](https://twitter.com/toku345) です。

この記事は Clojure Advent Calendar 2017 の 11 日目の記事です。

さて今回は、近頃盛り上がりを見せている Amazon Echo と好きなように会話するために ClojureScript (以降 cljs) で Alexa Skill を書いて、公開するまでをご紹介します。

どこからか「素の JS で書いたほうが楽だよー」みたいな声も聞こえて来そうですが、完全に私の趣味なので悪しからず。。。

では、始まり始まり〜

# 何を作ったの？

せっかく cljs で書くのだから、cljs に関係するものがいいなということで...
問いかけると最新の cljs のバージョンを教えてくれるスキルを作ってみました。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ClojureScriptで誰得？なスキルを公開してみた。<br>こんなこともちゃんと答えてくれるようになるなんてAlexa可愛いな！ <a href="https://t.co/dG11gdvPB9">pic.twitter.com/dG11gdvPB9</a></p>&mdash; Fumitaka Tokumitsu (@toku345) <a href="https://twitter.com/toku345/status/939355684725911552?ref_src=twsrc%5Etfw">2017年12月9日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# Alexa って何？ Echo と何が違うの？

## Amazon Alexa

[クラスメソッドさんのこちらの記事](https://dev.classmethod.jp/cloud/aws/amazon-alexa-basic-knowledge/)の説明がすごくわかりやすく、引用させていただくと

> Amazon Alexa とは、Amazon が提供する、クラウドベースの音声認識サービスです。Alexa に対応したデバイスが認識した音声はクラウドサービスに送信されます。クラウドサービスは音声をテキスト変換し、そのテキストを処理し、処理結果をデバイスに返して音声として再生されます。

![Alexaイメージ図](https://cdn-ssl-devio-img.classmethod.jp/wp-content/uploads/2017/01/bnalexa1.png)

Amazon Echo ≠ Alexa で、Echo は Amazon が販売している Alexa 対応ガジェットという位置づけみたいですね。

## Alexa Skill

こちらも[先程のクラスメソッドさんの記事](https://dev.classmethod.jp/cloud/aws/amazon-alexa-basic-knowledge/)から引用させていただくと、

> Alexa Skill とは、Alexa 対応デバイスから音声として送られてきて、Alexa が音声から変換したテキストに対する処理を実行するプログラムです。
> Amazon 自体、またはサードパーティが開発し公開しています。Skill は Lambda Function で実装されます。

![AlexaSkillイメージ図](https://qiita-image-store.s3.amazonaws.com/0/13998/8b04c723-a240-746d-1f8b-8cf593a77075.png)

Alexa Skill のエンドポイントとしては Lambda 以外にも、任意の HTTPS エンドポイントを設定できるみたいですが、今回はお手軽な Lambda を使って書くことにします。

# なぜ Clojure じゃなくて ClojureScript を選んだの？

[Lambda 上での Clojure/cljs のパフォーマンスを比較したこちらサイト](https://numergent.com/2016-01/AWS-Lambda-Clojure-and-ClojureScript.html)での検証結果をみると

|    Lambda     |  RAM  | First call | Once warm |
| :-----------: | :---: | :--------: | :-------: |
|    Clojure    | 256MB |   8900ms   |  1500ms   |
| ClojureScript | 128MB |   2300ms   |  1600ms   |

（※ 上記サイトからの引用）

Clojure(バックエンドは JVM）だと初動がものすごく遅いため、そんなに頻繁に使われないような Skill には向いてなさそうだと判断し ClojureScript で書くことを決めました。[^1]

[^1]: もちろん Clojure でも RAM を増やせば立ち上がりも早くなるみたいですが、クラウド破産が怖かったので...

# Alexa Skill を書き始める前にハマったこと

Alexa Skill Kit という SDK が公式に [Node.js](https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs)/[Java](https://github.com/amzn/alexa-skills-kit-java)向けに公開されているのでこれを使いたかったのですが、npm パッケージを cljs から呼び出すためにちょっとハマっちゃいました...

詳しくはこちらの私の記事を参照ください。
[ClojureScript 内から npm モジュールを呼び出す（npm-deps）](https://qiita.com/toku345/items/e104570340ba87a4388b)

# 自作 Alexa Skill を Echo 実機で喋ってもらうまで

自作 Alexa Skill を Echo 実機で喋ってもらうまでの大まかな流れは以下のとおりです。

**前準備**

1. [Amazon Developer アカウント作成](https://developer.amazon.com/ja/)
2. [AWS アカウント作成](https://portal.aws.amazon.com/billing/signup#/start)

Alexa Skill の公開、並びに実機でのテストには Amazon Developer アカウントが必要となります。
※ AWS アカウントとは違うので注意！

Amazon Developer アカウント作成は [こちらの記事](https://developer.amazon.com/ja/blogs/alexa/post/6e716e5c-55b0-445b-b936-9cfac4712e7b/training-1)のステップ１を、AWS アカウント作成は [こちらの記事](https://aws.amazon.com/jp/register-flow/)で詳しく説明されていますので、ここでは割愛します。

**スキル開発〜登録**

1. Skill の作成
2. Skill のバックエンドとなる Lambda 関数を準備
3. Skill に Lambda 関数を連携させる
4. サービスシュミレーターで動作確認
5. その他設定を入力
6. テスター設定

## 1. Skill の作成

Amazon Developer コンソールにサインインし、「Alexa」メニュー(①)をクリック。
画面遷移後、Alexa Skill Kit の「始める」ボタン(②)をクリック。
![アマゾン_アプリ_開発者ポータル.png](https://qiita-image-store.s3.amazonaws.com/0/13998/67bb4398-ed8e-e26a-c14a-a80ab0ade069.png)

右上の「新しいスキルを追加する」ボタン(③)をクリック。
![アマゾン_アプリ_開発者ポータル.png](https://qiita-image-store.s3.amazonaws.com/0/13998/27b6bbe9-76bc-89dc-bf6a-a95f4b289ab7.png)

### スキル情報

以下のように設定する。

![Pasted_Image_2017_12_10_16_37.png](https://qiita-image-store.s3.amazonaws.com/0/13998/e8698968-79bc-ec5d-a730-f861e7c773bb.png)

|     項目     |       設定値        |                   備考                    |
| :----------: | :-----------------: | :---------------------------------------: |
| スキルの種類 | カスタム対話モデル  |                     -                     |
|     言語     |      Japanese       |                     -                     |
|   スキル名   | cljs バージョン確認 |               任意の値で OK               |
|  呼び出し名  |   バージョン確認    | Echo に呼びかける際にキーとなるので注意！ |

※ グローバルフィールドはそのままで OK

設定後、「保存」ボタンをクリック。
保存が終わると「次へ」ボタンが現れるのでクリック。

### 対話モデル

こちらは以下のように設定する。

![Pasted_Image_2017_12_10_17_03.png](https://qiita-image-store.s3.amazonaws.com/0/13998/7bf4b80f-4d34-c5e2-6753-850bf5aef7c5.png)

```json:インテントスキーマ
{
  "intents": [
    {
      "intent": "CljsVersionIntent"
    },
    {
      "intent": "AMAZON.HelpIntent"
    },
    {
      "intent": "AMAZON.StopIntent"
    },
    {
      "intent": "AMAZON.CancelIntent"
    }
  ]
}
```

※ カスタムスロットタイプ関係は空欄のままで OK

```text:サンプル発話
CljsVersionIntent シーエルジェーエス の 最新のバージョン を 教えて
CljsVersionIntent クロージャースクリプト の 最新のバージョン を 教えて
CljsVersionIntent cljs の 最新のバージョン を 教えて
CljsVersionIntent ClojureScript の 最新のバージョン を 教えて
CljsVersionIntent 最新バージョン を 教えて
CljsVersionIntent 最新バージョン を 調べて
CljsVersionIntent 最新バージョン は どう
CljsVersionIntent 最新バージョン は
CljsVersionIntent バージョンチェック して

```

インテントスキーマ、カスタムスロットタイプについては[Alexa スキル開発トレーニング](https://developer.amazon.com/ja/alexa-skills-kit/training/building-a-skill)がわかりやすいので、こちらの記事を参照ください...

設定後、「保存」→「次へ」ボタンをクリック。
次のページで Lambda 関数のエンドポイントが必要になるので、一旦ここまででストップ。

## 2. Skill のバックエンドとなる Lambda 関数を準備

AWS コンソールにサインイン後、[Lambda のダッシュボード](https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1#/)を開き「関数の作成」ボタンをクリック。
![Lambda_Management_Console_and_1__rlwrap___Users_toku345_works_cljs_node-apps_hello-aws-cli__java_.png](https://qiita-image-store.s3.amazonaws.com/0/13998/40aa0838-c70f-97ba-3326-d68242f3f6a3.png)

### 関数の作成

「設計図」を選択し(①)、検索欄に「Alexa」を入力し Enter を押す(②)。
検索結果から「alexa-skill-kit-sdk-factskill」を選択(③)。
![Lambda_Management_Console.png](https://qiita-image-store.s3.amazonaws.com/0/13998/c864e3cc-ff1e-461b-8f83-c42084798118.png)

### 関数の設定

![Lambda_Management_Console.png](https://qiita-image-store.s3.amazonaws.com/0/13998/be523dd6-5146-3d86-af61-90f54af205d1.png)

|  項目  |                                                  設定値                                                   |                               備考                               |
| :----: | :-------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------: |
|  名前  |                                             cljsVersionNotify                                             |                          任意の値で OK                           |
| ロール | ![image.png](https://qiita-image-store.s3.amazonaws.com/0/13998/4c17d6db-d3e5-0bb8-9f7f-c9a56bb44c05.png) | ← を選択するとロール作成用の別ページが開くので「許可」をクリック |

まだこの段階では **Lambda 関数のコード**は変更できないので、右下の「関数の作成」ボタンをクリック。

### トリガーの追加

Alexa からの呼び出しイベントに対応するために、トリガーを設定します。

左のトリガー 一覧から「Alexa Skills Kit」を選択。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/13998/5e5394e4-d4ad-9d00-54a4-705b51de3557.png)

アクセス許可を設定する必要があるので、トリガー設定にある「追加」ボタンをクリック。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/13998/c73385a0-b7f3-1420-f2a8-f2f48cbcfa50.png)

さあ、これで Lambda 関数に関する設定ができました。
次はサンプルコードを自作コードに置き換えましょう！

### 関数を書く

ここからが楽しい実装の時間ですね

今回、ターゲットはブラウザではなく Node.js なので、`figwheel-node` テンプレートを使います。

```console
$ lein new figwheel-node cljs-version-notify
```

project.clj の :cljsbuild -> :builds の `"dev"` と `"prod"` に :npm-deps と :install-deps オプションを追加。

```diff:project.clj
diff --git a/project.clj b/project.clj
index 6ad593a..fda3880 100644
--- a/project.clj
+++ b/project.clj
@@ -25,6 +25,8 @@
                 :output-to "target/js/compiled/cljs_version_notify.js"
                 :output-dir "target/js/compiled/dev"
                 :target :nodejs
+                :npm-deps {:alexa-sdk "^1.0.23"}
+                :install-deps true
                 :optimizations :none
                 :source-map-timestamp true}}
              {:id "prod"
@@ -33,6 +35,7 @@
                 :output-to "server.js"
                 :output-dir "target/js/compiled/prod"
                 :target :nodejs
+                :npm-deps {:alexa-sdk "^1.0.23"}
                 :optimizations :simple}}]}

   :profiles {:dev {:dependencies [[figwheel-sidecar "0.5.13"]
```

src/cljs_version_notify/core.cljs に以下のように記述。

```clojurescript:src/cljs_version_notify/core.cljs
(ns cljs-version-notify.core
  (:require [cljs.nodejs :as nodejs]))

(nodejs/enable-util-print!)

(defonce Alexa (nodejs/require "alexa-sdk"))

(def ^:const cljs-latest-version "1.9.946") ; TODO: 最新バージョンを動的に取得したい...
(def ^:const stop-message "クロージャースクリプトの最新バージョン番号が気になったら、また声をかけてくださいね。")

(def skill-handlers #js {"LaunchRequest"       (fn [] (this-as this (.emit this "AMAZON.HelpIntent")))
                         "SessionEndedRequest" (fn [] (this-as this (.emit this "AMAZON.StopIntent")))
                         "AMAZON.HelpIntent"   (fn [] (this-as this
                                                        (.emit this
                                                               ":ask"
                                                               (str "クロージャースクリプトの最新バージョン番号をお知らせする非公式のスキルです。"
                                                                    "クロージャースクリプトの最新バージョン番号をお知らせしましょうか？"))))
                         "CljsVersionIntent"   (fn [] (this-as this
                                                        (let [message (str "最新のcljsのバージョンは" cljs-latest-version "です。")]
                                                          (.emit this ":tell" message)
                                                          (.log js/console (str "message: " message)))))
                         "AMAZON.CancelIntent" (fn [] (this-as this (.emit this "AMAZON.StopIntent")))
                         "AMAZON.StopIntent"   (fn [] (this-as this (.emit this ":tell" stop-message)))})

(defn- handler [event context callback]
  (let [alexa (.handler Alexa event context)]
    (.registerHandlers alexa skill-handlers)
    (.execute alexa)))

(set! (.-exports js/module) #js {:handler handler})
```

cljs → js

```console
$ lein cljsbuild once prod
Compiling ClojureScript...
Compiling ["server.js"] from ["src"]...
Successfully compiled ["server.js"] in 25.799 seconds.
```

プロジェクトのルートディレクトリに `server.js` という名で出力されます。

### 出力した JS コードを Lambda 関数として設定

zip にしてアップロードしても良いのですが、今回は手動で設定しましょう。

注意！
関数エディタが表示されていない場合は、関数名をクリックしてください。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/13998/2cc4e9a6-17ca-4197-7fcc-e0721d2bbfb8.png)

トランスパイルした JS コードを貼り付け(①)、エディタの変更を保存(②)
![Lambda_Management_Console.png](https://qiita-image-store.s3.amazonaws.com/0/13998/c5165fb4-b6c0-db25-68d5-b6f06da92708.png)

コード保存後、ページ右上の「保存」ボタンをクリック。
保存ボタンの上の ARN は次で使うので控えておいてください。
![Lambda_Management_Console.png](https://qiita-image-store.s3.amazonaws.com/0/13998/f04999e6-57b9-0b80-a3c7-1d9e2eb1de3e.png)

## 3. Skill に Lambda 関数を連携させる

再び Amazon Developer コンソールを開きます。

### 設定

![image.png](https://qiita-image-store.s3.amazonaws.com/0/13998/a872e6a4-f079-1b5a-ba81-82320256580b.png)

|              項目              |         設定値         |                備考                |
| :----------------------------: | :--------------------: | :--------------------------------: |
| サービスエンドポイントのタイプ |   AWS Lambda の ARN    |                 -                  |
|           デフォルト           | （環境によって異なる） | Lambda 関数設定時に確認した ARN 値 |

他の設定は「いいえ」、または未選択で OK。
設定後、「保存」→「次へ」ボタンをクリック。

## 4. サービスシュミレーターで動作確認

「発話を入力してください」欄に Skill を使う際に話しけたい文章を入力し、「cljs バージョン確認を呼び出す」ボタンをクリック。
右側の **サービスレスポンス** に `最新のcljsのバージョンは...` と言った文言が含まれていれば動作 OK!
![Pasted_Image_2017_12_10_21_06.png](https://qiita-image-store.s3.amazonaws.com/0/13998/ed57a8e3-c0aa-d7a2-d9ca-ad6fe50b2a2a.png)

実機がある場合は「次へ」をクリック。

## 5. その他設定を入力

**公開情報** と **プライバシーとコンプライアンス** をいい感じに入力します。

入力例

|                                                             公開情報                                                              |                                                  プライバシーとコンプライアンス                                                   |
| :-------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------: |
| ![Pasted_Image_2017_12_10_21_19.png](https://qiita-image-store.s3.amazonaws.com/0/13998/e9587b14-d9fc-4e55-c284-bf94d1e1b8b6.png) | ![Pasted_Image_2017_12_10_21_20.png](https://qiita-image-store.s3.amazonaws.com/0/13998/79d8505c-8e0f-0c41-9518-1fa4b39bd362.png) |

## 6. テスター設定

5 の設定が終わると 「☆ スキルのベータテスト」ボタンが現れるので、お使いの Echo に紐付けたメールアドレス宛にテストリンクを送付できるようになります。
![アマゾン_アプリ_開発者ポータル.png](https://qiita-image-store.s3.amazonaws.com/0/13998/ecd74bf4-e886-49dc-579c-a73f47b7f8d8.png)
↓
![Skills_Beta_Testing_for_Alexa.png](https://qiita-image-store.s3.amazonaws.com/0/13998/4d7c41e3-0850-f19c-b2af-d58bc30c3ad1.png)

これで実機でテストできるようになりました！

# まとめ

長々とお付き合いありがとうございました。
ご覧の通り ClojureScript でも簡単に Alexa Skill 書くことができました。

これを機にもっと ClojureScript で書かれた Skill が増えるといいなと思っています。

今回作った Skill は Alexa Skill ストアに並んでいるのでぜひ使ってみてください！

![Amazon_Alexa.png](https://qiita-image-store.s3.amazonaws.com/0/13998/177f53b5-06d1-6d9c-284f-8127768b5284.png)

コード全体をご覧になりたい方ははこちらをどうぞ
https://github.com/toku345/cljs-version-notify/tree/0.1.0

---

## 今回できなかったことリスト

### ClojureScript のバージョンを動的に取得する

→ こちらは現在、実装中です...

### AWS SAM を使ってデプロイする

→ こちらも実装中です...

### Clojure でも実装してみる

Clojure / cljs で Lambda を本格的に使いたいときは k2nakamura さんのこちらスライド [CLJ/CLJS 向け SERVERLESS ソリューション](https://gitpitch.com/k2n/clj-serverless-frameworks#/)がめっちゃ役に立ちます！