【GoogleHomeでメモ帳アプリを作る】6. GoogleHomeに呼びかけてSpreadSheetのメモを読み上げてもらう

概要

前回までで、heroku上にアプリを作成するところまでできた

今度は、それをGoogleHomeから呼び出すため(正確にはactions on googleから呼び出すため)に、Dialogflowの設定をする

資料全体の構成はここに記載: GoogleHomeに話しかけてメモを記録したりメモを読み上げてもらう

参考

参考 actions on googleの公式マニュアル(Dialogflowについて)

Dialogflowの公式マニュアル

その他の参考

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を作成

dialogflow_create

User saysに「 メモを読んで 」という文言を追加する

dialogflow_intents_new

一旦これでSAVE

Fulfillment

WebhookのURLにherokuのURLを追加する

dialogflow_fulfillment

これで

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リクエストを送ると、結果が返ってきた!!

advancedrestclient_test1

これを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が返ってきた

advancedrestclient_test2

修正したアプリに対してリクエストしてみる

Dialogflowからリクエストを送る

dialogflowの画面で、「try it now」に「メモを読んで」と入れてみると、test3が返ってきた

dialogflow_intents

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」と表示された

simulator1

simulator2

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件読んで」を入れて送ってみる

dialogflow_request1

リクエストの中身を確認

今回は、herokuのview.py側で出力させるdisplayTextを以下のようにして、何が送られているか確認してみる

req = json.loads(request.body.decode('utf-8'))

res = {
    "speech": last_value[2],
    "displayText":  str(req),
    "source": "spreadsheet memorandum"
}
  • リクエストの中身はエンコードされているので、decode関数をつける必要があることに注意

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のデータを読み込んで返すように修正すればいい

dialogflow_request2

Entitiesを修正

現時点の課題

  1. 件数を直接指定する必要がある
  2. 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, いくつか読んで, いくつか呼んで, いくつか, いくつかよんで  

こんな感じに色々なバリエーションを設定しておく

dialogflow_entity

アプリの修正

前述のnumber以外に、request_numsがあったときはこれを件数として扱ってそれだけデータを返すようにアプリを修正する

修正後 - https://github.com/ludwig125/googlehome/blob/master/memorandum/views.py

これで、件数を指定しても、「いくつか」などと言っても柔軟にデータを読み上げてもらえるようになった