最常见的对话模式之一是从用户那里收集几条信息从而完成某个事项,例如预定餐厅、调用 API、搜索数据库等,称之为槽填充。
1. 用法
要在 Rasa 中使用表单,我们需要确保将 RulePolicy
添加到策略配置中。例如:
policies:
- name: RulePolicy
1.1 定义表单
通过在域文件中 forms
部分添加表单,表单名称也是在故事或规则中用于处理表单执行的操作。我们需要为必需的 required_slots
建指定插槽名称。以下示例表单 restaurant_form
将填充 cuisine
和 num_people
插槽。
entities:
- cuisine
- number
slots:
cuisine:
type: text
mappings:
- type: from_entity
entity: cuisine
num_people:
type: any
mappings:
- type: from_entity
entity: number
forms:
restaurant_form:
required_slots:
- cuisine
- num_people
我们可以在 ignore_intents
键下为整个表单定义要忽略的意图列表,在 ignore_intents
下列出的意图将被添加到每个插槽映射的 not_intent
键中。
例如,如果我们不希望在意图为 chitchat
时,表单的任何插槽被填充,那么需要定义以下内容(在表单名称下的 ignore_intents
键):
entities:
- cuisine
- number
slots:
cuisine:
type: text
mappings:
- type: from_entity
entity: cuisine
num_people:
type: any
mappings:
- type: from_entity
entity: number
forms:
restaurant_form:
ignored_intents:
- chitchat
required_slots:
- cuisine
- num_people
首次调用表单操作后,表单将被激活,并提示用户输入一个所需的插槽值。它通过查找名为 utter_ask_<form_name>_<slot_name>
或 utter_ask_<slot_name>
的响应来执行此操作(如果未找到前者)。确保在域文件中为每个必需的插槽定义这些响应。
1.2 激活表单
要激活表单,我们需要添加一个故事或规则,它描述了机器人何时运行表单。在特定意图触发表单的情况下,我们可以使用以下规则:
rules:
- rule: Activate form
steps:
- intent: request_restaurant
- action: restaurant_form
- active_loop: restaurant_form
:::info
🐼 注意
————————————active_loop: restaurant_form
步骤表示应该在 restaurant_form
运行后激活表单。
:::
1.3 停用表单
填充完所有必需的插槽后,表单将自动停用。我们可以使用规则或故事来描述机器人在表单结束时的行为,如果没有添加合适的故事或规则,则机器人将在表单完成后自动监听下一条用户消息。以下示例在表单 your_form
填满所有必需的插槽后,立即运行 utter_submit
和 utter_slots_values
。
rules:
- rule: Submit form
condition:
# Condition that form is active.
- active_loop: restaurant_form
steps:
# Form is deactivated
- action: restaurant_form
- active_loop: null
- slot_was_set:
- requested_slot: null
# The actions we want to run when the form is submitted.
- action: utter_submit
- action: utter_slots_values
用户可能希望尽早脱离表单,针对这种情况,我们需要在故事或规则中编写意料之外表单路径,后续我们将进行介绍。
1.4 槽映射
:::danger
⌛ Rasa 3.0 中变化
——————————
在 Rasa 3.0 中,槽映射在域文件中的 slots
部分定义,此改动允许相同的槽映射在多个表单中重复使用,从而消除任何不必要的重复。注意,映射条件和唯一实体映射约束的作用。
:::
2. 为意料之外的表单路径编写故事/规则
用户不会总是回复我们要求提供的信息。通常,用户会提出问题、闲聊、改变注意,或者以其他方式偏离常规的对话路径。
当表单处于或活动状态时,如果用户的输入未填充请求的插槽,则表单的执行将被拒绝,即表单将自定引发 ActionExecutionRejection
。以下是表单引发 ActionExecutionRejection
的特定场景:
- 请求了一个插槽,但用户没有用最后一条消息填充插槽,并且没有定义用于验证插槽或提取槽值的自定义操作。
- 请求了一个插槽,但用于验证插槽或提取槽值的自定义操作未返回任何
SlotSet
事件。
如果要拒绝表单的执行,我们可以返回 ActionExecutionRejected
事件作为自定义验证或槽映射的一部分。要处理可能导致表单执行被拒绝的情况,我们可以编写包含预期中断的规则或故事。例如,如果我们希望用户与机器人闲聊,就可以添加一个规则来处理这个问题:
# @file: rules.yml
rules:
- rule: Example of an unhappy path
condition:
# Condition that form is active.
- active_loop: restaurant_form
steps:
# This unhappy path handles the case of an intent `chitchat`.
- intent: chitchat
- action: utter_chitchat
# Return to form after handling the `chitchat` intent
- action: restaurant_form
- active_loop: restaurant_form
在某些情况下,用户可能会在表单操作的中间改变注意,并决定不继续他们的初始请求。这种情况下,机器人应该停止插槽的请求。我们可以使用默认操作 action_deactive_loop
优雅地处理此类情况,这将停用表单并重置请求的插槽,此类对话的示例故事如下所示:
# @file: stories.yml
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
强烈建议使用交互式学习来构建这些规则或故事,如果手动编写这些规则/故事,我们可能会错过重要的事情。
3. 高阶用法
3.1 验证表单输入
从用户输入中提取槽值后,我们可以验证提取的槽值。默认情况下,Rasa 仅在请求插槽后验证是否填充了任何插槽。我们可以实施自定义操作 validate_<form_name>
来验证任何提取的插槽,确保将此操作添加到域文件的 actions
部分:
actions:
- validate_restaurant_form
当表单执行时,在每个用户验证最后填充的插槽后,Rasa 将运行自定义操作。该自定义操作可以扩展 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]:
"""Database of supported cuisines"""
return ["caribbean", "chinese", "french"]
def validate_cuisine(
self,
slot_value: Any,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: DomainDict,
) -> Dict[Text, Any]:
"""Validate cuisine value."""
if slot_value.lower() in self.cuisine_db():
# validation succeeded, set the value of the "cuisine" slot to value
return {"cuisine": slot_value}
else:
# validation failed, set this slot to None so that the
# user will be asked for the slot again
return {"cuisine": None}
3.2 自定义插槽映射
Rasa 将在窗体运行时触发此操作。
如果使用的是 Rasa SDK,建议在 FormValidationAction
上继续扩展。使用 FormValidationAction
时,在提取自定义插槽时有以下 3 个步骤:
- 为每个插槽(以自定义方式进行映射)定义
extract_<slot_name>
方法 - 在域文件中,对于表单所需的插槽,列出所有所需的插槽,以及预定义和自定义映射
此外,我们可以重写 requested_slots
方法来添加动态请求的插槽。
:::info
💬 注意
————————————
在域文件中的 slots
部分添加了一个自定义映射的插槽,如果我们的目的只是在表单上下文中通过扩展 FormValidationAction
的自定义操作进行验证,请确保该插槽使用的是 custom
类型,并且被包含在表单的 requested_slots
中。
:::
下面这个示例展示了一个表单的实现,除了使用预定义映射的插槽外,该表单还以自定义方式提取插槽。extract_outdoor_seating
方法根据关键字 outdoor
是否出现在最后一条用户消息中来设置 outdoor_seating
插槽。
# @file: actions.py
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.3 动态表单行为
默认情况下,Rasa 将在域文件中表单所列举的插槽中询问下一个空插槽。如果我们使用自定义插槽映射和 FormValidationAction
,它将询问 required_slots
方法返回的第一个空插槽。如果 required_slots
中的所有插槽都已经填满,则表单将被停用。
如果需要,我们可以动态更新表单的所需插槽。例如,的那个我们需要根据前一个插槽的填充方式获取更多详细信息,或者想要更改请求插槽的顺序时,这会很有用。
如果我们使用的是 Rasa SDK,建议使用 FormValidationAction
,并覆盖 required_slots
以适应动态行为。我们应该为每个不使用预定义映射的插槽实现一个方法 extract_<slot_name>
,正如自定义插槽映射中所述。下面的示例将询问用户是否想坐在阴凉处或阳光下,以防他们说想坐在外面。
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:
# If the user wants to sit outside, ask
# if they want to sit in the shade or in the sun.
additional_slots.append("shade_or_sun")
return additional_slots + domain_slots
3.4 requested_slot插槽
requested_slot
插槽作为作为文本类型的插槽被自动添加到域文件中。在对话期间,requested_slot
的槽值将被忽略。如果要更改此行为,则需要将 requested_slot
作为类别插槽添加到域文件中,并将 influence_conversation
设置为 true
。如果想以不同方式处理意外之外的路径,则可能需要执行此操作,具体取决于用户当前询问的插槽。例如,如果用户用另一个问题回答机器人的一个问题,如“why do you need to know that?”,explain
意图的响应取决于故事中所处的位置。在餐厅案例中,我们的故事如下:
# @file: stories.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
3.5 使用自定义操作询问下个插槽
一旦表单确定用户接下来必须填写哪个插槽,它将执行 utter_ask_<form_name>
或者 utter_ask_<slot_name>
操作。如果常规话术还不够,我们也可以使用自定义操作 action_ask_<form_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 []