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 agentTest 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 agentTest 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 整合,即可繼續進行這個研究室:
