原文:How to Connect Your Api.ai Assistant to the IoT
作者:Patrick Catanzariti
翻译:安翔
审校:屠敏
当个人助理通过物联网访问个人数据和现实世界时,它变得更加迷人,让人心驰神往。这可能出现新的应用场景,比方说请求你的助手打开灯光,或者向它询问你的睡眠质量。我们将把你的 Api.ai 助手连接到 Jawbone Up API,以此作为示例。
为了更好地掌握本教程的内容,你需要做如下准备:
本教程的所有代码提供免费下载和使用。 GitHub地址:https://github.com/sitepoint-editors/Api-AI-Personal-Assistant-IoT-Demo。
你的 Api.ai 助手连接到一个简单的 Web 应用之后,它通过 HTML5 语音识别 API 接受指令。此时,你需要添加一些新功能,用于监听你的 Api.ai 代理的特定 Action 。在本例中, Action 是“sleepHours”。
每当 JavaScript 检测到该 Action 时,它会触发调用你的 Node.js 应用程序,以向Jawbone API 请求数据。一旦 web 应用收到这些数据,它会将数据转换成友好的提示语并将其读出 - 为你的助手提供全新的智能体验!
我将应用程序从初始的 HTML 结构调整为使用 EJS 视图的应用程序,以便你通过 OAuth 登录到 Jawbone Up API 后在 Web 应用程序中切换页面。实际上,你只有一个页面,但是如果需要其他 IoT 设备,你可以通过此方法添加更多。这个唯一的界面在 /views/index.ejs 中定义。然后,你的 Node 服务器 server.js 和证书文件将位于根目录中。为了尽可能简单和灵活,所有前端 JavaScript 和 CSS 代码都应当是内联的。你可以随意将它们移动到 CSS 和 JS 文件中,将它们细化并使其变得更好。
正如你从以前的文章所了解到的那样,当 Api.ai 返回一个响应时,它提供了一个如下所示的 JSON 对象:
{
"id": "6b42eb42-0ad2-4bab-b7ea-853773b90219",
"timestamp": "2016-02-12T01:25:09.173Z",
"result": {
"source": "agent",
"resolvedQuery": "how did I sleep last night",
"speech": "I'll retrieve your sleep stats for you now, one moment!",
"action": "sleepHours",
"parameters": {
"sleep": "sleep"
},
"metadata": {
"intentId": "25d04dfc-c90c-4f55-a7bd-6681e83b45ec",
"inputContexts": [],
"outputContexts": [],
"contexts": ,
"intentName": "How many hours of @sleep:sleep did I get last night?"
}
},
"status": {
"code": 200,
"errorType": "success"
}
}
在这个 JSON 对象中有两个比特的数据你需要使用,它们是 action和 parameters.sleep。
"action": "sleepHours",
"parameters": {
"sleep": "sleep"
},
action
决定了触发Api.ai 行为的名称。在本示例中,你将其命名为“sleepHours”。parameters
包含了你的语句中可以改变细节的变量。在本例中,你的 parameters
定义了睡眠类型: “睡眠”,“深睡眠”,“浅睡眠”或“REM睡眠”(或“REM”)。
最初,在早期的一篇关于 Api.ai 的文章中,prepareResponse()
函数从 Api.ai 获取 JSON 响应,将其放在右下方的调试文本字段中,它提取出 Api.ai 的文本响应并显示在Web应用程序中。你完全依赖 Api.ai 代理的反馈,无需自己添加任何功能。
function prepareResponse(val) {
var debugJSON = JSON.stringify(val, undefined, 2),
spokenResponse = val.result.speech;
respond(spokenResponse);
debugRespond(debugJSON);
}
在 action 代码片段中,如果 action 包含”sleepHours”,就调用你自己的requestSleepData()函数。函数中的睡眠参数将决定请求什么类型的睡眠数据。
function prepareResponse(val) {
var debugJSON = JSON.stringify(val, undefined, 2),
spokenResponse = val.result.speech;
if (val.result.action == "sleepHours") {
requestSleepData(val.result.parameters.sleep);
} else {
respond(spokenResponse);
}
debugRespond(debugJSON);
}
在requestSleepData()函数中将从 Node 服务器请求所有的睡眠数据,然后通过数组data.items[0].details(昨晚的数据)的第一个值进行睡眠类型的区分。 data.items[0].details.rem代表 REM 睡眠;data.items[0].details.sound代表深度睡眠;data.items[0].details.light代表浅睡眠;data.items[0].details.duration则代表睡眠汇总数据。
function requestSleepData(type) {
$.ajax({
type: "GET",
url: "/sleep_data/",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(data) {
console.log("Sleep data!", data);
if (data.error) {
respond(data.error);
window.location.replace("/login/jawbone");
}
switch (type) {
case "REM sleep":
respond("You had " + toHours(data.items[0].details.rem) + " of REM sleep.");
break;
case "deep sleep":
respond("You had " + toHours(data.items[0].details.sound) + " of deep sleep.");
break;
case "light sleep":
respond("You had " + toHours(data.items[0].details.light) + " of light sleep.");
break;
case "sleep":
respond("You had " + toHours(data.items[0].details.duration) + " of sleep last night. That includes " + toHours(data.items[0].details.rem) + " of REM sleep, " + toHours(data.items[0].details.sound) + " of deep sleep and " + toHours(data.items[0].details.light) + " of light sleep.");
break;
}
},
error: function() {
respond(messageInternalError);
}
});
}
toHours()函数将时间格式转换为普通语句,比如:1小时53分59秒。
function toHours(secs) {
hours = Math.floor(secs / 3600),
minutes = Math.floor((secs - (hours * 3600)) / 60),
seconds = secs - (hours * 3600) - (minutes * 60);
hourText = hours + (hours > 1 ? " hours, " : " hour, ");
minuteText = minutes + (minutes > 1 ? " minutes " : " minute ");
secondText = seconds + (seconds > 1 ? " seconds" : " second");
return hourText + minuteText + "and " + secondText;
}
requestSleepData() 函数最终将调用response()函数,细心的你会发现response()函数在之前的获取 Api.ai 语音响应时使用过。通过复用现有的功能,从语音获得响应,让你的助手在监听到信息之后通知给用户。
不要忘了在前端 JavaScript 代码的最后加上错误处理。如果 Jawbone 返回数据出现问题(通常是因为没有登录到服务),服务器将以 JSON 字段{“error” : “Your error message”}的形式提示错误。助理识别到错误提示时将自动跳转到 OAuth 登录页面。
if (data.error) {
respond(data.error);
window.location.replace("/login/jawbone");
}
你的 Node 服务器基于使用Node.js连接到Jawbone Up API 一文中的服务器。此文讲述了所有关于通过OAuth连接到Jawbone API并设置一个HTTPS服务器且运行成功的方法,如果对代码有任何疑惑,请参考这篇文章。如果你手头没有 Jawbone Up ,可以使用其他 IoT 设备代替。这里的 Jawbone Up 数据只是一个例子,你可以自定义响应方法以获取不同的响应数据(你可能无需担心OAuth)。
你的 Jawbone 数据在本例中以简单的 JSON 格式展示,而不是像以往文章中那样被格式化为一个表单。变量up和options被定义为全局变量,以便多个请求 API 调用(其他SitePoint例子中,一次请求一大块数据)。
用户可以访问/login/jawbone,通过 OAuth 登录 Jawbone API。然而,如上所述,其实你不需要刻意关注此事,因为你的助理发现登录失败则会自动重定向到/login/jawbone。您的助理还可以重定向它们。
如果你想该功能更加完整,还可以向你的 Api.ai 代理添加一个新的 intent,用于识别“log me into my Jawbone Up data”。Node.js 登录流程如下:
app.get("/login/jawbone",
passport.authorize("jawbone", {
scope: ["basic_read","sleep_read"],
failureRedirect: "/"
})
);
一旦通过passport.use(“jawbone”, new JawboneStrategy())登录成功,将访问权限分配给up变量并将用户转到/barry。你可以将用户重定向到根目录(将会造成死循环)之外的任何路径。我选择重定向到/barry是因为我将我的助手命名为 Barry,它具有一定的自我标识性(页面显示完全相同的索引视图,通常很难区分)。如果您愿意,你也可以使用此方式为已经成功登录到 Jawbone 设备的用户提供不同的视图。登录后,用户可以返回到根页面:https://localhost:5000 ,并使用Up 函数。
收到/sleep_data的 GET 请求后,对 Jawbone 数据的检索非常简单。首先确认up变量是否被定义,如果没有则证明用户未登录,此时你需要告知 web 应用从而执行重定向操作并且提示用户登录。当你调用up.sleeps.get()而 Jawbone 返回错误或者jawboneData.items未定义时,你都需要重复此操作。
app.get("/sleep_data", function(req, resp) {
if (up !== undefined) {
up.sleeps.get({}, function(err, body) {
if (err) {
console.log("Error receiving Jawbone UP data");
resp.send({"error": "Your sleep tracker isn't talking to me. Let's try logging in again."});
} else {
var jawboneData = JSON.parse(body).data;
if (jawboneData.items) {
resp.send(jawboneData);
} else {
console.log("Error: " + jawboneData);
resp.send({"error": "Your sleep tracker isn't talking to me. Let's try logging in again."});
}
}
});
} else {
console.log("Up is not ready, lets ask to log in.");
resp.send({"error": "Your sleep tracker isn't talking to me. Let's try logging in again."});
}
});
这里的错误可能是由其他因素引起的,但是为了教程更加简单易懂,我选用重复登录来举例。在正式产品的应用程序中,你需要查看各种原因并调整对应的响应。
如果一切顺利,你将会收到有效的响应信息,将其用 JSON 的格式发送给 web 应用程序,以便更好的读取和解析:
if (jawboneData.items) {
resp.send(jawboneData);
}
随着 Web 应用程序和 Node.js 服务器协同工作,你应该能够从 Jawbone Up 设备中检索睡眠数据了。接下来,我们一同测试一下。
通过指令 node server.js 运行你的服务器。在这之前你需要执行 npm install 安装 npm 模块,同时需要运行基于 HTTPS 的服务器证书。
在浏览器中输入网址 https://localhost:5000 的方式访问你的 AI 助手(如果你使用Glitch 一类的服务,则需用具体的 Glitch URL 替换)。在界面中询问你的睡眠信息:
系统会提示你未登录,并且弹出 Jawbone Up OAuth 登录界面。你需要在此界面中登录并同意提供你的数据访问权限,然后单击“同意”:
此时你再次询问将会得到正确的回复。
你还可以询问一些更加细节的问题来进行参数测试,比如“How much REM did I get?”。
这是对当前 多种 Api.ai 功能的又一种探索和体验。你可以扩展这个例子,添加上日期范围(例如“星期二有多少睡眠?”),或者更好地格式化时间(在其个响应中报个小 bug)。你可以进行个性化的扩展,用更精巧的方式来描述响应。
如你所见,使用本文中的方法你可以将任何 Node.js 服务或者 web 应用连接到 Node.js 服务器,为 Api.ai 绑定一个 intent,并指定它如何进行反馈。你可以通过 IFTTT 连接大量的物联网设备,通过 IFTTT 连接 LIFX 智能灯 或者 连接你自己的 Nodebot。这些尝试仅仅受限于你拥有的设备!