1. 簡介
你正在看電視,但找不到遙控器,或是不想一一造訪各電視頻道,查明電視是否有比較好的內容?告訴 Google 助理電視有什麼內容!在這個研究室中,您將使用 Dialogflow 建構簡單的動作,並瞭解如何與 Google 助理整合。
這些練習經過排序,以反映常見的雲端開發人員體驗:
- 建立 Dialogflow v2 虛擬服務專員
- 建立自訂實體
- 建立意圖
- 使用 Firebase 函式設定 Webhook
- 測試聊天機器人
- 啟用 Google 助理整合功能
建構目標
我們會為 Google 助理打造互動式電視節目表聊天機器人代理程式。你可以詢問電視節目表,瞭解特定頻道正在播放的內容。例如:「MTV 上有什麼節目?」「電視節目指南」會顯示目前正在播放的內容以及即將播放的內容。 |
課程內容
- 如何使用 Dialogflow v2 建立聊天機器人
- 如何透過 Dialogflow 建立自訂實體
- 如何使用 Dialogflow 建立線性對話
- 如何使用 Dialogflow 和 Firebase 函式設定 Webhook 執行要求
- 如何透過 Actions on Google 將應用程式與 Google 助理整合
必要條件
- 您需要有 Google 身分 / Gmail 地址,才能建立 Dialogflow 虛擬服務專員。
- 您不一定要具備 JavaScript 的基本知識,但這有助於您變更 Webhook 執行要求程式碼。
2. 開始設定
在瀏覽器中啟用網路活動
- 確認應用程式活動已啟用:
建立 Dialogflow 虛擬服務專員
- 在左側列的標誌下方,選取 [Create New Agent] (建立新代理程式)。如果您有現有服務專員,請先點選下拉式選單。
- 指定代理程式名稱:
your-name-tvguide
(使用自己的名稱)
- 預設語言為 [英文 - en]。
- 將時區設為預設時區,請選擇離你最近的時區。
- 按一下 [建立]
設定 Dialogflow
- 在左側選單中,按一下專案名稱旁邊的「齒輪」圖示。
- 輸入下列代理程式說明:My TV Guide
- 向下捲動至「Log Settings」(記錄設定),然後左右切換兩個開關,以記錄 Dialogflow 互動,並在 Google Cloud Stackdriver 中記錄所有互動。稍後需要輸入這個 ID,以便對動作進行偵錯。
- 點選「儲存」。
設定 Actions on Google
- 在右側面板中點選「瞭解 Google 助理的運作方式」中的「Google 助理」連結。
隨即開啟:http://console.actions.google.com。
如果您是第一次使用 Actions on Google,請先填妥這份表單:
- 按一下專案名稱,嘗試在模擬器中開啟動作**。**
- 在選單列中選取「測試」
- 確認模擬工具已設為「English」,然後按一下「Talk to my test-app」
這項操作將以基本 Dialogflow 預設意圖向您問候。也就是說,與 Action on Google 整合的設定可順利運作!
3. 自訂實體
實體是指應用程式或裝置對其執行動作的物件。您可將其視為參數 / 變數。在電視節目表中,我們會詢問:「MTV 上有什麼節目」。MTV 是實體和變數。我也可以要求我顯示其他頻道,例如:「國家地理」或「喜劇中心」收集的實體將做為我向 TV Guide API Web-service 要求的參數。
如要進一步瞭解 Dialogflow 實體,請參閱本文。
建立 Channel 實體
- 在 Dialogflow 控制台中,按一下選單項目:[Entities] (實體)
- 按一下「建立實體」。
- 實體名稱:
channel
(必須全部為小寫) - 輸入頻道名稱。(有些頻道需要一個同義詞,以便 Google 助理理解其他內容)。您可以在輸入時使用 Tab 鍵和 Enter 鍵,輸入頻道編號做為參照值。頻道名稱會用同義詞,例如:
1 - 1, Net 1, Net Station 1
5**.**按一下藍色儲存按鈕旁的選單按鈕,切換至「原始編輯」模式。
- 複製和以 CSV 格式貼上其他實體:
"2","2","Net 2, Net Station 2"
"3","3","Net 3, Net Station 3"
"4","4","RTL 4"
"5","5","Movie Channel"
"6","6","Sports Channel"
"7","7","Comedy Central"
"8","8","Cartoon Network"
"9","9","National Geographic"
"10","10","MTV"
- 按一下「儲存」
4. 意圖
Dialogflow 使用意圖來分類使用者的意圖。意圖包含訓練詞組,這是使用者可能會向代理程式說的內容範例。舉例來說,如果使用者想知道電視上有什麼,就可以在詢問「今天有什麼電視節目?」、「現在播的是什麼?」,或是直接說「tvguide」。
當使用者撰寫或說出特定內容時 (稱為「使用者運算式」),Dialogflow 會將使用者表達內容與代理程式中最相符的意圖進行比對。比對意圖的作業也稱為「意圖分類」。
修改預設歡迎意圖
建立新的 Dialogflow 代理程式時,系統會自動建立兩個預設意圖。預設歡迎意圖是您與代理程式開始對話時進入的第一個流程。「預設備用意圖」是指在代理程式無法理解您,或無法將意圖與您剛剛說的內容配對時,您所使用的流程。
- 點選「Default Welcome Intent」(預設的歡迎意圖)
如果是 Google 助理,系統會自動以預設歡迎意圖自動啟動。這是因為 Dialogflow 正在監聽歡迎事件。不過,您也可以說出輸入的其中一個訓練詞組來叫用意圖。
以下是預設歡迎意圖的歡迎訊息:
使用者 | 代理程式 |
「Ok Google,說出你的名稱-tvguide。」 | 「歡迎,我是電視節目表服務專員。我可以告訴你目前正在電視頻道播放的內容。例如,你可以問我「有什麼 MTV 的內容」。 |
- 向下捲動至「回應」。
- 清除所有簡訊回應。
- 建立一個新的文字回應,其中包含以下問候語:
Welcome, I am the TV Guide agent. I can tell you what's currently playing on a TV channel. For example, you can ask me: What's on MTV?
- 點選「儲存」。
建立臨時測試意圖
為進行測試,我們會建立臨時測試意圖,以便稍後測試 Webhook。
- 再次按一下「Intents」(意圖) 選單項目。
- 點選「建立意圖」。
- 輸入意圖名稱:
Test Intent
(請務必使用大寫 T 和大寫 I.- 如果拼寫出不同的意圖,後端服務將無法運作!)
- 按一下「新增訓練詞組」。
Test my agent
Test intent
- 按一下「執行要求」>「執行要求」啟用執行要求
這次,我們不會將回應寫死。回應將來自 Cloud 函式!
- 翻轉「Enable Webhook call for this intent」切換鈕。
- 按一下「儲存」
建立頻道意圖
Channel Intent 會包含這段對話的部分:
使用者 | 代理程式 |
「What's on Comedy Central?」 | 「"目前正在播放下午 6 點的喜劇中心,The Simpsons 正在播放。下午 7 點後,《Family Guy》將開始。" |
- 再次按一下「Intents」(意圖) 選單項目。
- 點選「建立意圖」。
- 輸入意圖名稱:
Channel Intent
(請務必使用大寫 T 和大寫 I。- 如果拼寫出不同的意圖,後端服務將無法運作!) - 按一下 [新增訓練詞組],並新增下列內容:
What's on MTV?
What's playing on Comedy Central?
What show will start at 8 PM on National Geographic?
What is currently on TV?
What is airing now.
Anything airing on Net Station 1 right now?
What can I watch at 7 PM?
What's on channel MTV?
What's on TV?
Please give me the tv guide.
Tell me what is on television.
What's on Comedy Central from 10 AM?
What will be on tv at noon?
Anything on National Geographic?
TV Guide
- 向下捲動至「動作和參數」。
請注意,@channel 和@sys.time 實體。稍後在 Webhook 中,參數名稱和參數值將會傳送至您的網路服務。例如:
channel=8
time=2020-01-29T19:00:00+01:00
- 將 channel 標示為 quired
與電視節目指南服務專員對話時,一律需要填入版位參數名稱 channel。如果對話開始時沒有提及頻道名稱,Dialogflow 會進一步詢問,直到所有參數版位都填完為止。
請在提示訊息中輸入:
For which TV channel do you want to hear the tv guide information?
In which TV channel are you interested?
- 請勿視需要設定時間參數。
時間是選填資訊。如未指定時間,網路服務會傳回目前時間。
- 按一下「執行要求」。
這次,我們不會將回應寫死。回應將來自 Cloud 函式!因此,請翻轉「Enable Webhook call for this intent」切換鈕。
- 按一下「儲存」
5. Webhook 執行要求
如果您的代理程式需要更多靜態意圖回應,則您必須使用執行要求,將網路服務連結至代理程式。連結網路服務後,即可根據使用者表達內容執行動作,並將動態回應傳回給使用者。舉例來說,如果使用者想收到 MTV 的電視節目時間表,您的網路服務可以檢查資料庫,並回覆使用者「MTV 的節目」時間表。
- 按一下主選單中的「Fulfillment」。
- 啟用「Inline Editor」(內嵌編輯器) 切換按鈕
如要進行簡單的 Webhook 測試和實作,您可以使用內嵌編輯器。您使用無伺服器 Cloud Functions for Firebase。
- 按一下編輯器中的 index.js 分頁標籤,然後複製以下這一段適用於 Node.js 程式碼的 JavaScript:
'use strict';
process.env.DEBUG = 'dialogflow:debug';
const {
dialogflow,
BasicCard,
Button,
Image,
List
} = require('actions-on-google');
const functions = require('firebase-functions');
const moment = require('moment');
const TVGUIDE_WEBSERVICE = 'https://tvguide-e4s5ds5dsa-ew.a.run.app/channel';
const { WebhookClient } = require('dialogflow-fulfillment');
var spokenText = '';
var results = null;
/* When the Test Intent gets invoked. */
function testHandler(agent) {
let spokenText = 'This is a test message, when you see this, it means your webhook fulfillment worked!';
if (agent.requestSource === agent.ACTIONS_ON_GOOGLE) {
let conv = agent.conv();
conv.ask(spokenText);
conv.ask(new BasicCard({
title: `Test Message`,
subTitle: `Dialogflow Test`,
image: new Image({
url: 'https://dummyimage.com/600x400/000/fff',
alt: 'Image alternate text',
}),
text: spokenText,
buttons: new Button({
title: 'This is a button',
url: 'https://assistant.google.com/',
}),
}));
// Add Actions on Google library responses to your agent's response
agent.add(conv);
} else {
agent.add(spokenText);
}
}
/* When the Channel Intent gets invoked. */
function channelHandler(agent) {
var jsonResponse = `{"ID":10,"Listings":[{"Title":"Catfish Marathon","Date":"2018-07-13","Time":"11:00:00"},{"Title":"Videoclips","Date":"2018-07-13","Time":"12:00:00"},{"Title":"Pimp my ride","Date":"2018-07-13","Time":"12:30:00"},{"Title":"Jersey Shore","Date":"2018-07-13","Time":"13:00:00"},{"Title":"Jersey Shore","Date":"2018-07-13","Time":"13:30:00"},{"Title":"Daria","Date":"2018-07-13","Time":"13:45:00"},{"Title":"The Real World","Date":"2018-07-13","Time":"14:00:00"},{"Title":"The Osbournes","Date":"2018-07-13","Time":"15:00:00"},{"Title":"Teenwolf","Date":"2018-07-13","Time":"16:00:00"},{"Title":"MTV Unplugged","Date":"2018-07-13","Time":"16:30:00"},{"Title":"Rupauls Drag Race","Date":"2018-07-13","Time":"17:30:00"},{"Title":"Ridiculousness","Date":"2018-07-13","Time":"18:00:00"},{"Title":"Punk'd","Date":"2018-07-13","Time":"19:00:00"},{"Title":"Jersey Shore","Date":"2018-07-13","Time":"20:00:00"},{"Title":"MTV Awards","Date":"2018-07-13","Time":"20:30:00"},{"Title":"Beavis & Butthead","Date":"2018-07-13","Time":"22:00:00"}],"Name":"MTV"}`;
var results = JSON.parse(jsonResponse);
var listItems = {};
spokenText = getSpeech(results);
for (var i = 0; i < results['Listings'].length; i++) {
listItems[`SELECT_${i}`] = {
title: `${getSpokenTime(results['Listings'][i]['Time'])} - ${results['Listings'][i]['Title']}`,
description: `Channel: ${results['Name']}`
}
}
if (agent.requestSource === agent.ACTIONS_ON_GOOGLE) {
let conv = agent.conv();
conv.ask(spokenText);
conv.ask(new List({
title: 'TV Guide',
items: listItems
}));
// Add Actions on Google library responses to your agent's response
agent.add(conv);
} else {
agent.add(spokenText);
}
}
/**
* Return a text string to be spoken out by the Google Assistant
* @param {object} JSON tv results
*/
var getSpeech = function(tvresults) {
let s = "";
if(tvresults['Listings'][0]) {
let channelName = tvresults['Name'];
let currentlyPlayingTime = getSpokenTime(tvresults['Listings'][0]['Time']);
let laterPlayingTime = getSpokenTime(tvresults['Listings'][1]['Time']);
s = `On ${channelName} from ${currentlyPlayingTime}, ${tvresults['Listings'][0]['Title']} is playing.
Afterwards at ${laterPlayingTime}, ${tvresults['Listings'][1]['Title']} will start.`
}
return s;
}
/**
* Return a natural spoken time
* @param {string} time in 'HH:mm:ss' format
* @returns {string} spoken time (like 8 30 pm i.s.o. 20:00:00)
*/
var getSpokenTime = function(time){
let datetime = moment(time, 'HH:mm:ss');
let min = moment(datetime).format('m');
let hour = moment(datetime).format('h');
let partOfTheDay = moment(datetime).format('a');
if (min == '0') {
min = '';
}
return `${hour} ${min} ${partOfTheDay}`;
};
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
var agent = new WebhookClient({ request, response });
console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
let channelInput = request.body.queryResult.parameters.channel;
let requestedTime = request.body.queryResult.parameters.time;
let url = `${TVGUIDE_WEBSERVICE}/${channelInput}`;
var intentMap = new Map();
intentMap.set('Test Intent', testHandler);
intentMap.set('Channel Intent', channelHandler);
agent.handleRequest(intentMap);
});
- 按一下編輯器中的 package.json 分頁標籤,然後複製這段 JSON 程式碼,即可匯入所有 Node.js 套件管理工具 (NPM) 程式庫:
{
"name": "tvGuideFulfillment",
"description": "Requesting TV Guide information from a web service.",
"version": "1.0.0",
"private": true,
"license": "Apache Version 2.0",
"author": "Google Inc.",
"engines": {
"node": "8"
},
"scripts": {
"start": "firebase serve --only functions:dialogflowFirebaseFulfillment",
"deploy": "firebase deploy --only functions:dialogflowFirebaseFulfillment"
},
"dependencies": {
"actions-on-google": "^2.2.0",
"firebase-admin": "^5.13.1",
"firebase-functions": "^2.0.2",
"request": "^2.85.0",
"request-promise": "^4.2.5",
"moment" : "^2.24.0",
"dialogflow-fulfillment": "^0.6.1"
}
}
- 按一下「Deploy」按鈕。請稍候,系統正在部署無伺服器函式。系統會在畫面底部顯示一個彈出式視窗,說明你的狀態。
- 請測試 Webhook,看看程式碼是否能正常運作。在右側的模擬工具中輸入:
Test my agent.
如果一切正確無誤,畫面上應會顯示:「這是測試訊息」。
- 現在我們來測試頻道意圖,接下來請思考以下問題:
What's on MTV?
如果一切正確無誤,您應該會看到:
「在 MTV 下午 4 點 30 點開始,MTV Unplugged 正在播放。下午 5 點後,Rupauls 拖曳競速賽即將開始。」
選用步驟 - Firebase
使用不同的頻道進行測試時,會發現電視結果相同。這是因為 Cloud 函式尚未從實際的網路伺服器擷取。
為此,我們需要建立「傳出網路連線」。
如果您想使用網路服務測試這個應用程式,請將 Firebase 方案升級至 Blaze。注意:這些步驟是選擇性步驟。你也可以前往這個研究室的後續步驟,繼續在 Actions on Google 中測試應用程式。
- 前往 Firebase 控制台:https://console.firebase.google.com
- 按下畫面底部的「升級」按鈕
在彈出式視窗中選取「Blaze」方案。
- 我們已確認 Webhook 運作正常,現在可以繼續並將
index.js
的程式碼替換為以下程式碼。這將確保您可以向網路服務索取電視節目表資訊:
'use strict';
process.env.DEBUG = 'dialogflow:debug';
const {
dialogflow,
BasicCard,
Button,
Image,
List
} = require('actions-on-google');
const functions = require('firebase-functions');
const moment = require('moment');
const { WebhookClient } = require('dialogflow-fulfillment');
const rp = require('request-promise');
const TVGUIDE_WEBSERVICE = 'https://tvguide-e4s5ds5dsa-ew.a.run.app/channel';
var spokenText = '';
var results = null;
/* When the Test Intent gets invoked. */
function testHandler(agent) {
let spokenText = 'This is a test message, when you see this, it means your webhook fulfillment worked!';
if (agent.requestSource === agent.ACTIONS_ON_GOOGLE) {
let conv = agent.conv();
conv.ask(spokenText);
conv.ask(new BasicCard({
title: `Test Message`,
subTitle: `Dialogflow Test`,
image: new Image({
url: 'https://dummyimage.com/600x400/000/fff',
alt: 'Image alternate text',
}),
text: spokenText,
buttons: new Button({
title: 'This is a button',
url: 'https://assistant.google.com/',
}),
}));
// Add Actions on Google library responses to your agent's response
agent.add(conv);
} else {
agent.add(spokenText);
}
}
/* When the Channel Intent gets invoked. */
function channelHandler(agent) {
var listItems = {};
spokenText = getSpeech(results);
for (var i = 0; i < results['Listings'].length; i++) {
listItems[`SELECT_${i}`] = {
title: `${getSpokenTime(results['Listings'][i]['Time'])} - ${results['Listings'][i]['Title']}`,
description: `Channel: ${results['Name']}`
}
}
if (agent.requestSource === agent.ACTIONS_ON_GOOGLE) {
let conv = agent.conv();
conv.ask(spokenText);
conv.ask(new List({
title: 'TV Guide',
items: listItems
}));
// Add Actions on Google library responses to your agent's response
agent.add(conv);
} else {
agent.add(spokenText);
}
}
/**
* Return a text string to be spoken out by the Google Assistant
* @param {object} JSON tv results
*/
var getSpeech = function(tvresults) {
let s = "";
if(tvresults && tvresults['Listings'][0]) {
let channelName = tvresults['Name'];
let currentlyPlayingTime = getSpokenTime(tvresults['Listings'][0]['Time']);
let laterPlayingTime = getSpokenTime(tvresults['Listings'][1]['Time']);
s = `On ${channelName} from ${currentlyPlayingTime}, ${tvresults['Listings'][0]['Title']} is playing.
Afterwards at ${laterPlayingTime}, ${tvresults['Listings'][1]['Title']} will start.`
}
return s;
}
/**
* Return a natural spoken time
* @param {string} time in 'HH:mm:ss' format
* @returns {string} spoken time (like 8 30 pm i.s.o. 20:00:00)
*/
var getSpokenTime = function(time){
let datetime = moment(time, 'HH:mm:ss');
let min = moment(datetime).format('m');
let hour = moment(datetime).format('h');
let partOfTheDay = moment(datetime).format('a');
if (min == '0') {
min = '';
}
return `${hour} ${min} ${partOfTheDay}`;
};
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
var agent = new WebhookClient({ request, response });
console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
let channelInput = request.body.queryResult.parameters.channel;
let requestedTime = request.body.queryResult.parameters.time;
let url = `${TVGUIDE_WEBSERVICE}/${channelInput}`;
if (requestedTime) {
console.log(requestedTime);
let offsetMin = moment().utcOffset(requestedTime)._offset;
console.log(offsetMin);
let time = moment(requestedTime).utc().add(offsetMin,'m').format('HH:mm:ss');
url = `${TVGUIDE_WEBSERVICE}/${channelInput}/${time}`;
}
console.log(url);
var options = {
uri: encodeURI(url),
json: true
};
// request promise calls an URL and returns the JSON response.
rp(options)
.then(function(tvresults) {
console.log(tvresults);
// the JSON response, will need to be formatted in 'spoken' text strings.
spokenText = getSpeech(tvresults);
results = tvresults;
})
.catch(function (err) {
console.error(err);
})
.finally(function(){
// kick start the Dialogflow app
// based on an intent match, execute
var intentMap = new Map();
intentMap.set('Test Intent', testHandler);
intentMap.set('Channel Intent', channelHandler);
agent.handleRequest(intentMap);
});
});
6. Actions on Google
Actions on Google 是 Google 助理的開發平台,Gemini 可讓第三方開發「動作」,也就是為 Google 助理提供擴充功能的小工具。
您必須叫用 Google 動作,方法是要求 Google 開啟或對應用程式說話。
這項操作會開啟動作、變更語音,選擇保留「母語」Google 助理範圍。換句話說,您在此階段要求服務專員的所有資訊都必須由您建立。您無法突然要求 Google 助理提供 Google 天氣資訊 (如果這是自己想要的)您應先保留 (關閉) 動作範圍 (應用程式)。
在 Google 助理模擬工具中測試動作
我們來測試下列對話:
使用者 | Google 助理 |
「Ok Google,下達 your-name-tv-guide 的人。」 | 「沒問題,讓我來叫 your-name-tv-guide。」 |
使用者 | 您的姓名-TV-Guide 服務專員 |
- | 「歡迎,我是電視指南...」 |
測試我的虛擬服務專員 | 「這是測試訊息,如果看到這則訊息,表示 Webhook 執行要求運作正常!」 |
MTV 有什麼內容? | 在 MTV 中午 4 點後開始播放 MTV Unplugged。下午 5 點 30 分後,Rupauls 拖曳競速賽即將開始。 |
- 切換回 Google 助理模擬器
開啟: https://console.actions.google.com
- 按一下麥克風圖示,並詢問下列事項:
Talk to my test agent
Test my agent
Google 助理會回應以下內容:
- 現在,請思考:
What's on Comedy Central?
這應該會傳回:
目前播出時間為下午 6 點的喜劇中心,《The Simpsons》即將播放。下午 7 點後,Family Guy 將開始運動。
7. 恭喜
您已成功透過 Dialogflow 建立第一個 Google 助理動作!
您可能已經注意到,您的動作是以與 Google 帳戶連結的「測試模式」執行。請在 iOS 或 Android 手機上的 Nest 裝置或 Google 助理應用程式中使用相同的帳戶。您也可以測試動作。
這是研討會示範不過,如果你是為 Google 助理開發人員打造應用程式,可以提交該項動作申請核准。詳情請參閱這份指南。
涵蓋內容
- 如何使用 Dialogflow v2 建立聊天機器人
- 如何透過 Dialogflow 建立自訂實體
- 如何使用 Dialogflow 建立線性對話
- 如何使用 Dialogflow 和 Firebase 函式設定 Webhook 執行要求
- 如何透過 Actions on Google 將應用程式與 Google 助理整合
後續步驟
喜歡這個程式碼研究室嗎?快去看看這些實用的研究室功能吧!
將這個程式碼研究室與 Google Chat 整合,即可繼續進行這個研究室: