Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之FormAction
目录
- FormAction
- ActiveLoop
- Slot
- AnySlotDict
- SlotSet
- RemoteAction
- Action
-
- action_for_name_or_text
- default_actions
- ActionListen
- ActionRestart
- ActionSessionStart
- ActionDefaultFallback
- ActionDeactivateLoop
- ActionRevertFallbackEvents
- ActionDefaultAskAffirmation
- ActionDefaultAskRephrase
- ActionBotResponse
- TwoStageFallbackAction
- ActionUnlikelyIntent
- ActionBack
- ActionExtractSlots
- ActionRetrieveResponse
- ActionEndToEndResponse
- Event
- DefinePrevUserUtteredFeaturization
- ActionExecuted
- ActionExecutionRejected
- Agent
- EndpointConfig
- Domain
- CollectingOutputChannel
- TemplatedNaturalLanguageGenerator
- DialogueStateTracker
- MessageProcessor
- constants 常量
- train
- TrainingResult
- TrainingCache
- LocalTrainingCache
- LockStore
- TrackerStore
- Gavin大咖简介
- Rasa 3.x系列博客分享
FormAction
实现和执行表单逻辑的操作Action
class FormAction(LoopAction):
"""Action which implements and executes the form logic."""
def __init__(
self, form_name: Text, action_endpoint: Optional[EndpointConfig]
) -> None:
"""Creates a `FormAction`.
Args:
form_name: Name of the form.
action_endpoint: Endpoint to execute custom actions.
"""
self._form_name = form_name
self.action_endpoint = action_endpoint
# creating it requires domain, which we don't have in init
# we'll create it on the first call
self._unique_entity_mappings: Set[Text] = set()
self._have_unique_entity_mappings_been_initialized = False
def name(self) -> Text:
"""Return the form name."""
return self._form_name
activate
如果第一次调用表单,则激活表单。如果激活,请验证表单激活之前填充的所有所需词槽,并返回带有表单名称的’form’事件,以及预填充词槽验证中的任何’SlotSet’事件。
async def activate(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Activate form if the form is called for the first time.
If activating, validate any required slots that were filled before
form activation and return `Form` event with the name of the form, as well
as any `SlotSet` events from validation of pre-filled slots.
Args:
output_channel: The output channel which can be used to send messages
to the user.
nlg: `NaturalLanguageGenerator` to use for response generation.
tracker: Current conversation tracker of the user.
domain: Current model domain.
Returns:
Events from the activation.
"""
logger.debug(f"Activated the form '{
self.name()}'.")
# collect values of required slots filled before activation
prefilled_slots = {
}
for slot_name in self.required_slots(domain):
if not self._should_request_slot(tracker, slot_name):
prefilled_slots[slot_name] = tracker.get_slot(slot_name)
if not prefilled_slots:
logger.debug("No pre-filled required slots to validate.")
return []
logger.debug(f"Validating pre-filled required slots: {
prefilled_slots}")
return await self.validate_slots(
prefilled_slots, tracker, domain, output_channel, nlg
)
required_slots
表单必须填写的所需词槽的列表。
def required_slots(self, domain: Domain) -> List[Text]:
"""A list of required slots that the form has to fill.
Returns:
A list of slot names.
"""
return domain.required_slots_for_form(self.name())
_should_request_slot
检查表单动作是否应请求给定词槽
@staticmethod
def _should_request_slot(tracker: "DialogueStateTracker", slot_name: Text) -> bool:
"""Check whether form action should request given slot."""
return tracker.get_slot(slot_name) is None
validate_slots
验证提取的词槽。如果自定义一个Action验证槽,我们调用自定义Action来验证它们,否则就没有验证
async def validate_slots(
self,
slot_candidates: Dict[Text, Any],
tracker: "DialogueStateTracker",
domain: Domain,
output_channel: OutputChannel,
nlg: NaturalLanguageGenerator,
) -> List[Union[SlotSet, Event]]:
"""Validate the extracted slots.
If a custom action is available for validating the slots, we call it to validate
them. Otherwise there is no validation.
Args:
slot_candidates: Extracted slots which are candidates to fill the slots
required by the form.
tracker: The current conversation tracker.
domain: The current model domain.
output_channel: The output channel which can be used to send messages
to the user.
nlg: `NaturalLanguageGenerator` to use for response generation.
Returns:
The validation events including potential bot messages and `SlotSet` events
for the validated slots, if the custom form validation action is present in
domain actions.
Otherwise, returns empty list since the extracted slots already have
corresponding `SlotSet` events in the tracker.
"""
logger.debug(f"Validating extracted slots: {
slot_candidates}")
events: List[Union[SlotSet, Event]] = [
SlotSet(slot_name, value) for slot_name, value in slot_candidates.items()
]
validate_name = f"validate_{
self.name()}"
#如果domain文件无validate_的action方法,则不进行校验,返回空事件
if validate_name not in domain.action_names_or_texts:
return []
# create temporary tracker with only the SlotSet events added
# since last user utterance
_tracker = self._temporary_tracker(tracker, events, domain)
#调用微服务进行校验
_action = RemoteAction(validate_name, self.action_endpoint)
validate_events = await _action.run(output_channel, nlg, _tracker, domain)
# Only return the validated SlotSet events by the custom form validation action
# to avoid adding duplicate SlotSet events for slots that are already valid.
return validate_events
_validate_if_required
从self.validate(…)返回事件列表
def _validate_if_required(
self,
tracker: "DialogueStateTracker",
domain: Domain,
output_channel: OutputChannel,
nlg: NaturalLanguageGenerator,
) -> List[Event]:
"""Return a list of events from `self.validate(...)`.
Validation is required if:
- the form is active
- the form is called after `action_listen`
- form validation was not cancelled
"""
# No active_loop means there are no form filled slots to validate yet
if not tracker.active_loop:
return []
needs_validation = (
tracker.latest_action_name == ACTION_LISTEN_NAME
and not tracker.active_loop.get(LOOP_INTERRUPTED, False)
)
if needs_validation:
logger.debug(f"Validating user input '{
tracker.latest_message}'.")
return self.validate(tracker, domain, output_channel, nlg)
else:
# Needed to determine which slots to request although there are no slots
# to actually validate, which happens when coming back to the form after
# an unhappy path
return self.validate_slots({
}, tracker, domain, output_channel, nlg)
_user_rejected_manually
检查用户在slot_validation期间是否拒绝表单执行
def _user_rejected_manually(self, validation_events: List[Event]) -> bool:
"""Checks if user rejected the form execution during a slot_validation.
Args:
validation_events: Events returned by the custom slot_validation action
Returns:
True if the validation_events include an ActionExecutionRejected event,
else False.
"""
return any(
isinstance(event, ActionExecutionRejected) for event in validation_events
)
_ask_for_slot
def _ask_for_slot(
self,
domain: Domain,
nlg: NaturalLanguageGenerator,
output_channel: OutputChannel,
slot_name: Text,
tracker: DialogueStateTracker,
) -> List[Event]:
logger.debug(f"Request next slot '{
slot_name}'")
action_name_to_ask_for_next_slot = self._name_of_utterance(domain, slot_name)
if not action_name_to_ask_for_next_slot:
# Use a debug log as the user might have asked as part of a custom action
#如果 action_ask_表单名_词槽名、utter_ask_表单名_词槽名、 action_ask_词槽名、 utter_ask_词槽名方法在domain的action列表中都没有查询到,则没有发现请求填充词槽的Action,返回空事件,对于要填充的词槽不对用户进行提示说明。
logger.debug(
f"There was no action found to ask for slot '{
slot_name}' "
f"name to be filled."
)
return []
action_to_ask_for_next_slot = action.action_for_name_or_text(
action_name_to_ask_for_next_slot, domain, self.action_endpoint
)
#如果在domain中找到上述对应的方法,则按名称检索对应的ActionRetrieveResponse、ActionEndToEndResponse、FormAction、RemoteAction,调用其run方法执行,返回事件列表。
return action_to_ask_for_next_slot.run(
output_channel, nlg, tracker, domain
)
_name_of_utterance
构建search_path列表,构建found_actions Generator函数,next获取下一个值,4种查询的方式
- action_ask_表单名_词槽名
- utter_ask_表单名_词槽名
- action_ask_词槽名
- utter_ask_词槽名
def _name_of_utterance(self, domain: Domain, slot_name: Text) -> Optional[Text]:
search_path = [
f"action_ask_{
self._form_name}_{
slot_name}",
f"{
UTTER_PREFIX}ask_{
self._form_name}_{
slot_name}",
f"action_ask_{
slot_name}",
f"{
UTTER_PREFIX}ask_{
slot_name}",
]
found_actions = (
action_name
for action_name in search_path
if action_name in domain.action_names_or_texts
)
return next(found_actions, None)
_temporary_tracker
- 插入SlotSet事件以确保请求的REQUESTED_SLOT属于活动表单
- 插入ActionExecuted事件,以便清楚地区分哪些事件是新添加的。
def _temporary_tracker(
self,
current_tracker: DialogueStateTracker,
additional_events: List[Event],
domain: Domain,
) -> DialogueStateTracker:
return DialogueStateTracker.from_events(
current_tracker.sender_id,
current_tracker.events_after_latest_restart()
# Insert SlotSet event to make sure REQUESTED_SLOT belongs to active form.
+ [SlotSet(REQUESTED_SLOT, self.get_slot_to_fill(current_tracker))]
# Insert form execution event so that it's clearly distinguishable which
# events were newly added.
+ [ActionExecuted(self.name())] + additional_events,
slots=domain.slots,
)
_find_next_slot_to_request
def _find_next_slot_to_request(
self, tracker: DialogueStateTracker, domain: Domain
) -> Optional[Text]:
return next(
(
slot
for slot in self.required_slots(domain)
if self._should_request_slot(tracker, slot)
),
None,
)
request_next_slot
如果需要 ,请求下一个词槽和响应,否则返回“None”。
def request_next_slot(
self,
tracker: "DialogueStateTracker",
domain: Domain,
output_channel: OutputChannel,
nlg: NaturalLanguageGenerator,
events_so_far: List[Event],
) -> List[Union[SlotSet, Event]]:
"""Request the next slot and response if needed, else return `None`."""
request_slot_events: List[Event] = []
if self.is_done(output_channel, nlg, tracker, domain, events_so_far):
# The custom action for slot validation decided to stop the form early
return [SlotSet(REQUESTED_SLOT, None)]
slot_to_request = next(
(
event.value
for event in events_so_far
if isinstance(event, SlotSet) and event.key == REQUESTED_SLOT
),
None,
)
temp_tracker = self._temporary_tracker(tracker, events_so_far, domain)
if not slot_to_request:
slot_to_request = self._find_next_slot_to_request(temp_tracker, domain)
request_slot_events.append(SlotSet(REQUESTED_SLOT, slot_to_request))
if slot_to_request:
bot_message_events = self._ask_for_slot(
domain, nlg, output_channel, slot_to_request, temp_tracker
)
return request_slot_events + bot_message_events
# no more required slots to fill
return [SlotSet(REQUESTED_SLOT, None)]
do
def do(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
events_so_far: List[Event],
) -> List[Event]:
events = self._validate_if_required(tracker, domain, output_channel, nlg)
if not self._user_rejected_manually(events):
events += self.request_next_slot(
tracker, domain, output_channel, nlg, events_so_far + events
)
return events
is_done
def is_done(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
events_so_far: List[Event],
) -> bool:
"""Checks if loop can be terminated."""
if any(isinstance(event, ActionExecutionRejected) for event in events_so_far):
return False
# Custom validation actions can decide to terminate the loop early by
# setting the requested slot to `None` or setting `ActiveLoop(None)`.
# We explicitly check only the last occurrences for each possible termination
# event instead of doing `return event in events_so_far` to make it possible
# to override termination events which were returned earlier.
return (
next(
(
event
for event in reversed(events_so_far)
if isinstance(event, SlotSet) and event.key == REQUESTED_SLOT
),
None,
)
== SlotSet(REQUESTED_SLOT, None)
or next(
(
event
for event in reversed(events_so_far)
if isinstance(event, ActiveLoop)
),
None,
)
== ActiveLoop(None)
)
ActiveLoop
如果给定’name’:用’name’激活循环,否则停用活动循环,ActiveLoop继承至Event,如果当前loop 被禁用,则名称为“None”。
class ActiveLoop(Event):
"""If `name` is given: activates a loop with `name` else deactivates active loop."""
type_name = "active_loop"
def __init__(
self,
name: Optional[Text],
timestamp: Optional[float] = None,
metadata: Optional[Dict[Text, Any]] = None,
) -> None:
"""Creates event for active loop.
Args:
name: Name of activated loop or `None` if current loop is deactivated.
timestamp: When the event was created.
metadata: Additional event metadata.
"""
self.name = name
super().__init__(timestamp, metadata)
def __str__(self) -> Text:
"""Returns text representation of event."""
return f"Loop({
self.name})"
def __hash__(self) -> int:
"""Returns unique hash for event."""
return hash(self.name)
def __eq__(self, other: Any) -> bool:
"""Compares object with other object."""
if not isinstance(other, ActiveLoop):
return NotImplemented
return self.name == other.name
def as_story_string(self) -> Text:
"""Returns text representation of event."""
props = json.dumps({
LOOP_NAME: self.name})
return f"{
ActiveLoop.type_name}{
props}"
@classmethod
def _from_story_string(cls, parameters: Dict[Text, Any]) -> List["ActiveLoop"]:
"""Called to convert a parsed story line into an event."""
return [
ActiveLoop(
parameters.get(LOOP_NAME),
parameters.get("timestamp"),
parameters.get("metadata"),
)
]
def as_dict(self) -> Dict[Text, Any]:
"""Returns serialized event."""
d = super().as_dict()
d.update({
LOOP_NAME: self.name})
return d
def apply_to(self, tracker: "DialogueStateTracker") -> None:
"""Applies event to current conversation state."""
tracker.change_loop_to(self.name)
Slot
Key-value 键-值存储,用于存储会话期间的信息
class Slot:
"""Key-value store for storing information during a conversation."""
type_name = None
def __init__(
self,
name: Text,
mappings: List[Dict[Text, Any]],
initial_value: Any = None,
value_reset_delay: Optional[int] = None,
influence_conversation: bool = True,
) -> None:
"""Create a Slot.
Args:
name: The name of the slot.
initial_value: The initial value of the slot.
mappings: List containing slot mappings.
value_reset_delay: After how many turns the slot should be reset to the
initial_value. This is behavior is currently not implemented.
influence_conversation: If `True` the slot will be featurized and hence
influence the predictions of the dialogue polices.
"""
self.name = name
self.mappings = mappings
self._value = initial_value
self.initial_value = initial_value
self._value_reset_delay = value_reset_delay
self.influence_conversation = influence_conversation
self._has_been_set = False
def feature_dimensionality(self) -> int:
"""How many features this single slot creates.
Returns:
The number of features. `0` if the slot is unfeaturized. The dimensionality
of the array returned by `as_feature` needs to correspond to this value.
"""
if not self.influence_conversation:
return 0
return self._feature_dimensionality()
def _feature_dimensionality(self) -> int:
"""See the docstring for `feature_dimensionality`."""
return 1
def has_features(self) -> bool:
"""Indicate if the slot creates any features."""
return self.feature_dimensionality() != 0
def value_reset_delay(self) -> Optional[int]:
"""After how many turns the slot should be reset to the initial_value.
If the delay is set to `None`, the slot will keep its value forever."""
# TODO: FUTURE this needs to be implemented - slots are not reset yet
return self._value_reset_delay
def as_feature(self) -> List[float]:
if not self.influence_conversation:
return []
return self._as_feature()
def _as_feature(self) -> List[float]:
raise NotImplementedError(
"Each slot type needs to specify how its "
"value can be converted to a feature. Slot "
"'{}' is a generic slot that can not be used "
"for predictions. Make sure you add this "
"slot to your domain definition, specifying "
"the type of the slot. If you implemented "
"a custom slot type class, make sure to "
"implement `.as_feature()`."
"".format(self.name)
)
def reset(self) -> None:
"""Resets the slot's value to the initial value."""
self.value = self.initial_value
self._has_been_set = False
@property
def value(self) -> Any:
"""Gets the slot's value."""
return self._value
@value.setter
def value(self, value: Any) -> None:
"""Sets the slot's value."""
self._value = value
self._has_been_set = True
@property
def has_been_set(self) -> bool:
"""Indicates if the slot's value has been set."""
return self._has_been_set
def __str__(self) -> Text:
return f"{
self.__class__.__name__}({
self.name}: {
self.value})"
def __repr__(self) -> Text:
return f"<{
self.__class__.__name__}({
self.name}: {
self.value})>"
@staticmethod
def resolve_by_type(type_name: Text) -> Type["Slot"]:
"""Returns a slots class by its type name."""
for cls in rasa.shared.utils.common.all_subclasses(Slot):
if cls.type_name == type_name:
return cls
try:
return rasa.shared.utils.common.class_from_module_path(type_name)
except (ImportError, AttributeError):
raise InvalidSlotTypeException(
f"Failed to find slot type, '{
type_name}' is neither a known type nor "
f"user-defined. If you are creating your own slot type, make "
f"sure its module path is correct. "
f"You can find all build in types at {
DOCS_URL_SLOTS}"
)
def persistence_info(self) -> Dict[str, Any]:
"""Returns relevant information to persist this slot."""
return {
"type": rasa.shared.utils.common.module_path_from_instance(self),
"initial_value": self.initial_value,
"influence_conversation": self.influence_conversation,
"mappings": self.mappings,
}
def fingerprint(self) -> Text:
"""Returns a unique hash for the slot which is stable across python runs.
Returns:
fingerprint of the slot
"""
data = {
"slot_name": self.name, "slot_value": self.value}
data.update(self.persistence_info())
return rasa.shared.utils.io.get_dictionary_fingerprint(data)
AnySlotDict
通过按需创建词槽来假装每个词槽都存在的词槽字典。这只使用通用槽类型!这意味着某些功能将无法工作,例如特征化插槽
class AnySlotDict(dict):
"""A slot dictionary that pretends every slot exists, by creating slots on demand.
This only uses the generic slot type! This means certain functionality wont work,
e.g. properly featurizing the slot."""
def __missing__(self, key: Text) -> Slot:
value = self[key] = Slot(key, mappings=[])
return value
def __contains__(self, key: Any) -> bool:
return True
SlotSet
用户指定词槽slot的值,每个词槽都有一个名称和一个值,SlotSet继承至Event类
class SlotSet(Event):
"""The user has specified their preference for the value of a `slot`.
Every slot has a name and a value. This event can be used to set a
value for a slot on a conversation.
As a side effect the `Tracker`'s slots will be updated so
that `tracker.slots[key]=value`.
"""
type_name = "slot"
def __init__(
self,
key: Text,
value: Optional[Any] = None,
timestamp: Optional[float] = None,
metadata: Optional[Dict[Text, Any]] = None,
) -> None:
"""Creates event to set slot.
Args:
key: Name of the slot which is set.
value: Value to which slot is set.
timestamp: When the event was created.
metadata: Additional event metadata.
"""
self.key = key
self.value = value
super().__init__(timestamp, metadata)
def __str__(self) -> Text:
"""Returns text representation of event."""
return f"SlotSet(key: {
self.key}, value: {
self.value})"
def __hash__(self) -> int:
"""Returns unique hash for event."""
return hash((self.key, jsonpickle.encode(self.value)))
def __eq__(self, other: Any) -> bool:
"""Compares object with other object."""
if not isinstance(other, SlotSet):
return NotImplemented
return (self.key, self.value) == (other.key, other.value)
def as_story_string(self) -> Text:
"""Returns text representation of event."""
props = json.dumps({
self.key: self.value}, ensure_ascii=False)
return f"{
self.type_name}{
props}"
@classmethod
def _from_story_string(
cls, parameters: Dict[Text, Any]
) -> Optional[List["SlotSet"]]:
slots = []
for slot_key, slot_val in parameters.items():
slots.append(SlotSet(slot_key, slot_val))
if slots:
return slots
else:
return None
def as_dict(self) -> Dict[Text, Any]:
"""Returns serialized event."""
d = super().as_dict()
d.update({
"name": self.key, "value": self.value})
return d
@classmethod
def _from_parameters(cls, parameters: Dict[Text, Any]) -> "SlotSet":
try:
return SlotSet(
parameters.get("name"),
parameters.get("value"),
parameters.get("timestamp"),
parameters.get("metadata"),
)
except KeyError as e:
raise ValueError(f"Failed to parse set slot event. {
e}")
def apply_to(self, tracker: "DialogueStateTracker") -> None:
"""Applies event to current conversation state."""
tracker._set_slot(self.key, self.value)
RemoteAction
class RemoteAction(Action):
def __init__(self, name: Text, action_endpoint: Optional[EndpointConfig]) -> None:
self._name = name
self.action_endpoint = action_endpoint
def _action_call_format(
self, tracker: "DialogueStateTracker", domain: "Domain"
) -> Dict[Text, Any]:
"""Create the request json send to the action server."""
from rasa.shared.core.trackers import EventVerbosity
tracker_state = tracker.current_state(EventVerbosity.ALL)
return {
"next_action": self._name,
"sender_id": tracker.sender_id,
"tracker": tracker_state,
"domain": domain.as_dict(),
"version": rasa.__version__,
}
@staticmethod
def action_response_format_spec() -> Dict[Text, Any]:
"""Expected response schema for an Action endpoint.
Used for validation of the response returned from the
Action endpoint."""
schema = {
"type": "object",
"properties": {
"events": EVENTS_SCHEMA,
"responses": {
"type": "array", "items": {
"type": "object"}},
},
}
return schema
def _validate_action_result(self, result: Dict[Text, Any]) -> bool:
from jsonschema import validate
from jsonschema import ValidationError
try:
validate(result, self.action_response_format_spec())
return True
except ValidationError as e:
e.message += (
f". Failed to validate Action server response from API, "
f"make sure your response from the Action endpoint is valid. "
f"For more information about the format visit "
f"{
DOCS_BASE_URL}/custom-actions"
)
raise e
@staticmethod
async def _utter_responses(
responses: List[Dict[Text, Any]],
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
) -> List[BotUttered]:
"""Use the responses generated by the action endpoint and utter them."""
bot_messages = []
for response in responses:
generated_response = response.pop("response", None)
if generated_response:
draft = await nlg.generate(
generated_response, tracker, output_channel.name(), **response
)
if not draft:
continue
draft["utter_action"] = generated_response
else:
draft = {
}
buttons = response.pop("buttons", []) or []
if buttons:
draft.setdefault("buttons", [])
draft["buttons"].extend(buttons)
# Avoid overwriting `draft` values with empty values
response = {
k: v for k, v in response.items() if v}
draft.update(response)
bot_messages.append(create_bot_utterance(draft))
return bot_messages
async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Runs action. Please see parent class for the full docstring."""
json_body = self._action_call_format(tracker, domain)
if not self.action_endpoint:
raise RasaException(
f"Failed to execute custom action '{
self.name()}' "
f"because no endpoint is configured to run this "
f"custom action. Please take a look at "
f"the docs and set an endpoint configuration via the "
f"--endpoints flag. "
f"{
DOCS_BASE_URL}/custom-actions"
)
try:
logger.debug(
"Calling action endpoint to run action '{}'.".format(self.name())
)
response: Any = await self.action_endpoint.request(
json=json_body, method="post", timeout=DEFAULT_REQUEST_TIMEOUT
)
self._validate_action_result(response)
events_json = response.get("events", [])
responses = response.get("responses", [])
bot_messages: List[Event] = await self._utter_responses(
responses, output_channel, nlg, tracker
)
evts = events.deserialise_events(events_json)
return bot_messages + evts
except ClientResponseError as e:
if e.status == 400:
response_data = json.loads(e.text)
exception = ActionExecutionRejection(
response_data["action_name"], response_data.get("error")
)
logger.error(exception.message)
raise exception
else:
raise RasaException("Failed to execute custom action.") from e
except aiohttp.ClientConnectionError as e:
logger.error(
"Failed to run custom action '{}'. Couldn't connect "
"to the server at '{}'. Is the server running? "
"Error: {}".format(self.name(), self.action_endpoint.url, e)
)
raise RasaException("Failed to execute custom action.")
except aiohttp.ClientError as e:
# not all errors have a status attribute, but
# helpful to log if they got it
# noinspection PyUnresolvedReferences
status = getattr(e, "status", None)
raise RasaException(
"Failed to run custom action '{}'. Action server "
"responded with a non 200 status code of {}. "
"Make sure your action server properly runs actions "
"and returns a 200 once the action is executed. "
"Error: {}".format(self.name(), status, e)
)
def name(self) -> Text:
return self._name
Action
对对话状态的下一步回应行动
class Action:
"""Next action to be taken in response to a dialogue state."""
def name(self) -> Text:
"""Unique identifier of this simple action."""
raise NotImplementedError
async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Execute the side effects of this action.
Args:
nlg: which ``nlg`` to use for response generation
output_channel: ``output_channel`` to which to send the resulting message.
tracker (DialogueStateTracker): the state tracker for the current
user. You can access slot values using
``tracker.get_slot(slot_name)`` and the most recent user
message is ``tracker.latest_message.text``.
domain (Domain): the bot's domain
Returns:
A list of :class:`rasa.core.events.Event` instances
"""
raise NotImplementedError
def __str__(self) -> Text:
"""Returns text representation of form."""
return f"{
self.__class__.__name__}('{
self.name()}')"
def event_for_successful_execution(
self, prediction: PolicyPrediction
) -> ActionExecuted:
"""Event which should be logged for the successful execution of this action.
Args:
prediction: Prediction which led to the execution of this event.
Returns:
Event which should be logged onto the tracker.
"""
return ActionExecuted(
self.name(),
prediction.policy_name,
prediction.max_confidence,
hide_rule_turn=prediction.hide_rule_turn,
metadata=prediction.action_metadata,
)
action_for_name_or_text
根据Action动作的名称或它的文本(如果是端到端action)检索Action。
def action_for_name_or_text(
action_name_or_text: Text, domain: Domain, action_endpoint: Optional[EndpointConfig]
) -> "Action":
"""Retrieves an action by its name or by its text in case it's an end-to-end action.
Args:
action_name_or_text: The name of the action.
domain: The current model domain.
action_endpoint: The endpoint to execute custom actions.
Raises:
ActionNotFoundException: If action not in current domain.
Returns:
The instantiated action.
"""
if action_name_or_text not in domain.action_names_or_texts:
domain.raise_action_not_found_exception(action_name_or_text)
defaults = {
a.name(): a for a in default_actions(action_endpoint)}
if (
action_name_or_text in defaults
and action_name_or_text not in domain.user_actions_and_forms
):
return defaults[action_name_or_text]
if action_name_or_text.startswith(UTTER_PREFIX) and is_retrieval_action(
action_name_or_text, domain.retrieval_intents
):
return ActionRetrieveResponse(action_name_or_text)
if action_name_or_text in domain.action_texts:
return ActionEndToEndResponse(action_name_or_text)
if action_name_or_text.startswith(UTTER_PREFIX):
return ActionBotResponse(action_name_or_text)
is_form = action_name_or_text in domain.form_names
# Users can override the form by defining an action with the same name as the form
user_overrode_form_action = is_form and action_name_or_text in domain.user_actions
if is_form and not user_overrode_form_action:
from rasa.core.actions.forms import FormAction
return FormAction(action_name_or_text, action_endpoint)
return RemoteAction(action_name_or_text, action_endpoint)
default_actions
默认Action的列表
def default_actions(action_endpoint: Optional[EndpointConfig] = None) -> List["Action"]:
"""List default actions."""
from rasa.core.actions.two_stage_fallback import TwoStageFallbackAction
return [
ActionListen(),
ActionRestart(),
ActionSessionStart(),
ActionDefaultFallback(),
ActionDeactivateLoop(),
ActionRevertFallbackEvents(),
ActionDefaultAskAffirmation(),
ActionDefaultAskRephrase(),
TwoStageFallbackAction(action_endpoint),
ActionUnlikelyIntent(),
ActionBack(),
ActionExtractSlots(action_endpoint),
]
ActionListen
在任何回合的第一个动作-机器人等待一个用户消息,机器人应该停止采取进一步的行动,等待用户说些什么。
class ActionListen(Action):
"""The first action in any turn - bot waits for a user message.
The bot should stop taking further actions and wait for the user to say
something."""
def name(self) -> Text:
return ACTION_LISTEN_NAME
async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Runs action. Please see parent class for the full docstring."""
return []
ActionRestart
将跟踪器重置为初始状态。如果可用,发出重启响应。
class ActionRestart(ActionBotResponse):
"""Resets the tracker to its initial state.
Utters the restart response if available.
"""
def name(self) -> Text:
"""Returns action restart name."""
return ACTION_RESTART_NAME
def __init__(self) -> None:
"""Initializes action restart."""
super().__init__("utter_restart", silent_fail=True)
async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Runs action. Please see parent class for the full docstring."""
# only utter the response if it is available
evts = await super().run(output_channel, nlg, tracker, domain)
return evts + [Restarted()]
ActionSessionStart
应用会话开始。获取前一个会话中的所有“SlotSet”事件,并将它们应用到新会话中。run方法新加ActionExecuted(ACTION_LISTEN_NAME)事件
class ActionSessionStart(Action):
"""Applies a conversation session start.
Takes all `SlotSet` events from the previous session and applies them to the new
session.
"""
def name(self) -> Text:
"""Returns action start name."""
return ACTION_SESSION_START_NAME
@staticmethod
def _slot_set_events_from_tracker(
tracker: "DialogueStateTracker",
) -> List["SlotSet"]:
"""Fetch SlotSet events from tracker and carry over key, value and metadata."""
return [
SlotSet(key=event.key, value=event.value, metadata=event.metadata)
for event in tracker.applied_events()
if isinstance(event, SlotSet)
]
async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Runs action. Please see parent class for the full docstring."""
_events: List[Event] = [SessionStarted()]
if domain.session_config.carry_over_slots:
_events.extend(self._slot_set_events_from_tracker(tracker))
_events.append(ActionExecuted(ACTION_LISTEN_NAME))
return _events
ActionDefaultFallback
执行fallback后退动作,run方法通过UserUtteranceReverted返回到对话的前一状态
class ActionDefaultFallback(ActionBotResponse):
"""Executes the fallback action and goes back to the prev state of the dialogue."""
def name(self) -> Text:
"""Returns action default fallback name."""
return ACTION_DEFAULT_FALLBACK_NAME
def __init__(self) -> None:
"""Initializes action default fallback."""
super().__init__("utter_default", silent_fail=True)
async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Runs action. Please see parent class for the full docstring."""
# only utter the response if it is available
evts = await super().run(output_channel, nlg, tracker, domain)
return evts + [UserUtteranceReverted()]
ActionDeactivateLoop
使活动循环失效,run方法中加入ActiveLoop(None), SlotSet(REQUESTED_SLOT, None)事件
class ActionDeactivateLoop(Action):
"""Deactivates an active loop."""
def name(self) -> Text:
return ACTION_DEACTIVATE_LOOP_NAME
async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Runs action. Please see parent class for the full docstring."""
return [ActiveLoop(None), SlotSet(REQUESTED_SLOT, None)]
ActionRevertFallbackEvents
恢复在’ TwoStageFallbackPolicy '期间完成的事件。这将恢复在TwoStageFallbackPolicy的回退过程中用户的消息和机器人的话语。为不同的路径编写定制的故事,这样做是没有必要的, 但只针对快乐的路径,已弃用,可在“TwoStageFallbackPolicy”删除。
class ActionRevertFallbackEvents(Action):
"""Reverts events which were done during the `TwoStageFallbackPolicy`.
This reverts user messages and bot utterances done during a fallback
of the `TwoStageFallbackPolicy`. By doing so it is not necessary to
write custom stories for the different paths, but only of the happy
path. This is deprecated and can be removed once the
`TwoStageFallbackPolicy` is removed.
"""
def name(self) -> Text:
return ACTION_REVERT_FALLBACK_EVENTS_NAME
async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Runs action. Please see parent class for the full docstring."""
from rasa.core.policies.two_stage_fallback import has_user_rephrased
# User rephrased
if has_user_rephrased(tracker):
return _revert_successful_rephrasing(tracker)
# User affirmed
elif has_user_affirmed(tracker):
return _revert_affirmation_events(tracker)
else:
return []
ActionDefaultAskAffirmation
默认的实现,要求用户确认他的意图。建议使用自定义操作覆盖此默认操作,以便为确认提供更有意义的提示。例如,使用intent的描述而不是标识符名。
class ActionDefaultAskAffirmation(Action):
"""Default implementation which asks the user to affirm his intent.
It is suggested to overwrite this default action with a custom action
to have more meaningful prompts for the affirmations. E.g. have a
description of the intent instead of its identifier name.
"""
def name(self) -> Text:
return ACTION_DEFAULT_ASK_AFFIRMATION_NAME
async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Runs action. Please see parent class for the full docstring."""
latest_message = tracker.latest_message
if latest_message is None:
raise TypeError(
"Cannot find last user message for detecting fallback affirmation."
)
intent_to_affirm = latest_message.intent.get(INTENT_NAME_KEY)
intent_ranking: List["IntentPrediction"] = (
latest_message.parse_data.get(INTENT_RANKING_KEY) or []
)
if (
intent_to_affirm == DEFAULT_NLU_FALLBACK_INTENT_NAME
and len(intent_ranking) > 1
):
intent_to_affirm = intent_ranking[1][INTENT_NAME_KEY]
affirmation_message = f"Did you mean '{
intent_to_affirm}'?"
message = {
"text": affirmation_message,
"buttons": [
{
"title": "Yes", "payload": f"/{
intent_to_affirm}"},
{
"title": "No", "payload": f"/{
USER_INTENT_OUT_OF_SCOPE}"},
],
"utter_action": self.name(),
}
return [create_bot_utterance(message)]
ActionDefaultAskRephrase
默认的实现,要求用户重新表达他的意图,继承至ActionBotResponse
class ActionDefaultAskRephrase(ActionBotResponse):
"""Default implementation which asks the user to rephrase his intent."""
def name(self) -> Text:
"""Returns action default ask rephrase name."""
return ACTION_DEFAULT_ASK_REPHRASE_NAME
def __init__(self) -> None:
"""Initializes action default ask rephrase."""
super().__init__("utter_ask_rephrase", silent_fail=True)
ActionBotResponse
在运行时发出响应 唯一有效的 Action
class ActionBotResponse(Action):
"""An action which only effect is to utter a response when it is run."""
def __init__(self, name: Text, silent_fail: Optional[bool] = False) -> None:
"""Creates action.
Args:
name: Name of the action.
silent_fail: `True` if the action should fail silently in case no response
was defined for this action.
"""
self.utter_action = name
self.silent_fail = silent_fail
async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Simple run implementation uttering a (hopefully defined) response."""
message = await nlg.generate(self.utter_action, tracker, output_channel.name())
if message is None:
if not self.silent_fail:
logger.error(
"Couldn't create message for response '{}'."
"".format(self.utter_action)
)
return []
message["utter_action"] = self.utter_action
return [create_bot_utterance(message)]
TwoStageFallbackAction
可以传入外部HTTP端点的配置的参数
class TwoStageFallbackAction(LoopAction):
def __init__(self, action_endpoint: Optional[EndpointConfig] = None) -> None:
self._action_endpoint = action_endpoint
def name(self) -> Text:
return ACTION_TWO_STAGE_FALLBACK_NAME
async def do(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
events_so_far: List[Event],
) -> List[Event]:
if _user_should_affirm(tracker, events_so_far):
return await self._ask_affirm(output_channel, nlg, tracker, domain)
return await self._ask_rephrase(output_channel, nlg, tracker, domain)
ActionUnlikelyIntent
指示NLU所预测的意图是意外的动作。这个动作可以被’UnexpecTEDIntentPolicy '预测。
class ActionUnlikelyIntent(Action):
"""An action that indicates that the intent predicted by NLU is unexpected.
This action can be predicted by `UnexpecTEDIntentPolicy`.
"""
def name(self) -> Text:
"""Returns the name of the action."""
return ACTION_UNLIKELY_INTENT_NAME
async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Runs action. Please see parent class for the full docstring."""
return []
ActionBack
通过两句用户话语恢复跟踪器状态,run方法中新加入2个UserUtteranceReverted事件
class ActionBack(ActionBotResponse):
"""Revert the tracker state by two user utterances."""
def name(self) -> Text:
"""Returns action back name."""
return ACTION_BACK_NAME
def __init__(self) -> None:
"""Initializes action back."""
super().__init__("utter_back", silent_fail=True)
async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Runs action. Please see parent class for the full docstring."""
# only utter the response if it is available
evts = await super().run(output_channel, nlg, tracker, domain)
return evts + [UserUtteranceReverted(), UserUtteranceReverted()]
ActionExtractSlots
在每个用户回合后运行的默认动作。在下一个预测操作运行之前,动作在MessageProcessor.handle_message(…)中自动执行。
根据指定的词槽映射, 设置词槽以从用户消息中提取值
class ActionExtractSlots(Action):
"""Default action that runs after each user turn.
Action is executed automatically in MessageProcessor.handle_message(...)
before the next predicted action is run.
Sets slots to extracted values from user message
according to assigned slot mappings.
"""
def __init__(self, action_endpoint: Optional[EndpointConfig]) -> None:
"""Initializes default action extract slots."""
self._action_endpoint = action_endpoint
def name(self) -> Text:
"""Returns action_extract_slots name."""
return ACTION_EXTRACT_SLOTS
@staticmethod
def _matches_mapping_conditions(
mapping: Dict[Text, Any], tracker: "DialogueStateTracker", slot_name: Text
) -> bool:
slot_mapping_conditions = mapping.get(MAPPING_CONDITIONS)
if not slot_mapping_conditions:
return True
if (
tracker.active_loop
and tracker.active_loop.get(LOOP_REJECTED)
and tracker.get_slot(REQUESTED_SLOT) == slot_name
):
return False
# check if found mapping conditions matches form
for condition in slot_mapping_conditions:
active_loop = condition.get(ACTIVE_LOOP)
if active_loop and active_loop == tracker.active_loop_name:
condition_requested_slot = condition.get(REQUESTED_SLOT)
if not condition_requested_slot:
return True
if condition_requested_slot == tracker.get_slot(REQUESTED_SLOT):
return True
return False
@staticmethod
def _verify_mapping_conditions(
mapping: Dict[Text, Any], tracker: "DialogueStateTracker", slot_name: Text
) -> bool:
if mapping.get(MAPPING_CONDITIONS) and mapping[MAPPING_TYPE] != str(
SlotMappingType.FROM_TRIGGER_INTENT
):
if not ActionExtractSlots._matches_mapping_conditions(
mapping, tracker, slot_name
):
return False
return True
async def _run_custom_action(
self,
custom_action: Text,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
slot_events: List[Event] = []
remote_action = RemoteAction(custom_action, self._action_endpoint)
disallowed_types = set()
try:
custom_events = await remote_action.run(
output_channel, nlg, tracker, domain
)
for event in custom_events:
if isinstance(event, SlotSet):
if tracker.get_slot(event.key) != event.value:
slot_events.append(event)
elif isinstance(event, BotUttered):
slot_events.append(event)
else:
disallowed_types.add(event.type_name)
except (RasaException, ClientResponseError) as e:
logger.warning(
f"Failed to execute custom action '{
custom_action}' "
f"as a result of error '{
str(e)}'. The default action "
f"'{
self.name()}' failed to fill slots with custom "
f"mappings."
)
for type_name in disallowed_types:
logger.info(
f"Running custom action '{
custom_action}' has resulted "
f"in an event of type '{
type_name}'. This is "
f"disallowed and the tracker will not be "
f"updated with this event."
)
return slot_events
async def _execute_custom_action(
self,
mapping: Dict[Text, Any],
executed_custom_actions: Set[Text],
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> Tuple[List[Event], Set[Text]]:
custom_action = mapping.get("action")
if not custom_action or custom_action in executed_custom_actions:
return [], executed_custom_actions
slot_events = await self._run_custom_action(
custom_action, output_channel, nlg, tracker, domain
)
executed_custom_actions.add(custom_action)
return slot_events, executed_custom_actions
async def _execute_validation_action(
self,
extraction_events: List[Event],
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
slot_events: List[SlotSet] = [
event for event in extraction_events if isinstance(event, SlotSet)
]
slot_candidates = "\n".join([e.key for e in slot_events])
logger.debug(f"Validating extracted slots: {
slot_candidates}")
if ACTION_VALIDATE_SLOT_MAPPINGS not in domain.user_actions:
return cast(List[Event], slot_events)
_tracker = DialogueStateTracker.from_events(
tracker.sender_id,
tracker.events_after_latest_restart() + cast(List[Event], slot_events),
slots=domain.slots,
)
validate_events = await self._run_custom_action(
ACTION_VALIDATE_SLOT_MAPPINGS, output_channel, nlg, _tracker, domain
)
validated_slot_names = [
event.key for event in validate_events if isinstance(event, SlotSet)
]
# If the custom action doesn't return a SlotSet event for an extracted slot
# candidate we assume that it was valid. The custom action has to return a
# SlotSet(slot_name, None) event to mark a Slot as invalid.
return validate_events + [
event for event in slot_events if event.key not in validated_slot_names
]
def _fails_unique_entity_mapping_check(
self,
slot_name: Text,
mapping: Dict[Text, Any],
tracker: "DialogueStateTracker",
domain: "Domain",
) -> bool:
from rasa.core.actions.forms import FormAction
if mapping[MAPPING_TYPE] != str(SlotMappingType.FROM_ENTITY):
return False
form_name = tracker.active_loop_name
if not form_name:
return False
if tracker.get_slot(REQUESTED_SLOT) == slot_name:
return False
form = FormAction(form_name, self._action_endpoint)
if slot_name not in form.required_slots(domain):
return False
if form.entity_mapping_is_unique(mapping, domain):
return False
return True
async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Runs action. Please see parent class for the full docstring."""
slot_events: List[Event] = []
executed_custom_actions: Set[Text] = set()
user_slots = [
slot for slot in domain.slots if slot.name not in DEFAULT_SLOT_NAMES
]
for slot in user_slots:
for mapping in slot.mappings:
mapping_type = SlotMappingType(mapping.get(MAPPING_TYPE))
if not SlotMapping.check_mapping_validity(
slot_name=slot.name,
mapping_type=mapping_type,
mapping=mapping,
domain=domain,
):
continue
intent_is_desired = SlotMapping.intent_is_desired(
mapping, tracker, domain
)
if not intent_is_desired:
continue
if not ActionExtractSlots._verify_mapping_conditions(
mapping, tracker, slot.name
):
continue
if self._fails_unique_entity_mapping_check(
slot.name, mapping, tracker, domain
):
continue
if mapping_type.is_predefined_type():
value = extract_slot_value_from_predefined_mapping(
mapping_type, mapping, tracker
)
else:
value = None
if value:
if not isinstance(slot, ListSlot):
value = value[-1]
if tracker.get_slot(slot.name) != value:
slot_events.append(SlotSet(slot.name, value))
should_fill_custom_slot = mapping_type == SlotMappingType.CUSTOM
if should_fill_custom_slot:
(
custom_evts,
executed_custom_actions,
) = await self._execute_custom_action(
mapping,
executed_custom_actions,
output_channel,
nlg,
tracker,
domain,
)
slot_events.extend(custom_evts)
validated_events = await self._execute_validation_action(
slot_events, output_channel, nlg, tracker, domain
)
return validated_events
ActionRetrieveResponse
查询响应选择器以获得适当响应的操作
class ActionRetrieveResponse(ActionBotResponse):
"""An action which queries the Response Selector for the appropriate response."""
def __init__(self, name: Text, silent_fail: Optional[bool] = False) -> None:
"""Creates action. See docstring of parent class."""
super().__init__(name, silent_fail)
self.action_name = name
self.silent_fail = silent_fail
@staticmethod
def intent_name_from_action(action_name: Text) -> Text:
"""Resolve the name of the intent from the action name."""
return action_name.split(UTTER_PREFIX)[1]
def get_full_retrieval_name(
self, tracker: "DialogueStateTracker"
) -> Optional[Text]:
"""Returns full retrieval name for the action.
Extracts retrieval intent from response selector and
returns complete action utterance name.
Args:
tracker: Tracker containing past conversation events.
Returns:
Full retrieval name of the action if the last user utterance
contains a response selector output, `None` otherwise.
"""
latest_message = tracker.latest_message
if latest_message is None:
return None
if RESPONSE_SELECTOR_PROPERTY_NAME not in latest_message.parse_data:
return None
response_selector_properties = latest_message.parse_data[
RESPONSE_SELECTOR_PROPERTY_NAME
]
if (
self.intent_name_from_action(self.action_name)
in response_selector_properties
):
query_key = self.intent_name_from_action(self.action_name)
elif RESPONSE_SELECTOR_DEFAULT_INTENT in response_selector_properties:
query_key = RESPONSE_SELECTOR_DEFAULT_INTENT
else:
return None
selected = response_selector_properties[query_key]
full_retrieval_utter_action = selected[RESPONSE_SELECTOR_PREDICTION_KEY][
RESPONSE_SELECTOR_UTTER_ACTION_KEY
]
return full_retrieval_utter_action
async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Query the appropriate response and create a bot utterance with that."""
latest_message = tracker.latest_message
if latest_message is None:
return []
response_selector_properties = latest_message.parse_data[
RESPONSE_SELECTOR_PROPERTY_NAME
]
if (
self.intent_name_from_action(self.action_name)
in response_selector_properties
):
query_key = self.intent_name_from_action(self.action_name)
elif RESPONSE_SELECTOR_DEFAULT_INTENT in response_selector_properties:
query_key = RESPONSE_SELECTOR_DEFAULT_INTENT
else:
if not self.silent_fail:
logger.error(
"Couldn't create message for response action '{}'."
"".format(self.action_name)
)
return []
logger.debug(f"Picking response from selector of type {
query_key}")
selected = response_selector_properties[query_key]
# Override utter action of ActionBotResponse
# with the complete utter action retrieved from
# the output of response selector.
self.utter_action = selected[RESPONSE_SELECTOR_PREDICTION_KEY][
RESPONSE_SELECTOR_UTTER_ACTION_KEY
]
return await super().run(output_channel, nlg, tracker, domain)
def name(self) -> Text:
"""Returns action name."""
return self.action_name
ActionEndToEndResponse
向用户发出端到端响应的动作Action
class ActionEndToEndResponse(Action):
"""Action to utter end-to-end responses to the user."""
def __init__(self, action_text: Text) -> None:
"""Creates action.
Args:
action_text: Text of end-to-end bot response.
"""
self.action_text = action_text
def name(self) -> Text:
"""Returns action name."""
# In case of an end-to-end action there is no label (aka name) for the action.
# We fake a name by returning the text which the bot sends back to the user.
return self.action_text
async def run(
self,
output_channel: "OutputChannel",
nlg: "NaturalLanguageGenerator",
tracker: "DialogueStateTracker",
domain: "Domain",
) -> List[Event]:
"""Runs action (see parent class for full docstring)."""
message = {
"text": self.action_text}
return [create_bot_utterance(message)]
def event_for_successful_execution(
self, prediction: PolicyPrediction
) -> ActionExecuted:
"""Event which should be logged for the successful execution of this action.
Args:
prediction: Prediction which led to the execution of this event.
Returns:
Event which should be logged onto the tracker.
"""
return ActionExecuted(
policy=prediction.policy_name,
confidence=prediction.max_confidence,
action_text=self.action_text,
hide_rule_turn=prediction.hide_rule_turn,
metadata=prediction.action_metadata,
)
Event
描述对话中的事件以及事件如何影响对话状态, 在用户与助手对话期间发生的所有事情的不可变表示,告诉rasa.shared.core.trackers.DialogueStateTracker如何在事件发生时更新其状态。
class Event(ABC):
"""Describes events in conversation and how the affect the conversation state.
Immutable representation of everything which happened during a conversation of the
user with the assistant. Tells the `rasa.shared.core.trackers.DialogueStateTracker`
how to update its state as the events occur.
"""
type_name = "event"
def __init__(
self,
timestamp: Optional[float] = None,
metadata: Optional[Dict[Text, Any]] = None,
) -> None:
self.timestamp = timestamp or time.time()
self.metadata = metadata or {
}
def __ne__(self, other: Any) -> bool:
# Not strictly necessary, but to avoid having both x==y and x!=y
# True at the same time
return not (self == other)
@abc.abstractmethod
def as_story_string(self) -> Optional[Text]:
"""Returns the event as story string.
Returns:
textual representation of the event or None.
"""
# Every class should implement this
raise NotImplementedError
@staticmethod
def from_story_string(
event_name: Text,
parameters: Dict[Text, Any],
default: Optional[Type["Event"]] = None,
) -> Optional[List["Event"]]:
event_class = Event.resolve_by_type(event_name, default)
if not event_class:
return None
return event_class._from_story_string(parameters)
@staticmethod
def from_parameters(
parameters: Dict[Text, Any], default: Optional[Type["Event"]] = None
) -> Optional["Event"]:
event_name = parameters.get("event")
if event_name is None:
return None
event_class: Optional[Type[Event]] = Event.resolve_by_type(event_name, default)
if not event_class:
return None
return event_class._from_parameters(parameters)
@classmethod
def _from_story_string(
cls: Type[E], parameters: Dict[Text, Any]
) -> Optional[List[E]]:
"""Called to convert a parsed story line into an event."""
return [cls(parameters.get("timestamp"), parameters.get("metadata"))]
def as_dict(self) -> Dict[Text, Any]:
d = {
"event": self.type_name, "timestamp": self.timestamp}
if self.metadata:
d["metadata"] = self.metadata
return d
def fingerprint(self) -> Text:
"""Returns a unique hash for the event which is stable across python runs.
Returns:
fingerprint of the event
"""
data = self.as_dict()
del data["timestamp"]
return rasa.shared.utils.io.get_dictionary_fingerprint(data)
@classmethod
def _from_parameters(cls, parameters: Dict[Text, Any]) -> Optional["Event"]:
"""Called to convert a dictionary of parameters to a single event.
By default uses the same implementation as the story line
conversation ``_from_story_string``. But the subclass might
decide to handle parameters differently if the parsed parameters
don't origin from a story file.
"""
result = cls._from_story_string(parameters)
if len(result) > 1:
logger.warning(
f"Event from parameters called with parameters "
f"for multiple events. This is not supported, "
f"only the first event will be returned. "
f"Parameters: {
parameters}"
)
return result[0] if result else None
@staticmethod
def resolve_by_type(
type_name: Text, default: Optional[Type["Event"]] = None
) -> Optional[Type["Event"]]:
"""Returns a slots class by its type name."""
for cls in rasa.shared.utils.common.all_subclasses(Event):
if cls.type_name == type_name:
return cls
if type_name == "topic":
return None # backwards compatibility to support old TopicSet evts
elif default is not None:
return default
else:
raise ValueError(f"Unknown event name '{
type_name}'.")
def apply_to(self, tracker: "DialogueStateTracker") -> None:
"""Applies event to current conversation state.
Args:
tracker: The current conversation state.
"""
pass
@abc.abstractmethod
def __eq__(self, other: Any) -> bool:
"""Compares object with other object."""
# Every class should implement this
raise NotImplementedError()
def __str__(self) -> Text:
"""Returns text representation of event."""
return f"{
self.__class__.__name__}()"
UserUtteranceReverted
Bot会还原最近的用户消息之前的所有内容。bot将在最近的“UserUttered”之后恢复所有事件,这也意味着跟踪器上的最后一个事件通常是“action_listen”,bot正在等待一个新的用户消息。
class UserUtteranceReverted(AlwaysEqualEventMixin):
"""Bot reverts everything until before the most recent user message.
The bot will revert all events after the latest `UserUttered`, this
also means that the last event on the tracker is usually `action_listen`
and the bot is waiting for a new user message.
"""
type_name = "rewind"
def __hash__(self) -> int:
"""Returns unique hash for event."""
return hash(32143124315)
def as_story_string(self) -> Text:
"""Returns text representation of event."""
return self.type_name
def apply_to(self, tracker: "DialogueStateTracker") -> None:
"""Applies event to current conversation state."""
tracker._reset()
tracker.replay_events()
DefinePrevUserUtteredFeaturization
存储是否基于文本或意图预测操作的信息。
class DefinePrevUserUtteredFeaturization(SkipEventInMDStoryMixin):
"""Stores information whether action was predicted based on text or intent."""
type_name = "user_featurization"
def __init__(
self,
use_text_for_featurization: bool,
timestamp: Optional[float] = None,
metadata: Optional[Dict[Text, Any]] = None,
) -> None:
"""Creates event.
Args:
use_text_for_featurization: `True` if message text was used to predict
action. `False` if intent was used.
timestamp: When the event was created.
metadata: Additional event metadata.
"""
super().__init__(timestamp, metadata)
self.use_text_for_featurization = use_text_for_featurization
def __str__(self) -> Text:
"""Returns text representation of event."""
return f"DefinePrevUserUtteredFeaturization({
self.use_text_for_featurization})"
def __hash__(self) -> int:
"""Returns unique hash for event."""
return hash(self.use_text_for_featurization)
@classmethod
def _from_parameters(
cls, parameters: Dict[Text, Any]
) -> "DefinePrevUserUtteredFeaturization":
return DefinePrevUserUtteredFeaturization(
parameters.get(USE_TEXT_FOR_FEATURIZATION),
parameters.get("timestamp"),
parameters.get("metadata"),
)
def as_dict(self) -> Dict[Text, Any]:
"""Returns serialized event."""
d = super().as_dict()
d.update({
USE_TEXT_FOR_FEATURIZATION: self.use_text_for_featurization})
return d
def apply_to(self, tracker: "DialogueStateTracker") -> None:
"""Applies event to current conversation state.
Args:
tracker: The current conversation state.
"""
if tracker.latest_action_name != ACTION_LISTEN_NAME:
# featurization belong only to the last user message
# a user message is always followed by action listen
return
if not tracker.latest_message:
return
# update previous user message's featurization based on this event
tracker.latest_message.use_text_for_featurization = (
self.use_text_for_featurization
)
def __eq__(self, other: Any) -> bool:
"""Compares object with other object."""
if not isinstance(other, DefinePrevUserUtteredFeaturization):
return NotImplemented
return self.use_text_for_featurization == other.use_text_for_featurization
ActionExecuted
操作描述所采取的Action及其结果。
class ActionExecuted(Event):
"""An operation describes an action taken + its result.
It comprises an action and a list of events. operations will be appended
to the latest `Turn`` in `Tracker.turns`.
"""
type_name = "action"
def __init__(
self,
action_name: Optional[Text] = None,
policy: Optional[Text] = None,
confidence: Optional[float] = None,
timestamp: Optional[float] = None,
metadata: Optional[Dict] = None,
action_text: Optional[Text] = None,
hide_rule_turn: bool = False,
) -> None:
"""Creates event for a successful event execution.
Args:
action_name: Name of the action which was executed. `None` if it was an
end-to-end prediction.
policy: Policy which predicted action.
confidence: Confidence with which policy predicted action.
timestamp: When the event was created.
metadata: Additional event metadata.
action_text: In case it's an end-to-end action prediction, the text which
was predicted.
hide_rule_turn: If `True`, this action should be hidden in the dialogue
history created for ML-based policies.
"""
self.action_name = action_name
self.policy = policy
self.confidence = confidence
self.unpredictable = False
self.action_text = action_text
self.hide_rule_turn = hide_rule_turn
if self.action_name is None and self.action_text is None:
raise ValueError(
"Both the name of the action and the end-to-end "
"predicted text are missing. "
"The `ActionExecuted` event cannot be initialised."
)
super().__init__(timestamp, metadata)
def __members__(self) -> Tuple[Optional[Text], Optional[Text], Text]:
meta_no_nones = {
k: v for k, v in self.metadata.items() if v is not None}
return (self.action_name, self.action_text, jsonpickle.encode(meta_no_nones))
def __repr__(self) -> Text:
"""Returns event as string for debugging."""
return "ActionExecuted(action: {}, policy: {}, confidence: {})".format(
self.action_name, self.policy, self.confidence
)
def __str__(self) -> Text:
"""Returns event as human readable string."""
return str(self.action_name) or str(self.action_text)
def __hash__(self) -> int:
"""Returns unique hash for event."""
return hash(self.__members__())
def __eq__(self, other: Any) -> bool:
"""Compares object with other object."""
if not isinstance(other, ActionExecuted):
return NotImplemented
return self.__members__() == other.__members__()
def as_story_string(self) -> Optional[Text]:
"""Returns event in Markdown format."""
if self.action_text:
raise UnsupportedFeatureException(
f"Printing end-to-end bot utterances is not supported in the "
f"Markdown training format. Please use the YAML training data format "
f"instead. Please see {
DOCS_URL_TRAINING_DATA} for more information."
)
return self.action_name
@classmethod
def _from_story_string(
cls, parameters: Dict[Text, Any]
) -> Optional[List["ActionExecuted"]]:
return [
ActionExecuted(
parameters.get("name"),
parameters.get("policy"),
parameters.get("confidence"),
parameters.get("timestamp"),
parameters.get("metadata"),
parameters.get("action_text"),
parameters.get("hide_rule_turn", False),
)
]
def as_dict(self) -> Dict[Text, Any]:
"""Returns serialized event."""
d = super().as_dict()
d.update(
{
"name": self.action_name,
"policy": self.policy,
"confidence": self.confidence,
"action_text": self.action_text,
"hide_rule_turn": self.hide_rule_turn,
}
)
return d
def as_sub_state(self) -> Dict[Text, Text]:
"""Turns ActionExecuted into a dictionary containing action name or action text.
One action cannot have both set at the same time
Returns:
a dictionary containing action name or action text with the corresponding
key.
"""
if self.action_name:
return {
ACTION_NAME: self.action_name}
else:
# FIXME: we should define the type better here, and require either
# `action_name` or `action_text`
return {
ACTION_TEXT: cast(Text, self.action_text)}
def apply_to(self, tracker: "DialogueStateTracker") -> None:
"""Applies event to current conversation state."""
tracker.set_latest_action(self.as_sub_state())
tracker.clear_followup_action()
ActionExecutionRejected
通知核心 执行操作已被拒绝。
class ActionExecutionRejected(SkipEventInMDStoryMixin):
"""Notify Core that the execution of the action has been rejected."""
type_name = "action_execution_rejected"
def __init__(
self,
action_name: Text,
policy: Optional[Text] = None,
confidence: Optional[float] = None,
timestamp: Optional[float] = None,
metadata: Optional[Dict[Text, Any]] = None,
) -> None:
"""Creates event.
Args:
action_name: Action which was rejected.
policy: Policy which predicted the rejected action.
confidence: Confidence with which the reject action was predicted.
timestamp: When the event was created.
metadata: Additional event metadata.
"""
self.action_name = action_name
self.policy = policy
self.confidence = confidence
super().__init__(timestamp, metadata)
def __str__(self) -> Text:
"""Returns text representation of event."""
return (
"ActionExecutionRejected("
"action: {}, policy: {}, confidence: {})"
"".format(self.action_name, self.policy, self.confidence)
)
def __hash__(self) -> int:
"""Returns unique hash for event."""
return hash(self.action_name)
def __eq__(self, other: Any) -> bool:
"""Compares object with other object."""
if not isinstance(other, ActionExecutionRejected):
return NotImplemented
return self.action_name == other.action_name
@classmethod
def _from_parameters(cls, parameters: Dict[Text, Any]) -> "ActionExecutionRejected":
return ActionExecutionRejected(
parameters.get("name"),
parameters.get("policy"),
parameters.get("confidence"),
parameters.get("timestamp"),
parameters.get("metadata"),
)
def as_dict(self) -> Dict[Text, Any]:
"""Returns serialized event."""
d = super().as_dict()
d.update(
{
"name": self.action_name,
"policy": self.policy,
"confidence": self.confidence,
}
)
return d
def apply_to(self, tracker: "DialogueStateTracker") -> None:
"""Applies event to current conversation state."""
tracker.reject_action(self.action_name)
Agent
Agent类为最重要的Rasa功能提供了一个接口。这包括训练、处理消息、加载对话模型、获取下一个action操作和处理通道。
class Agent:
"""The Agent class provides an interface for the most important Rasa functionality.
This includes training, handling messages, loading a dialogue model,
getting the next action, and handling a channel.
"""
def __init__(
self,
domain: Optional[Domain] = None,
generator: Union[EndpointConfig, NaturalLanguageGenerator, None] = None,
tracker_store: Optional[TrackerStore] = None,
lock_store: Optional[LockStore] = None,
action_endpoint: Optional[EndpointConfig] = None,
fingerprint: Optional[Text] = None,
model_server: Optional[EndpointConfig] = None,
remote_storage: Optional[Text] = None,
http_interpreter: Optional[RasaNLUHttpInterpreter] = None,
):
"""Initializes an `Agent`."""
self.domain = domain
self.processor: Optional[MessageProcessor] = None
self.nlg = NaturalLanguageGenerator.create(generator, self.domain)
self.tracker_store = self._create_tracker_store(tracker_store, self.domain)
self.lock_store = self._create_lock_store(lock_store)
self.action_endpoint = action_endpoint
self.http_interpreter = http_interpreter
self._set_fingerprint(fingerprint)
self.model_server = model_server
self.remote_storage = remote_storage
@classmethod
def load(
cls,
model_path: Union[Text, Path],
domain: Optional[Domain] = None,
generator: Union[EndpointConfig, NaturalLanguageGenerator, None] = None,
tracker_store: Optional[TrackerStore] = None,
lock_store: Optional[LockStore] = None,
action_endpoint: Optional[EndpointConfig] = None,
fingerprint: Optional[Text] = None,
model_server: Optional[EndpointConfig] = None,
remote_storage: Optional[Text] = None,
http_interpreter: Optional[RasaNLUHttpInterpreter] = None,
) -> Agent:
"""Constructs a new agent and loads the processer and model."""
agent = Agent(
domain=domain,
generator=generator,
tracker_store=tracker_store,
lock_store=lock_store,
action_endpoint=action_endpoint,
fingerprint=fingerprint,
model_server=model_server,
remote_storage=remote_storage,
http_interpreter=http_interpreter,
)
agent.load_model(model_path=model_path, fingerprint=fingerprint)
return agent
EndpointConfig
外部HTTP端点的配置
class EndpointConfig:
"""Configuration for an external HTTP endpoint."""
def __init__(
self,
url: Optional[Text] = None,
params: Optional[Dict[Text, Any]] = None,
headers: Optional[Dict[Text, Any]] = None,
basic_auth: Optional[Dict[Text, Text]] = None,
token: Optional[Text] = None,
token_name: Text = "token",
cafile: Optional[Text] = None,
**kwargs: Any,
) -> None:
"""Creates an `EndpointConfig` instance."""
self.url = url
self.params = params or {
}
self.headers = headers or {
}
self.basic_auth = basic_auth or {
}
self.token = token
self.token_name = token_name
self.type = kwargs.pop("store_type", kwargs.pop("type", None))
self.cafile = cafile
self.kwargs = kwargs
def session(self) -> aiohttp.ClientSession:
"""Creates and returns a configured aiohttp client session."""
# create authentication parameters
if self.basic_auth:
auth = aiohttp.BasicAuth(
self.basic_auth["username"], self.basic_auth["password"]
)
else:
auth = None
return aiohttp.ClientSession(
headers=self.headers,
auth=auth,
timeout=aiohttp.ClientTimeout(total=DEFAULT_REQUEST_TIMEOUT),
)
Domain
域指定bot的策略在其中运行的范围。域子类提供了机器人可以采取的行动、可以识别的意图和实体。
class Domain:
"""The domain specifies the universe in which the bot's policy acts.
A Domain subclass provides the actions the bot can take, the intents
and entities it can recognise.
"""
@classmethod
def empty(cls) -> "Domain":
"""Returns empty Domain."""
return Domain.from_dict({
})
@classmethod
def load(cls, paths: Union[List[Union[Path, Text]], Text, Path]) -> "Domain":
"""Returns loaded Domain after merging all domain files."""
if not paths:
raise InvalidDomain(
"No domain file was specified. Please specify a path "
"to a valid domain file."
)
elif not isinstance(paths, list) and not isinstance(paths, set):
paths = [paths]
domain = Domain.empty()
for path in paths:
other = cls.from_path(path)
domain = domain.merge(other)
return domain
@classmethod
def from_path(cls, path: Union[Text, Path]) -> "Domain":
"""Loads the `Domain` from a path."""
path = os.path.abspath(path)
if os.path.isfile(path):
domain = cls.from_file(path)
elif os.path.isdir(path):
domain = cls.from_directory(path)
else:
raise InvalidDomain(
"Failed to load domain specification from '{}'. "
"File not found!".format(os.path.abspath(path))
)
return domain
from_yaml
验证后从YAML文本加载“域”
@classmethod
def from_yaml(cls, yaml: Text, original_filename: Text = "") -> "Domain":
"""Loads the `Domain` from YAML text after validating it."""
try:
rasa.shared.utils.validation.validate_yaml_schema(yaml, DOMAIN_SCHEMA_FILE)
data = rasa.shared.utils.io.read_yaml(yaml)
if not rasa.shared.utils.validation.validate_training_data_format_version(
data, original_filename
):
return Domain.empty()
return cls.from_dict(data)
except YamlException as e:
e.filename = original_filename
raise e
required_slots_for_form
检索域中定义的表单所需词槽的列表。
def required_slots_for_form(self, form_name: Text) -> List[Text]:
"""Retrieve the list of required slot names for a form defined in the domain.
Args:
form_name: The name of the form.
Returns:
The list of slot names or an empty list if no form was found.
"""
form = self.forms.get(form_name)
if form:
return form[REQUIRED_SLOTS_KEY]
return []
CollectingOutputChannel
在列表中收集发送消息的输出通道,(不发送到任何地方,只收集)
class CollectingOutputChannel(OutputChannel):
"""Output channel that collects send messages in a list
(doesn't send them anywhere, just collects them)."""
def __init__(self) -> None:
"""Initialise list to collect messages."""
self.messages: List[Dict[Text, Any]] = []
@classmethod
def name(cls) -> Text:
"""Name of the channel."""
return "collector"
@staticmethod
def _message(
recipient_id: Text,
text: Text = None,
image: Text = None,
buttons: List[Dict[Text, Any]] = None,
attachment: Text = None,
custom: Dict[Text, Any] = None,
) -> Dict:
"""Create a message object that will be stored."""
obj = {
"recipient_id": recipient_id,
"text": text,
"image": image,
"buttons": buttons,
"attachment": attachment,
"custom": custom,
}
# filter out any values that are `None`
return {
k: v for k, v in obj.items() if v is not None}
def latest_output(self) -> Optional[Dict[Text, Any]]:
if self.messages:
return self.messages[-1]
else:
return None
async def _persist_message(self, message: Dict[Text, Any]) -> None:
self.messages.append(message)
async def send_text_message(
self, recipient_id: Text, text: Text, **kwargs: Any
) -> None:
for message_part in text.strip().split("\n\n"):
await self._persist_message(self._message(recipient_id, text=message_part))
async def send_image_url(
self, recipient_id: Text, image: Text, **kwargs: Any
) -> None:
"""Sends an image. Default will just post the url as a string."""
await self._persist_message(self._message(recipient_id, image=image))
async def send_attachment(
self, recipient_id: Text, attachment: Text, **kwargs: Any
) -> None:
"""Sends an attachment. Default will just post as a string."""
await self._persist_message(self._message(recipient_id, attachment=attachment))
async def send_text_with_buttons(
self,
recipient_id: Text,
text: Text,
buttons: List[Dict[Text, Any]],
**kwargs: Any,
) -> None:
await self._persist_message(
self._message(recipient_id, text=text, buttons=buttons)
)
async def send_custom_json(
self, recipient_id: Text, json_message: Dict[Text, Any], **kwargs: Any
) -> None:
await self._persist_message(self._message(recipient_id, custom=json_message))
TemplatedNaturalLanguageGenerator
基于响应生成消息的自然语言生成器,回答可以根据对话的状态使用变量来定制话语
class TemplatedNaturalLanguageGenerator(NaturalLanguageGenerator):
"""Natural language generator that generates messages based on responses.
The responses can use variables to customize the utterances based on the
state of the dialogue.
"""
def __init__(self, responses: Dict[Text, List[Dict[Text, Any]]]) -> None:
"""Creates a Template Natural Language Generator.
Args:
responses: responses that will be used to generate messages.
"""
self.responses = responses
def _matches_filled_slots(
self, filled_slots: Dict[Text, Any], response: Dict[Text, Any]
) -> bool:
"""Checks if the conditional response variation matches the filled slots."""
constraints = response.get(RESPONSE_CONDITION, [])
for constraint in constraints:
name = constraint["name"]
value = constraint["value"]
if filled_slots.get(name) != value:
return False
return True
def _responses_for_utter_action(
self, utter_action: Text, output_channel: Text, filled_slots: Dict[Text, Any]
) -> List[Dict[Text, Any]]:
"""Returns array of responses that fit the channel, action and condition."""
default_responses = list(
filter(
lambda x: (x.get(RESPONSE_CONDITION) is None),
self.responses[utter_action],
)
)
conditional_responses = list(
filter(
lambda x: (
x.get(RESPONSE_CONDITION)
and self._matches_filled_slots(
filled_slots=filled_slots, response=x
)
),
self.responses[utter_action],
)
)
conditional_channel = list(
filter(lambda x: (x.get(CHANNEL) == output_channel), conditional_responses)
)
conditional_no_channel = list(
filter(lambda x: (x.get(CHANNEL) is None), conditional_responses)
)
default_channel = list(
filter(lambda x: (x.get(CHANNEL) == output_channel), default_responses)
)
default_no_channel = list(
filter(lambda x: (x.get(CHANNEL) is None), default_responses)
)
if conditional_channel:
return conditional_channel
if default_channel:
return default_channel
if conditional_no_channel:
return conditional_no_channel
return default_no_channel
# noinspection PyUnusedLocal
def _random_response_for(
self, utter_action: Text, output_channel: Text, filled_slots: Dict[Text, Any]
) -> Optional[Dict[Text, Any]]:
"""Select random response for the utter action from available ones.
If channel-specific responses for the current output channel are given,
only choose from channel-specific ones.
"""
import numpy as np
if utter_action in self.responses:
suitable_responses = self._responses_for_utter_action(
utter_action, output_channel, filled_slots
)
if suitable_responses:
selected_response = np.random.choice(suitable_responses)
condition = selected_response.get(RESPONSE_CONDITION)
if condition:
formatted_response_conditions = self._format_response_conditions(
condition
)
logger.debug(
"Selecting response variation with conditions:"
f"{
formatted_response_conditions}"
)
return selected_response
else:
return None
else:
return None
async def generate(
self,
utter_action: Text,
tracker: DialogueStateTracker,
output_channel: Text,
**kwargs: Any,
) -> Optional[Dict[Text, Any]]:
"""Generate a response for the requested utter action."""
filled_slots = tracker.current_slot_values()
return self.generate_from_slots(
utter_action, filled_slots, output_channel, **kwargs
)
def generate_from_slots(
self,
utter_action: Text,
filled_slots: Dict[Text, Any],
output_channel: Text,
**kwargs: Any,
) -> Optional[Dict[Text, Any]]:
"""Generate a response for the requested utter action."""
# Fetching a random response for the passed utter action
r = copy.deepcopy(
self._random_response_for(utter_action, output_channel, filled_slots)
)
# Filling the slots in the response with placeholders and returning the response
if r is not None:
return self._fill_response(r, filled_slots, **kwargs)
else:
return None
def _fill_response(
self,
response: Dict[Text, Any],
filled_slots: Optional[Dict[Text, Any]] = None,
**kwargs: Any,
) -> Dict[Text, Any]:
"""Combine slot values and key word arguments to fill responses."""
# Getting the slot values in the response variables
response_vars = self._response_variables(filled_slots, kwargs)
keys_to_interpolate = [
"text",
"image",
"custom",
"buttons",
"attachment",
"quick_replies",
]
if response_vars:
for key in keys_to_interpolate:
if key in response:
response[key] = interpolator.interpolate(
response[key], response_vars
)
return response
@staticmethod
def _response_variables(
filled_slots: Dict[Text, Any], kwargs: Dict[Text, Any]
) -> Dict[Text, Any]:
"""Combine slot values and key word arguments to fill responses."""
if filled_slots is None:
filled_slots = {
}
# Copying the filled slots in the response variables.
response_vars = filled_slots.copy()
response_vars.update(kwargs)
return response_vars
@staticmethod
def _format_response_conditions(response_conditions: List[Dict[Text, Any]]) -> Text:
formatted_response_conditions = [""]
for index, condition in enumerate(response_conditions):
constraints = []
constraints.append(f"type: {
str(condition['type'])}")
constraints.append(f"name: {
str(condition['name'])}")
constraints.append(f"value: {
str(condition['value'])}")
condition_message = " | ".join(constraints)
formatted_condition = f"[condition {
str(index + 1)}] {
condition_message}"
formatted_response_conditions.append(formatted_condition)
return "\n".join(formatted_response_conditions)
generate
为所请求的utter action.生成响应。
def generate(
self,
utter_action: Text,
tracker: DialogueStateTracker,
output_channel: Text,
**kwargs: Any,
) -> Optional[Dict[Text, Any]]:
"""Generate a response for the requested utter action."""
filled_slots = tracker.current_slot_values()
return self.generate_from_slots(
utter_action, filled_slots, output_channel, **kwargs
)
generate_from_slots
为所请求的utter action 生成响应response 。
def generate_from_slots(
self,
utter_action: Text,
filled_slots: Dict[Text, Any],
output_channel: Text,
**kwargs: Any,
) -> Optional[Dict[Text, Any]]:
"""Generate a response for the requested utter action."""
# Fetching a random response for the passed utter action
r = copy.deepcopy(
self._random_response_for(utter_action, output_channel, filled_slots)
)
# Filling the slots in the response with placeholders and returning the response
if r is not None:
return self._fill_response(r, filled_slots, **kwargs)
else:
return None
_fill_response
结合词槽和关键字参数来填充响应
def _fill_response(
self,
response: Dict[Text, Any],
filled_slots: Optional[Dict[Text, Any]] = None,
**kwargs: Any,
) -> Dict[Text, Any]:
"""Combine slot values and key word arguments to fill responses."""
# Getting the slot values in the response variables
response_vars = self._response_variables(filled_slots, kwargs)
keys_to_interpolate = [
"text",
"image",
"custom",
"buttons",
"attachment",
"quick_replies",
]
if response_vars:
for key in keys_to_interpolate:
if key in response:
response[key] = interpolator.interpolate(
response[key], response_vars
)
return response
DialogueStateTracker
保持对话的状态。
class DialogueStateTracker:
"""Maintains the state of a conversation.
The field max_event_history will only give you these last events,
it can be set in the tracker_store"""
@classmethod
def from_dict(
cls,
sender_id: Text,
events_as_dict: List[Dict[Text, Any]],
slots: Optional[Iterable[Slot]] = None,
max_event_history: Optional[int] = None,
) -> "DialogueStateTracker":
"""Create a tracker from dump.
The dump should be an array of dumped events. When restoring
the tracker, these events will be replayed to recreate the state.
"""
evts = events.deserialise_events(events_as_dict)
return cls.from_events(sender_id, evts, slots, max_event_history)
@classmethod
def from_events(
cls,
sender_id: Text,
evts: List[Event],
slots: Optional[Iterable[Slot]] = None,
max_event_history: Optional[int] = None,
sender_source: Optional[Text] = None,
domain: Optional[Domain] = None,
) -> "DialogueStateTracker":
"""Creates tracker from existing events.
Args:
sender_id: The ID of the conversation.
evts: Existing events which should be applied to the new tracker.
slots: Slots which can be set.
max_event_history: Maximum number of events which should be stored.
sender_source: File source of the messages.
domain: The current model domain.
Returns:
Instantiated tracker with its state updated according to the given
events.
"""
tracker = cls(sender_id, slots, max_event_history, sender_source)
for e in evts:
tracker.update(e, domain)
return tracker
def __init__(
self,
sender_id: Text,
slots: Optional[Iterable[Slot]],
max_event_history: Optional[int] = None,
sender_source: Optional[Text] = None,
is_rule_tracker: bool = False,
) -> None:
"""Initialize the tracker.
A set of events can be stored externally, and we will run through all
of them to get the current state. The tracker will represent all the
information we captured while processing messages of the dialogue."""
# maximum number of events to store
self._max_event_history = max_event_history
# list of previously seen events
self.events = self._create_events([])
# id of the source of the messages
self.sender_id = sender_id
# slots that can be filled in this domain
if slots is not None:
self.slots = {
slot.name: copy.copy(slot) for slot in slots}
else:
self.slots = AnySlotDict()
# file source of the messages
self.sender_source = sender_source
# whether the tracker belongs to a rule-based data
self.is_rule_tracker = is_rule_tracker
###
# current state of the tracker - MUST be re-creatable by processing
# all the events. This only defines the attributes, values are set in
# `reset()`
###
# if tracker is paused, no actions should be taken
self._paused = False
# A deterministically scheduled action to be executed next
self.followup_action = ACTION_LISTEN_NAME
self.latest_action = None
# Stores the most recent message sent by the user
self.latest_message: Optional[UserUttered] = None
self.latest_bot_utterance = None
self._reset()
self.active_loop: "TrackerActiveLoop" = {
}
# Optional model_id to add to all events.
self.model_id: Optional[Text] = None
E:\starspace\GavinNLP\rasa-3.1.0\rasa\shared\constants.py
# Keys related to Forms (in the Domain)
REQUIRED_SLOTS_KEY = "required_slots"
IGNORED_INTENTS = "ignored_intents"
get_slot
获取词槽的值
def get_slot(self, key: Text) -> Optional[Any]:
"""Retrieves the value of a slot."""
if key in self.slots:
return self.slots[key].value
else:
logger.info(f"Tried to access non existent slot '{
key}'")
return None
from_events
从现有事件创建跟踪程序
@classmethod
def from_events(
cls,
sender_id: Text,
evts: List[Event],
slots: Optional[Iterable[Slot]] = None,
max_event_history: Optional[int] = None,
sender_source: Optional[Text] = None,
domain: Optional[Domain] = None,
) -> "DialogueStateTracker":
"""Creates tracker from existing events.
Args:
sender_id: The ID of the conversation.
evts: Existing events which should be applied to the new tracker.
slots: Slots which can be set.
max_event_history: Maximum number of events which should be stored.
sender_source: File source of the messages.
domain: The current model domain.
Returns:
Instantiated tracker with its state updated according to the given
events.
"""
tracker = cls(sender_id, slots, max_event_history, sender_source)
for e in evts:
tracker.update(e, domain)
return tracker
MessageProcessor
消息处理器是与机器人模型通信的接口
class MessageProcessor:
"""The message processor is interface for communicating with a bot model."""
def __init__(
self,
model_path: Union[Text, Path],
tracker_store: rasa.core.tracker_store.TrackerStore,
lock_store: LockStore,
generator: NaturalLanguageGenerator,
action_endpoint: Optional[EndpointConfig] = None,
max_number_of_predictions: int = MAX_NUMBER_OF_PREDICTIONS,
on_circuit_break: Optional[LambdaType] = None,
http_interpreter: Optional[RasaNLUHttpInterpreter] = None,
) -> None:
"""Initializes a `MessageProcessor`."""
self.nlg = generator
self.tracker_store = tracker_store
self.lock_store = lock_store
self.max_number_of_predictions = max_number_of_predictions
self.on_circuit_break = on_circuit_break
self.action_endpoint = action_endpoint
self.model_filename, self.model_metadata, self.graph_runner = self._load_model(
model_path
)
self.model_path = Path(model_path)
self.domain = self.model_metadata.domain
self.http_interpreter = http_interpreter
@staticmethod
def _load_model(
model_path: Union[Text, Path]
) -> Tuple[Text, ModelMetadata, GraphRunner]:
"""Unpacks a model from a given path using the graph model loader."""
try:
if os.path.isfile(model_path):
model_tar = model_path
else:
model_tar = get_latest_model(model_path)
if not model_tar:
raise ModelNotFound(f"No model found at path '{
model_path}'.")
except TypeError:
raise ModelNotFound(f"Model {
model_path} can not be loaded.")
logger.info(f"Loading model {
model_tar}...")
with tempfile.TemporaryDirectory() as temporary_directory:
try:
metadata, runner = loader.load_predict_graph_runner(
Path(temporary_directory),
Path(model_tar),
LocalModelStorage,
DaskGraphRunner,
)
return os.path.basename(model_tar), metadata, runner
except tarfile.ReadError:
raise ModelNotFound(f"Model {
model_path} can not be loaded.")
constants 常量
E:\starspace\GavinNLP\rasa-3.1.0\rasa\shared\core\constants.py
DEFAULT_CATEGORICAL_SLOT_VALUE = "__other__"
USER_INTENT_RESTART = "restart"
USER_INTENT_BACK = "back"
USER_INTENT_OUT_OF_SCOPE = "out_of_scope"
USER_INTENT_SESSION_START = "session_start"
SESSION_START_METADATA_SLOT = "session_started_metadata"
DEFAULT_INTENTS = [
USER_INTENT_RESTART,
USER_INTENT_BACK,
USER_INTENT_OUT_OF_SCOPE,
USER_INTENT_SESSION_START,
constants.DEFAULT_NLU_FALLBACK_INTENT_NAME,
]
LOOP_NAME = "name"
ACTION_LISTEN_NAME = "action_listen"
ACTION_RESTART_NAME = "action_restart"
ACTION_SESSION_START_NAME = "action_session_start"
ACTION_DEFAULT_FALLBACK_NAME = "action_default_fallback"
ACTION_DEACTIVATE_LOOP_NAME = "action_deactivate_loop"
ACTION_REVERT_FALLBACK_EVENTS_NAME = "action_revert_fallback_events"
ACTION_DEFAULT_ASK_AFFIRMATION_NAME = "action_default_ask_affirmation"
ACTION_DEFAULT_ASK_REPHRASE_NAME = "action_default_ask_rephrase"
ACTION_BACK_NAME = "action_back"
ACTION_TWO_STAGE_FALLBACK_NAME = "action_two_stage_fallback"
ACTION_UNLIKELY_INTENT_NAME = "action_unlikely_intent"
RULE_SNIPPET_ACTION_NAME = "..."
ACTION_EXTRACT_SLOTS = "action_extract_slots"
ACTION_VALIDATE_SLOT_MAPPINGS = "action_validate_slot_mappings"
DEFAULT_ACTION_NAMES = [
ACTION_LISTEN_NAME,
ACTION_RESTART_NAME,
ACTION_SESSION_START_NAME,
ACTION_DEFAULT_FALLBACK_NAME,
ACTION_DEACTIVATE_LOOP_NAME,
ACTION_REVERT_FALLBACK_EVENTS_NAME,
ACTION_DEFAULT_ASK_AFFIRMATION_NAME,
ACTION_DEFAULT_ASK_REPHRASE_NAME,
ACTION_TWO_STAGE_FALLBACK_NAME,
ACTION_UNLIKELY_INTENT_NAME,
ACTION_BACK_NAME,
RULE_SNIPPET_ACTION_NAME,
ACTION_EXTRACT_SLOTS,
]
E:\starspace\GavinNLP\rasa-3.1.0\rasa\shared\constants.py
LATEST_TRAINING_DATA_FORMAT_VERSION = "3.1"
DEFAULT_NLU_FALLBACK_INTENT_NAME = "nlu_fallback"
E:\starspace\GavinNLP\rasa-3.1.0\rasa\shared\core\constants.py
LOOP_NAME = "name"
REQUESTED_SLOT = "requested_slot"
# slots for knowledge base
SLOT_LISTED_ITEMS = "knowledge_base_listed_objects"
SLOT_LAST_OBJECT = "knowledge_base_last_object"
SLOT_LAST_OBJECT_TYPE = "knowledge_base_last_object_type"
DEFAULT_KNOWLEDGE_BASE_ACTION = "action_query_knowledge_base"
DEFAULT_SLOT_NAMES = {
REQUESTED_SLOT,
SESSION_START_METADATA_SLOT,
SLOT_LISTED_ITEMS,
SLOT_LAST_OBJECT,
SLOT_LAST_OBJECT_TYPE,
}
SLOT_MAPPINGS = "mappings"
MAPPING_CONDITIONS = "conditions"
MAPPING_TYPE = "type"
E:\starspace\GavinNLP\rasa-3.1.0\rasa\shared\nlu\constants.py
TEXT = "text"
TEXT_TOKENS = "text_tokens"
INTENT = "intent"
NOT_INTENT = "not_intent"
RESPONSE = "response"
RESPONSE_SELECTOR = "response_selector"
INTENT_RESPONSE_KEY = "intent_response_key"
ACTION_TEXT = "action_text"
ACTION_NAME = "action_name"
INTENT_NAME_KEY = "name"
FULL_RETRIEVAL_INTENT_NAME_KEY = "full_retrieval_intent_name"
METADATA = "metadata"
METADATA_INTENT = "intent"
METADATA_EXAMPLE = "example"
METADATA_MODEL_ID = "model_id"
INTENT_RANKING_KEY = "intent_ranking"
PREDICTED_CONFIDENCE_KEY = "confidence"
E:\starspace\GavinNLP\rasa-3.1.0\rasa\shared\constants.py
DEFAULT_SENDER_ID = "default"
UTTER_PREFIX = "utter_"
train
训练 Rasa模型 (Core and NLU).
def train(
domain: Text,
config: Text,
training_files: Optional[Union[Text, List[Text]]],
output: Text = rasa.shared.constants.DEFAULT_MODELS_PATH,
dry_run: bool = False,
force_training: bool = False,
fixed_model_name: Optional[Text] = None,
persist_nlu_training_data: bool = False,
core_additional_arguments: Optional[Dict] = None,
nlu_additional_arguments: Optional[Dict] = None,
model_to_finetune: Optional[Text] = None,
finetuning_epoch_fraction: float = 1.0,
) -> TrainingResult:
"""Trains a Rasa model (Core and NLU).
Args:
domain: Path to the domain file.
config: Path to the config file.
training_files: List of paths to training data files.
output: Output directory for the trained model.
dry_run: If `True` then no training will be done, and the information about
whether the training needs to be done will be printed.
force_training: If `True` retrain model even if data has not changed.
fixed_model_name: Name of model to be stored.
persist_nlu_training_data: `True` if the NLU training data should be persisted
with the model.
core_additional_arguments: Additional training parameters for core training.
nlu_additional_arguments: Additional training parameters forwarded to training
method of each NLU component.
model_to_finetune: Optional path to a model which should be finetuned or
a directory in case the latest trained model should be used.
finetuning_epoch_fraction: The fraction currently specified training epochs
in the model configuration which should be used for finetuning.
Returns:
An instance of `TrainingResult`.
"""
file_importer = TrainingDataImporter.load_from_config(
config, domain, training_files
)
stories = file_importer.get_stories()
nlu_data = file_importer.get_nlu_data()
training_type = TrainingType.BOTH
if nlu_data.has_e2e_examples():
rasa.shared.utils.common.mark_as_experimental_feature("end-to-end training")
training_type = TrainingType.END_TO_END
if stories.is_empty() and nlu_data.contains_no_pure_nlu_data():
rasa.shared.utils.cli.print_error(
"No training data given. Please provide stories and NLU data in "
"order to train a Rasa model using the '--data' argument."
)
return TrainingResult(code=1)
domain_object = file_importer.get_domain()
if domain_object.is_empty():
rasa.shared.utils.cli.print_warning(
"Core training was skipped because no valid domain file was found. "
"Only an NLU-model was created. Please specify a valid domain using "
"the '--domain' argument or check if the provided domain file exists."
)
training_type = TrainingType.NLU
elif stories.is_empty():
rasa.shared.utils.cli.print_warning(
"No stories present. Just a Rasa NLU model will be trained."
)
training_type = TrainingType.NLU
# We will train nlu if there are any nlu example, including from e2e stories.
elif nlu_data.contains_no_pure_nlu_data() and not nlu_data.has_e2e_examples():
rasa.shared.utils.cli.print_warning(
"No NLU data present. Just a Rasa Core model will be trained."
)
training_type = TrainingType.CORE
with telemetry.track_model_training(file_importer, model_type="rasa"):
return _train_graph(
file_importer,
training_type=training_type,
output_path=output,
fixed_model_name=fixed_model_name,
model_to_finetune=model_to_finetune,
force_full_training=force_training,
persist_nlu_training_data=persist_nlu_training_data,
finetuning_epoch_fraction=finetuning_epoch_fraction,
dry_run=dry_run,
**(core_additional_arguments or {
}),
**(nlu_additional_arguments or {
}),
)
TrainingResult
保存关于训练结果的信息
class TrainingResult(NamedTuple):
"""Holds information about the results of training."""
model: Optional[Text] = None
code: int = 0
dry_run_results: Optional[Dict[Text, Union[FingerprintStatus, Any]]] = None
TrainingCache
将训练结果存储在持久缓存中。当数据/配置在两次训练之间没有变化时,用于最小化再训练
class TrainingCache(abc.ABC):
"""Stores training results in a persistent cache.
Used to minimize re-retraining when the data / config didn't change in between
training runs.
"""
@abc.abstractmethod
def cache_output(
self,
fingerprint_key: Text,
output: Any,
output_fingerprint: Text,
model_storage: ModelStorage,
) -> None:
"""Adds the output to the cache.
If the output is of type `Cacheable` the output is persisted to disk in addition
to its fingerprint.
Args:
fingerprint_key: The fingerprint key serves as key for the cache. Graph
components can use their fingerprint key to lookup fingerprints of
previous training runs.
output: The output. The output is only cached to disk if it's of type
`Cacheable`.
output_fingerprint: The fingerprint of their output. This can be used
to lookup potentially persisted outputs on disk.
model_storage: Required for caching `Resource` instances. E.g. `Resource`s
use that to copy data from the model storage to the cache.
"""
...
@abc.abstractmethod
def get_cached_output_fingerprint(self, fingerprint_key: Text) -> Optional[Text]:
"""Retrieves fingerprint of output based on fingerprint key.
Args:
fingerprint_key: The fingerprint serves as key for the lookup of output
fingerprints.
Returns:
The fingerprint of a matching output or `None` in case no cache entry was
found for the given fingerprint key.
"""
...
@abc.abstractmethod
def get_cached_result(
self, output_fingerprint_key: Text, node_name: Text, model_storage: ModelStorage
) -> Optional[Cacheable]:
"""Returns a potentially cached output result.
Args:
output_fingerprint_key: The fingerprint key of the output serves as lookup
key for a potentially cached version of this output.
node_name: The name of the graph node which wants to use this cached result.
model_storage: The current model storage (e.g. used when restoring
`Resource` objects so that they can fill the model storage with data).
Returns:
`None` if no matching result was found or restored `Cacheable`.
"""
...
LocalTrainingCache
将训练结果缓存到本地磁盘上
class LocalTrainingCache(TrainingCache):
"""Caches training results on local disk (see parent class for full docstring)."""
Base: DeclarativeMeta = declarative_base()
class CacheEntry(Base):
"""Stores metadata about a single cache entry."""
__tablename__ = "cache_entry"
fingerprint_key = sa.Column(sa.String(), primary_key=True)
output_fingerprint_key = sa.Column(sa.String(), nullable=False, index=True)
last_used = sa.Column(sa.DateTime(timezone=True), nullable=False)
rasa_version = sa.Column(sa.String(255), nullable=False)
result_location = sa.Column(sa.String())
result_type = sa.Column(sa.String())
def __init__(self) -> None:
"""Creates cache.
The `Cache` setting can be configured via environment variables.
"""
self._cache_location = LocalTrainingCache._get_cache_location()
self._max_cache_size = float(
os.environ.get(CACHE_SIZE_ENV, DEFAULT_CACHE_SIZE_MB)
)
self._cache_database_name = os.environ.get(
CACHE_DB_NAME_ENV, DEFAULT_CACHE_NAME
)
if not self._cache_location.exists() and not self._is_disabled():
logger.debug(
f"Creating caching directory '{
self._cache_location}' because "
f"it doesn't exist yet."
)
self._cache_location.mkdir(parents=True)
self._sessionmaker = self._create_database()
self._drop_cache_entries_from_incompatible_versions()
LockStore
class LockStore:
@staticmethod
def create(obj: Union["LockStore", EndpointConfig, None]) -> "LockStore":
"""Factory to create a lock store."""
if isinstance(obj, LockStore):
return obj
try:
return _create_from_endpoint_config(obj)
except ConnectionError as error:
raise ConnectionException("Cannot connect to lock store.") from error
@staticmethod
def create_lock(conversation_id: Text) -> TicketLock:
"""Create a new `TicketLock` for `conversation_id`."""
return TicketLock(conversation_id)
def get_lock(self, conversation_id: Text) -> Optional[TicketLock]:
"""Fetch lock for `conversation_id` from storage."""
raise NotImplementedError
def delete_lock(self, conversation_id: Text) -> None:
"""Delete lock for `conversation_id` from storage."""
raise NotImplementedError
def save_lock(self, lock: TicketLock) -> None:
"""Commit `lock` to storage."""
raise NotImplementedError
def issue_ticket(
self, conversation_id: Text, lock_lifetime: float = LOCK_LIFETIME
) -> int:
"""Issue new ticket with `lock_lifetime` for lock associated with
`conversation_id`.
Creates a new lock if none is found.
"""
logger.debug(f"Issuing ticket for conversation '{
conversation_id}'.")
try:
lock = self.get_or_create_lock(conversation_id)
ticket = lock.issue_ticket(lock_lifetime)
self.save_lock(lock)
return ticket
except Exception as e:
raise LockError(f"Error while acquiring lock. Error:\n{
e}")
@asynccontextmanager
async def lock(
self,
conversation_id: Text,
lock_lifetime: float = LOCK_LIFETIME,
wait_time_in_seconds: float = 1,
) -> AsyncGenerator[TicketLock, None]:
"""Acquire lock with lifetime `lock_lifetime`for `conversation_id`.
Try acquiring lock with a wait time of `wait_time_in_seconds` seconds
between attempts. Raise a `LockError` if lock has expired.
"""
ticket = self.issue_ticket(conversation_id, lock_lifetime)
try:
yield await self._acquire_lock(
conversation_id, ticket, wait_time_in_seconds
)
finally:
self.cleanup(conversation_id, ticket)
async def _acquire_lock(
self, conversation_id: Text, ticket: int, wait_time_in_seconds: float
) -> TicketLock:
logger.debug(f"Acquiring lock for conversation '{
conversation_id}'.")
while True:
# fetch lock in every iteration because lock might no longer exist
lock = self.get_lock(conversation_id)
# exit loop if lock does not exist anymore (expired)
if not lock:
break
# acquire lock if it isn't locked
if not lock.is_locked(ticket):
logger.debug(f"Acquired lock for conversation '{
conversation_id}'.")
return lock
items_before_this = ticket - (lock.now_serving or 0)
logger.debug(
f"Failed to acquire lock for conversation ID '{
conversation_id}' "
f"because {
items_before_this} other item(s) for this "
f"conversation ID have to be finished processing first. "
f"Retrying in {
wait_time_in_seconds} seconds ..."
)
# sleep and update lock
await asyncio.sleep(wait_time_in_seconds)
self.update_lock(conversation_id)
raise LockError(
f"Could not acquire lock for conversation_id '{
conversation_id}'."
)
def update_lock(self, conversation_id: Text) -> None:
"""Fetch lock for `conversation_id`, remove expired tickets and save lock."""
lock = self.get_lock(conversation_id)
if lock:
lock.remove_expired_tickets()
self.save_lock(lock)
def get_or_create_lock(self, conversation_id: Text) -> TicketLock:
"""Fetch existing lock for `conversation_id` or create a new one if
it doesn't exist."""
existing_lock = self.get_lock(conversation_id)
if existing_lock:
return existing_lock
return self.create_lock(conversation_id)
def is_someone_waiting(self, conversation_id: Text) -> bool:
"""Return whether someone is waiting for lock associated with
`conversation_id`."""
lock = self.get_lock(conversation_id)
if lock:
return lock.is_someone_waiting()
return False
def finish_serving(self, conversation_id: Text, ticket_number: int) -> None:
"""Finish serving ticket with `ticket_number` for `conversation_id`.
Removes ticket from lock and saves lock.
"""
lock = self.get_lock(conversation_id)
if lock:
lock.remove_ticket_for(ticket_number)
self.save_lock(lock)
def cleanup(self, conversation_id: Text, ticket_number: int) -> None:
"""Remove lock for `conversation_id` if no one is waiting."""
self.finish_serving(conversation_id, ticket_number)
if not self.is_someone_waiting(conversation_id):
self.delete_lock(conversation_id)
@staticmethod
def _log_deletion(conversation_id: Text, deletion_successful: bool) -> None:
if deletion_successful:
logger.debug(f"Deleted lock for conversation '{
conversation_id}'.")
else:
logger.debug(f"Could not delete lock for conversation '{
conversation_id}'.")
TrackerStore
class TrackerStore:
"""Represents common behavior and interface for all `TrackerStore`s."""
def __init__(
self,
domain: Optional[Domain],
event_broker: Optional[EventBroker] = None,
**kwargs: Dict[Text, Any],
) -> None:
"""Create a TrackerStore.
Args:
domain: The `Domain` to initialize the `DialogueStateTracker`.
event_broker: An event broker to publish any new events to another
destination.
kwargs: Additional kwargs.
"""
self._domain = domain or Domain.empty()
self.event_broker = event_broker
self.max_event_history = None
@staticmethod
def create(
obj: Union["TrackerStore", EndpointConfig, None],
domain: Optional[Domain] = None,
event_broker: Optional[EventBroker] = None,
) -> "TrackerStore":
"""Factory to create a tracker store."""
if isinstance(obj, TrackerStore):
return obj
from botocore.exceptions import BotoCoreError
import pymongo.errors
import sqlalchemy.exc
try:
return _create_from_endpoint_config(obj, domain, event_broker)
except (
BotoCoreError,
pymongo.errors.ConnectionFailure,
sqlalchemy.exc.OperationalError,
ConnectionError,
pymongo.errors.OperationFailure,
) as error:
raise ConnectionException(
"Cannot connect to tracker store." + str(error)
) from error
def get_or_create_tracker(
self,
sender_id: Text,
max_event_history: Optional[int] = None,
append_action_listen: bool = True,
) -> "DialogueStateTracker":
"""Returns tracker or creates one if the retrieval returns None.
Args:
sender_id: Conversation ID associated with the requested tracker.
max_event_history: Value to update the tracker store's max event history to.
append_action_listen: Whether or not to append an initial `action_listen`.
"""
self.max_event_history = max_event_history
tracker = self.retrieve(sender_id)
if tracker is None:
tracker = self.create_tracker(
sender_id, append_action_listen=append_action_listen
)
return tracker
def init_tracker(self, sender_id: Text) -> "DialogueStateTracker":
"""Returns a Dialogue State Tracker."""
return DialogueStateTracker(
sender_id,
self.domain.slots,
max_event_history=self.max_event_history,
)
Gavin大咖语录
Nlp或者说对话机器人确实是一个引人入胜的一个领域,就是你越研究越感觉有意思,而且越研究你也有可能感觉越复杂,有很多问题,如果你不深入下去或者你没有经验的话,你不会意识到他是问题,但是一旦这个项目很复杂的时候,它就会暴露出很多技术的问题,或者说你必须考虑的点。
我们整个 NLP高手之路课程,我们有 kernel版本,kernel版本就是101课,我们这个也有PRO版本的,PRO版本是137课,我相信你听着听到这里面你肯定是已经参加了我们pro课程的内容,当然我们也有168节课的 master版本的内容,我们的课程随着课程推进,只能说项目越来越复杂,同时谈的架构或者是背后的思想越来越具有统治的力量,它不仅仅是信息或者知识,他是给你带来能够再生其他的信息或者支持,或者能够让你一听这个东西会对以前的很多信息或者知识突然贯通,或者说一些以前困惑你的问题,你感觉突然不困惑,突然恍然大悟的一些感觉,这就是技术具有生产力的一些东西或者统治力的东西。
我们随着课程的进行,会跟大家谈更多这方面的,尤其是基于Gavin大咖过去这么多年做星空对话机器人的一些经验,会结合Rasa的代码或者项目跟大家都会有更多的分享,也包括我们的整个Bayesian deep learning的部分,Bayesian这个是大家做deep learning或者做神经网络,是相当于一个皇冠上的技能上的巅峰性的东西,我们会至少有20~30节的课的时间跟大家讲解。好,大家如果对这个内容都很感兴趣的话,可以随时联系Gavin大咖,也可以联系我们的工作人员。今天的内容我们就到这里。
Gavin大咖简介
星空智能对话机器人创始人、AI通用双线思考法作者,现工作于硅谷顶级的AI实验室。专精于Conversational AI. 在美国曾先后工作于硅谷最顶级的机器学习和人工智能实验室
Gavin大咖微信:NLP_Matrix_Space
联系电话:+1 650-603-1290
联系邮箱:[email protected]
助教老师微信:Spark_AI_NLP
Rasa 3.x系列博客分享
-
Rasa课程、Rasa培训、Rasa面试系列 Rasa 3.X 项目实战之银行金融Financial Bot智能业务对话机器人
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Diet Architecture How it Works
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Diet Architecture Why it Works(Design Decisions)
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Diet Architecture Benchmarking
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Understanding Word Embeddings Just Letters
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Understanding Word Embeddings CBOW and Skip Gram
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Understanding Word Embeddings GloVe
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Understanding Word Embeddings Whatlies
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Transformers & Attention Self Attention
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之 Countvectors and Spelling Errors
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Subword Embeddings and Spelling
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之免费录播课Rasa 3.X 智能对话机器人案例开发硬核实战高手之路 (7大项目Expert版本)之 Debugging项目实战系列
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Implementation of Subword Embeddings
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之星空NLP对话机器人论文班:NLP领域10篇最高质量的对话机器人经典论文解密
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Measuring Bias in Word Embeddings
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Gavin大咖免费公益课程 Rasa Paper对话机器人经典论文解读班
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Using Projections to Remove Bias from Word Embeddings
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之 Debiasing via Projections Doesnot Always Work
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Hugging Face bert-base-chinese 使用
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Gavin大咖免费公益课程Rasa Paper论文解析核心版
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之The Maths Behind De-Biasing in Word Embeddings
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Word Analogies don‘t Hold in General
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之General Embeddings vs. Specific Problems
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之 UnexpecTEDIntentPolicy Details
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之NER for Personal Indentifiable Information is Hard
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之Translation Issues及Bulk Labelling
-
Rasa课程、Rasa培训、Rasa面试、Rasa实战系列之MessageContainerForCoreFeaturization