1. 簡介
你正在看電視,但找不到遙控器,或者不想逐一瀏覽每個電視頻道,看看是否有喜歡的節目?讓我們問問 Google 助理電視上正在播放什麼節目!在本實驗室中,您將使用 Dialogflow 建構簡單的動作,並瞭解如何與 Google 助理整合。
本實驗室包含多個練習,順序與常見雲端開發作業類似,包括:
- 建立 Dialogflow V2 代理程式
- 建立自訂實體
- 建立意圖
- 使用 Firebase Functions 設定 Webhook
- 測試聊天機器人
- 啟用 Google 助理整合功能
建構目標
我們將為 Google 助理建構互動式電視節目表聊天機器人代理程式。你可以詢問電視節目表,或特定頻道目前播放的內容。例如:「MTV 正在播什麼節目?」電視節目表動作會告訴你目前正在播放的節目,以及接下來的節目。 |
|
課程內容
- 如何使用 Dialogflow V2 建立聊天機器人
- 如何使用 Dialogflow 建立自訂實體
- 如何使用 Dialogflow 建立線性對話
- 如何使用 Dialogflow 和 Firebase Functions 設定 Webhook 執行要求
- 如何透過 Actions on Google 將應用程式整合至 Google 助理
必要條件
- 您需要 Google 身分 / Gmail 地址,才能建立 Dialogflow 虛擬服務專員。
- 您不需要具備 JavaScript 基礎知識,但如果想變更 Webhook 履行程式碼,這項知識就派得上用場。
2. 開始設定
在瀏覽器中啟用網路活動
- 確認「網路和應用程式活動」已啟用:

建立 Dialogflow 代理程式
- 在左側列的標誌下方,選取「建立新代理程式」。如果您已有代理程式,請先按一下下拉式選單。

- 指定代理名稱:
your-name-tvguide(使用您自己的名稱)

- 選擇預設語言:英文 - en
- 選擇最接近您所在位置的時區做為預設時區。
- 按一下「Create」(建立)
設定 Dialogflow
- 在左選單中,點選專案名稱旁的齒輪圖示。

- 輸入下列代理程式說明:My TV Guide

- 向下捲動至「記錄設定」,然後將兩個切換鈕都設為記錄 Dialogflow 的互動,以及記錄 Google Cloud Stackdriver 中的所有互動。我們稍後會需要這個 ID,以便偵錯動作。

- 按一下「儲存」。
設定 Actions on Google
- 按一下右側面板「瞭解 Google 助理的運作方式」中的「Google 助理」連結。

系統會開啟:http://console.actions.google.com
如果您是 Google 助理動作的新手,請先填寫這份表單:

- 點選專案名稱,在模擬器中開啟動作。**
- 選取選單列中的「測試」

- 確認模擬器已設為「英文」,然後按一下「對我的測試應用程式下達語音指令」
這項動作會使用基本的 Dialogflow 預設意圖向您問候。這表示與 Google 助理整合的設定已完成!
3. 自訂實體
實體是指應用程式或裝置執行的動作對象。這就像參數 / 變數。在電視節目表,我們會詢問「MTV 正在播放什麼節目?」,MTV 是實體和變數。我也可以要求其他頻道,例如「國家地理頻道」或「Comedy Central」。收集到的實體將做為參數,用於向 TV Guide API 網路服務發出的要求。
建立頻道實體
- 在 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 會使用意圖將使用者的目的分類。意圖包含訓練詞組,這是使用者可能會向代理程式說的內容範例。舉例來說,想知道電視節目內容的使用者可能會問「今天電視上播什麼?」,「現在播放的是什麼?」或直接說「電視節目表」。
當使用者輸入或說出某項內容時 (稱為「使用者表達內容」),Dialogflow 會為使用者表達內容進行比對,找出代理程式中最相符的意圖。比對意圖的作業也稱為「意圖分類」。
修改預設的歡迎意圖
建立新的 Dialogflow 代理程式時,系統會自動建立兩個預設意圖。預設歡迎意圖是您與代理程式展開對話時,首先會遇到的流程。如果代理程式無法理解您說的內容,或無法比對您剛才說的內容與意圖,就會進入「Default Fallback Intent」(預設備用意圖) 流程。
- 按一下「Default Welcome Intent」
以 Google 助理為例,系統會自動啟動預設歡迎意圖。這是因為 Dialogflow 正在監聽歡迎事件。不過,你也可以說出輸入的訓練詞組,來叫用意圖。

以下是預設歡迎意圖的歡迎訊息:
使用者 | Agent |
「Ok Google,與你的電視節目表對話。」 | 「歡迎,我是電視節目表專員。我可以告訴你電視頻道目前播放的內容。例如,你可以問我「MTV 在播什麼?」 |
- 向下捲動至「Responses」。
- 清除所有文字回覆。
- 建立一個新的文字回應,其中包含以下問候語:
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。
- 再次按一下「意圖」選單項目。
- 按一下「建立意圖」
- 輸入意圖名稱:
Test Intent(請務必使用大寫 T 和大寫 I。- 如果意圖的拼字不同,後端服務就無法運作!)

- 按一下「新增訓練詞組」
Test my agentTest intent

- 依序點選「完成」>「啟用完成」

這次我們不會將回應寫死在程式碼中。回覆內容來自雲端函式!
- 將「Enable Webhook call for this intent」(為這個意圖啟用 Webhook 呼叫) 切換鈕設為開啟。

- 按一下「儲存」
建立管道意圖
頻道意圖會包含對話的這部分:
使用者 | Agent |
「Comedy Central 有什麼節目?」 | 「目前 Comedy Central 正在播放《辛普森家庭》,隨後在晚上 7 點,『居家大改造』就會開始。」 |
- 再次按一下「意圖」選單項目。
- 按一下「建立意圖」
- 輸入意圖名稱:
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

- 向下捲動至「動作和參數」

請注意 Dialogflow 已知的 @channel 和 @sys.time 實體。稍後,系統會將參數名稱和參數值傳送至 Web 服務。例如:
channel=8
time=2020-01-29T19:00:00+01:00
- 將頻道標示為必要
與電視節目表代理程式對話時,一律須填寫空位參數名稱 channel。如果對話開頭未提及頻道名稱,Dialogflow 會進一步詢問,直到填滿所有參數欄位為止。
輸入下列提示:
For which TV channel do you want to hear the tv guide information?In which TV channel are you interested?

- 請勿將時間參數設為必要。
時間為選填欄位。如未指定時間,Web 服務會傳回目前時間。
- 按一下「履行」。
這次我們不會將回應寫死在程式碼中。回應會來自 Cloud Functions!因此,請將「Enable Webhook call for this intent」(為這個意圖啟用 Webhook 呼叫) 切換鈕設為開啟。
- 按一下「儲存」
5. Webhook 履行
如果代理程式需要的不只是靜態意圖回應,您則必須使用「執行要求」將網路服務連結至代理程式。連結網路服務可讓您依據使用者表達內容採取行動,並將動態回應傳回使用者端。舉例來說,如果使用者想接收 MTV 的電視節目表,您的網路服務可以檢查資料庫,並將 MTV 的節目表傳送給使用者。
- 按一下主選單中的「Fulfillment」
- 啟用「Inline Editor」(內嵌編輯器) 切換按鈕

如要進行簡單的 Webhook 測試和導入作業,可以使用內嵌編輯器。這項服務會使用無伺服器的 Cloud Functions for Firebase。
- 在編輯器中按一下「index.js」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」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"
}
}

- 按一下「部署」按鈕。部署無伺服器函式需要一些時間,畫面底部會顯示彈出式視窗,說明狀態。
- 讓我們測試 Webhook,看看程式碼是否正常運作。在右側的模擬工具中輸入:
Test my agent.
如果一切正常,您應該會看到「這是測試訊息」。
- 現在來測試頻道意圖,請提出以下問題:
What's on MTV?
一切正確無誤時,您應該會看到:
「On MTV from 4 30 pm, MTV Unplugged is playing. 隨後在下午 5 點 30 分,將播出魯保羅變裝皇后秀。」
選用步驟 - Firebase
使用其他管道測試時,你會發現電視結果相同。這是因為雲端函式尚未從實際的網路伺服器擷取資料。
為此,我們需要建立輸出網路連線。
如要使用網路服務測試這個應用程式,請將 Firebase 方案升級為 Blaze。注意:這些步驟為選用步驟。您也可以繼續進行本實驗室的後續步驟,在 Actions on Google 中繼續測試應用程式。
- 前往 Firebase 控制台:https://console.firebase.google.com
- 按下畫面底部的「升級」按鈕

在彈出式視窗中選取「Blaze」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 助理的開發平台。第三方開發人員可透過這項服務開發「動作」,也就是 Google 助理的小程式,提供擴充功能。
你必須呼叫 Google 動作,要求 Google 開啟或與應用程式互動。
這會開啟動作、變更語音,並離開「原生」Google 助理範圍。也就是說,您之後要求代理執行的所有動作,都必須由您建立。如果想請 Google 助理提供 Google 天氣資訊,您不能突然提出要求,必須先離開 (關閉) 動作範圍 (您的應用程式)。
在 Google 助理模擬器中測試動作
讓我們測試以下對話:
使用者 | Google 助理 |
「Ok Google,跟『你的名稱電視節目表』your-name-tv-guide對話。」 | 「沒問題,請稍待,我會為你開啟your-name-tv-guide。」 |
使用者 | Your-Name-TV-Guide Agent |
- | 「歡迎,我是電視節目表....」 |
測試我的代理 | 「This is a test message, when you see this, it means your webhook fulfillment worked!」(這是測試訊息,看到這則訊息表示 Webhook 履行作業已順利完成!) |
MTV 有哪些節目? | MTV 頻道下午 4 點 30 分開始播放 MTV Unplugged。隨後在下午 5 點 30 分,將開始播放《魯保羅變裝皇后秀》。 |
- 改回使用 Google 助理模擬器
開啟: https://console.actions.google.com
- 按一下麥克風圖示,然後提出以下問題:

Talk to my test agentTest my agent
Google 助理應會回覆:

- 現在來問問:
What's on Comedy Central?
這應該會傳回:
目前 Comedy Central 正在播放《辛普森家庭》(晚上 6 點起)。隨後在晚上 7 點開始播放《居家大改造》。
7. 恭喜
您已使用 Dialogflow 建立第一個 Google 助理動作,做得好!
如您所見,動作是以測試模式執行,並連結至您的 Google 帳戶。如果使用同一個帳戶登入 Nest 裝置或 iOS/Android 手機上的 Google 助理應用程式,你也可以測試動作。
現在是研討會的示範。但如果您要實際為 Google 助理建構應用程式,可以提交動作以供核准。詳情請參閱這份指南。
涵蓋內容
- 如何使用 Dialogflow V2 建立聊天機器人
- 如何使用 Dialogflow 建立自訂實體
- 如何使用 Dialogflow 建立線性對話
- 如何使用 Dialogflow 和 Firebase Functions 設定 Webhook 執行要求
- 如何透過 Actions on Google 將應用程式整合至 Google 助理
後續步驟
喜歡這個程式碼研究室嗎?快來看看這些精彩的實驗室!
繼續完成本程式碼研究室,將其整合至 Google Chat:
