def _back_to_the_future( tracker: DialogueStateTracker, again: bool = False) -> Optional[DialogueStateTracker]: """Send Marty to the past to get the new featurization for the future""" idx_of_first_action = None idx_of_second_action = None # we need to find second executed action for e_i, event in enumerate(tracker.applied_events()): # find second ActionExecuted if isinstance(event, ActionExecuted): if idx_of_first_action is None: idx_of_first_action = e_i else: idx_of_second_action = e_i break # use first action, if we went first time and second action, if we went again idx_to_use = idx_of_second_action if again else idx_of_first_action if idx_to_use is None: return # make second ActionExecuted the first one events = tracker.applied_events()[idx_to_use:] if not events: return mcfly_tracker = tracker.init_copy() for e in events: mcfly_tracker.update(e) return mcfly_tracker
def _should_skip_prediction(tracker: DialogueStateTracker, domain: Domain,) -> bool: """Checks if the policy should skip making a prediction. A prediction can be skipped if: 1. There is no event of type `UserUttered` in the tracker. 2. If the `UserUttered` event's intent is new and not in domain (a new intent can be created from rasa interactive and not placed in domain yet) 3. There is an event of type `ActionExecuted` after the last `UserUttered` event. This is to prevent the dialogue manager from getting stuck in a prediction loop. For example, if the last `ActionExecuted` event contained `action_unlikely_intent` predicted by `UnexpecTEDIntentPolicy` and if `UnexpecTEDIntentPolicy` runs inference on the same tracker, it will predict `action_unlikely_intent` again which would make the dialogue manager get stuck in a prediction loop. Returns: Whether prediction should be skipped. """ applied_events = tracker.applied_events() for event in reversed(applied_events): if isinstance(event, ActionExecuted): return True elif isinstance(event, UserUttered): if event.intent_name not in domain.intents: return True return False # No event of type `ActionExecuted` and `UserUttered` means # that there is nothing for `UnexpecTEDIntentPolicy` to predict on. return True
async def _update_tracker_session( self, tracker: DialogueStateTracker, output_channel: OutputChannel, metadata: Optional[Dict] = None, ) -> None: """Check the current session in `tracker` and update it if expired. An 'action_session_start' is run if the latest tracker session has expired, or if the tracker does not yet contain any events (only those after the last restart are considered). Args: metadata: Data sent from client associated with the incoming user message. tracker: Tracker to inspect. output_channel: Output channel for potential utterances in a custom `ActionSessionStart`. """ if not tracker.applied_events() or self._has_session_expired(tracker): logger.debug( f"Starting a new session for conversation ID '{tracker.sender_id}'." ) await self._run_action( action=self._get_action(ACTION_SESSION_START_NAME), tracker=tracker, output_channel=output_channel, nlg=self.nlg, metadata=metadata, prediction=PolicyPrediction.for_action_name( self.domain, ACTION_SESSION_START_NAME), )
def _get_max_applied_events_for_max_history( tracker: DialogueStateTracker, max_history: Optional[int], ) -> Optional[int]: """Computes the number of events in the tracker that correspond to max_history. Args: tracker: Some tracker holding the events max_history: The number of actions to count Returns: The number of actions, as counted from the end of the event list, that should be taken into accout according to the `max_history` setting. If all events should be taken into account, the return value is `None`. """ if not max_history: return None num_events = 0 num_actions = 0 for event in reversed(tracker.applied_events()): num_events += 1 if isinstance(event, ActionExecuted): num_actions += 1 if num_actions > max_history: return num_events return None
def _get_max_applied_events_for_max_history( tracker: DialogueStateTracker, max_history: Optional[int]) -> Optional[int]: """Computes the number of events in the tracker that correspond to max_history. To ensure that the last user utterance is correctly included in the prediction states, return the index of the most recent `action_listen` event occuring before the tracker would be truncated according to the value of `max_history`. Args: tracker: Some tracker holding the events max_history: The number of actions to count Returns: The number of events, as counted from the end of the event list, that should be taken into accout according to the `max_history` setting. If all events should be taken into account, the return value is `None`. """ if not max_history: return None num_events = 0 num_actions = 0 for event in reversed(tracker.applied_events()): num_events += 1 if isinstance(event, ActionExecuted): num_actions += 1 if num_actions > max_history and event.action_name == ACTION_LISTEN_NAME: return num_events return None
def _is_reminder_still_valid(tracker: DialogueStateTracker, reminder_event: ReminderScheduled) -> bool: """Check if the conversation has been restarted after reminder.""" for e in reversed(tracker.applied_events()): if MessageProcessor._is_reminder(e, reminder_event.name): return True return False # not found in applied events --> has been restarted
def _extract_examples( self, tracker: DialogueStateTracker, domain: Domain, omit_unset_slots: bool = False, ignore_action_unlikely_intent: bool = False, ) -> Iterator[Tuple[List[State], List[Text], List[Dict[Text, Any]]]]: """Creates an iterator over training examples from a tracker. Args: trackers: The tracker from which to extract training examples. domain: The domain of the training data. omit_unset_slots: If `True` do not include the initial values of slots. ignore_action_unlikely_intent: Whether to remove `action_unlikely_intent` from training states. Returns: An iterator over example states, labels, and entity data. """ tracker_states = self._create_states( tracker, domain, omit_unset_slots=omit_unset_slots ) events = tracker.applied_events() if ignore_action_unlikely_intent: tracker_states = self._remove_action_unlikely_intent_from_states( tracker_states ) events = self._remove_action_unlikely_intent_from_events(events) label_index = 0 entity_data = {} for event in events: if isinstance(event, UserUttered): entity_data = self._entity_data(event) elif isinstance(event, ActionExecuted): label_index += 1 # use only actions which can be predicted at a stories start if event.unpredictable: continue sliced_states = self.slice_state_history( tracker_states[:label_index], self.max_history ) label = [event.action_name or event.action_text] entities = [entity_data] yield sliced_states, label, entities # reset entity_data for the the next turn entity_data = {}
async def _update_tracker_session( self, tracker: DialogueStateTracker, output_channel: OutputChannel, metadata: Optional[Dict] = None, ) -> None: """Check the current session in `tracker` and update it if expired. An 'action_session_start' is run if the latest tracker session has expired, or if the tracker does not yet contain any events (only those after the last restart are considered). Args: metadata: Data sent from client associated with the incoming user message. tracker: Tracker to inspect. output_channel: Output channel for potential utterances in a custom `ActionSessionStart`. """ if not tracker.applied_events() or self._has_session_expired(tracker): logger.debug( f"Starting a new session for conversation ID '{tracker.sender_id}'." ) action_session_start = self._get_action(ACTION_SESSION_START_NAME) # TODO: Remove in 3.0.0 and describe migration to `session_start_metadata` # slot in migration guide. if isinstance( action_session_start, rasa.core.actions.action.ActionSessionStart ): # Here we set optional metadata to the ActionSessionStart, which will # then be passed to the SessionStart event. action_session_start.metadata = metadata if metadata: tracker.update( SlotSet(SESSION_START_METADATA_SLOT, metadata), self.domain ) await self._run_action( action=action_session_start, tracker=tracker, output_channel=output_channel, nlg=self.nlg, prediction=PolicyPrediction.for_action_name( self.domain, ACTION_SESSION_START_NAME ), )
def _extract_examples( self, tracker: DialogueStateTracker, domain: Domain, omit_unset_slots: bool = False, ignore_action_unlikely_intent: bool = False, ) -> Iterator[Tuple[List[State], List[Text], List[Dict[Text, Any]]]]: """Creates an iterator over training examples from a tracker. Args: tracker: The tracker from which to extract training examples. domain: The domain of the training data. omit_unset_slots: If `True` do not include the initial values of slots. ignore_action_unlikely_intent: Whether to remove `action_unlikely_intent` from training states. Returns: An iterator over example states, labels, and entity data. """ tracker_states = self._create_states(tracker, domain, omit_unset_slots=omit_unset_slots) events = tracker.applied_events() if ignore_action_unlikely_intent: tracker_states = self._remove_action_unlikely_intent_from_states( tracker_states) events = self._remove_action_unlikely_intent_from_events(events) label_index = 0 for event in events: if isinstance(event, ActionExecuted): label_index += 1 elif isinstance(event, UserUttered): sliced_states = self.slice_state_history( tracker_states[:label_index], self.max_history) label = cast(List[Text], [event.intent_name or event.text]) entities: List[Dict[Text, Any]] = [{}] yield sliced_states, label, entities
def _strip_leading_events_until_action_executed( tracker: DialogueStateTracker, again: bool = False) -> Optional[DialogueStateTracker]: """Truncates the tracker to begin at the next `ActionExecuted` event. Args: tracker: The tracker to truncate. again: When true, truncate tracker at the second action. Otherwise truncate to the first action. Returns: The truncated tracker if there were actions present. If none are found, returns `None`. """ idx_of_first_action = None idx_of_second_action = None applied_events = tracker.applied_events() # we need to find second executed action for e_i, event in enumerate(applied_events): if isinstance(event, ActionExecuted): if idx_of_first_action is None: idx_of_first_action = e_i else: idx_of_second_action = e_i break # use first action, if we went first time and second action, if we went again idx_to_use = idx_of_second_action if again else idx_of_first_action if idx_to_use is None: return None # make second ActionExecuted the first one events = applied_events[idx_to_use:] if not events: return None truncated_tracker = tracker.init_copy() for e in events: truncated_tracker.update(e) return truncated_tracker
async def _revert_fallback_events( self, output_channel: OutputChannel, nlg: NaturalLanguageGenerator, tracker: DialogueStateTracker, domain: Domain, events_so_far: List[Event], ) -> List[Event]: revert_events = [UserUtteranceReverted(), UserUtteranceReverted()] temp_tracker = DialogueStateTracker.from_events( tracker.sender_id, tracker.applied_events() + events_so_far + revert_events) while temp_tracker.latest_message and not await self.is_done( output_channel, nlg, temp_tracker, domain, []): temp_tracker.update(revert_events[-1]) revert_events.append(UserUtteranceReverted()) return revert_events
def _trim_tracker_by_max_history( tracker: DialogueStateTracker, max_history: Optional[int], ) -> DialogueStateTracker: """Removes events from the tracker until it has `max_history` actions. Args: tracker: Some tracker. max_history: Number of actions to keep. Returns: A new tracker with up to `max_history` actions, or the same tracker if `max_history` is `None`. """ max_applied_events = _get_max_applied_events_for_max_history(tracker, max_history) if not max_applied_events: return tracker applied_events = tracker.applied_events()[-max_applied_events:] new_tracker = tracker.init_copy() for event in applied_events: new_tracker.update(event) return new_tracker