Rasa根据会话session超时情况切分日志

背景与目的

Rasa状态日志以sender_id为key存储
利用Rasa构建对话机器人时,若使用TrackerStore记录日志,则默认以sender_id(sender,conversation_id,都一样)作为key进行存储,意味着对于用户sender_id=user1,无论何时与机器人对话,其对话记录永远会更新到以user1为key的字典中。

同一用户的状态日志越积越多
表面看起来没有问题,但存在隐含风险,因为Rasa Core的机制是,每次接收到用户message,会根据sender_id召回历史日志,解析历史状态,进而预测下一步该执行的Action。Rasa对历史日志默认使用InMemoryTrackerStore,保存在内存中,所以不太影响速度,但对于实际业务场景,Rasa的历史状态日志会记录到Redis、Mongo、ES等其他存储空间中,用户的历史状态日志会越来越大,召回日志会变得越来越困难,影响系统性能。

期望能按session切分日志
所以我们期望对日志进行分隔,很直观的思路就是按照用户回复是否超时进行会话session切分,每次只召回当前session的状态日志,进入到新session时丢弃上一session的所有状态。例如用户超过60分钟未回复,则下次再回复时,分配新的session_id,并以新session_id为key记录状态日志。

要点

  • 默认情况下,conversation_id就是sender_id。
  • 重启session不重启conversation。

思路与方案

方案一二三都是失败方案,想解决问题可以直接跳到方案四五。

方案一:session_expiration_time

失败原因:重启session不打断对话

在domain.yml配置session_config,例如:

session_config:
	carry_over_slots_to_new_session: false
	session_expiration_time: 0.2

表示用户回复超时0.2分钟后重启session,且丢弃所有槽值。

示例:第一次“你好”后等待至少0.2分钟,再次输入“你好”。日志如下,可以看到,虽然第二次“你好”触发了session重启,但并不是整个对话重启。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nyBgZaIv-1598772456417)(http://km.oa.com/files/photos/pictures/202008/1598772162_94_w1468_h371.png)]
注意看上图最下面一行的“name”,这就是对话的sender_id和conversation_id,不管怎样重启session,这个值是不变的,也没有生成什么新的session_id,始终就是这一条大日志,只是多记录了“action_session_start”Action和"session_started"Event,这样无法对日志切分保存。

参考资料:
Session configuration
Conversation and Sender ID

方案二:Tracker中增加session_id

失败原因:牵扯太多,异常复杂,改动巨大

既然默认的conversation_id不受session重启影响,那可不可以我们自己在Tracker里新增加一个session_id,当重启session时更新session_id并以session_id做key存储新session的日志呢?这就涉及到修改源码了。

从上面的日志看到,重启时启动了一个名为“action_session_start”的Action,首先想在执行这一步时重置session_id,就从这里出发,查找源码,基本路线如下:

  1. action_session_start
  2. 类:rasa.core.actions.action.ActionSessionStart
    函数:run()
    代码:_events = [SessionStarted()]
  3. 类:rasa.core.events.SessionStarted
    函数:apply_to()
    代码:tracker._reset()
  4. 类:rasa.core.trackers.DialogueStateTracker
    函数:_reset()

当你终于找到重启session实际操作Tracker的代码,并增加上session_id的更新程序

def _reset(self, reset_session_id=False) -> None:
	"""Reset tracker to initial state - doesn't delete events though!."""
	
	self._reset_slots()
	self._paused = False
	self.latest_action_name = None
	self.latest_message = UserUttered.empty()
	self.latest_bot_utterance = BotUttered.empty()
	self.followup_action = ACTION_LISTEN_NAME
	self.active_form = {}

	if reset_session_id:
		self._reset_session_id()
		
		
def _reset_session_id(self) -> None:
	"""Set session id to sender_id + timestamp"""
	
	self.session_id = str(self.sender_id) + '_' + datetime.now().strftime("%Y%m%d%H%M%S")
	print("self.session_id: ", self.session_id)

然后你在DialogueStateTracker.init()里增加session_id的时候发现,这货每次初始化都会调用一次_reset(),即,每次Tracker更新,session_id也会跟着自动更新…结果就是session_id没办法在某段时间内固定住。

除了上述问题,你还会发现,rasa底层涉及到conversation的基本上都是以sender_id作为基准,要想记日志以session_id做key,那每次解析日志获取历史状态时也要以session_id为准,包括状态复制、状态更新等等,导致rasa.core.processor甚至rasa_sdk全都要跟着改,异常难搞。

参考资料:
Customizing the session start action
Start a new conversation session
Resetting a session (slots, active forms etc) if last user event is older than N seconds

方案三:Tracker.events_after_latest_restart()

失败原因:没啥卵用

你以为这个函数会获取每次session重启到现在的events,然后在session启动时间上来做文章。但实际上他是获取conversation重启到现在的events,而session又不打断conversation,所以没用。

方案四:自定义Channel

成功原因:在sender进入对话前就进行处理

看来从Tracker内部进行改造是比较困难的,只能保持Rasa现在以sender_id记录对话的逻辑,通过传不同的sender_id来切分不同session(方案五),或者在Rasa接收到sender_id时先进行一步超时的判断处理,然后再传进对话内部。

具体做法是,自定义个性化Channel,在自己的webhook里对sender进行超时判断。接收到sender后,获取缓存的该用户上次交互时间,判断当前时间是否超时或是否是新用户,若超时或是新用户则更新sender_id为sender_timestamp,并记录缓存。

  1. 继承rasa/core/channels/channel.py中的InputChannel类,创建个性化Channel,如:ChatBotChannel,重写sender_id获取部分等。
    ChatBotChannel类

  2. 在credentials.yml中进行配置,如:ChatBotChannel:
    credentials.yml

  3. 启动rasa后,向自定义的Webhook发送信息,如:POST http://localhost:5005/webhooks/chatbot/webhook
    {
    “sender”: “user1”,
    “message”: “你好”
    }

这样,即使后台调用时传过来的是同一用户,也可以在Webhook接收到sender且尚未传给conversation时,将sender_id变成sender_id+’_’+timestamp,并进行超时判断与更新。

可以看到超时后统一用户记录的日志里“name”和"_id"都更新了。
在这里插入图片描述

参考资料:
Rasa user session management
Custom Connectors

方案五:java后台处理

成功原因:同方案四

也可以在java后台侧做超时判断,把更新的session_id传给Rasa,但这样会导致java侧也引入对话相关的逻辑,不够解耦,所以没有采用。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
如果您想要删除一个会话中的一个session,需要先获取该会话对应的session ID以及待删除的session ID。然后,使用session ID和待删除的session ID调用会话管理器的removeSession方法即可删除对应的session。具体代码实现如下: ```python from rasa.core.channels.channel import UserMessage from rasa.core.channels.channel import OutputChannel from rasa.core.channels.channel import InputChannel from rasa.core.channels.channel import CollectingOutputChannel from rasa.core.channels.channel import RestInput from rasa.core.channels.channel import SocketBlueprint class MyInputChannel(InputChannel): @classmethod def name(cls): return "my_input_channel" @staticmethod def on_message_wrapper(on_new_message): def wrapper(*args, **kwargs): output_channel = CollectingOutputChannel() message = UserMessage( kwargs.get("text"), output_channel, kwargs.get("sender_id"), input_channel=kwargs.get("input_channel") ) on_new_message(message) # 获取会话管理器 session_manager = kwargs.get("session_manager") # 获取当前会话session ID current_session_id = message.output_channel.get_session_id(message.sender_id) # 获取待删除的session ID session_to_remove_id = "session_1" # 这里假设待删除的session ID为"session_1" # 如果待删除的session ID和当前会话session ID不同,则删除对应的session if session_to_remove_id != current_session_id: session_manager.remove_session(session_to_remove_id) return output_channel.messages return wrapper class MyOutputChannel(OutputChannel): @classmethod def name(cls): return "my_output_channel" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.messages = [] def send_text_message(self, recipient_id, text): self.messages.append(text) class MyRestInput(RestInput): @classmethod def name(cls): return "my_rest_input" class MySocketBlueprint(SocketBlueprint): @classmethod def name(cls): return "my_socket_blueprint" ``` 在上述代码中,我们通过获取当前会话session ID和待删除的session ID来判断是否需要删除对应的session。如果需要删除,我们就调用会话管理器的removeSession方法来删除对应的session

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值