对话机器人(六)——RASA:Actions

action:接收用户输入和对话状态信息,按照业务逻辑进行处理,并输出改变对话状态的事件回复用户的消息

1. 回复动作

和domain中的response关联在一起,当调用这类动作时,会自动查找response中同名的模板并渲染。

需要和回复模板名字相同。

2. 表单

填表:多次和用户交互,收集任务所需要的要素,直到所需的要素收集完整。

a. 流程

(1)用户表达自己的需求(意图和实体)。

(2)根据用户意图,确定合适的表单,将用户在对话中提供的实体信息填入其中。机器人查看表单中缺失的字段,按照一定的策略询问用户关于缺失字段的问题。

(3)用户提供缺失字段信息。

(4)机器人将缺失信息填入表单,询问下一个缺失字段。

(5)往复迭代,直到机器人发现表单填写完整,于是开始执行具体任务。

b. 用法

需要添加RulePolicy到policies中:

policies:
- name: RulePolicy
(1) 定义表单
  • 表单名:stories/rule中执行处理表单操作的名称。

  • required_slots:列出需要的槽

# domain.yml
entities:
- cuisine
- number
slots:
  cuisine:
    type: text
    mappings:
    - type: from_entity
      entity: cuisine
  num_people:
    type: any
    mappings:
    - type: from_entity
      entity: number
# 表单restaurant_form,需要填充的槽:cuisine、num_people
forms:
  restaurant_form:
    required_slots:
        - cuisine
        - num_people

ignored_intents:表单要忽略的意图列表,这些意图将添加到表单中每个槽映射的not_intent键中。

如,若不希望在意图是“闲聊”时填写表单的任何插槽,需要定义以下内容:

forms:
  restaurant_form:
    ignored_intents: 
    - chitchat
    required_slots:
        - cuisine
        - num_people

一旦表单操作被第一次调用,表单就会被激活,并提示用户输入下一个所需的槽值。它通过查找一个名为utter_ask_<form_name>_<slot_name>utter_ask_<slot_name>的回复来实现。请确保在domain文件中为每个所需的插槽定义这些响应。

(2) 激活表单

添加一个故事或规则,描述机器人何时运行表单。

# 在request_restaurant意图下触发表单
rules:
- rule: Activate form
  steps:
  - intent: request_restaurant
  - action: restaurant_form
  # 表示表单在运行restaurant_form后被激活。
  - active_loop: restaurant_form
(3) 停用表单

一旦所有必需的槽位都被填满,表单将自动停用。若没有添加适用的故事或规则,则机器人将在表单完成后自动侦听下一条用户消息。

rules:
- rule: 提交表单
  condition:
  # 条件是:表单处于活跃状态
  - active_loop: restaurant_form
  steps:
  # 表单停用
  - action: restaurant_form
  - active_loop: null
  - slot_was_set:
    - requested_slot: null
  # 一旦槽填满后,提交表单时要运行的动作
  - action: utter_submit
  - action: utter_slots_values
(4)为不愉快的表单路径编写故事/规则

用户有可能没有按照表单的填充形式回复,即用户回复的内容不是需要填充槽的内容。

当表单处于活跃状态时,用户输入未填满请求的槽,则表单操作的执行将被拒绝,即自动引发ActionExecutionRejection。以下是表单将引发ActionExecutionRejection的特定场景:

  • 已请求插槽,但用户最新一条消息没有填充该插槽,并且没有定义用于验证插槽或提取插槽的自定义动作。
  • 已请求插槽,但用于验证插槽或提取插槽的自定义操作未返回任何SlotSet事件。
rules:
- rule: Example of an unhappy path
  condition:
  # 条件是:表单处于活跃状态
  - active_loop: restaurant_form
  steps:
  # 这条不愉快的路径处理的是意图“chitchat”的情况。 
  - intent: chitchat
  - action: utter_chitchat
  # 处理完“chitchat”意图后回到状态
  - action: restaurant_form
  - active_loop: restaurant_form

用户可能会在表单操作过程中改变主意,并决定不继续最初的请求。在这种情况下,机器人应停止要求提供所需的插槽,action_deactivate_loop,该操作将停用表单并重置请求的槽。

stories:
- story: User interrupts the form and doesn't want to continue
  steps:
  - intent: request_restaurant
  - action: restaurant_form
  - active_loop: restaurant_form
  - intent: stop
  - action: utter_ask_continue
  - intent: stop
  - action: action_deactivate_loop
  - active_loop: null

c. 高级用法

可以使用自定义动作自定义表单。

(1) 验证表单输入

自定义动作validate_<form_name>来验证任何提取的插槽。

# domain.yml
actions:
- validate_restaurant_form

执行表单时,它将运行自定义动作。此自定义动作可以扩展FormValidationAction类以简化验证提取的插槽的过程。在这种情况下,需要为每个提取的插槽编写名为validate_<slot_name>的函数。

下面的示例显示了一个自定义动作的实现,该动作验证槽cuisine是否有效。

from typing import Text, List, Any, Dict
from rasa_sdk import Tracker, FormValidationAction
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk.types import DomainDict

class ValidateRestaurantForm(FormValidationAction):
    def name(self) -> Text:
        return "validate_restaurant_form"

    @staticmethod
    def cuisine_db() -> List[Text]:
        """cuisines数据库"""
        return ["caribbean", "chinese", "french"]

    def validate_cuisine(
        self,
        slot_value: Any,
        dispatcher: CollectingDispatcher,
        tracker: Tracker,
        domain: DomainDict,
    ) -> Dict[Text, Any]:
        """验证 cuisine值"""
        if slot_value.lower() in self.cuisine_db():
            # 如果验证成功,将槽“cuisine”的值设置为slot_value
            return {"cuisine": slot_value}
        else:
            # 验证失败,将此槽设置为None,以便用户将再次请求该槽
            return {"cuisine": None}

还可以扩展Action类并使用tracker.slots_to_validate检索提取的插槽,以完全自定义验证过程。

(2) 自定义插槽映射

如果预定义的槽映射都不适合您的用例,那么您可以使用自定义动作validate_<form_name>来编写自己的提取代码。Rasa开源将在表单运行时触发此操作。

如果使用的是Rasa SDK,可以继承FormValidationAction。使用FormValidationAction时,需要三个步骤来提取自定义槽:

  1. 为每个以自定义方式映射的插槽定义方法extract_<slot_name>
  2. domain文件中,表单required_slots列出所必需插槽,包括预定义和自定义映射。
  3. 重写required_slots,将具有自定义映射的插槽添加到表单要求的插槽列表中。
# 除了使用预定义映射的插槽外,还以自定义的方式提取了一个outdoor_seat插槽。extract_outdoor_seating方法根据关键字outdoor是否出现在上一条用户消息中来设置outdoor_seating槽。
from typing import Dict, Text, List, Optional, Any

from rasa_sdk import Tracker
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk.forms import FormValidationAction


class ValidateRestaurantForm(FormValidationAction):
    def name(self) -> Text:
        return "validate_restaurant_form"

    async def extract_outdoor_seating(
        self, 
        dispatcher: CollectingDispatcher, 
        tracker: Tracker, domain: Dict
    ) -> Dict[Text, Any]:
        text_of_last_user_message = tracker.latest_message.get("text")
        sit_outside = "outdoor" in text_of_last_user_message

        return {"outdoor_seating": sit_outside}

默认情况下,FormValidationAction会自动将requested_slot设置为required_slots中指定的第一个未填充的插槽。

(3) 动态表单行为

❃ 默认情况下,Rasa将从domain文件的表单下列出的插槽中请求下一个空插槽。如果使用自定义槽映射和FormValidationAction,它将请求required_slots方法返回的第一个空槽。如果required_slots中的所有槽都已填满,则表格将被停用。

❃ 若有需要,**可以动态更新表单中所需的槽。**如需要基于上一个插槽的填充方式的更多详细信息,或者希望更改请求插槽的顺序。

方法:继承FormValidationAction并重写required_slots,为每个不使用预定义映射的插槽实现extract_<slot name>方法。

  • 在特定条件下从domain.yml中定义的表单添加一个插槽:
# 万一用户说想坐在外面,询问他们是想坐在阴凉处还是阳光下。
from typing import Text, List, Optional
from rasa_sdk.forms import FormValidationAction

class ValidateRestaurantForm(FormValidationAction):
    def name(self) -> Text:
        return "validate_restaurant_form"
    async def required_slots(
        self,
        domain_slots: List[Text],
        dispatcher: "CollectingDispatcher",
        tracker: "Tracker",
        domain: "DomainDict",
    ) -> List[Text]:
        additional_slots = ["outdoor_seating"]
        if tracker.slots.get("outdoor_seating") is True:
            # 如果用户想坐在外面,问他们是想坐在阴凉处还是太阳底下。
            additional_slots.append("shade_or_sun")

        return additional_slots + domain_slots
  • 在特定条件下从domain.yml中定义的表单删除一个插槽:

domain_slots复制到新变量,并将更改应用于该新变量,而不是直接修改 domain_slots. 直接修改domain_slots可能会导致意外行为。

from typing import Text, List, Optional
from rasa_sdk.forms import FormValidationAction

class ValidateBookingForm(FormValidationAction):
    def name(self) -> Text:
        return "validate_booking_form"
    async def required_slots(
        self,
        domain_slots: List[Text],
        dispatcher: "CollectingDispatcher",
        tracker: "Tracker",
        domain: "DomainDict",
    ) -> List[Text]:
        # 将domain_slots复制到新变量
        updated_slots = domain_slots.copy()
        if tracker.slots.get("existing_customer") is True:
            # 如果用户是现有客户,删除' email_address '插槽
            updated_slots.remove("email_address")
        return updated_slots
(4) The requested_slot slot

默认情况下,槽requested_slot将作为文本类型的槽自动添加到domain中,对话期间将忽略requested_slot的值。

更改该现象:需要将requested_slot添加到domain文件中,并将其influence_conversation属性设置为true。如果希望以不同的方式处理不愉快的路径,则可能需要这样做,具体取决于用户当前请求的插槽。例如,如果用户用另一个问题回答机器人的一个问题,比如你为什么需要知道这个问题?对这种explain意图的反应取决于我们在故事中的位置。在餐馆的案例中,故事看起来是这样的:

# domain.yml
stories:
- story: explain cuisine slot
  steps:
  - intent: request_restaurant
  - action: restaurant_form
  - active_loop: restaurant
  - slot_was_set:
    - requested_slot: cuisine			      #
  - intent: explain
  - action: utter_explain_cuisine
  - action: restaurant_form
  - active_loop: null

- story: explain num_people slot
  steps:
  - intent: request_restaurant
  - action: restaurant_form
  - active_loop: restaurant
  - slot_was_set:
    - requested_slot: cuisine				#
  - slot_was_set:
    - requested_slot: num_people		     #
  - intent: explain
  - action: utter_explain_num_people
  - action: restaurant_form
  - active_loop: null
(5) 使用自定义操作请求下一个插槽

一旦表单确定用户下一步要填充哪个插槽,它将执行操作utter_ask_<form_name>_<slot_name>utter_ask_<slot_name>以要求用户提供必要的信息。如果常规的话语还不够,您还可以使用自定义动作action_ask_<form_name>_<slot_name>action_ask_<slot_name>来请求下一个插槽。

from typing import Dict, Text, List
from rasa_sdk import Tracker
from rasa_sdk.events import EventType
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk import Action
class AskForSlotAction(Action):
    def name(self) -> Text:
        return "action_ask_cuisine"

    def run(
        self, dispatcher: CollectingDispatcher, tracker: Tracker, domain: Dict
    ) -> List[EventType]:
        dispatcher.utter_message(text="What cuisine?")
        return []

3. 默认动作

默认动作可以被同名的自定义动作替代。

名称功能在客户端输入命令来执行动作
action_listen停止预测动作,等待用户输入。/
action_restart重启对话过程,清理对话历史和词槽。/restart
action_session_start启动对话过程。所有的对话开始前都会执行该动作。当不超过session_expiration_time的值时,该动作会复制上一轮会话中所有词槽至新的会话。/session_start
action_default_fallback重置系统状态至上一轮。渲染utter_default模板返回用户。/
action_deactivate_loop停用当前激活的active_loop,并重置requested_slot的词槽。/
action_two_stage_fallback处理NLU得分较低时触发的fallback逻辑。/
action_default_ask_affirmation被action_two_stage_fallback使用,要求用户确认意图。/
action_default_ask_rephrase被action_two_stage_fallback使用,要求用户重新表达。/
action_action_back回退一轮。回退到最后一次用户消息前。/back

4. 自定义动作

将自定义动作独立成一个接口,自行开发服务后,用HTTP接口的形式和rasa交互,可以使用Rasa SDK构建自定义动作服务器。

a. Rasa SDK

运行自定义动作:

rasa run actions

自定义动作需要继承SDK动作类,服务器可以自动发现并注册动作。

  1. 继承action的类。根据domain.yml设定的动作,创建了继承了action的动作类。
  2. 函数name和run。每个类包含函数name和run,name返回动作名,run包含当前对话信息和用户消息对象。
    • tracker对象
    • domain对象
    • dispatcher
  3. 若对当前的对话状态进行更改(如更改词槽),需要返回事件;若无变化,需要返回一个空的列表。
from rasa_sdk import Action
from rasa_sdk.executor import SlotSet
class ActionCheckRestaurants(Action):
    # 重写name方法,返回一个字符串,向服务器申明该动作名
    def name(self) -> Text:
        return "action_check_restaurants"
    # 重写run方法,获得当前的对话信息(tracker对象和domain对象)、用户消息对象(dispatcher)
    def run(
        self,
        dispatcher: CollectingDispatcher, 
        tracker: Tracker,
        domain: Dict[Text, Any],
    ) ->List[Dict[Text, Any]]:
        
        cuisine = tracker.get_slot('cuisine')
        q="select *from restaurants where cuisine='{0}' limit 1".format(cuisine)
        result=db.query(q)
        # 若对当前的对话状态进行更改(如更改词槽),需要返回事件;若无变化,需要返回一个空的列表。
        return [SlotSet("matches", result if result is not None else [])]

b. tracker 对象

tracker:对话状态追踪,即对话的历史记忆。开发者通过tracker对象来获取当前/历史的对话状态(实体情况和词槽情况)。

tracker对象的属性:

属性说明
sender_id字符串。当前对话用户的唯一ID
slots列表。词槽的列表。
latest_message字典。包含3个键:intent(意图)、entities(实体)、text(用户的话)
events历史上所有的事件
active_form字符串。当前被激活的表单,可能为空
latest_action_name字符串。最后一个动作的名字

tracker对象的方法

方法说明
current_state()返回当前的tracker对象
is_paused()返回当前的tracker对象过程是否被暂停
get_latest_entity_values()返回某个实体的最后值
get_latest_input_channel()返回最后用户所用的输入通道的名字
events_after_latest_restart()返回最后一次重启后的所有事件
get_slot()返回一个词槽的具体值

c. 事件对象

如果想要更改对话状态,需要用到事件(event)对象。

通用事件对象:

事件对象说明
SlotSet(key, value=None)要求系统将名字为key的词槽值设置为value
Restarted()重启对话过程
AllSlotReset()重置所有词槽
ReminderScheduled()在指定的时间发起一个意图和实体都给定的请求(定时任务)
ReminderCancelled()取消一个定时任务
ConversationPaused()暂停对话过程
ConversationResumed()继续对话过程
FollowupAction(name)强制设定下一轮动作(不通过预测得到)

Rasa自动跟踪事件(系统创建):

事件对象说明
UserUttered()用户发送的消息
BotUttered()机器人发送给用户的消息
UserUtteranceReverted()撤销用户最后消息后发生的所有事件,通常情况下,只剩下action_listen,机器人会回到等待用户输入的状态。
ActionReverted()撤销上一个动作,清除上个动作所有的事件效果,机器人会重新开始预测下一个动作。
ActionExecuted()记录一个动作,动作创造的事件会被单独记录。
SessionStarted()开始新的对话会话。重置tracker,并触发执行ActionSessionStart(默认情况下,将已存在的SlotSet拷贝到新的会话)

参考文献:
[1] 孔小泉,王冠.Rasa实战:构建开源对话机器人[M].电子工业出版社.2022:201.
[2] RASA官方文档 https://rasa.com/docs/rasa/rules

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值