问答机器人框架中非常重要的几个概念为意图识别、槽位填充
通常使用机器学习算法进行意图的分类,但最终我们是让将用户的输入提取出我们想要的核心信息。--槽位提取
因此slot在rasa中极为重要, 如何我们想要深入自定义slot,以及solt前后逻辑,那么FormAction是必须要了解清楚的。
位于rasa_sdk/forms.py
class FormAction(Action):
def name(self) -> Text:
"""Unique identifier of the form"""
raise NotImplementedError("A form must implement a name")
@staticmethod
def required_slots(tracker: "Tracker") -> List[Text]:
"""A list of required slots that the form has to fill.
Use `tracker` to request different list of slots
depending on the state of the dialogue
"""
raise NotImplementedError(
"A form must implement required slots that it has to fill"
)
源码中可以看到name,required_slots,是必须实现的函数,作用是告诉rasa 此action都有哪些slot,此aciton名为什么,rasa官方demo中也如此进行了简介
class SalesForm(FormAction):
"""Collects sales information and adds it to the spreadsheet"""
def name(self) -> Text:
return "sales_form"
@staticmethod
def required_slots(tracker) -> List[Text]:
return [
"job_function",
"use_case",
"budget",
"person_name",
"company",
"business_email",
]
那么rasa将按照required_slots list顺序进行slot填充
流程是一致的,我们在story中定义意图以及匹配此意图是的机器人行为,
我们想要使用自定义的action,需要在 domain.yml 和stories.md中进行配置
## sales form
* contact_sales
- sales_form <!--Run the sales_form action-->
- form{"name": "sales_form"} <!--Activate the form-->
- form{"name": null} <!--Deactivate the form-->
domain中定义fromaction
forms:
- sales_form
当我们运行 rasa run, 和 新开一个窗口 rasa run actions时,会进行sales_form注册到rasa中
python ../start.py run actions
2020-09-07 14:03:54 INFO rasa_sdk.endpoint - Starting action endpoint server...
2020-09-07 14:03:54 INFO rasa_sdk.executor - Registered function for 'sales_form'.
2020-09-07 14:03:54 INFO rasa_sdk.endpoint - Action endpoint is up and running on http://localhost:5055
重点知识-story中的定义的何时触发action, domain.yml 中定义。
运行之后,发现会自动识别每一个slot槽位的,向用户提问。这里是因为formaction会自动循环required_slots中的slot,同时自动匹配 domain.yml utter_ask_+ 意图。
源码中此处进行了定义
def request_next_slot(
self,
dispatcher: "CollectingDispatcher",
tracker: "Tracker",
domain: Dict[Text, Any],
) -> Optional[List[EventType]]:
"""Request the next slot and utter template if needed,
else return None"""
for slot in self.required_slots(tracker):
if self._should_request_slot(tracker, slot):
logger.debug(f"Request next slot '{slot}'")
dispatcher.utter_message(template=f"utter_ask_{slot}", **tracker.slots)
return [SlotSet(REQUESTED_SLOT, slot)]
# no more required slots to fill
return None
因此我们可以重写request_next_slot,做前后slot信息验证
def validate_business_email(
self, value, dispatcher, tracker, domain
) -> Dict[Text, Any]:
"""Check to see if an email entity was actually picked up by duckling."""
if any(tracker.get_latest_entity_values("email")):
# entity was picked up, validate slot
return {"business_email": value}
else:
# no entity was picked up, we want to ask again
dispatcher.utter_message(template="utter_no_email")
return {"business_email": None}
函数定义以 validate_ + slot rasa将自动进行输入校验
源码定义如下:
async def validate_slots(
self,
slot_dict: Dict[Text, Any],
dispatcher: "CollectingDispatcher",
tracker: "Tracker",
domain: Dict[Text, Any],
) -> List[EventType]:
"""Validate slots using helper validation functions.
Call validate_{slot} function for each slot, value pair to be validated.
If this function is not implemented, set the slot to the value.
"""
for slot, value in list(slot_dict.items()):
validate_func = getattr(self, f"validate_{slot}", lambda *x: {slot: value})
if utils.is_coroutine_action(validate_func):
validation_output = await validate_func(
value, dispatcher, tracker, domain
)
else:
validation_output = validate_func(value, dispatcher, tracker, domain)
if not isinstance(validation_output, dict):
logger.warning(
"Returning values in helper validation methods is deprecated. "
+ f"Your `validate_{slot}()` method should return "
+ "a dict of {'slot_name': value} instead."
)
validation_output = {slot: validation_output}
slot_dict.update(validation_output)
# validation succeed, set slots to extracted values
return [SlotSet(slot, value) for slot, value in slot_dict.items()]
定义特定槽位的用户输入验证,有时候还是无法满足我们需求,我们得自定义request_next_slot, 方法中放飞自我
def request_next_slot(
self,
dispatcher: "CollectingDispatcher",
tracker: "Tracker",
domain: Dict[Text, Any],
) -> Optional[List[EventType]]:
"""Request the next slot and utter template if needed,
else return None"""
for slot in self.required_slots(tracker):
print(slot)
if slot == "person_name" and tracker.get_slot("person_name") == "yxp":
dispatcher.utter_message(text="对不起,不欢迎你,走开!!")
return self.deactivate()
if self._should_request_slot(tracker, slot):
print("_should_request_slot")
## Condition of validated slot that triggers deactivation
if slot == "person_name" and tracker.get_slot("person_name") == "yxp":
dispatcher.utter_message(text="对不起,不欢迎你,走开!!")
return self.deactivate()
## For all other slots, continue as usual
logger.debug(f"Request next slot '{slot}'")
dispatcher.utter_message(
template=f"utter_ask_{slot}", ** tracker.slots
)
REQUESTED_SLOT = "requested_slot"
return [SlotSet(REQUESTED_SLOT, slot)]
print("not _should_request_slot")
return None
最后两个重要方法为 slot_mappings, 和 submit
slot_mappings 正如函数名称定义的那样,允许用户进行slot槽位提取时的复杂方式。
比如用户输入 确认意图,填充true,输入否定意图,填充false等
def slot_mappings(self) -> Dict[Text, Union[Dict, List[Dict]]]:
"""A dictionary to map required slots to
- an extracted entity
- intent: value pairs
- a whole message
or a list of them, where a first match will be picked"""
return {
"job_function": [
self.from_entity(entity="job_function"),
self.from_text(intent="enter_data"),
],
"use_case": [self.from_text(intent="enter_data")],
"budget": [
self.from_entity(entity="amount-of-money"),
self.from_entity(entity="number"),
self.from_text(intent="enter_data"),
],
"person_name": [
self.from_entity(entity="name"),
self.from_text(intent="enter_data"),
],
"company": [
self.from_entity(entity="company"),
self.from_text(intent="enter_data"),
],
"business_email": [
self.from_entity(entity="email"),
self.from_text(intent="enter_data"),
],
}
最后一个action结束时候,进行最后处理 submit函数为,当收集到所有的意图slot,最终处理方法。
def submit(
self,
dispatcher: CollectingDispatcher,
tracker: Tracker,
domain: Dict[Text, Any],
) -> List[Dict]:
person_name = tracker.get_slot("person_name")
import datetime
budget = tracker.get_slot("budget")
company = tracker.get_slot("company")
email = tracker.get_slot("business_email")
job_function = tracker.get_slot("job_function")
person_name = tracker.get_slot("person_name")
use_case = tracker.get_slot("use_case")
date = datetime.datetime.now().strftime("%d/%m/%Y")
sales_info = [company, use_case, budget, date, person_name, job_function, email]
print(sales_info)
dispatcher.utter_message(template="utter_confirm_salesrequest", user_name=person_name)
return []
以上,我们完成了FormAction 自定义处理, 可以帮助我们实现复杂的多轮次对话。