Fablog

社会人マイナス1年生のブログ / プログラミング / 料理 / ビットコイン

GoogleAssistantアプリを作るチュートリアル的なものを作りたかった

概要

GoogleAssistantアプリの開発プラットフォームがついに日本語にも対応しましたね

developers.googleblog.com

せっかくなので簡単なアプリをリリースしました

そしてその作ったアプリの過程をまとめて、GoogleAssistantアプリを作るチュートリアルを作りました。。。。

と言いたかったのですがチュートリアルとしては微妙なものができてしまいました

しっかりした解説はないけど、GoogleAssistantアプリを作ったことないけど作りたい人には役立つと思うのでよかったら読んでください

Projectの作成

Actions on Google の Console に行って add Project をします

https://console.actions.google.com/

するとProjectNameとCountryを選ぶ箇所があるので選んでCreateProjectを押してください

するとこんな感じで API.AI を使うか Actions SDK を使うかみたいに選ぶ画面になるので選んでください 今回はAPI.AIを使って行くのでAPI.AIを選んでください

ここで選んだプラットフォームは取り消せないので、間違えて選んでしまった人は新しくプロジェクトを作り直してください

API.AIを選ぶとこんな感じでAPI.AIのプロジェクトを作れよと言われるので作ります

API.AIの画面に行くとdescription, language, timezone などを設定する場所があります。Alarmとかみたいなものを選択できる箇所もあるのですが、日本語は対応していないので、languageで日本語を選択するとその箇所は消えるので埋めなくても大丈夫です。

選択して保存するとこんな感じになります

DefaultIntent

まずは最初に呼ばれるDefaultWelcomeIntentというトリガー的なものを設定します 左のIntentsを選んだあとに default welcome intent を選んでください

その後、GoogleAssistantで起動された時にこのdefault welcome intent が起動するように、Eventsのところに Google_ASSISTANT_WELCOME を入力してください

次に受け答えを設定します UserSaysのところに「こんにちは」 textResponseのところに「こんにちは!フリーブックスです!」 と入力してください

ここで、一度動くかテストをしてみましょう 画面の右端にある Try it now に「こんにちは」と入力してみてください

すると先ほど入力しように「こんにちは!フリーブックスです!」とのレスポンスが返ってきます。

このように、API.AIではユーザーの入力とそのレスポンスを事前に定義しておくことでBotを作ることができます。

しかし、この侭では事前にレスポンスを全て用意しておかなければならず大変です。 そこでAPI.AIのwebhook機能を使います。

CloudFunction の利用

ここで一旦API.AIはおいて、cloudfunctionに移ります

Actions on Google を参考にして ApiAiApp を作って行きます

Build Fulfillment  |  Actions on Google  |  Google Developers

fablog.hatenadiary.com

コードを貼るので詳しい説明は省きますが、基本的にやっていることは

  • appを引数に取るIntentメソッドを作る
  • Intentメソッドの中でいい感じにレスポンス文言を返すようにする
  • Actionと呼ばれる、後々API.AIで定義する文字列とIntentメソッドを関連づける

というだけです。 GoogleHome とそれ以外の端末でレスポンスを分けたりとか、カルーセルの使い方とかを知りたいって方はここを読んでください

fablog.hatenadiary.com

'use strict';
/*
* Modules
*/
const App = require('actions-on-google').ApiAiApp;
const functions = require('firebase-functions');
const request = require('request');
const cheerio = require('cheerio')

/*
* Constants
* 定数系
*/
const BASE_URL = 'https://www.amazon.co.jp/';
const Status = {
welcome: 0
, showBooks: 1
, selectedBook: 2
}

const Actions = {
WELCOME: 'welcome',
SHOW_BOOKS: 'show.books',
CONFIRM: 'confirm',
SELECTED_BOOK: 'selected.book',
}

let options = {
method: 'GET',
url: BASE_URL,
encoding: 'UTF-8',
headers: {
'Content-Type': 'text/plain;charset=utf-8',
'User-Agent': 'Mozilla/5.0(Linux;Android6.0;Nexus5Build/MRA58N)AppleWebKit/537.36(KHTML,likeGecko)Chrome/61.0.3163.100MobileSafari/537.36'
}
}

const books = [{ url: 'https://www.amazon.co.jp//gp/aw/d/B00BB1ZSJA?ref_=msw_best_freehome_0&storeType=ebooks',
title: 'レッツ☆ラグーン(1) (ヤングマガジンコミックス)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/51tbQJsZxLL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B075NKCVKD?ref_=msw_best_freehome_1&storeType=ebooks',
title: 'ゴールデンカムイ【期間限定無料】 1 (ヤングジャンプコミックスDIGITAL)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/618tG-9ZD0L._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B00BML5V6E?ref_=msw_best_freehome_2&storeType=ebooks',
title: '亜人(1) (アフタヌーンコミックス)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/51VKi59sPPL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B075NL61FD?ref_=msw_best_freehome_3&storeType=ebooks',
title: 'ゴールデンカムイ【期間限定無料】 2 (ヤングジャンプコミックスDIGITAL)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/61mxKIeQYDL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B075TY9SD3?ref_=msw_best_freehome_4&storeType=ebooks',
title: 'アヤメくんののんびり肉食日誌(1)【期間限定 無料お試し版】 (FEEL COMICS)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/513a5UTtm9L._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B00KCL6Y2U?ref_=msw_best_freehome_5&storeType=ebooks',
title: 'いぬやしき(1) (イブニングコミックス)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/51YEu609hIL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B075J8HKBZ?ref_=msw_best_freehome_6&storeType=ebooks',
title: 'うわばみ彼女【期間限定無料版】 1 (ジェッツコミックス)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/51S8gTApfPL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B00UN48X98?ref_=msw_best_freehome_7&storeType=ebooks',
title: '本当にあった笑える話【無料連載版】',
image: 'https://images-fe.ssl-images-amazon.com/images/I/61XtF2XgZxL._BG0,0,0,0_FMpng_SX300_.jpg' },
{ url: 'https://www.amazon.co.jp//gp/aw/d/B00DW4ZYBG?ref_=msw_best_freehome_8&storeType=ebooks',
title: '宝石の国(1) (アフタヌーンコミックス)',
image: 'https://images-fe.ssl-images-amazon.com/images/I/61FXZ-+RZLL._BG0,0,0,0_FMpng_SX300_.jpg' }]

/*
* Model
* sessionの間値を維持しておくための仕組み
*/
function initData(app) {
let data = app.data;
if(!data.books) {
data.books = {
data: [],
page: 0
}
}

if(!data.status) {
data.status = Status.welcome
}

return data;
};


/*
* Intents
* API.AI 呼び出されるIntent
*/
function showBooksIntent(app) {
console.log('called showBooksIntent action')
initData(app)
app.data.status = Status.showBooks
if (app.hasSurfaceCapability(app.SurfaceCapabilities.SCREEN_OUTPUT)) {
console.log('*** has screen ***')
let response = '本日のおすすめの無料コミックはこちらです。'
const items = books.map((book) => {
const item = app.buildOptionItem(book.title, book.title)
item.setTitle(book.title)
item.setImage(book.image, book.title)
return item
})

const carousel = app.buildCarousel().addItems(items)
app.data.books.data = books
app.askWithCarousel(response, carousel)
return

} else {
console.log('*** do not has screen ***')
const offset = i * 3
if(offset + 3 > books.length) return app.tell('申し訳ございません。おすすめコミックが見つかりませんでした。')
let response = '本日無料のおすすめコミックは「' + books[offset] + '」「' + books[offset + 1] + '」「' + books[offset+ 2] + '」'
response += '他のコミックを探しますか?'
app.ask(response);
return
}
}

function confirmIntent(app) {
console.log('called showMoreIntent action')
initData(app)

if(app.data.status == Status.showBooks) {
app.data.books.page += 1
showBooksIntent(app)
return
} else {
app.tell('申し訳ございません。よくわかりませんでした。');
}
}

function selectedBookIntent(app) {
console.log('called selectedBookIntent action')
initData(app)
app.data.status = Status.SELECTED_BOOK

const book = app.data.books.data.find(book => {
return book.title == app.getSelectedOption()
})

if(!book) return app.tell('申し訳ございません。通信に失敗致しました。しばらくしてからもう一度御利用ください。');

const response = app.buildRichResponse()
response.addSimpleResponse(book.title + 'ですね')
const card = app.buildBasicCard(book.title)
.setTitle(book.title)
.setImage(book.image, book.title)
.addButton('コミックを入手する', book.url)

response.addBasicCard(card)
app.tell(response)
}

/*
* Mapping
* 実際に cloud functions に公開するメソッド
* API.AIからwebhookでこのメソッドが毎回呼ばれる
* Actions on Google SDK を使ってactions名とIntentメソッドの紐付けを行う
*/
const Functions = functions.https.onRequest((request, response) => {
const app = new App({request, response});
console.log('Request headers: ' + JSON.stringify(request.headers));
console.log('Request body: ' + JSON.stringify(request.body));

const actionMap = new Map();
actionMap.set(Actions.WELCOME, showBooksIntent);
actionMap.set(Actions.SHOW_BOOKS, showBooksIntent);
actionMap.set(Actions.CONFIRM, confirmIntent)
actionMap.set(Actions.SELECTED_BOOK, selectedBookIntent)
app.handleRequest(actionMap);
})

module.exports = {
Functions
}

デプロイとAPI.AIとの紐付け

ここを参照するといい https://developers.google.com/actions/apiai/deploy-fulfillment

以下のことをやる

$ npm install -g firebase-tools
$ firebase login
$ firebase init
$ npm install
$ firebase deploy --only functions

deployが終わると console に

Function URL (Functions): https://hogehogehogehoge

みたいのが出てくるのでこれをコピーしておく

API.AIに戻り、 左端のFulfillmentをクリックしてDisabledになっている項目をEnabledにする そうするとwebhookを設定できるのでURLに先ほどの FUNCTION URL を入力する

Actionの登録

先ほどコード上でこのように定義したActionを実際にAPI.AIにも定義して、API.AIとコードを紐づける

const Actions = {
WELCOME: 'welcome',
SHOW_BOOKS: 'show.books',
CONFIRM: 'confirm',
SELECTED_BOOK: 'selected.book',
}

API.AI左端の intents の+ボタンを押して、intentを新規作成 その後はuser says, action, を設定する

user says には 「おすすめのコミックを教えて」 action には selected.book を記入した

スクリーンショット 2017-10-08 23.05.09.png

また、一番下に fulfillment -> use webhook というチェックボックスがあるのでチェックを入れる

これでIntentの定義完了

おなじようにSHOW_BOOKSのintentも作る さっきと同じように設定して行くのだが、唯一違うのはEventsにactions_intent_OPTIONを設定する点 これをするとリストなどの要素から選択されたときにそのインテントが呼ばれるようになる これが設定し終わったら完成

テスト

左の integrations -> google assistant を選択し、upload draft をする

スクリーンショット 2017-10-08 23.07.27.png

そうすると actions on google の console に飛ばされて、アプリの情報の入力を迫られる

とりあえず全部埋めておく

infomationの入力が終わったら左のタブのsimulatorという項目を選ぶ

あとは試す

こんなかんじになる 

https://photos.app.goo.gl/w4ckVZVEH00cXRaO2

Google Play とかと違ってデベロッパーアカウントを購入する必要はなさそう とりあえずできたぜ。リリースもしてみたぜ。

審査通るといいな