最常见的对话模式之一是从用户那里收集一些信息,以便做一些事情(预订餐厅、调用API、搜索数据库等)。这也称为 插槽填充。
Usage
要将Form
与Rasa开源一起使用,需要确保将规则策略添加到策略配置中。例如:
policies:
- name: RulePolicy
定义Form(Defining a Form)
通过将表单添加到domain
中的form
部分来定义表单。表单的名称同时也是在故事或规则中执行处理表单操作的名称。您还需要为表单应填充的每个插槽定义插槽映射。可以为每个要填充的插槽指定一个或多个插槽映射。
以下示例表单restaurant_form
将用提取的实体cuisine
填充槽cuisine
,实体number
填充槽num_people
。
forms:
restaurant_form:
required_slots:
cuisine:
- type: from_entity
entity: cuisine
num_people:
- type: from_entity
entity: number
您可以在ignored_intents
键下为整个表单定义要忽略的意图列表。ignored_intents
下列出的意图将添加到表单中每个槽映射的not_intent
键中。
例如,如果您不希望在聊天时填写表单的任何插槽,则需要定义以下内容(在表单名称之后,在ignored_intents
关键字下):
forms:
restaurant_form:
ignored_intents:
- chitchat
required_slots:
cuisine:
- type: from_entity
entity: cuisine
num_people:
- type: from_entity
entity: number
一旦表单操作被第一次调用,表单就会被激活,并提示用户输入下一个所需的槽值。它通过查找一个名为utter_ask_<form_name>_<slot_name>
或utter_ask_<slot_name>
的响应(如果找不到前者)来实现。请确保在domain
文件中为每个所需的插槽定义这些响应。
激活Form(Activating a Form)
要激活表单,您需要添加一个故事或规则,描述助理何时运行表单。在特定意图情况下触发表单,例如,可以使用以下规则:
rules:
- rule: Activate form
steps:
- intent: request_restaurant
- action: restaurant_form
- active_loop: restaurant_form
注意
active_loop: restaurant_form
步骤表示表单在运行restaurant_form
后被激活。
停用Form(Deactivating a Form)
一旦所有必需的槽位都被填满,表单将自动停用。你可以用一条规则或一个故事来描述你助理在表格结尾的行为。如果您没有添加适用的故事或规则,则助手将在表单完成后自动侦听下一条用户消息。下面的示例在表单your_form
填充完所有必需的槽时立即运行语句utter_all_slots_filled
。
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
用户可能希望尽早脱离表单。有关如何编写此案例的故事或规则,请参见编写不愉快表单路径的故事/规则。
Slot Mappings
Rasa附带了四个预定义的映射,用于根据最新的用户消息填充表单的槽。如果需要自定义函数来提取所需信息,请参阅自定义插槽映射。
from_entity
from_entity
映射基于提取的实体填充插槽。它将寻找一个名为entity_name
的实体来填充一个slot_name
槽。如果intent_name
为None
,则无论intent_name
是什么,槽都将被填充。否则,只有当用户的意图是intent_name
时,才会填充槽。
如果提供了role_name
和/或group_name
,则实体的role/group
标签还需要与给定的值匹配。如果消息的意图是excluded_intent
,则槽映射将不适用。注意,还可以为参数intent
和not_intent
定义意图列表。
forms:
your_form: # 表单名称
required_slots:
slot_name: # 插槽名称
- type: from_entity # 插槽填充类型
entity: entity_name
role: role_name
group: group name
intent: intent_name
not_intent: excluded_intent
在from_entity
映射中,当提取的实体唯一地映射到某个槽时,即使表单没有请求该槽,也会填充该槽。如果映射不唯一,则将忽略提取的实体。
forms:
your_form:
required_slots:
departure_city:
- type: from_entity
entity: city
role: from
- type: from_entity
entity: city
arrival_city:
- type: from_entity
entity: city
role: to
- type: from_entity
entity: city
arrival_date:
- type: from_entity
entity: date
在上面的示例中,实体date
唯一地设置槽arrival_date
,具有角色(role)from
的实体city
唯一地设置槽departure_city
,具有角色to
的实体city
唯一地设置槽arrival_city
,因此,即使没有请求这些槽,也可以使用它们来匹配相应的槽。但是,不带角色的实体city
可以填充departure_city
和arrival_city
两个槽位,具体取决于请求的槽位,因此如果在请求槽位arrival_date
时提取实体城市,则表单将忽略该实体城市。
from_text
from_text
映射将使用下一个用户语句的文本填充槽slot_name
。如果intent_name
为None
,则无论意图名称是什么,槽都将被填充。否则,只有当用户的意图是intent_name
时,才会填充槽。
如果消息的意图是excluded_intent
,则槽映射将不适用。请注意,可以为参数intent
和not_intent
定义意图列表。
forms:
your_form:
required_slots:
slot_name:
- type: from_text
intent: intent_name
not_intent: excluded_intent
from_intent
如果用户意图为intent_name
或None
,则from_intent
映射将用值my_value
填充槽slot_name
。如果消息的意图是excluded_intent
,则槽映射将不适用。注意,还可以为参数intent
和not_intent
定义意图列表。
注意
在窗体的初始激活期间,from_intent
槽映射将不适用。要根据激活窗体的意图填充槽,请使用from_trigger_intent mapping
。
forms:
your_form:
required_slots:
slot_name:
- type: from_intent
value: my_value
intent: intent_name
not_intent: excluded_intent
from_trigger_intent
如果表单是由意图为intent_name
的用户消息激活的,则from_trigger_intent
映射将用值my_value
填充槽slot_name
。如果消息的意图是excluded_intent
,则槽映射将不适用。注意,还可以为参数intent
和not_intent
定义意图列表。
forms:
your_form:
required_slots:
slot_name:
- type: from_trigger_intent
value: my_value
intent: intent_name
not_intent: excluded_intent
为不愉快的表单路径编写故事/规则(Writing Stories / Rules for Unhappy Form Paths)
用户不会总是以您对他们的要求作出响应。通常,用户会问问题,聊天,改变主意,或者偏离快乐的道路。
当表单处于活跃状态时,如果用户的输入未填满请求的槽,则表单操作的执行将被拒绝,即表单将自动引发ActionExecutionRejection
。以下是表单将引发ActionExecutionRejection
的特定场景:
要有意拒绝表单执行,还可以将ActionExecutionRejected
事件作为自定义验证或槽映射的一部分返回。
要处理可能导致表单执行被拒绝的情况,可以编写包含预期中断的规则或故事。例如,如果希望用户与bot聊天,可以添加一个规则来处理此问题:
rules:
- rule: Example of an unhappy path
condition:
# 条件是:表单处于活跃状态
- 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_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
强烈建议您使用交互式学习构建这些规则或故事。如果你手写这些规则/故事,你很可能会错过重要的事情。
高级用法(Advanced Usage)
表单可以使用自定义操作(Custom Actions)完全自定义。
验证表单输入(Validating Form Input)
从用户输入中提取插槽值后,可以验证提取的插槽。默认情况下,Rasa只验证在请求插槽之后是否所有插槽被填满。
您可以实现一个自定义操作validate_<form_name>
来验证任何提取的插槽。请确保将此操作添加到domain
的actions
部分:
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]:
"""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
检索提取的插槽,以完全自定义验证过程。
自定义插槽映射(Custom Slot Mappings)
如果预定义的槽映射都不适合您的用例,那么您可以使用自定义操作validate_<form_name>
来编写您自己的提取代码。Rasa开源将在窗体运行时触发此操作。
如果您使用的是Rasa SDK,我们建议您扩展提供的FormValidationAction
。使用FormValidationAction
时,需要三个步骤来提取自定义槽:
- 为每个以自定义方式映射的插槽定义方法
extract_<slot_name>
。 - 确定在
domain
文件中为表单只列出那些使用预定义映射的插槽。 - 重写
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 required_slots(
self,
slots_mapped_in_domain: List[Text],
dispatcher: "CollectingDispatcher",
tracker: "Tracker",
domain: "DomainDict",
) -> Optional[List[Text]]:
required_slots = slots_mapped_in_domain + ["outdoor_seating"]
return required_slots
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
中指定的第一个未填充的插槽。
动态表单行为(Dynamic Form Behavior)
默认情况下,Rasa将从domain
文件的表单下列出的插槽中请求下一个空插槽。如果使用自定义槽映射和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,
slots_mapped_in_domain: List[Text],
dispatcher: "CollectingDispatcher",
tracker: "Tracker",
domain: "DomainDict",
) -> Optional[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 + slots_mapped_in_domain
The requested_slot slot
槽requested_slot
将作为文本类型的槽自动添加到域中。对话期间将忽略requested_slot
的值。如果要更改此行为,需要将requested_slot
添加到domain
文件中,并将其influence_conversation
属性设置为true
。如果希望以不同的方式处理不愉快的路径,则可能需要这样做,具体取决于用户当前请求的插槽。例如,如果你的用户用另一个问题回答机器人的一个问题,比如你为什么需要知道这个问题?对这种explain
意图的反应取决于我们在故事中的位置。在餐馆的案例中,你的故事看起来是这样的:
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
同样,强烈建议您使用交互式学习来构建这些故事。
使用自定义操作请求下一个插槽(Using a Custom Action to Ask For the Next Slot)
一旦表单确定用户下一步要填充哪个插槽,它将执行操作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 []