实践:使用 Dialogflow 和 Actions on Google 为 Google 助理创建电视指南操作

1. 简介

您正在看电视,但找不到遥控器,或者您可能不想逐个访问电视频道来弄清楚电视上是否有好看的节目?我们来问问 Google 助理电视上在放什么!在本实验中,您将使用 Dialogflow 构建一个简单的操作,并学习如何将其与 Google 助理相集成。

练习按如下顺序排列,以反映常见的云开发者体验:

  1. 创建 Dialogflow v2 代理
  2. 创建自定义实体
  3. 创建意图
  4. 使用 Firebase Functions 设置 webhook
  5. 测试聊天机器人
  6. 启用 Google 助理集成

构建内容

我们将为 Google 助理构建一个交互式电视指南聊天机器人代理。您可以向收视指南询问特定频道当前正在播放的内容。例如:“MTV 上有什么节目?”“收视指南”操作会告诉您当前播放的内容以及接下来要播放的内容。

学习内容

  • 如何使用 Dialogflow v2 创建聊天机器人
  • 如何使用 Dialogflow 创建自定义实体
  • 如何使用 Dialogflow 创建线性对话
  • 如何使用 Dialogflow 和 Firebase Functions 设置 webhook 执行方式
  • 如何通过 Actions on Google 将应用引入 Google 助理

前提条件

  • 您需要一个 Google Identity / Gmail 地址才能创建 Dialogflow 代理。
  • 您不一定具备 JavaScript 的基础知识,但在更改 webhook 执行代码时,这非常实用。

2. 准备工作

在浏览器中启用“网络活动”

  1. 点击:http://myaccount.google.com/activitycontrols

  1. 确保网络和“应用活动记录”已启用

bf8d16b828d6f79a.png

创建 Dialogflow 代理

  1. 打开:https://console.dialogflow.com

  1. 在左栏中徽标的正下方,选择“Create New Agent”(创建新代理)。如果您已有客服人员,请先点击下拉菜单。

1d7c2b56a1ab95b8

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

35237b5c5c539ecc

  1. 作为默认语言,请选择:English - en
  2. 选择离您最近的时区作为默认时区。
  3. 点击创建

配置 Dialogflow

  1. 点击左侧菜单中项目名称旁边的齿轮图标。

1d7c2b56a1ab95b8

  1. 输入以下代理说明:My TV Guide(我的电视指南)

26f262d359c49075

  1. 向下滚动到 Log Settings(日志设置),将两个开关切换为“记录 Dialogflow 的互动”以及记录 Google Cloud Stackdriver 中的所有互动。我们稍后需要使用它,以便调试操作。

e80c17acc3cce993.png

  1. 点击保存

配置 Actions on Google

  1. 在右侧面板中,点击了解它在 Google 助理中的工作原理中的 Google 助理链接。

5a4940338fc351e3

此操作将打开:http://console.actions.google.com

初次使用 Actions on Google,您需要先填写此表单:

3fd4e594fa169072

  1. 点击项目名称,尝试在模拟器中打开您的操作**。**
  2. 在菜单栏中选择 Test

6adb83ffb7adeb78.png

  1. 确保模拟器设置为 English,然后点击 Talk to my test-app

该操作将使用基本的 Dialogflow 默认 intent 向您致以问候。这意味着,您成功设置了与 Action on Google 的集成!

3. 自定义实体

实体是您的应用或设备对其执行操作的对象。可以将其视为参数 / 变量。在我们的电视指南中,我们会询问:“MTV 上有什么节目”。MTV 是实体和变量。也可以让我播放其他频道,例如“国家地理”或“喜剧中心”在我向 TV Guide API 网络服务发送的请求中,会将收集到的实体用作参数。

详细了解 Dialogflow 实体

创建频道实体

  1. 在 Dialogflow 控制台中点击菜单项实体
  2. 点击创建实体
  3. 实体名称:channel(请确保全为小写)
  4. 传入频道名称。(如果 Google 助理能理解其他内容,某些频道将需要同义词)。您可以在输入内容时使用 Tab 键和 Enter 键。输入频道号作为 reference 值。频道名称则设为同义词,例如:
  • 1 - 1, Net 1, Net Station 1

ee4e4955aa77232d.png

5**。**点击蓝色保存按钮旁边的菜单按钮,切换到 **Raw Edit** 模式。

e294b49b123e034f.png

  1. 复制 &以 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"

ed78514afd5badef.png

  1. 点击保存

4. intent

Dialogflow 使用意图对用户的意图进行分类。意图包含训练短语,即用户可能对代理说的话的示例。例如,用户想知道电视上在播什么,可能会问:“今天电视上有什么节目?”,“现在正在播放的是什么?”,或者直接说“tvguide”。

当用户输入或说出某些内容(称为“用户表述”)时,Dialogflow 会将用户表述与代理中的最佳意图进行匹配。匹配 intent 也称为“意图分类”

详细了解 Dialogflow intent

修改默认欢迎 intent

创建新的 Dialogflow 代理时,系统会自动创建两个默认意图。默认欢迎意图是您与代理开始对话时到达的第一个流程。默认后备意图 (Default Fallback Intent) 是当代理无法理解您的指令或无法将某个意图与您刚刚说过的内容相匹配时您获得的流程。

  1. 点击默认欢迎意图

如果是 Google 助理,则会自动以“默认欢迎 intent”启动。这是因为 Dialogflow 正在监听欢迎事件。但是,您也可以通过说出其中一个输入的训练短语来调用 intent。

6beee64e8910b85d

以下是“默认欢迎意图”的欢迎消息:

用户

客服人员

“Ok Google,跟你的-name-tvguide 对话。”

“欢迎,我是 TV Guide 客服人员。我可以告诉你当前某个电视频道正在播放什么内容。例如,你可以问我:MTV 上有什么。”

  1. 向下滚动到响应
  2. 清除所有文本响应。
  3. 创建一条新的文本响应,其中包含以下问候语:

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?

84a1110a7f7edba2.png

  1. 点击保存

创建临时测试 intent

出于测试目的,我们将创建临时的测试 intent,以便稍后测试网络钩子。

  1. 再次点击意图菜单项。
  2. 点击创建意图
  3. 输入 intent 名称:Test Intent (务必使用大写的 T 和大写的 I。- 如果 intent 的拼写形式不同,后端服务将不起作用!)

925e02caa4de6b99

  1. 点击添加训练短语
  • Test my agent
  • Test intent

2e44ddb2fae3c841

  1. 点击 Fulfillment >启用 Fulfillment

7eb73ba04d76140e

这次,我们不会对响应进行硬编码。响应将来自 Cloud Functions 函数!

  1. 切换为此 intent 启用网络钩子调用开关。

748a82d9b4d7d253

  1. 点击保存

创建频道 intent

频道 Intent 将包含这部分对话:

用户

客服人员

“Comedy Central 上有什么?”

“《辛普森一家》目前在喜剧中心频道 (Comedy Central) 上映,将于下午 6 点开始放映。之后晚上 7 点,Family Guy 就会开始。””

  1. 再次点击意图菜单项。
  2. 点击创建意图
  3. 输入 intent 名称:Channel Intent(请确保使用大写的 T 和大写的 I。- 如果 intent 的拼写形式不同,后端服务将不起作用!)
  4. 点击添加训练短语并添加以下内容:
  • 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

6eee02db02831397

  1. 向下滚动到操作和参数

b7e917026760218a.png

请注意 @channel@sys.time 实体。稍后,在您的 webhook 中,参数名称和参数值将发送到您的 Web 服务。例如:

channel=8

time=2020-01-29T19:00:00+01:00

  1. 通道标为等号

与 TV Guide 代理对话时,您始终需要填写槽参数名称 channel。如果对话开始时没有提到渠道名称,Dialogflow 将进一步询问,直到填满所有参数槽位。6f36973fd789c182

提示输入以下内容:

  • For which TV channel do you want to hear the tv guide information?
  • In which TV channel are you interested?

cdb5601ead9423f8.png

  1. 请勿将时间参数设置为必需参数。

时间是可选的。如果未指定时间,网络服务将返回当前时间。

  1. 点击 Fulfillment

这次,我们不会对响应进行硬编码。响应将来自 Cloud Functions 函数!因此,请翻转为此 intent 启用网络钩子调用开关。

  1. 点击保存

5. 网络钩子执行

如果代理需要的不仅仅是静态 intent 响应,则您需要使用 fulfillment 将您的 Web 服务连接到代理。通过连接网络服务,您可以根据用户表达执行操作,并将动态响应发送回用户。例如,如果用户想要接收 MTV 的电视时间表,您的 Web 服务可以查看您的数据库并响应用户(MTV 的时间表)。

  1. 点击主菜单中的 Fulfillment
  2. 启用内嵌编辑器开关

cc84351f0d03ab6f.png

对于简单的网络钩子测试和实现,您可以使用内嵌编辑器。它使用无服务器 Cloud Functions for Firebase

  1. 点击编辑器中的 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);
});

cc84351f0d03ab6f.png

  1. 点击编辑器中的 package.json 标签页,然后复制并粘贴下面这段 JSON 代码,该代码会导入所有的 Node.js Package Manager (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"
  }
}

af01460c2a023e68.png

  1. 点击部署按钮。请稍等片刻,因为需要部署无服务器函数。屏幕底部会显示一个弹出式窗口,其中会显示您的状态。
  2. 让我们测试一下 webhook,看看代码能否正常运行。在右侧的模拟器中,输入:

Test my agent.

如果一切正常,您应该会看到“This is a test message”(这是测试消息)。

  1. 我们来测试一下频道 Intent,现在请思考以下问题:

What's on MTV?

如果一切都正确无误,您应该会看到:

“MTV 从下午 4 30 点开始播放《MTV Unplugged》。之后,下午 5 点 30 分,Rupauls 变装比赛将开始。”

可选步骤 - Firebase

使用其他频道进行测试时,您会注意到电视结果是一样的。这是因为 Cloud Functions 函数尚未从真实的 Web 服务器中提取数据。

为此,我们需要建立出站网络连接

如果您希望使用 Web 服务测试此应用,请将您的 Firebase 方案升级为 Blaze。注意:这些步骤是可选的。您也可以执行此实验的后续步骤,继续在 Actions on Google 中测试您的应用。

  1. 前往 Firebase 控制台:https://console.firebase.google.com

  1. 按屏幕底部的升级按钮

ad38bc6d07462abf.png

在弹出式窗口中选择 Blaze 方案。

  1. 现在,我们已确定网络钩子正常运行,可以继续将 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 助理模拟器中测试您的 Action

让我们来测试以下对话:

用户

Google 助理

“Hey Google,跟 your-name-tv-guide 交谈。”

“当然可以。让我来找 your-name-tv-guide。”

用户

《Your-Name-TV-Guide-Guide》客服人员

-

“欢迎,我是电视指南...”

测试我的代理

“这是一条测试消息,如果您看到这条消息,则表示您的 webhook 执行方式成功了!”

MTV 上有什么?

在下午 4 点 30 分的 MTV 上,《MTV Unplugged》正在播放。之后,下午 5 点 30 分,Rupauls 变装比赛将开始。

  1. 切换回 Google 助理模拟器

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

  1. 点击麦克风图标,然后进行下面的问题:

c3b200803c7ba95e.png

  • Talk to my test agent
  • Test my agent

Google 助理应做出以下响应:

5d93c6d037c8c8eb

  1. 现在,我们来问问:
  • What's on Comedy Central?

此时应返回:

《辛普森一家》目前在喜剧中心频道晚上 6 点放映。之后晚上 7 点,《Family Guy》即会开始。

7. 恭喜

您已经使用 Dialogflow 创建了您的第一个 Google 助理操作,太棒了!

您可能已经注意到,您的 Action 是在与您的 Google 账号关联的测试模式下运行的。如果您想使用同一账号登录 Nest 设备或 iOS 或 Android 手机上的 Google 助理应用。您也可以测试自己的操作。

现在是研讨会演示。但是,如果你正在构建真正的 Google 助理应用,则可以提交你的 Action 以供审批。如需了解详情,请阅读本指南。

所学内容

  • 如何使用 Dialogflow v2 创建聊天机器人
  • 如何使用 Dialogflow 创建自定义实体
  • 如何使用 Dialogflow 创建线性对话
  • 如何使用 Dialogflow 和 Firebase Functions 设置 webhook 执行方式
  • 如何通过 Actions on Google 将应用引入 Google 助理

后续操作

喜欢此 Codelab?快来看看这些优秀的实验吧!

为 Google Chat 集成此 Codelab:

使用 G Suite 和 Dialogflow 创建电视指南 Google Chat