はじめに
本記事は、GoogleアシスタントのInteractive Canvasというフレームワークを使用してみて、
色々覚えることが多くてすぐ忘れそうになるので
記事として残しておこうと記載したものです。
想定は、Google Nest Hubで動作するゲームです。
公式ドキュメントは基本全て英語である上に、欲しい情報が全て載っていないこともあったので、実践してみないと分からないことが多くありました。
本記事は実践してみたほんの一部の内容ですが、同じように情報少ないと感じている人に役立てれば幸いです。
本記事の内容
今回はこちらの公式のサンプルを例に、説明を付加しながら記載していきます。
https://github.com/actions-on-google/actions-builder-canvas-codelab-nodejs
CodeLabも用意されていますので、興味ある方はそちらを見てやっても良いでしょう。
本記事にも一部重複している内容がございます。
https://codelabs.developers.google.com/codelabs/actions-canvas/
なお、このサンプルは2DフレームワークのPhaserが使用されています。
この部分の構築については本題から逸れますので、本記事では割愛させていただきます。
アプリの見た目と注意点
開発には、シミュレータが用意されていますので、実機のNest Hubがなくても動作確認は一応できます。
が、下の画像の通り、実機とシミュレータではだいぶ表示が異なることが分かりますので、この辺りは注意が必要です。
見た目の部分は、Firebase Hostingにデプロイする形となります。
環境構築
ここは公式に分かりやすく記載されているので、簡潔に書きます。
なお、GCPのプロジェクトの作成方法やAPIの有効化などのGCPの基礎知識に関する内容は割愛しています。
-
node
: 私の環境はv12.16.1
でおこなっています。 -
ライブラリ
:npm install @assistant/conversation
-
Firebase CLI
:npm install -g firebase-tools
-
gactions
コマンドラインツール: 下記リンクから使用しているOSのパッケージをダウンロード
Actions SDK and Builder quick start guide
以下はmacでダウンロード後に行っている処理。
tar -zxvf gactions-sdk_linux.tar.gz
cp ./aog_cli/gactions /usr/local/bin
ファイル構成
今回参考にしているのは上記の完成しているプロジェクトcomplete
というディレクトリ内にあるファイルです。(2020/10/8のソースコード)
├── firebase.json
├── package.json
├── public【Firebaseにデプロイされる部分)】
│ ├── assets
│ │ ├── 見た目に使用されている画像(詳細略)
│ ├── css
│ │ └── main.css
│ ├── index.html
│ └── js
│ ├── action.js
│ ├── log.js
│ ├── main.js
│ └── scene.js
└── sdk【gactionsでデプロイされる部分】
├── actions
│ └── actions.yaml
├── custom
│ ├── global
│ │ ├── actions.intent.CANCEL.yaml
│ │ ├── actions.intent.MAIN.yaml
│ │ ├── actions.intent.NO_INPUT_1.yaml
│ │ ├── actions.intent.NO_INPUT_2.yaml
│ │ ├── actions.intent.NO_INPUT_FINAL.yaml
│ │ ├── actions.intent.NO_MATCH_1.yaml
│ │ ├── actions.intent.NO_MATCH_2.yaml
│ │ ├── actions.intent.NO_MATCH_FINAL.yaml
│ │ └── actions.intent.PLAY_GAME.yaml
│ ├── intents
│ │ ├── guess.yaml
│ │ ├── instructions.yaml
│ │ ├── play_again.yaml
│ │ └── start_game.yaml
│ ├── scenes
│ │ ├── Game.yaml
│ │ └── Welcome.yaml
│ └── types
│ ├── letter.yaml
│ └── word.yaml
├── manifest.yaml
├── settings
│ └── settings.yaml
└── webhooks
├── ActionsOnGoogleFulfillment
│ ├── index.js
│ └── package.json
└── ActionsOnGoogleFulfillment.yaml
各種設定と説明
ここからは断片的になりますが、必要な設定をどこでどのように書けば良いかをメモ的にまとめています。
アプリの表示名と呼び名の設定
sdk/setting/setting.yaml
category: GAMES_AND_TRIVIA
defaultLocale: en
localizedSettings:
displayName: Snow Pal sample
pronunciation: Snow Pal sample
projectId: <PROJECT_ID>
usesInteractiveCanvas: true
アプリの呼び名はこちらで設定します。
projectIdには使用しているGCPのプロジェクトIDを入力して下さい。
defaultLocaleは日本語にしたいならjaでOKです。
ちなみにアプリ名が英語だと、なぜか実機から呼び出せなかったので、私は日本語にして動作確認しています。
見た目の部分(FirebaseのURL)を設定
sdk/custom/global/actions.intent.MAIN.yaml
handler:
staticPrompt:
candidates:
- promptResponse:
canvas:
url: https://PROJECT_ID.web.app
transitionToScene: Welcome
FirebaseのURLは、こちらのurlの部分に記載します。
アプリが起動すると、初めにこのMAIN.yamlに記載されている設定が実行されます。
staticPromptには色々設定できますが、ここでのポイントは、
1. canvasで指定したURLが表示され、
2. 次にWelcomeというシーンに移動する
という内容であることを押さえておけば大丈夫かと思います。
intent(インテント)とは?
intentは、アプリを声で操作する際のトリガーとなるキーワードを設定する場所、というイメージです。
インテントには、大きく次の2種類あります。
システムインテント
: 初めから定義されている標準的な非会話型のインテント。1番初めに呼び出されるMain, キャンセルした時のCancel, 入力がなかったりどのインテントにもマッチしなかったときの挙動、これらが主に定義されています。
NO_INPUTとNO_MATCHが3つずつ存在するのは、3回ダメならアプリを終了するという挙動にするためだそうです。
sdk/custom/global
にそれらのyamlファイルがあります。ユーザーインテント
: 自分で定義できるカスタムトレーニングフレーズです。詳細は次に記載します。
sdk/custom/intents
に追加していきます。
単純なインテント
例えば、タイトル画面からゲームを始めるためのキーワードを、このプロジェクトでは以下のように設定しています。
sdk/custom/intents/start_game.yaml
trainingPhrases:
- "yes"
- let's play
- okay
- let's do it
- go for it
- "y"
全部Yes的な内容ですよね。
これ、実はGoogleさんの自然言語処理の賢さが発揮出来るところでして、どういうことかというと、ここに提供するフレーズの例が多いほど、インテントが正しく一致する可能性が高くなる、と公式のヘルプに書いてあるんです。
ここに書いた文例集(?)から、言語モデルが生成されて、その言語モデルはアシスタントNLUを強化し、さらに理解する能力を高める、と公式に記載されています。
つまり、完全にこれらのフレーズに一致していなくても、似たような言葉でも反応してくれるようになるということです。
例えば、このリストにはないLet's Go
でもちゃんと認識してくれます。
複雑なインテント
インテントは変数(parameter)に置き換えることもできます。
以下のファイルは、正解の単語を当てるために設定されたインテントです。
sdk/custom/intents/guess.yaml
(一部抜粋)
parameters:
- name: letter
type:
name: letter
- name: word
type:
name: word
trainingPhrases:
- ($letter 'A' auto=true)
- letter ($letter 'Y' auto=true)
- the word is ($word 'home' auto=false)
- what about ($word 'water' auto=false)
- it's ($word 'Google' auto=false)
- I guess its ($word 'red' auto=false)
- I guess it's ($word 'house' auto=false)
- its ($letter 'B' auto=true)
- it is ($letter 'A' auto=true)
これはletter
とword
の2つのパラメータを設定しています。
typeには、そのパラメータがどのような内容かを定義されたyamlファイル名を指定しています。
typeはsdk/custom/types/
内にyamlファイルとして保存します。
letterには各種アルファベットが定義されています。
sdk/custom/types/letter.yaml
(一部抜粋)
synonym:
entities:
a:
synonyms:
- a
b:
synonyms:
- b
(中略)
"y":
synonyms:
- "y"
z:
synonyms:
- z
matchType: EXACT_MATCH
wordは自由な単語です。
sdk/custom/types/word.yaml
freeText: {}
インテントには、
yaml
- ($letter 'A' auto=true)
- it's ($word 'Google' auto=false)
フレーズ例 $変数名 値の例 auto=曖昧さを許容する(True)/しない(False)
こんな感じの組み合わせで書きます。
もしも上記を
- ($letter 'A' auto=false)
とauto=falseにしてしまうと、「A」しか受け付けてくれなくなりますが、auto=trueなら設定したtypeの候補A〜Zどれでも受け付けてくれるようになる、
とこう書くと曖昧さの許容の意味が理解できそうでしょうか。
なお、ここで設定したパラメータ名は、後に記載するWebhook(CloudFunction)で以下のように拾うことが出来ます。
app.handle('guess', (conv) => {
const word = conv.intent.params.word;
conv.add(`You Said ${word}`);
}
上記は、こちらが例えば「眠い」と喋ったら、アプリが「You Said 眠い」と答える、みたいな処理をイメージして書いています。
(実際のソースコードではありません)
良いインテントの設計には
公式の説明より、抜粋して記載しておきます。
* 会話の設計を行う
* 会話を開始するトリガー文を決める
* 会話を成立させるための条件を決める
* レスポンスを決める
* 1つの目的につき1つのIntentを作成する
* 挨拶をするIntent
* 計算結果を返すIntent
* 検索して結果を返すIntent
インテントにMatchした際の内容をWebhookで拾うには?
公式のサンプルと違う内容で申し訳ないですが、
例えば次のようなtypesの設定でnumberというパラメータを取得したいとします。
synonym:
entities:
"1":
synonyms:
- "1"
- "いち"
- "first"
- "one"
- "ワン"
"2":
synonyms:
- "2"
- "に"
- "second"
- "two"
- "ツー"
matchType: EXACT_MATCH
このtypeのインテントにマッチした際に、Webhook上で取得して何か処理したい場合はどうすれば良いでしょう?
答えは、以下のように書けばOKです。
ここで、「ツー」と受け取った時、
app.handle('hoge',(conv) => {
let original = conv.intent.params.number.original;
let resolved = conv.intent.params.number.resolved;
functions.logger.log(`original = ${original}`); // original = ツー
functions.logger.log(`resolved = ${resolved}`); // resolved = 2
}
originalとresolvedの違いが見てお分かりになるでしょうか?
上記のログはFirebaseのFunctionsのログで見ることが出来ます。
また、シミュレータでも下記のように確認ができます。
{
"interactionMatch": {
"intentId": "インテント名が表示",
"nextSceneId": "シーン名が表示",
"intentParameters": [
{
"key": "number",
"value": {
"original": "ツー",
"resolved": "2"
}
}
]
},
"responses": [
{
"canvas": {}
}
]
}
これで、聞き取った生の言葉を処理するのか、紐付けているentityを使用するのかを選んで設計出来ますね。
Scenes(シーン)とは
Scenesは、イメージするなら場面設定です。
今回参考にしているサンプルは、
* Welcome
というアプリ起動時のスタート画面の場面
* Game
というゲーム中の場面
の2つのシーンで構成されています。
昔のRPGで例えるなら、
* タイトル画面
* セーブデータを選択する場面
* フィールドを自由に歩く場面
* 敵に遭遇した時の戦闘場面
などと大まかにシーンが分けられますよね。
そしてそれぞれのシーンに対応した設定をしたいですよね。
そんなイメージがこのScenes(シーン)であると私は認識しておりますが、正しい文章で覚えたい方は公式を読んで下さいね。
Scenesの構成
シーンにはライフサイクルというものがあります。
公式の説明をコピペしても意味がないので、ざっくりイメージしやすいように記載します。
1.On enter
On Enterは、シーンが呼び出されて最初に呼び出されるフェーズ。
例えばsdk/custom/scenes/Welcome.yaml
を見てみると、最後の2行にこのように書いています。
onEnter:
webhookHandler: greeting
これは、シーン名「Welcome」に入ったら、webhookの「greeting」という関数を実行してね、という意味です。
実際にgreetingでは以下のような処理が記載されています。
(説明のために抜粋しています)
sdk/webhooks/ActionsOnGoogleFulfillment/index.js
const RETURNING_GREETINGS = [`Hey, you're back to Snow Pal!`,
`Welcome back to Snow Pal!`,
`I'm glad you're back to play!`,
`Hey there, you made it! Let's play Snow Pal`];
app.handle('greeting', (conv) => {
conv.add(`<speak>${randomArrayItem(RETURNING_GREETINGS)}</speak>`);
});
Welcomeシーンに入ると、ランダムに選択されたセリフを喋ってくれるようになっていることが分かりますね。
2.Conditions
Conditionsは、
条件が整ったのか会話として情報が足りないのかなど状況を判断するフェーズ。
このフェーズを私は利用したことがないのですが、条件によって処理を分岐したいときに使用するものだそうです。
例えば、次に説明するSlotが埋まっていればこの処理をする、というような分岐が出来ると公式には説明されています。
詳しい文法は下記リンクに記載されています。
3.Slot filling
Slot fillingは、ユーザから情報収集する場面で使用します。
このフェーズも私は利用したことないのですが、例えば都道府県・名前・年齢を聞くシーンであれば、その3つの情報を聞いたかどうかといった判定に使えるようです。
設定できる項目を見ると、以下のようになっています。
* Slot name
: WebhookロジックおよびConditionsで使用される一意のスロット名。
* Type
: Slotの値に使用する定義
* This slot is required
: Slotを必須にするかどうか
* Assign default value to this slot
: デフォルト値を設定するかどうか
* Customize slot value writeback
:スロットがいっぱいになると、セッションパラメータに書き戻される値がカスタマイズされる
app.handle('hoge', conv => {
// 全てのスロットに値が入ると"FINAL"になる
conv.scene.slotFillingStatus
// 各スロットの値にアクセスするには
conv.scene.slots['slot_name'].<property_name>
});
4.Promts
Promtsは、ユーザへの応答。
文字や画像、カードや音声など様々なコンテンツを設定できます。
例えば単純な音声の応答(Simple responses)を書いている部分。
参考のプロジェクトでは、初めのシーンWelcome
で以下のように設定されています。
- handler:
staticPrompt:
candidates:
- promptResponse:
canvas:
sendStateDataToCanvasApp: true
firstSimple:
variants:
- speech: Try guessing a letter in the word, or guess the entire word
if you think you know what it is.
- speech: Try guessing a letter in the word, or guess the entire word
if you're feeling confident!
- speech: Try guessing a letter in the word or guessing the word.
見るところはfirstSimple
のspeech
です。
この場合、設定された3つの文章のうち、1つがランダムに選択されて応答される、という動作になります。
Rich responses
やVisual selection responses
なら、カードや画像イメージ、テーブルの表示が可能です。
今回のサンプルには使用されていませんが、実際にカード形式とリスト形式を用いた画面は次のようになります。
onEnter:
staticPrompt:
candidates:
- promptResponse:
content:
card:
image:
url: https://{image's URL}.png
subtitle: SubTitle
text: This is the Demo Application
title: Demo Application
下の画像の通り、こちらも実機とシミュレータでは表示や色合いが異なります。。
onEnter:
staticPrompt:
candidates:
- promptResponse:
content:
list:
items:
- description: 1問目
image:
url: https://{image's URL}/sample01.png
title: "1"
- description: 2問目
image:
url: https://{image's URL}/sample02.png
title: "2"
- description: 3問目
image:
url: https://{image's URL}/sample03.png
title: "3"
- description: 4問目
image:
url: https://{image's URL}/sample04.png
title: "4"
title: Quiz
firstSimple:
variants:
- speech: クイズを選択してください。
suggestions:
- title: "1"
- title: "2"
- title: "3"
- title: "4"
こちらも実機とシミュレータで表示が異なります。
解像度問題はともかく、画像の位置くらいシミュレータも同じように表示してくれても良いのに...と思うのは私だけでしょうか?
5.Input
最後にInput
ユーザが何かしら声や画面へのタッチで入力するフェーズです。
ここで各Intentに合致したのか、Slotに合致したのか、そもそも入力がない場合など、様々な入力によって動作が分岐するフェーズですね。
シーンを遷移するには
シーンを遷移する方法は、大きく2つに分かれます。
1. yamlファイルで設定
2. Webhookの中に記載
例えば、SecondSceneという名前のシーンに遷移する場合、
yamlに書く場合は、次のように記載します。
transitionToScene: SecondScene
Webhookに記載する場合は、例えばhogeというhandler内に次のように書きます。
app.handle('hoge', (conv) => {
conv.scene.next.name = 'SecondScene';
}
WebHookを増やしていく上で注意点
色々Webhookを増やしたときに躓きやすい点と感じた点です。
それは、増やしたらそのWebhook名を下記のファイルに追記して置かなければ動作しないという点です。
動作しないというより、Deploy出来ません。
sdk/webhooks/ActionsOnGoogleFulfillment.yaml
handlers:
- name: hoge
- name: fuga
inlineCloudFunction:
executeFunction: ActionsOnGoogleFulfillment
このように、handlersにname: {Webhook名}を追加しておく必要があります。
結構忘れそうになる部分なので、1つ増やすたびに追記しておくのが望ましいでしょう。
おわりに
ざっと粗削りでまとめてしまいましたが、以上が簡単ではありますがGoogleアシスタントでアプリを作る上での基本的な知識でした。
公式に色々書いてあっても、実機とシミュレータの見た目の違いまでは記載されていないので、知りたい方には少しでもお役に立てたのではないかと思います。
さらに詳しく良い情報をご存じの方がいらっしゃいましたら、コメント等いただけるとありがたいです。