こんにちは、ポキオです。
スキルの処理を簡単に書きたい
以前、簡単なClovaスキルを作ったんですが、意外と簡単で、しかも無料で、そしてすぐに実機で試せて、最高でした。
- Clova + Clova Extension Kit + IBM Cloud FoundryでClovaスキルをチョッパヤで作ってみる(前半)
- Clova + Clova Extension Kit + IBM Cloud FoundryでClovaスキルをチョッパヤで作ってみる(後半)
ただし、一つだけ問題があって、LINEではPaaSは提供していない点です。
Dialogflowと比較をすると一目瞭然ですが、Clovaスキルのメインの処理の部分をLINE以外のサービスで立ち上げて設定する必要がありました。前回はIBM Cloud Foundryを無料で使うということをしました。ただ、そこにデプロイするのはコード(Node.js)で、バリバリのコードです。
なんとかして、ノンプログラミングな方法でスキルの中身を書けないかと模索していたところ、出会ったのがenebularでした。
enebularとは
enebular(エネブラー)は、IoT製品・サービスづくりを包括的に支援する、開発・運用サービスです。エッジとクラウドにまたがるIoTアプリケーションを開発し、様々なデバイスへ迅速にデプロイ、さらに膨大な量のデータを可視化やAIにより利活用することで、IoTアプリケーションの最適な運用を支援します。
enebularの名前は、星の数(nebular)ほどあるデバイスを、アップデートにより賢くし(enable)、分散しながら協調するアプリケーションの開発から由来しています。
無料でも使えるツールで、Node−REDベースのツールで、グラフィカルに、大半をノンプログラミングで処理を設計できます。
- LINE Clova Extensions Kitとenebularを連携して会話させてみよう
今回、enebularを使うきっかけになったのは、こちらの記事です。
要は「Clovaで発話を受け取り、Clova Extension Kit経由でenebularでリクエストを受けて、enebular上でレスポンスを作成し、Clovaに発話させる」ということが簡単にできてしまうということらしいです。Node-REDでグラフィカルに設計できるのであれば、かなりClovaスキル開発のハードルが下がるなと思い、ためしに開発してみました。
せっかくなので、大好きな京急にちなみ、京急の運行情報を教えてくれるClovaスキルを作ってみようと思います。
Clovaスキルを作ってみる
とりあえずClovaスキルを立ち上げる
何はともあれ、LINEの開発者向けのページで、新しくClovaスキルのプロジェクトを立ち上げます。よしなにスキル名を名付けていきます。
そして、このスキルに対して対話モデルを設計していきます。細かいやり方については、上記記事と私のブログを御覧いただきつつ、ここでの説明は簡単なものとさせていただきます。
ポイントは大きく2点。
- インテントは2つ
- 京急の運行情報を確認するインテント
- 会話を終えるインテント
- 前者のインテントで「京急」という表現にバリエーションがありそう
- カスタムスロットで、京急の言い回しをある程度吸収するように設計
こんな感じでスキルの設定をしました。
enebularでもプロジェクトを立ち上げる
enebular側でもプロジェクトを新規作成。
デフォルトのままだと、プロジェクト内にアセットが何もない状態ですが、このままでOKです。
OKというは、先程のClovaとenebular連携の記事で、Node−REDのフローが公開されているので、それをForkして使ってみようと思います。
上記サイトに飛ぶと、Clova向けにサンプルのフローが公開されているので、それをForkします。
Forkボタンを押すと、自分のプロジェクトが選べるようになっているので、しれっと選択してForkします。
すると、自分のプロジェクトに先程のフローがコピーされています。これをいじりながら京急スキルを組んでいきます。
フローを実装する
Forkしてきたフローを開き、Edit Flow
からフローの編集画面を開きます。
このフローを以下のように編集していきます。
- LaunchRequest時のセリフを変える
- IntentRequest時は、Intentの種類で分岐させ処理を変える
- 運行情報確認のIntentの場合、京急のHPから運行情報をパースし、レスポンスとする
- 会話終了のIntentの場合、終了っぽいセリフをレスポンスとしつつ、レスポンスを返したあとはユーザーの発話待受をやめる
- 上記以外のIntentは、未知のIntentとして、再度発話を促すようなセリフをレスポンスとする
で、出来上がったものがこちらです(笑)
フローのデータはこちら。
[{"id":"2c4d10e5.3fadf","type":"http in","z":"de769471.1b1e68","name":"","url":"/clova","method":"post","swaggerDoc":"","x":90,"y":160,"wires":[["57574f82.b0b49","898d36c2.da7418"]]},{"id":"c233d368.fd81","type":"function","z":"de769471.1b1e68","name":"LaunchRequestセリフ","func":"msg.payload =\n{\n \"version\": \"1.0\",\n \"sessionAttributes\": {},\n \"response\": {\n \"outputSpeech\": {\n \"type\": \"SimpleSpeech\",\n \"values\": {\n \"type\": \"PlainText\",\n \"lang\": \"ja\",\n \"value\": \"こんにちは!赤い電車です!\"\n }\n },\n \"card\": {},\n \"directives\": [],\n \"shouldEndSession\": false\n }\n}\nreturn msg;","outputs":1,"noerr":0,"x":520,"y":120,"wires":[["474b4507.0e327c"]]},{"id":"57574f82.b0b49","type":"switch","z":"de769471.1b1e68","name":"request.type 判定","property":"payload.request.type","propertyType":"msg","rules":[{"t":"eq","v":"LaunchRequest","vt":"str"},{"t":"eq","v":"IntentRequest","vt":"str"}],"checkall":"true","outputs":2,"x":290,"y":160,"wires":[["c233d368.fd81"],["b761c32c.0f634"]]},{"id":"bfd2a44e.ad6b08","type":"comment","z":"de769471.1b1e68","name":"LaunchRequest","info":"","x":500,"y":80,"wires":[]},{"id":"898d36c2.da7418","type":"debug","z":"de769471.1b1e68","name":"","active":true,"console":"false","complete":"false","x":270,"y":200,"wires":[]},{"id":"474b4507.0e327c","type":"http response","z":"de769471.1b1e68","name":"","x":710,"y":120,"wires":[]},{"id":"1e537e25.5a5d32","type":"comment","z":"de769471.1b1e68","name":"IntentRequest","info":"","x":490,"y":280,"wires":[]},{"id":"c8e079d1.fec438","type":"comment","z":"de769471.1b1e68","name":"Intentの種類を判定","info":"","x":510,"y":320,"wires":[]},{"id":"12bed258.8d1a7e","type":"comment","z":"de769471.1b1e68","name":"CHECK_KEIKYU","info":"","x":760,"y":160,"wires":[]},{"id":"2c6bb4c4.a10aec","type":"comment","z":"de769471.1b1e68","name":"起動時の応答","info":"","x":730,"y":80,"wires":[]},{"id":"75d7f9c6.966138","type":"comment","z":"de769471.1b1e68","name":"Clovaからの受け取る","info":"","x":120,"y":120,"wires":[]},{"id":"b761c32c.0f634","type":"switch","z":"de769471.1b1e68","name":"request.intent.name 判定","property":"payload.request.intent.name","propertyType":"msg","rules":[{"t":"eq","v":"CHECK_KEIKYU","vt":"str"},{"t":"eq","v":"END_KEIKYU","vt":"str"},{"t":"else"}],"checkall":"true","outputs":3,"x":530,"y":360,"wires":[["3c4833ab.ae84cc"],["f9928a98.a148b8"],["42aad12d.8b656"]]},{"id":"6fcc8005.7365c","type":"http response","z":"de769471.1b1e68","name":"","x":1050,"y":360,"wires":[]},{"id":"f9928a98.a148b8","type":"function","z":"de769471.1b1e68","name":"ご乗車ありがとうございました!","func":"msg.payload =\n{\n \"version\": \"1.0\",\n \"sessionAttributes\": {},\n \"response\": {\n \"outputSpeech\": {\n \"type\": \"SimpleSpeech\",\n \"values\": {\n \"type\": \"PlainText\",\n \"lang\": \"ja\",\n \"value\": \"ご乗車ありがとうございました!\"\n }\n },\n \"card\": {},\n \"directives\": [],\n \"shouldEndSession\": true\n }\n}\nreturn msg;","outputs":1,"noerr":0,"x":820,"y":360,"wires":[["6fcc8005.7365c"]]},{"id":"b003fb9a.8c29e8","type":"comment","z":"de769471.1b1e68","name":"END_KEIKYU","info":"","x":750,"y":320,"wires":[]},{"id":"42aad12d.8b656","type":"function","z":"de769471.1b1e68","name":"もう一度言ってください!","func":"msg.payload =\n{\n \"version\": \"1.0\",\n \"sessionAttributes\": {},\n \"response\": {\n \"outputSpeech\": {\n \"type\": \"SimpleSpeech\",\n \"values\": {\n \"type\": \"PlainText\",\n \"lang\": \"ja\",\n \"value\": \"もう一度言ってください!\"\n }\n },\n \"card\": {},\n \"directives\": [],\n \"shouldEndSession\": false\n }\n}\nreturn msg;","outputs":1,"noerr":0,"x":800,"y":460,"wires":[["6fcc8005.7365c"]]},{"id":"965df001.3de56","type":"comment","z":"de769471.1b1e68","name":"UNKNOWN","info":"","x":750,"y":420,"wires":[]},{"id":"50925403.9d586c","type":"comment","z":"de769471.1b1e68","name":"発話時の応答","info":"","x":1070,"y":400,"wires":[]},{"id":"3c4833ab.ae84cc","type":"http request","z":"de769471.1b1e68","name":"京急運行情報取得","method":"GET","ret":"txt","url":"http://unkou.keikyu.co.jp","tls":"","x":770,"y":200,"wires":[["449159af.958908"]]},{"id":"449159af.958908","type":"function","z":"de769471.1b1e68","name":"運行情報パース","func":"msg.payload = msg.payload.split(\"<div class=unko-panel>\")[1];\nmsg.payload = msg.payload.split(\"</div>\")[0];\nmsg.payload = msg.payload.replace(/\\\\r?\\\\n/g,\"\");\nmsg.keikyu = msg.payload;\nmsg.payload = null;\nreturn msg;","outputs":1,"noerr":0,"x":760,"y":240,"wires":[["8c8bd701.c2cb88"]]},{"id":"8c8bd701.c2cb88","type":"function","z":"de769471.1b1e68","name":"運行情報反映","func":"msg.payload =\n{\n \"version\": \"1.0\",\n \"sessionAttributes\": {},\n \"response\": {\n \"outputSpeech\": {\n \"type\": \"SimpleSpeech\",\n \"values\": {\n \"type\": \"PlainText\",\n \"lang\": \"ja\",\n \"value\": \"\"\n }\n },\n \"card\": {},\n \"directives\": [],\n \"shouldEndSession\": false\n }\n}\nmsg.payload.response.outputSpeech.values.value = msg.keikyu;\nreturn msg;","outputs":1,"noerr":0,"x":760,"y":280,"wires":[["6fcc8005.7365c"]]}]
入力に対するパースの仕方や、レスポンスのボディーのサンプルが、Forkしたサンプルに丁寧に記述されているため、用意に理解でき、自分のやりたいことを実現するために、どこをどう直せばいいか一目瞭然でした。
主にどこを変えたかというと、
- IntentRequestだったときの処理を変更
- Intent名でSwitch
- Intent名は
payload.request.intent.name
で取得可能 - 運行情報取得Intentときは、HTMLをパースしレスポンスに突っ込む
- 会話終了Intentのときは、レスポンスの
shouldEnd
をtrue
にする
こんな感じです。HTMLパースの部分はゴリゴリ書いていますが、それ以外はほぼパラメタ変更程度で、スキルの処理が記述できてしまいます。
最後に、フロー編集画面の右上のiボタンから、このフローへアクセスするためのURLをコピーして、ClovaスキルのExtensionサーバー設定のところに貼り付けます。ただし、実際にはこのURLの後ろにclova/
をつけたURLで待ち受けているので、Clovaスキルの設定にはclova/
の文字列を末尾に追加して登録します。
実際に動かしてみる
Clovaスキルのダッシュボードから、ビルドを行い完了すると、ダッシュボードや実機上で動作確認ができるようになります。実機上で試すためには、スマホのClovaアプリから、今回作ったスキルがスキルストア上に表示されているので、Clovaで使えるように設定してあげると、準備完了です。
京急なClovaスキルを作ってみた。 pic.twitter.com/MohVvvLVYD
— ポキオ@MFT2018 H/04-01 (@pokiiio) 2018年8月25日
実際に実機でスキルを試してみました。起動ワードはJessicaになっています。
諸事情で暗闇でコーディングしていますが、気にしないでください・・・(笑)