【GoogleHomeでメモ帳アプリを作る】6. GoogleHomeに呼びかけてSpreadSheetのメモを読み上げてもらう
概要
前回までで、heroku上にアプリを作成するところまでできた
今度は、それをGoogleHomeから呼び出すため(正確にはactions on googleから呼び出すため)に、Dialogflowの設定をする
資料全体の構成はここに記載: GoogleHomeに話しかけてメモを記録したりメモを読み上げてもらう
参考
参考 actions on googleの公式マニュアル(Dialogflowについて)
Dialogflowの公式マニュアル
その他の参考
- https://qiita.com/kenz_firespeed/items/0979ceb05e4e3299f313
- https://qiita.com/kotatu_km/items/c06f45c6692dceb9258d
- https://qiita.com/doki_k/items/11f8a23d71dce59409da
- https://qiita.com/ume1126/items/f60335179b1cee25fa2f
- https://qiita.com/miso_develop/items/28cbfcdfb61d455ce346
- https://kotodama.today/?p=45
- https://docs.google.com/presentation/d/1HjxtUtMhphdBTm_KOGDb1dq7Dy1R8sNCQj80zDdCJqg/mobilepresent?slide=id.g2aeb9765a5_1_584
Dialogflowの詳しい作り方は上や色々なサイトに書いてあるのでここでは自分のアプリのために必要な最低限のことだけを記載する
Dialogflowでアプリ作成
actions on googleとDialogflowを連携する
https://developers.google.com/actions/
ACTIONS CONSOLE ボタンを押す
https://console.actions.google.com/
Add/import project を選択
Dialogflowを選択
※Dialogflow以外もあるようだが、参考資料が多かったのでここではDialogflowを使う
Dialogflowの設定
Intents
公式のマニュアル - https://dialogflow.com/docs/intents
新規のIntentsを作成 - ここでは、「 getMymemo 」 という名前のIntentsを作成
User saysに「 メモを読んで 」という文言を追加する
一旦これでSAVE
Fulfillment
WebhookのURLにherokuのURLを追加する
これで
ENABLEDにする
IntentsとFulfillmentを連携
先程のIntentsに戻って、Fulfillmentの連携を有効にする
ここまででDialogflow側の最低限の設定は終わり
DialogflowからWebアプリへのリクエストとレスポンス制約
DialogflowからWebアプリを呼び出すためには以下のフォーマットに従う必要がある
公式のドキュメント - https://dialogflow.com/docs/fulfillment
Webhook
Setting up a webhook allows you to pass information from a matched intent into a web service and get a result from it.
Webhookの仕様(制約)
Request
Your web service receives a POST request from Dialogflow.
Response
The response from the service should have the following fields:
Name | Type | Description |
---|---|---|
speech | String | Response to the request. |
displayText | String Text displayed on the user device screen. | |
data | Object | Additional data required for performing the action on the client side. The data is sent to the client in the original form and is not processed by Dialogflow. |
contextOut | Array of context objects | Array of context objects set after intent completion. |
source | String | Data source. |
followupEvent | Object | Event name and optional parameters sent from the web service to Dialogflow. Read more |
Caution: The header must be “Content-type: application/json”.
つまり要約すると・・・ - リクエスト - リクエストはPOSTでJSON形式で送られる - レスポンス - アプリからのレスポンスはJSON形式で、ヘッダーは“Content-type: application/json” である必要がある - レスポンスにはspeech, displayTextなどの項目が必要らしい
heroku(Django)の修正
前述の仕様を満たすように、Djangoアプリの修正をする
https://github.com/ludwig125/googlehome/blob/master/memorandum/views.py
必要な項目を返すようにする
- HttpResponse になっていた部分を以下のように書き直す
- また、レスポンスの項目として「speech」にSpreadSheetの最新の項目を返すようにする
差分
git diff views.py
diff --git a/memorandum/views.py b/memorandum/views.py index a544014..65e37fc 100644 --- a/memorandum/views.py +++ b/memorandum/views.py @@ -1,4 +1,5 @@ -from django.http import HttpResponse +from django.http import JsonResponse import json import gspread import oauth2client.client @@ -32,4 +33,12 @@ def get_value(gc): def index(request): gc = get_client() last_value = get_value(gc) - return HttpResponse(last_value) + res = { + "speech": last_value[2], + "displayText": ””, + "data": "", + "contextOut": "", + "source": "" + } + return JsonResponse(res)
JsonResponseとは
参考Django公式 - https://docs.djangoproject.com/en/2.0/ref/request-response/#jsonresponse-objects
An HttpResponse subclass that helps to create a JSON-encoded response. It inherits most behavior from its superclass with a couple differences: Its default Content-Type header is set to application/json.
他の参考 - https://simpleisbetterthancomplex.com/tutorial/2016/07/27/how-to-return-json-encoded-response.html
Its default Content-Type header is set to application/json.
とあるので、これを使えばfullfilementの制約が満たせそう
ブラウザから呼び出して確認
これでDjangoサーバを立ててみる
(googlehome-Z81pgPAi) [~/git/ludwig125-heroku/googlehome] $python manage.py runserver Performing system checks... System check identified no issues (0 silenced). February 11, 2018 - 22:00:40 Django version 2.0.2, using settings 'googlehome.settings' Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C. [11/Feb/2018 22:00:44] "GET /memorandum/ HTTP/1.1" 200 109 [11/Feb/2018 22:01:06] "GET /memorandum/ HTTP/1.1" 200 79
http://127.0.0.1:8000/memorandum/
→ 「{"speech": "test3", "displayText": "", "contextOut", "", "source": ""}」
ただ、POSTリクエストで送って返ってくるかどうかがわからない
Advanced REST client でリクエストを送って確認
Advanced REST client - https://chrome.google.com/webstore/detail/advanced-rest-client/hgmloofddffdnphfgcellkdfbfbjeloo?hl=ja
これを使うと、GET, POSTリクエストを選んだり、レスポンスのステータスをチェックしたり色々便利、これで確認する
これでPOSTリクエストを送ると、結果が返ってきた!!
これをDialogflowが要求する形式に直す
このあたりのサンプルを見ると、dataとcontextOutはコメントアウトしていても問題なさそうなので、コメントアウトする - https://qiita.com/ume1126/items/f60335179b1cee25fa2f - https://github.com/dialogflow/fulfillment-webhook-weather-python/blob/966959087a5c82a50b21ce43515c7133e092f13c/app.py#L108-L112
views.pyを以下のように書き換える
from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt import json import gspread import oauth2client.client import os def get_client(): """ return google spreadsheet client see, http://gspread.readthedocs.io/en/latest/#gspread.authorize """ scope = ['https://spreadsheets.google.com/feeds'] client_email = os.environ['CLIENT_EMAIL'] private_key = os.environ['PRIVATE_KEY'].replace('\\n', '\n').encode('utf-8') credentials = oauth2client.client.SignedJwtAssertionCredentials(client_email, private_key, scope) gc = gspread.authorize(credentials) return gc def get_value(gc): """ get google spreadsheet values return last value """ ss = gc.open("memorandum") sh = ss.worksheet("memo1") values = sh.get_all_values() last = len(values) - 1 return values[last] @csrf_exempt def index(request): gc = get_client() last_value = get_value(gc) res = { "speech": last_value[2], "displayText": last_value[2], #"data": {"kik": {}}, #"contextOut": [{"name":"weather", "lifespan":2, "parameters":{"city":"Rome"}}], "source": "spreadsheet memorandum" } return JsonResponse(res)
@csrf_exempt は以下を参考に付けた https://docs.djangoproject.com/ja/2.0/ref/csrf/
クロスサイトリクエストフォージェリ (CSRF) 対策
上の内容をherokuにデプロイして(git push heroku masterして) Advanced REST clientを使うと200が返ってきた
修正したアプリに対してリクエストしてみる
Dialogflowからリクエストを送る
dialogflowの画面で、「try it now」に「メモを読んで」と入れてみると、test3が返ってきた
SHOW JSONボタンを押すと以下が得られる
{ "id": "XXXXXX", "timestamp": "2018-02-12T12:43:43.804Z", "lang": "ja", "result": { "source": "agent", "resolvedQuery": "メモを読んで", "action": "", "actionIncomplete": false, "parameters": {}, "contexts": [], "metadata": { "intentId": "XXXXXX", "webhookUsed": "true", "webhookForSlotFillingUsed": "false", "webhookResponseTime": 1153, "intentName": "getMymemo" }, "fulfillment": { "speech": "test3", "source": "spreadsheet memorandum", "displayText": "test3", "messages": [ { "type": 0, "speech": "test3" } ] }, "score": 1 }, "status": { "code": 200, "errorType": "success", "webhookTimedOut": false }, "sessionId": "XXXXXX" }
※IDは適当にXXXXXXとかに置き換えている
actions on googleでテストする
テストまでできたらintegrations - integrationsの方法は一番上に書いた参考ページなどを見ればすぐわかる
ここまでで、Actions on Googleで以下のようにテストすると、スプレッドシートからかえってくることが確認できた
自分「テスト用アプリにつないで」と記入
↓
画面「はい、テスト用アプリのテストバージョンです。」 「こんにちは!」
↓
自分「メモを読んで」
↓
画面「test3」と表示された
GoogleHomeで実際に動作を確認する
これをGoogleHome相手にやっても返ってきた!!
自分「OK Google, テスト用アプリにつないで」とGoogleHomeに話す
↓
GoogleHome「はい、テスト用アプリのテストバージョンです。」 「こんにちは!」
↓
自分「メモを読んで」
↓
GoogleHome「test3」
最新のデータを読み上げさせるところまで成功した!
Dialogflowから送ったリクエストに応じて処理を変える
- Dialogflowから送ったリクエストのパラメータを見て、それに応じた件数だけSpreadsheetからデータを返すように修正する
Dialogflow参考
公式
- https://dialogflow.com/docs/fulfillment
- https://qiita.com/kenz_firespeed/items/0979ceb05e4e3299f313
他 - https://qiita.com/sakamoto_koji/items/8a9bb8a3f2063b4b5f34
Dialogflowからリクエストを送る
Intentsを修正
試しにDialogflowからリクエストを送ってみる
「1件読んで」 というサンプルリクエストを書いて、PARAMETER NAME に「number」を、「1」の部分にENTITYの@sys.numberを割り当てる
これによって、「1件読んで」をリクエストすれば、numberに1が、「3件読んで」とすればnumberに3が入る事になる
試しにTry it nowに「3件読んで」を入れて送ってみる
リクエストの中身を確認
今回は、herokuのview.py側で出力させるdisplayTextを以下のようにして、何が送られているか確認してみる
req = json.loads(request.body.decode('utf-8')) res = { "speech": last_value[2], "displayText": str(req), "source": "spreadsheet memorandum" }
SHOW JSONを押すと送った内容とレスポンスが見られる JSON
{ "id": "XXXXXX", "timestamp": "2018-02-23T14:27:16.974Z", "lang": "ja", "result": { "source": "agent", "resolvedQuery": "1件読んで", "action": "", "actionIncomplete": false, "parameters": { "number": 3 }, "contexts": [], "metadata": { "intentId": "XXXXXX", "webhookUsed": "true", "webhookForSlotFillingUsed": "false", "webhookResponseTime": 983, "intentName": "getMymemo" }, "fulfillment": { "speech": "test3", "source": "spreadsheet memorandum", "displayText": "{'id': 'XXXXXX', 'timestamp': '2018-02-23T14:27:16.974Z', 'lang': 'ja', 'result': {'source': 'agent', 'resolvedQuery': '1件読んで', 'speech': '', 'action': '', 'actionIncomplete': False, 'parameters': {'number': 1}, 'contexts': [], 'metadata': {'intentId': 'XXXXXX', 'webhookUsed': 'true', 'webhookForSlotFillingUsed': 'false', 'intentName': 'getMymemo'}, 'fulfillment': {'speech': '', 'messages': [{'type': 0, 'speech': ''}]}, 'score': 1.0}, 'status': {'code': 200, 'errorType': 'success', 'webhookTimedOut': False}, 'sessionId': 'XXXXXX'}", "messages": [ { "type": 0, "speech": "test3" } ] }, "score": 1 }, "status": { "code": 200, "errorType": "success", "webhookTimedOut": false }, "sessionId": "XXXXXX" }
リクエストの中身をそのまま返せていることが確認できた
リクエストの中身から必要な項目を取り出す
- 「○件読んで」の○部分を取り出したい
リクエストで来ているJSONの中身は「request」の中の「parameters」の中の「number」で取得できる
よって、以下のようにしてやれば、「○件読んで」の○が取得できるはず
@csrf_exempt def index(request): req = json.loads(request.body.decode('utf-8')) number = req['result']['parameters']['number'] gc = get_client() last_value = get_value(gc) res = { "speech": last_value[2], "displayText": number, "source": "spreadsheet memorandum" } return JsonResponse(res)
試しにDialogflowのSHOW JSONで得られたJSONをAdvanced REST client のクエリに入れて実行するとこうなる
{ speech: "test3", displayText: 3, source: "spreadsheet memorandum" }
「3」が取得できている
あとはこのnumberを使って、それだけの件数Spreadsheetのデータを読み込んで返すように修正すればいい
Entitiesを修正
現時点の課題
- 件数を直接指定する必要がある
- GoogleHome想定通りに命令を認識しれてくれ無いことがある
Intentsを上述のように追加したことで「3件読んで」「5件読んで」というように件数を数字を指定すればそれだけの件数返してもらえるようになった しかし、毎回「○件読んで」と件数を指定するのは面倒なときもある
また、GoogleHomeから呼び出すと、うまく「3件読んで」などと認識してもらえず、「三件読んで」や「三件呼んで」と認識されてしまうことがある - ※どう発声したと認識されているかはGoogleHomeアプリのアクティビティを見れば履歴がわかる
解決方法としてEntitiesに登録する
上の課題を解決するためにEntitiesを使う
Entities - https://dialogflow.com/docs/entities
Entities are powerful tools used for extracting parameter values from natural language inputs. Any important data you want to get from a user's request, will have a corresponding entity
Entitiesでは、上にあげたような、「3件読んで」「三件読んで」「三件呼んで」などの違った命令が来たときに、等しく「3」をリクエストパラメータとするように扱ってもらうことができる
DialogflowのEntitiesを開いて、「request_nums」として以下のように登録する
左側がリクエストとして送られる数字 | シノニム |
---|---|
1 | 1, メモを読んで, 1件読んで, 一件読んで, いっけんよんで | |
2 | 2, 2件読んで, 二件読んで, 二件呼んで, にけんよんで |
3 | 3, 3件読んで, 三件読んで, 三件呼んで, さんけんよんで |
3 | 3, いくつか読んで, いくつか呼んで, いくつか, いくつかよんで |
こんな感じに色々なバリエーションを設定しておく
アプリの修正
前述のnumber以外に、request_numsがあったときはこれを件数として扱ってそれだけデータを返すようにアプリを修正する
修正後 - https://github.com/ludwig125/googlehome/blob/master/memorandum/views.py
これで、件数を指定しても、「いくつか」などと言っても柔軟にデータを読み上げてもらえるようになった