目录
用法
定义表单
激活 Form
停用表单
Slot Mappings
用户不按照预期回答 Writing Stories / Rules for Unhappy Form Paths
高级用法
验证表单输入
自定义槽位提取 Custom Slot Mappings
动态表单
requested_slot 插槽
自定义 ask slot 话术
常见的对话模式: 从用户那里收集一些信息以执行某些操作(预订餐厅、调用 API、搜索数据库等),从用户收集信息的过程,称为"槽填充"(slot filling)。而 form 表单是用来实现此功能的。
要在 Rasa 中使用Form表单,需要确保将 Rule Policy 策略添加到策略配置中。例如:
policies:
- name: RulePolicy
需要将表单添加到domain的表单部分来定义表单。表单包括两部分:
以下示例,定义了一个表单:
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
此处一定要注意 slot 的 type,如果 type 设置为 any,则不影响对话流程,如果 slot 设置为其他类型,并且 influence_conversation 设置为 true,在写story的时候必须显示的写出来 slot_was_set , 会在其他文档详细分析
Form 可以忽略掉一些 intent ,不进行槽位填充,通过 ignored_intents 实现, ignored_intents 下列出的意图将添加到每个插槽映射的 not_intent 键中
Eg: 下面代码实现当 intent 为 chitchat 时,不填充表单的任何槽位
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 表单,在 story 和 rule 里面该如何写?
以 rule 为例,当用户意图满足特定意图就触发表单,可以使用以下规则:
rules:
- rule: Activate form
steps:
- intent: request_restaurant
- action: restaurant_form
- active_loop: restaurant_form
以上rule 表示:当 intent 是 request_restaurant 的时候,激活表单
active_loop: restaurant_form 步骤表示在运行 action: restaurant_form 之后应激活表单,可以简单粗暴理解为这两个 step 组合起来是激活表单的操作:
- action: restaurant_form
- active_loop: restaurant_form
一旦填满所有必需的插槽,表单将自动停用,也可提前终止表单,停用表单的 steps:
如下图所示,表示停用 name 为 restaurant_form 的 Form 表单
- action: restaurant_form
- active_loop: null
可以使用 rule 或 story 来描述 bot 在 form 末尾的行为。如果在表单结束之后不添加后续要执行的 action ,bot 将在表单完成后自动监听下一条用户消息。下面的示例表示在表单 restaurant_form 填满所有必需的槽后立即运行 utter_submit 和 utter_slots_values 这两个 action
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
用户不会总是按照预期去回应机器人向他们询问的信息,通常,用户会提出其他问题、闲聊、改变主意或以其他方式偏离我们预期的行为。
当表单处于激活状态时,如果没有从用户的输入中提取到对应请求的槽,则表单操作的执行将被拒绝,即表单将自动引发 ActionExecutionRejection。以下是表单将引发 ActionExecutionRejection 的特定场景:
要有意拒绝表单执行,您还可以返回一个 ActionExecutionRejected 事件作为自定义验证或插槽映射的一部分。(我没有验证过)
要处理可能导致表单执行被拒绝的情况,您可以编写包含预期中断的规则或故事。例如,如果您希望您的用户与您的机器人聊天,您可以添加一个规则来处理这个问题:
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
在某些情况下,用户可能会在表单操作的中间改变主意并决定不继续他们的初始请求。在这种情况下,机器人应该停止 ask slot。
可以使用默认操作 action_deactivate_loop 处理此类情况,这将停用表单并重置 requested_slot 插槽。此类对话的示例 story 如下所示:
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
使用自定义操作可以完全自定义表单
从用户输入中提取槽值后,可以验证提取的槽。默认情况下,Rasa 仅在请求插槽后验证是否有任何插槽被填充。
可以用自定义操作 validate_
actions:
- validate_restaurant_form
当 form 被执行时,在每个用户回合后,它将运行您的自定义操作来验证最新填充的槽位。
这个自定义操作 validate_
以下示例显示了自定义 action 的实现,该操作验证名为 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}
您还可以扩展 Action 类并使用 tracker.slots_to_validate 检索提取的槽以完全自定义验证过程。(这种方法我没有试过)
如果预定义的 slot mapping 都不能够满足需求, 可以自定义 action : extract_
如果您使用的是 Rasa SDK,我们建议您扩展提供的 FormValidationAction 类。使用 FormValidationAction 时,提取槽需要三个步骤:
forms:
form_last_four_digits:
required_slots:
- outdoor_seating
slots:
outdoor_seating:
type: bool
# initial_value: false
influence_conversation: false
mappings:
- type: custom
如果您在 domain 文件的槽位部分中添加了一个具有自定义映射的槽位,并且您只希望在自定义动作(通过扩展 FormValidationAction)的表单上下文中进行验证,请确保此槽位具有自定义类型的映射,并且槽位名称包含在表单的 required_slots 中。(没太明白官方文档这句话)
以下示例显示了一个表单的实现,该表单除了使用预定义映射的插槽之外,还以自定义方式提取插槽 outdoor_seating。 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 中指定的第一个未填充的插槽。
默认情况下,Rasa 将从 domain 文件 form 表单 列出的 slots, 请求下一个空槽位。如果使用自定义 slot mappings 和 FormValidationAction,它将从 required_slots 方法返回的第一个空槽位开始请求。如果 required_slots 中的所有槽位都已填满,则表单将被停用。
可以动态更新表单的所需槽位。例如,当您需要基于先前填写的槽位填写其他槽位时,或者当您想要更改请求槽位的顺序时,这非常有用。
如果您正在使用 Rasa SDK,我们强烈建议您使用 FormValidationAction 并覆盖 required_slots 来适应您的动态行为。您必须为每个不使用预定义映射的槽实现一个 extract_
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
相反地,如果您想在特定条件下从表单的 required_slots 中删除一个槽位,您应该将 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]:
updated_slots = domain_slots.copy()
if tracker.slots.get("existing_customer") is True:
# If the user is an existing customer,
# do not request the `email_address` slot
updated_slots.remove("email_address")
return updated_slots
槽位 requested_slot 会自动作为文本类型的槽位添加到Domain中。在对话过程中,requested_slot 的值将被忽略。如果您想改变这种行为,您需要将 requested_slot 添加到您的 Domain 文件中,作为带有 influence_conversation 设置为 true 的分类槽位。如果您想根据当前向用户询问的槽位来处理 unhappy 的路径,您可能想这样做。例如,如果您的用户对机器人的一个问题回复了另一个问题,比如“你为什么要知道这个?”这时候你的回答取决于我们在故事中的位置,可以理解为正在 ask 哪一个 slot 。在餐厅案例中,您的故事看起来可能像这样:
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
同样,强烈建议您使用交互式学习来构建这些故事。
一旦表单确定了下一个需要用户填写的槽位,它将执行动作 utter_ask_
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 []
如果插槽有多个询问话术,Rasa 将按以下顺序排列优先级: