🚀 原文地址:
Rasa 默认打开的 API 接口是 http://localhost:5005/webhooks/rest/webhook,我们可以通过 POST 数据到该端口下,从而拿到 Rasa 机器人的回复。例如,你可以 POST 如下一个请求,并拿到机器人的回复。
$ curl -XPOST http://localhost:5005/webhooks/rest/webhook \
-d '{"sender": "user1", "message": "你好"}' \
-H "Content-type: application/json"
# Terminal 中得到的响应
[{"recipient_id":"user1","text":"Hello World!"}]
上述便是 Rasa 默认开启的 Rest 通道,但在实际场景中,我们并不会使用这个 Rest 通道,因为所传进去的不可能是简单的 {"sender": "user1", "message": "你好"}
JSON 格式,可能包含更多的键值对,且传进去的数据我们大概率也是需要在程序中进行使用的。
Rasa 支持我们自定义通道,它可以帮助我们实现特定的 API 接口,也就是说我们可以新开启 一个http://localhost:5005/webhooks/bird/webhook API 端口,然后传进去 JSON 数据也可以在通道中进行处理。
当然,Rasa 中提供了多个通道连接器,包括 Facebook Messager、Slack、Telegram、Twilio、Google Hangouts Chat、Microsoft Bot Framework、Cisco Webex Teams、RocketChat、MatterMost,同时你还可以连接你自己的个人网站。目前我并没有使用过这些通道,就不多作介绍了。
在 Rasa 中是通过定义一个 Python 类来实现自定义通道,可以使用 rasa.core.channels.rest.RestInput
类作为模板代码进行修改,能让你快速上手定义自定义通道。
1. name 方法
所实现的类必须继承自 InputChannel
类,且必须包含 name
方法,它的作用是定义连接器 webhook 中的 URL前缀,例如上面的 URL 中的 bird。同时,name
方法也定义了通道的名称,以便你使用任何通道下特殊变种响应。同样地,你应该将通道的名称传给 output_channel
中查询参数来触发意图。
下面我们将自定义一个名为 bird
的通道,具体 name
方法定义如下:
from rasa.core.channels.channel import InputChannel
class BirdInput(InputChannel):
def name() -> Text:
"""Name of your custom channel."""
return "bird"
如果你希望使用 bird
通道来实现一个特定的回复,例如在 domain.yml 文件中这样进行使用:
responses:
utter_greet:
- text: Hi! I'm the default greeting.
- text: Hi! I'm the custom channel greeting
channel: myio
这样定义之后,我们自定义通道的 webhook 将会是 http://localhost:5005/webhooks/bird/webhook。
2. blueprint 方法
蓝图的方法的使用需要创建一个基于 Sanic 服务器的 blueprint,所实现的 blueprint 至少要包含两种路由:
- health:对应
/
路由 - receive:对应
/webhook
路由
先来看一个完整的示例,我们即将所发出的请求是这样的:
curl -XPOST http://localhost:5005/webhooks/bird/webhook \
-H "Content-type: charaset=utf-8" \
-d '
{
"sender_id": "9a05df6b",
"question": "are you a bot?",
"metadata": {
"name": "张三",
"gender": "男",
"age": 16,
"occ": "student",
"school": "北京大学"
},
"sendTime": "202109081625300214",
"answerResult": {}
}'
接下来我们使用 BirdInput 通道(利用 CollectingOutputChannel
类)来接收它,且打印出传进来的内容:
import json
import asyncio
import inspect
import rasa.utils.endpoints
from sanic import Sanic, Blueprint, response
from sanic.request import Request
from sanic.response import HTTPResponse
from typing import Text, Dict, Any, Optional, Callable, Awaitable, NoReturn
from rasa.core.channels.channel import (
InputChannel,
CollectingOutputChannel,
UserMessage,
)
class BirdInput(InputChannel):
@classmethod
def name(cls) -> Text:
"""Name of your custom channel."""
return "bird"
def get_metadata(self, request: Request) -> Dict[Text, Any]:
content_type = request.headers.get("content-type")
if content_type == "application/json":
return request.json.setdefault("metadata", {})
else:
return {}
def blueprint(
self,
on_new_message: Callable[[UserMessage], Awaitable[None]]
) -> Blueprint:
bird_webhook = Blueprint(
"bird_webhook_{}".format(type(self).__name__),
inspect.getmodule(self).__name__,
)
@bird_webhook.route("/", methods=["GET"])
async def health(request: Request) -> HTTPResponse:
return response.json({"status": "ok"})
@bird_webhook.route("/webhook", methods=["POST"])
async def receive(request: Request) -> HTTPResponse:
sender_id = request.json.get("sender")
text = request.json.get("question")
send_time = request.json.get("send_time")
metadata = self.get_metadata(request)
input_channel = self.name()
collector = CollectingOutputChannel()
await on_new_message(
UserMessage(
text=text,
output_channel=collector,
sender_id=sender_id,
input_channel=input_channel,
metadata=metadata,
)
)
output_json = [
{
"senderId": sender_id,
"question": text,
"metadata": metadata,
"sendTime": send_time,
"answerResult": {
"answer": collector.messages[0]["text"],
}
}
]
return HTTPResponse(json.dumps(output_json, ensure_ascii=False),
content_type="application/json;charset=utf-8")
return bird_webhook
health
部分的实现比较简单,主要是用于测试访问是否正常。
receive
部分的实现中,你需要告诉 Rasa 如何去处理用户所发出的消息,对应到上述代码部分为:
on_new_message(
rasa.core.channels.channel.UserMessage(
text=text,
output_channel=collector,
sender_id=sender_id,
input_channel=input_channel,
metadata=metadata,
)
)
调用 on_new_message
时,将会把用户消息传给 handle_message
方法。output_channel
参数是指实现 OutputChannel
类的输出通道,你也可以使用方法来实现自己输出通道类(例如发送文本和图像的方法),或者当机器人在处理的消息时,你也可以使用 CollectingOutputChannel
收集机器人的响应,并将它们作为返回端点响应中的一部分,这就是 RestInput
通道的实现方式。在实现自定义输出通道时,参考一下其他输出通道的实现方式,例如 rasa.core.channels.slack
中的 SlackBot
,将对你非常有帮助。
:::info
💡 值得一提的是,如果使用 response.json(collector.messages)
的话,使用 CURL 得到结果将会是 Unicode 字符,这对于终端调试显然不太友好,所以使用 HTTPResponse(...)
可以中文字符显示出来。
:::
3. 消息中的元数据
如果你需要在自定义操作中使用来自前端的额外信息,可以在消息中通过定义 metadata
键来进行传递。如果适用的话,元数据信息将随着用户消息从 Rasa Server 进入 Action Server,它将存储在 **tracker**
中。消息中元数据并不会直接影响 NLU 分类或者动作的预测。
💡 注意一下,我们所传递进来的元数据是存储在
tracker
中的,后面我们将会根据元数据信息来进行个性化的回复。
InputChannel
类中 get_metadata
实现方法默认忽略了所有元数据,如果想在自定义连接器中提取元数据,那么需要实现 get_metadata
方法。 SlackInput
类中 get_metadata
方法实现了根据通道的响应格式提取元数据,你可以将其参考作为样例。
4. 自定义通道的凭证
要使用自定义通道的话,你需要在 credentials.yml 配置文件提供对应的凭证,必须包含通道模块路径(非通道的名称)以及必需的配置参数。
例如上面的 BirdInput
的自定义连接器类,我们将其保存在文件 addons/bird_channel.py 中,对应的路径为 addons.bird_channel.BirdInput
,对应的凭证如下:
addons.bird_channel.BirdInput:
username: "user_name"
another_parameter: "some value"
为了让 Rasa 知道自定义通道,在启动 Rasa Server 前,我们需要在 credentials.yml 指定路径,并通过命令行参数 --credentials
来加载该文件。
5. 测试自定义连接器的Webhook
为测试自定义连接器,我们可以 POST 消息到对应的 Webhook,相应的 JSON 数据如下:
{
"sender_id": "9a05df6b",
"question": "are you a bot?",
"metadata": {
"name": "张三",
"gender": "男",
"age": 28,
"occ": "student",
"schoOl": "北京大学"
},
"sendTime": "202109081625300214",
"answerResult": {}
}
完成上述所有操作之后,我们来运行 Rasa Server:
rasa run -p 5005 --enable-api \
--endpoints ./endpoints.yml \
--credentials ./credentials.yml \
-m models/
我们通过如下请求来获取机器人的回复:
$ curl -XPOST http://localhost:5005/webhooks/bird/webhook \
-H "Content-type: application/json" \
-d '{
"sender_id": "9a05df6b",
"question": "are you a bot?",
"metadata": {
"name": "张三",
"gender": "男",
"age": 16,
"occ": "student",
"school": "北京大学"
},
"sendTime": "202109081625300214",
"answerResult": {}
}'
# 得到的结果
[{"senderId": null, "question": "are you a bot?", "metadata": {"name": "张三", "gender": "男", "age": 16, "occ": "student", "school": "北京大学"}, "sendTime": null, "answerResult": {"answer": "I am a bot, powered by Rasa."}}]