处理DuerOS发送的请求
基于云服务的自定义技能大多数编码任务都与以下两点相关:
- 处理接收到的各种不同类型的DuerOS请求 ;
- 返回对这些请求的正确响应 。
本文档描述DuerOS请求的类型,并提供响应示例。
目录
代码示例
你可以用任何编程语言编写技能。DuerOS为开发者提供了开发工具包SDK,SDK提供了开发技能所需的各种API接口。常用接口包括技能打开和退出、NLU交互、对话控制、展示卡片、事件监听和插件等。GitHub上基于Java和Php语言的SDK地址如下。
- Java: java-bot-sdk
- Php: php-bot-sdk
- javascript: javascript-bot-sdk
本文档基于java-bot-sdk,以Java语言作为示例。
java-bot-sdk中提供了一个基类BaseBot
,该类中提供了一些接口,使用这些接口,你可以方便地处理你的技能接收到的各种类型的请求。
当在云提供商上的web服务端上部署了你的技能后,你可以通过HTTP地址访问到你的技能,在技能的入口处,可以这样调用你的技能,这里以基于servlet的简单web服务为例。
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 根据request创建Bot
TaxBot bot = new TaxBot(request);
// 打开签名验证
// bot.enableVerify();
// 线下调试时,可以关闭签名验证
bot.disableVerify();
try {
// 调用bot的run方法
String responseJson = bot.run();
// 设置response的编码UTF-8
response.setCharacterEncoding("UTF-8");
// 返回response
response.getWriter().append(responseJson);
} catch (Exception e) {
e.printStackTrace();
response.getWriter().append("{\"status\":1,\"msg\":\"\"}");
}
}
为了实现自定义的技能,你可以扩展BaseBot
基类。BaseBot
基类实现了序列化和反序列化HTTP请求的主体,并可以根据请求调用对应的方法,如onLaunch()
、onIntent()
、onSessionEnded()
等等。
DuerOS向技能发送请求的种类
通过下面的request请求体消息格式,可以看出请求类型type
字段是request请求体的一个属性。
{
"version": "1.0",
"session": {
(session properties not shown)
},
"context": {
(context properties not shown)
}
"request": {
"type": "LaunchRequest",
"requestId": "request.id.string",
"timestamp": "string"
}
}
请求体中的type属性值可以是:
- LaunchRequest
- IntentRequest
- SessionEndedRequest
- 以AudioPlayer为前缀的音频播放相关事件,如AudioPlayer.PlaybackStarted
- 以VideoPlayer为前缀的视频播放相关事件,如VideoPlayer.PlaybackQueueCleared
- 其他event requests,如屏幕点击事件Display.ElementSelected
你的技能可以根据需要接收并响应这几种类型的请求。
在java-bot-sdk的基类BaseBot
中,dispatch()
方法完成了请求类型的判定进而调用相应的处理函数,如onLaunch()
,onIntent()
,onSessionEnded()
等。这三个方法分别接收相应类型的Request请求体作为参数(LaunchRequest,IntentRequest,SessionEndedRequest)。
对于AudioPlayer requests和VideoPlayer requests,要处理这两种类型的请求,你的bot需要继承VideoPlayer和 AudioPlayer来实现其中的接口(VideoPlayer和AudioPlayer也是BaseBot
的子类,只是提供了处理AudioPlayer requests和VideoPlayer requests请求的接口),你可以重写其中的接口来完成对相关请求的处理,比如onPlaybackStartedEvent()
、onPlaybackNearlyFinishedEvent()
等。
基类BaseBot
中同时包含了Request请求的序列化和反序列化,基于request对象,可以获取请求中的Context和Session信息。
// 获取请求中的context
Context context = this.getRequest().getContext();
// 获取session
Session session = this.getRequest().getSession();
// 对于session值的获取,basebot中直接提供了一个接口获取session中的参数
this.getSessionAttribute("要查的key");
以下详细介绍这几种请求类型。
LaunchRequest
当用户使用技能的调用名称打开技能时,你的技能将收到一个LaunchRequest。一个LaunchRequest永远是开始一个新的会话。
用户:小度小度,打开个税查询
对于 LaunchRequest,技能可以直接做出响应,不需要从用户获取额外的信息。一般会在onLaunch()
方法中返回欢迎语或者技能首页。
下面的Java 例子,技能对LaunchRequest请求的处理是返回欢迎语及首页文本卡片。
@Override
protected Response onLaunch(LaunchRequest launchRequest) {
// 新建文本卡片
TextCard textCard = new TextCard("所得税为您服务");
// 设置链接地址
textCard.setUrl("www:....");
// 设置链接内容
textCard.setAnchorText("setAnchorText");
// 添加引导话术
textCard.addCueWord("欢迎进入");
// 新建返回的语音内容
OutputSpeech outputSpeech = new OutputSpeech(SpeechType.PlainText, "所得税为您服务");
// 构造返回的Response
Response response = new Response(outputSpeech, textCard);
return response;
}
IntentRequest
当用户的query语句映射到你的技能的一个意图时,技能会收到一个IntentRequest。IntentRequest请求中会携带特定的意图及该意图相关的槽位值。
一个意图可以包含一些槽位作为参数,槽位是收集处理该意图需要的重要参数。 关于意图和槽位的相关概念,可以参考文档意图,槽位,用户表达。
值得注意的是,一个IntentRequest可以是一个新启动的会话或继续一个现有的会话。
用户:打开个人所得税。我在北京,月薪八千,交多少税
在这种情况下,发送到你的技能的是IntentRequest, 而不是LaunchRequest。
下面的Java例子中,onInent()
方法中判断意图名称,然后对意图进行相应的处理。
@Override
protected Response onInent(IntentRequest intentRequest) {
// 判断NLU解析的意图名称是否匹配 inquiry
if ("inquiry".equals(intentRequest.getIntentName())) {
// 判断NLU解析解析后是否存在这个槽位
if (getSlot("monthlysalary") == null) {
// 询问月薪槽位monthlysalary
ask("monthlysalary");
return askSalary();
} else if (getSlot("location") == null) {
// 询问城市槽位location
ask("location");
return askLocation();
} else if (getSlot("compute_type") == null) {
// 询问查询种类槽位compute_type
ask("compute_type");
return askComputeType();
} else {
// 具体计算方法
return compute();
}
}
return null;
}
技能开放平台将一些通用的意图,如欢迎意图、取消意图、确认意图等做成了系统意图,提供给开发者,开发者可以直接引用,具体可参考系统意图引用。
为了简化对话模型,java-bot-sdk 提供了Dialog.Delegate指令。使用该指令可以将对话代理给DuerOS完成,DuerOS询问和确认槽位的话术使用的是开发者在技能开放平台默认配置的话术。这样就可以不用在你的技能代码里用写代码的方式去返回话术。详见对话指令。
SessionEndedRequest
你的技能在会话关闭时将收到一个SessionEndedRequest,可能的情况有以下几种:
- 用户主动说:“小度小度,退出”;
- 当设备监听用户的响应时,用户不响应或回复内容与定义的意图不匹配,超过指定监听时间或reprompt次数后,DuerOS会发送SessionEndedRequest到技能关闭会话;
- 服务出现错误,即抛出了异常或出现error。
下面的Java例子中,onSessionEnded()
方法对关闭所得税服务进行处理。
@Override
protected Response onSessionEnded(SessionEndedRequest sessionEndedRequest) {
// 构造TextCard
TextCard textCard = new TextCard("感谢使用所得税服务");
textCard.setAnchorText("setAnchorText");
textCard.addCueWord("欢迎再次使用");
// 构造OutputSpeech
OutputSpeech outputSpeech = new OutputSpeech(SpeechType.PlainText, "欢迎再次使用所得税服务");
// 构造Response
Response response = new Response(outputSpeech, textCard);
return response;
}
需要注意的是,如果你在技能服务响应中设置了shouldEndSession标志为true, 那么此次响应结束后当前会话会关闭,而且你的技能不会收到SessionEndedRequest。
AudioPlayerRequests和VideoPlayerRequests
当你的技能实现了AudioPlayer和VideoPlayer中的接口时,你的技能就可以处理AudioPlayer和VideoPlayer 的相关请求。DuerOS通过发送AudioPlayer和VideoPlayer Requests到你的技能来告知端上的音视频播放状态。
比如端上正在播放音乐,音乐播放即将结束时,会上报事件给DuerOS, DuerOS会发送一个AudioPlayer.PlaybackNearlyFinished事件给你的技能。 技能通过继承AudioPlayer或者VideoPlayer实现其中的接口就可以接收并处理AudioPlayer Requests或者VideoPlayer Requests。
相关请求详见:
一个AudioPlayer Requests的技能请求样例如下。
{
"version": "2.0",
"context": {
(context properties not shown)
},
"request": {
"type": "AudioPlayer.PlaybackNearlyFinished",
"requestId": "33c78c8f-22db-4d6a-bfd5-075d264377b7",
"timestamp": "1501127440",
"token": "12329898321",
"offsetInMilliSeconds": 1000
}
}
继承了AudioPlayer的技能代码实现了处理对应请求的接口onPlaybackNearlyFinishedEvent()
如下。
@Override
protected Response onPlaybackNearlyFinishedEvent(PlaybackNearlyFinishedEvent playbackNearlyFinishedEvent) {
TextCard textCard = new TextCard();
textCard.setContent("处理即将播放完成事件");
textCard.setUrl("www:...");
textCard.setAnchorText("setAnchorText");
textCard.addCueWord("即将完成");
OutputSpeech outputSpeech = new OutputSpeech(SpeechType.PlainText, "处理即将播放完成事件");
// 新建Play指令
Play play = new Play(PlayBehaviorType.ENQUEUE, "url", 1000);
// 添加返回的指令
this.addDirective(play);
Reprompt reprompt = new Reprompt(outputSpeech);
Response response = new Response(outputSpeech, textCard, reprompt);
return response;
}
其他EventRequests
其它的Event Requests,如列表卡片点击事件Screen.LinkClicked和模板列表点击事件Display.ElementSelected等。 以列表卡片的点击事件为例,当点击列表卡片中的某一个卡片时,技能会收到如下格式的请求。
{
"version": "2.0",
"context": {},
"request": {
"type": "Screen.LinkClicked",
"requestId": "33c78c8f-22db-4d6a-bfd5-075d264377b7",
"timestamp": "1501127440",
"token": "点击列表项的对应token"
}
}
对于这类event,BaseBot
已经提供了相应的处理接口,你的技能只需要继承Basebot
并重写其中的方法即可。对应上面的事件,技能可以重写onLinkClickedEvent()
方法即可。
@Override
protected Response onLinkClickedEvent(LinkClickedEvent linkClickedEvent) {
StandardCard card = new StandardCard();
card.setTitle("测试listcard点击事件");
card.setImage("https://skillstore.cdn.bcebos.com/icon/100/c709eed1-c07a-be4a-b242-0b0d8b777041.jpg");
card.setContent("该例子中,点击后返回的被点击项的token");
card.setToken(linkClickedEvent.getToken());
OutputSpeech outputSpeech = new OutputSpeech(SpeechType.PlainText, "测试Screen.LinkClicked事件");
Response response = new Response(outputSpeech, card);
return response;
}
技能返回给DuerOS的响应
响应格式
对于DuerOS发送的请求,你的技能需要完成相应处理并返回响应。技能通过HTTPS(调试环境可使用HTTP/1)协议返回响应数据,数据格式为json。
{
"version" : "2.0",
"context" : {
"intent" : {
"name" : "{{STRING}}",
"slots" : {
"{{STRING}}" : {
"name" : "{{STRING}}",
"value" : "{{STRING}}",
}
}
}
},
"session" : {
"attributes" : {
"{{STRING}}": "{{STRING}}"
},
},
"response" : {
"outputSpeech" : {
"type" : "{{STRING}}",
"text" : "{{STRING}}",
"ssml" : "{{STRING}}",
},
"reprompt" : {
"outputSpeech" : {
"type" : "{{STRING}}",
"text" : "{{STRING}}",
"ssml" : "{{STRING}}",
}
},
"card" : {}
"directives" : [],
"expectSpeech": {{BOOLEAN}},
"shouldEndSession" : {{BOOLEAN}}
}
}
响应的json格式如上,详细的字段说明请参见技能响应。
当你使用Java-bot-sdk来开发你的技能时,ResponseEncapsulation
类表示一个有效的请求响应(该类包含成员version,context,session,response),即该类的实例化对象经过json序列化后就可以得到上述格式的json响应。在实际技能的返回中,你并不需要去手工创建该类的实例,Basebot
中的build()
方法已经完成了将相关响应内容封装到ResponseEncapsulation
对象的过程。你只需要构造一个Response
对象,或者利用basebot
中提供的一些接口去设置session数据,添加返回指令到directives即可。
技能响应包含以下内容
- version
协议版本,当前为“2.0”。 - context
技能返回给DuerOS的信息,用于反馈技能对本次请求的意图理解。 - session
会话信息。 - response
响应内容,DuerOS将会把这些内容转换成对应的设备端指令或DuerOS定义的操作。
响应中的 context 说明
用于反馈给DuerOS的intent结果;当技能结合自己的资源,对本次query有更加合理的理解时,可以修改对应槽位并返回该信息,DuerOS会在后续的query中优化意图解析模型。
"context" : {
"intent" : {
"name" : "{{STRING}}",
"slots" : {
"{{STRING}}" : {
"name" : "{{STRING}}",
"value" : "{{STRING}}",
}
}
}
}
响应中的 session 说明
技能存储在DuerOS的临时数据。如果本次session不结束(shouldEndSession为false),那么在DuerOS发送给技能的下次请求中,session.attributes字段会携带这些临时数据下发至技能。
技能响应中使用session保存临时数据的例子如下。
setSessionAttribute("key_1", "value_1");
setSessionAttribute("key_2", "value_2");
像上面这样设置session后,响应中的session字段数据如下。
"session" : {
"attributes" : {
"key_1": "value_1",
"key_2": "value_2",
}
}
如果本次会话没有结束,下个请求中,技能可以通过如下方法获取session中的数据。
String value_1 = getSessionAttribute("key_1");
响应中的 response 说明
- outputSpeech
表示本次返回结果中需要播报的语音信息。 - outputSpeech.type
TTS类型。 - reprompt
在需要用户输入时,如果用户离开了麦克风没有进行语音输入,或用户输入的语音请求系统无法解析成技能的任意意图,则播报reprompt内容。reprompt.outputSpeech参数定义与上述定义一致。 - card
用户用于输出的card。详见card展示模板。 - directives
技能输出的指令,目前DuerOS支持的指令包括对话指令、AudioPlayer音频播放指令、VideoPlayer视频播放指令和展现模板指令。 - shouldEndSession
是否需要结束本次会话。- true: DuerOS结束本次会话,端上关闭麦克风。
- false:如果expectSpeech是true或者不设置expectSpeech,端上打开麦克风;如果expectSpeech是false,端上关闭麦克风,只有这种情况可以返回AudioPlayer.Play和VideoPlayer.play指令。
- expectSpeech
在shouldEndSession返回false时,可以指定这个字段。- true:端上打开麦克风;
- false:端上关闭麦克风,只有这种情况可以返回AudioPlayer.Play和VideoPlayer.play指令;
- 不设置该字段:端上打开麦克风。
java-bot-sdk 提供了一些接口让你可以方便的设置响应内容,从而得到上述json格式的技能响应。下面以一个对IntentRequest的响应为例,介绍如何用java-bot-sdk库提供的类和接口构造技能响应数据。
@Override
protected Response onInent(IntentRequest intentRequest) {
// 判断NLU解析的意图名称是否匹配 video_play_intent
if ("video_play_intent".equals(intentRequest.getIntentName())) {
Play play = new Play("http://www.video");
play.setPlayBehavior(PlayBehaviorType.REPLACE_ALL);
play.setToken("token");
// 添加指令
this.addDirective(play);
// 构造resonse
OutputSpeech outputSpeech = new OutputSpeech(SpeechType.PlainText, "即将为您播放视频");
Response response = new Response(outputSpeech);
return response;
}
// 判断NLU解析的意图名称是否匹配 video_stop_intent
else if ("video_stop_intent".equals(intentRequest.getIntentName())) {
Stop stop = new Stop();
// 添加指令
this.addDirective(stop);
// 构造resonse
OutputSpeech outputSpeech = new OutputSpeech(SpeechType.PlainText, "即将为您停止播放视频");
Response response = new Response(outputSpeech);
return response;
}
else {
OutputSpeech outputSpeech = new OutputSpeech(SpeechType.PlainText, "其他意图");
Response response = new Response(outputSpeech);
return response;
}
}
SSML使用
技能响应中包含outputspeech字段,DuerOS会将技能返回的response消息里面的文本信息按照一定的规则转化成语音信息进行播放。转化后的语音有着相同特征,如语调、语速、停顿等都相同。
但是在不同的场景下,可能需要不同的语音效果。例如在朗读儿童故事《白雪公主》时,公主说话的声音和小矮人说话的声音,在语调、语速、音色上都不同。这种情况下,开发者可以通过使用SSML标签得到不同的语音效果。
如以下数字会读作“一百二十三”。
<say-as type="number:ordinal">123</say-as>
也可以利用SSML在outputspeech中嵌入音频,提升用户的技能使用体验。
<speak>播放清晨<audio src="http://ssml-example.bj.bcebos.com/qingchen.wav"></audio>,这是一首让你每次聆听都会有不同感动的音乐,早晨若少了纯真鸟语,这一天洒下的阳光都是寂寞的。
</speak>
在技能中利用SSML构造outputSpeech的示例如下。
OutputSpeech outputSpeech = new OutputSpeech(SpeechType.SSML, "<speak>走廊里传来脚步声,乱哄哄的教室突然安静下来。<slience time=\"5s\"></slience>所有人都盯着教室门。</speak>");
DuerOS支持的SSML标签及使用说明详见SSML(语音合成标记语言)。
卡片展现
在有屏设备上,你的技能在回复用户时,可以通过使用卡片展现更生动、丰富的内容。常用的展现卡片类型有文本卡片、标准卡片、标准列表卡片、图片卡片。展现卡片随Response消息一起发送给DuerOS。详见展现卡片。
卡片使用示例如下。
@Override
protected Response onSessionEnded(SessionEndedRequest sessionEndedRequest) {
// 构造TextCard
TextCard textCard = new TextCard("感谢使用所得税服务");
textCard.setAnchorText("setAnchorText");
textCard.addCueWord("欢迎再次使用");
// 构造OutputSpeech
OutputSpeech outputSpeech = new OutputSpeech(SpeechType.PlainText, "欢迎再次使用所得税服务");
// 构造Response
Response response = new Response(outputSpeech, textCard);
return response;
}
展现模板
为了更好地在有屏设备端上展现技能,在卡片展现之外,DuerOS提供了多种展现模板供开发者使用。展现模板分body template和list template两种类型。其中body template由图片和文字组成,list template由一系列list item组成,每个list item由图片和文字组成。不同的展现模板适合不同的场景,开发者可以根据技能展现的需求选择合适的模板。 详见展现模板。
模板使用示例如下。
@Override
protected Response onLaunch(LaunchRequest launchRequest) {
ListTemplate1 listTemplate = new ListTemplate1();
listTemplate.setTitle("title");
listTemplate.setToken("token");
// 设置模版列表数组listItems其中一项,即列表的一个元素
ListItem listItem = new ListItem();
listItem.setPlainPrimaryText("一级标题");
// 也可以链式设置信息
listItem.setPlainSecondaryText("二级标题")
.setPlainTertiaryText("三级标题")
.setImage("https://skillstore.cdn.bcebos.com/icon/100/c709eed1-c07a-be4a-b242-0b0d8b777041.jpg");
// 把listItem添加到模版listTemplate
listTemplate.addListItem(listItem);
// 定义RenderTemplate指令
RenderTemplate renderTemplate = new RenderTemplate(listTemplate);
this.addDirective(renderTemplate);
OutputSpeech outputSpeech = new OutputSpeech(SpeechType.PlainText, "BodyTemplate1 模板");
Response response = new Response(outputSpeech);
return response;
}
请求认证
为了防止恶意请求的攻击,你的技能可以对请求进行校验,校验请求是否来自DuerOS。验证方法如下为获取HTTP请求header中的签名signature、证书地址signaturecerturl以及body信息message。 详见通信认证的原理。
java-bot-sdk中已经封装了校验请求的方法,只需要简单的几行代码即可对接收到的请求进行校验。
// 根据签名、证书地址、HTTP body构造Certificate对象
Certificate certificate = new Certificate(message, signature, signaturecerturl);
bot.setCertificate(certificate);
// 打开签名验证
bot.enableVerify();
在调试过程中,可以关闭认证。
// 关闭签名验证
bot.disableVerify();
数据统计
DuerOS支持技能使用回调功能,只有当技能开启回调功能时,才能够向DuerOS发送请求,实现技能的数据统计、技能通知等功能。使用回调功能的技能在使用Web Service部署时需要完成以下操作。
- 在技能平台上填写部署地址和Public Key
- 技能需要验证DuerOS发送的请求
- 技能给DuerOS发送请求时,需要满足DuerOS认证要求
详见Web Service部署 。
java-bot-sdk里已经集成了数据统计功能(默认该功能是关闭的),你只需要进行简单的几步操作就可以使用技能的数据统计功能。
- 打开终端,执行
openssl genrsa -out rsa_private_key.pem 1024
命令生成私钥。 - 执行
openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
生成和私钥匹配的公钥。 - 执行
openssl pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt >> rsa_private_key_java.psk
命令,将私钥转换成pkcs8格式。 - 使用编辑器打开公钥文件,将文件的完整内容上传到DBP平台对应的技能空间。 保存rsa_private_key_java.psk私钥文件。
- 在技能的构造函数里开启数据上报功能。假设你在
TaxBot()
使用HttpServletRequest作为参数实现构造方法,把rsa_private_key_java.psk文件里内容赋值给String类型的privateKey变量。public TaxBot(HttpServletRequest request) throws IOException { super(request); String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALcD5iNhmR/fN9Sw\n" + "D9dmiyZo31wVapoUmDTjqHOe0qpxQTgRYNqaR8cxmIKpk1IzPskJEYwAl37wu12X\n" + "tobCZuIZSScrnuSZFozC33DWg3DR4dngf7S1FS08FLKUfAZ7H0rPzuOMRtFUj6Yk\n" + "iVKArnzNVoTT2bmlrEeq6ttAY5RnAgMBAAECgYBe5MGmZMudsALl3+hG2p+Z6dSu\n" + "jVg5ziXhfo1wbdBzmcekR7Z9gnNnQDsAvOZrP7D1UiNsAT6MDkxISgrVMuVew91q\n" + "rEfu7MbmUx6dp4wlVSJOtzhF7VqiisV3zr8EHbf9utWX9yqwhUlszBrsx8Cqvy/B\n" + "mTsWSmkCST1jFBzV+QJBAOMpYhoyUBjrbq0Y7y7oa8RmW+tmMjUfCbR9W4lFPomN\n" + "bxnVpwA7OefLzdBjzRM/pfEfQZYSPJYWnENPO7LTxyMCQQDOP8icc9sjWRY//Jtr\n" + "IIvq3jyAV/o6GwJVXUvwCLTZD+RxkNwUsVVio+bfQ7eBgbb8j7tKKMvftrjKQ11O\n" + "WvPtAkA19vHQSV2P3fZH9uFzYlGfsbVqgbexuPLkRteFD8cghFH9cC0hN/C0qUz2\n" + "kY75YKh6VLOPBDwSZ8KtltgWzorDAkBKgoh63PAB6SE8pImRPgTOKNM6mo3vh+pj\n" + "5HyWjs6mzDL/RBH998KdDBFP/yrAQphUzagftnVQsLY5e/StZfZRAkEAnrolcj06\n" + "+77j6Ibc++C+IAgUYiuo+ZZmVTDOI0BS1lC6kZz8HMlAqDl4Mf7HulijcdHqm/Z0\n" + "XgtBxVoMpmbJmQ=="; //privateKey为私钥内容,0代表你的技能在DBP平台debug环境,1或者其他整数代表online环境,botMonitor对象已经在bot-sdk里初始化,可以直接调用 this.botMonitor.setEnvironmentInfo(privateKey, 0); }
你也可以在代码里随时开启和关闭数据统计功能。
//true代表开启,false代表关闭。调用setEnvironmentInfo之后会默认开启
this.botMonitor.setMonitorEnabled(false);