背景
天猫精灵是受众很广泛的智能音箱品牌,虽然天猫精灵目前的功能已经很丰富,但是面对个人高个性化的应用场景的需求仍无法得到满足。比如查询指定网站订阅状态、学校课表、班车表等。万幸天猫精灵提供了AliGenie技能应用平台的能力,我们可以轻松的申请开发权限来动手创造这些自定义技能\个人技能。
实践目标
本文通过开发一个语音技能来让天猫精灵实现随机提供关于<今天吃什么>的建议。我们总共有两大步:1、创建一个语音交互模型,这是为了让天猫精灵识别并知道我们的意图。2、搭建后端服务实现逻辑功能并回复。
具体步骤
注册or登录
AliGenie技能应用平台可以直接使用淘宝账号进行登录,初次使用需要进行开发者身份的实名认证。
创建语音技能<今天吃什么>
登录完点击创建新技能,选择自定义技能。值得注意的是我们选择自定义技能时技能名称、技能调用词需要有唯一性,个人技能正在内测中。
技能属性分为公有技能和私有技能,公有应用:将发布到公开市场,审核通过后在所有天猫精灵设备都可使用。私有应用:不发布到公开市场,只在定制合作设备上使用。当我们选择公有技能时可以使用平台托管和阿里云开发、自建Web服务器的方式;而选择私有技能时没有平台托管的选项,会增加NLU结果传递的选项。出于快速开发的需求,我们这里选择公有技能-在线开发(平台托管)。
确认完信息后我们就可以点击【确认创建】。
我们使用Java语言开发-选择示例模板。
然后跳转到应用页面可以看到我们没有定义语音交互模型、后端空白模板已经构建完成。
配置语音交互模型
意图
意图是用户进行交互对话的根本目的。例如我们设置一个意图名为“吃什么”,当用户说吃什么的时候平台会把识别到的意图标识传给后端进行逻辑处理,本次实践中,后端会从菜单列表里面随机返回一项。
本次实践设定了四个意图。
欢迎:清空用户缓存,定义一个用户。
吃什么:随机选择一个内容回复,并将index追加到用户数据的LinkedList集合最后。
上一个:将用户数据的LinkedList集合中最后一个index移除,并返回该index的内容。首先要判断集合内是否有元素,没有则不需要移除。
退出:清除用户缓存。
我们将吃什么设定为默认意图,默认意图并不是其他意图不匹配时使用的兜底意图。当我们只说技能调用词时会触发默认意图。我们还可以设定前置意图和对话表达来让天猫精灵的意图判断更为准确。
实体
实体是指某领域词汇的集合,技能平台提供了丰富的系统词典来支持技能引用。
我们暂时不需要配置实体。
问答
我们可以设置固定的问答句子来让技能更加丰满。
• 你会做什么?
• 我可以帮你随机提供今天吃什么的建议。我不会告诉你”随便“哦~你可以对我说”吃什么“。
配置页面执行意图
我们可以在这里指定页面地址。
后端服务搭建
服务搭建的方式有很多种,我们使用阿里云云开发平台进行搭建,它的优点在于有免费额度、且不用关心真实服务器,只需要编写自定义逻辑即可。
创建云开发应用
我们进入后端服务部署页面,单击【创建应用】。如果平台托管创建应用失败,我们可以转为使用阿里云Faas。
开通完点击【开发】就可以登录到阿里云开发平台,我们使用绑定的阿里云账号来登录。
在IDE界面编写代码
我们使用了空白模板,它内置了一个基本java文件。我们把GenieEntry.java代码修改如下:菜单可以在代码里面个性化自定义,修改完需要重新发布一下。
package com.alibaba.ailabs;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import com.alibaba.ailabs.common.AbstractEntry;
import com.alibaba.da.coin.ide.spi.meta.ExecuteCode;
import com.alibaba.da.coin.ide.spi.meta.ResultType;
import com.alibaba.da.coin.ide.spi.standard.ResultModel;
import com.alibaba.da.coin.ide.spi.standard.TaskQuery;
import com.alibaba.da.coin.ide.spi.standard.TaskResult;
import com.alibaba.fastjson.JSON;
import com.aliyun.fc.runtime.Context;
/**
- @Description 天猫精灵技能函数入口,FC handler:com.alibaba.ailabs.GenieEntry::handleRequest
- @Version 1.0
**/
public class GenieEntry extends AbstractEntry {
private static final Map<String, LinkedList<Integer>> USER_MAP = new ConcurrentHashMap<>();
private static final List<String> KNOWLEDGE;
private static Random random = new Random();
static {
//这里的列表就是所有的词条
KNOWLEDGE = Arrays.asList(
"馄饨", "油泼面", "炸酱面", "炒面", "酸辣粉", "土豆粉", "螺狮粉", "黄焖鸡米饭", "麻辣香锅", "火锅", "酸菜鱼", "烤串", "披萨" ,
"烤鸭", "汉堡", "炸鸡", "寿司", "煎饼果子", "南瓜粥", "小龙虾", "牛排", "砂锅", "沙县小吃", "烤鱼", "海鲜", "铁板烧", "韩国料理",
"鸭血粉丝汤", "烩面", "热干面", "刀削面", "重庆小面", "米线", "凉皮儿", "麻辣烫", "肉夹馍", "羊肉泡馍", "炒饭", "盖浇饭", "烤肉饭",
"西餐", "自助餐", "小笼包", "西北风", "烧烤", "泡面", "水饺", "日本料理", "涮羊肉", "兰州拉面", "肯德基", "面包", "臊子面", "小笼包"
);
}
@Override
public ResultModel<TaskResult> execute(TaskQuery taskQuery, Context context) {
context.getLogger().info("taskQuery: " + JSON.toJSONString(taskQuery));
String userId = taskQuery.getRequestData().get("userOpenId");
userId = userId == null ? "testUser" : userId;
LinkedList<Integer> list = USER_MAP.get(userId);
//欢迎意图 或 用户缓存数据为空
if ("welcome".equals(taskQuery.getIntentName()) || list == null) {
list = new LinkedList<>();
int randomIndex = random.nextInt(KNOWLEDGE.size());
list.addLast(randomIndex);
USER_MAP.put(userId, list);
return intentChangeReply(KNOWLEDGE.get(randomIndex));
}
//吃什么意图,随机选择一个内容回复,并将index追加到用户数据的LinkedList集合最后
if ("what".equals(taskQuery.getIntentName())) {
int randomIndex = random.nextInt(KNOWLEDGE.size());
list.addLast(randomIndex);
return intentChangeReply(KNOWLEDGE.get(randomIndex));
}
//上一个意图,将用户数据的LinkedList集合中最后一个index移除,并返回该index的内容。首先要判断集合内是否有元素,没有则不需要移除
if ("prev".equals(taskQuery.getIntentName())) {
if (list.size() > 0) {
list.removeLast();
}
if (list.size() == 0) {
return intentChangeReply("这已经是第一个了。");
}
return intentChangeReply(KNOWLEDGE.get(list.get(list.size() - 1)));
}
//退出意图,清除用户缓存
if ("exit".equals(taskQuery.getIntentName())) {
USER_MAP.remove(userId);
return reply("已为您退出,再见。");
}
return reply("请检查意图名称是否正确,或者新增的意图没有在代码里添加对应的处理分支。");
}
/**
* 结束对话的回复,回复后音箱闭麦
*/
private ResultModel<TaskResult> reply(String reply) {
return getResult(reply, ResultType.RESULT);
}
/**
* 未指定追问参数,音箱自动开麦,用户的回答可跳转到其它意图
*/
private ResultModel<TaskResult> intentChangeReply(String reply) {
return getResult(reply, ResultType.ASK_INF);
}
private ResultModel<TaskResult> getResult(String reply, ResultType askInf) {
ResultModel<TaskResult> res = new ResultModel<>();
TaskResult taskResult = new TaskResult();
taskResult.setReply(reply);
taskResult.setExecuteCode(ExecuteCode.SUCCESS);
taskResult.setResultType(askInf);
res.setReturnCode("0");
res.setReturnValue(taskResult);
return res;
}
}
提交并发布代码
如图单击保存并提交。
点选左侧导航栏第一个部署按钮-选择预发环境并开始部署。我们可以在输出栏看到部署进度。
测试并发布
返回到应用测试页面进行测试:
测试无问题我们可以选择发布出去,但是由于我们是高度个性化的技能,可以跳过发布流程。
小结
本次分享到这里就结束了。
今天吃什么?问天猫精灵就好了~
补充:
我们使用云开发平台部署的线上运行情况可以通过云开发平台的日志查询进行查看。
如果想要将应用下线也需要移步到云开发平台进行操作。
如果找不到你开发的天猫精灵应用可以检查一下你当前查看的企业组织是否为【天猫精灵的组织】。
如果你的技能涉及复杂的依赖或测试,可以考虑使用Serverless Devs在本地自动化部署。