对话机器人 Rasa(三十二):新建一个基于 socket.io 的自定义 channel

更新日期: 2024-02-14 阅读次数: 374 字数: 1145 分类: AI

在之前的笔记中,整理了如何新建一个独立的 channel 来区分不同的客户端类型。

对话机器人 Rasa(十九):rasa 不同客户端类型区分处理 custom channel

但是这个是基于 HTTP 协议的,无法实现实时双向消息通信。例如,在一个 rasa 请求中, 需要执行一系列耗时的操作,需要在每个操作执行前,向客户端发送一个提示, 这个需求,用 HTTP 的接口是无法实现的。而,基于 WebSocket 协议的 Socket.IO 则能很好的解决这个问题。

此外,在需要人工客服向客户端主动推送消息的场景下,Socket.IO 也优势尽显。 可见,在聊天机器人领域,Socket.IO 是第一选择。

前提

需要对 Socket.IO 的概念,及 python socket.io 库有基础的了解。比如 emit 函数是用来干啥的。

参考这里:

https://socket.io/zh-CN/docs/v4/

更新策略

新建一个 socket.io 的 channel, 同时保留原有的两个基于 HTTP 的 channel, 保证在更新时,不影响现有的 HTTP 接口。再客户端完全支持 socket.io 协议之后,再去掉原有的 HTTP custom channel。

实现方案

参考 Rasa 官方论坛里的这个讨论。

https://forum.rasa.com/t/custom-output-payloads-with-socket-io/38658/3

I ran a few more tests on the socket.io connector. I think the issue is in the socketio channel. This is what i did: Copied the “rasa/core/channels/socketio.py” implementation into my rasa open source folder. Added it as a custom connector in the credentials.yml

思路很简单,就是复制一份内置的 socketio channel 代码,然后做修改调整。

github 上的参考项目

from rasa.core.channels.channel import (
    InputChannel,
    CollectingOutputChannel,
    UserMessage,
)

通过搜索关键词:

rasa.core.channels.channel

确实能找到不少开源的类似实现:

  • 一个实现: https://github.com/LuoFanA595/Medical-Robot-AI/blob/b9fdfdff8f759db3193ad01617a77488bb694e58/socketio_connector.py#L29
  • 另一个实现: https://github.com/iai-group/dagfinn/blob/main/addons/customconnector.py

内置的实现

例如本机上:

/home/sunzhongwei.com/.local/lib/python3.8/site-packages/rasa/core/channels

查看目录中的 python 代码文件列表:

> ls -lah

__init__.py
__pycache__/
botframework.py
callback.py
channel.py
console.py
facebook.py
hangouts.py
mattermost.py
rasa_chat.py
rest.py
rocketchat.py
slack.py
socketio.py
telegram.py
twilio.py
twilio_voice.py
webexteams.py

socketio.py 应该就是我们需要的。复制一份放到 addons 目录下。

socket.io 地址

https://github.com/iai-group/dagfinn/blob/main/ui/furhat-screen/index.js

网页端,js 代码:

const IP = "localhost"
const PORT = "5005"
var socket = io(`http://${IP}:${PORT}`, { path: "/new_path_socket.io" });

Rasa custom channel 配置:

class SocketIOOutput(OutputChannel):
    @classmethod
    def name(cls) -> Text:
        return "new_path_socket.io"

有好几个地方需要修改,需要留意。

error: Blueprint names must be unique

AssertionError: A blueprint with the name "socketio_webhook" is already registered. Blueprint names must be unique.

修改名称:

def blueprint(
    self, on_new_message: Callable[[UserMessage], Awaitable[Any]]
) -> Blueprint:
    """Defines a Sanic blueprint."""
    # Workaround so that socketio works with requests from other origins.
    # https://github.com/miguelgrinberg/python-socketio/issues/205#issuecomment-493769183
    sio = AsyncServer(async_mode="sanic", cors_allowed_origins=[])
    socketio_webhook = SocketBlueprint(
        sio, self.socketio_path, "socketio_webhook_windows", __name__
    )

error: Route already registered

sanic_routing.exceptions.RouteExists: Route already registered: socket.io [GET,OPTIONS,POST]

修改:

class SocketIOInput(InputChannel):
    """A socket.io input channel."""

    @classmethod
    def name(cls) -> Text:
        return "windows_socketio"

    @classmethod
    def from_credentials(cls, credentials: Optional[Dict[Text, Any]]) -> InputChannel:
        credentials = credentials or {}
        return cls(
            credentials.get("user_message_evt", "user_uttered"),
            credentials.get("bot_message_evt", "bot_uttered"),
            credentials.get("namespace"),
            credentials.get("session_persistence", False),
			#credentials.get("socketio_path", "/socket.io"),
			credentials.get("socketio_path", "/new_path_socket.io"),
            credentials.get("jwt_key"),
            credentials.get("jwt_method", "HS256"),
            credentials.get("metadata_key", "metadata"),
        )

error: emit() got an unexpected keyword argument

await self.sio.emit(self.bot_message_evt, **json_message)
TypeError: emit() got an unexpected keyword argument 'my_custom_key'

由于我的 domains.yml 中的话术中大量添加了自定义字段 custom,但是发现复制过来的 socketio.py 无法处理这个自定义字段。

async def send_custom_json(
	self, recipient_id: Text, json_message: Dict[Text, Any], **kwargs: Any
) -> None:
	"""Sends custom json to the output"""

	json_message.setdefault("room", recipient_id)

	print(json_message)
	await self.sio.emit(self.bot_message_evt, **json_message)

json_message 打印出来格式示例

{'my_custom_key': 'sunzhongwei.com', 'room': 'rIir_Hbz_vAc8-QBAAAB'}

修复方法:

async def send_custom_json(
	self, recipient_id: Text, json_message: Dict[Text, Any], **kwargs: Any
) -> None:
	"""Sends custom json to the output"""

	json_message.setdefault("room", recipient_id)

	print(json_message)
	# await self.sio.emit(self.bot_message_evt, **json_message)
	data = {"custom": json_message}
	await self.sio.emit(self.bot_message_evt, data)

消息格式统一

从浏览器端的网络调试工具可以看到网页端 js 与 rasa socket.io 的通信消息格式:

["user_uttered",{"message":"hello from client","session_id":"04729e074b694d4da2ec26082878d5bf"}]
["bot_uttered",{"text":"hello from sunzhongwei.com server"}]
["bot_uttered",{"custom":{"key1":"value1","room":"DV_DILgJW72n5sJiAAAD"}}]

根据之前使用 flask socket.io 的经验可知,消息中的 user_uttered/bot_uttered 是对应的监听的事件名。 这两个事件名可以在 credentials.yml 中进行修改。

具体的消息内容跟之前 HTTP 消息格式最大的区别是,这里都是 dict 而不是 list。

metadata

很多时候,需要客户端将一些自定义的数据发送到 Rasa,就需要用到 metadata 字段。

参考:

https://rasa.com/docs/rasa/connectors/your-own-website/

The socket client can pass an object named metadata to supply metadata to the channel. You can configure an alternative key using the metadata_key setting. For example, if your client wants to pass metadata on a key named customData, the setting would be:

这个自定义数据的 key 可以在 credentials.yml 配置文件中做自定义:

socketio:
  metadata_key: customData

可以基于网页 js 写个测试客户端,或者 python client 来验证。

查看合集

📖 对话机器人 Rasa 中文系列教程

参考

  • https://rasa.com/docs/rasa/connectors/custom-connectors

tags: rasa

关于作者 🌱

我是来自山东烟台的一名开发者,有敢兴趣的话题,或者软件开发需求,欢迎加微信 zhongwei 聊聊, 查看更多联系方式