def test_get_actions_for_valid_num_actions(agenda, constraintA, constraintB): """ Tests whether retrieving actions from the stack with a number of actions is smaller or equal to the stack size will return the last num_actions items from the stack in reversed order. Args: agenda: Agenda object (given in conftest.py) constraintA (dict): an existing slot-value pair in the domain (given in conftest_<domain>.py) constraintB (dict): another existing slot-value pair in the domain (given in conftest_<domain>.py) """ stack = [ UserAct(act_type=UserActionType.Inform, slot=constraintA['slot'], value=constraintA['value']), UserAct(act_type=UserActionType.Confirm, slot=constraintB['slot'], value=constraintB['value']), UserAct(act_type=UserActionType.Request, slot=constraintA['slot'], value=constraintA['value']) ] agenda.stack = stack.copy() num_actions = 2 res = agenda.get_actions(num_actions=num_actions) assert len(res) == num_actions assert res == stack[-1:(len(stack) - (num_actions + 1)):-1]
def test_reset_informs_resets_only_informs(bst): """ Tests whether reset informs removes only inform slots. Args: bst: BST Object (given in conftest.py) """ acts = [ UserAct(act_type=UserActionType.Inform, slot='foo', value='bar'), UserAct(act_type=UserActionType.Request, slot='bar', value='foo') ] bst.bs['informs'] = { 'foo': { 'bar': 0.5 }, 'bar': { 'foo': 0.3 }, 'baz': { 'foo': 0.6 } } bst._reset_informs(acts) assert all(act.slot not in bst.bs['informs'] for act in acts if act.type == UserActionType.Inform) assert 'bar' in bst.bs['informs']
def test_get_actions_for_invalid_num_actions(agenda, constraintA, constraintB, num_actions): """ Tests whether retrieving actions from the stack with a number of actions that is less or greater than the stack size will return the complete stack in reversed order. Args: agenda: Agenda object (given in conftest.py) constraintA (dict): an existing slot-value pair in the domain (given in conftest_<domain>.py) constraintB (dict): another existing slot-value pair in the domain (given in conftest_<domain>.py) num_actions (int): number of actions to be selected """ stack = [ UserAct(act_type=UserActionType.Inform, slot=constraintA['slot'], value=constraintA['value']), UserAct(act_type=UserActionType.Confirm, slot=constraintB['slot'], value=constraintB['value']) ] agenda.stack = stack.copy() res = agenda.get_actions(num_actions=num_actions) assert len(res) == len(stack) assert res == stack[::-1]
def test_remove_fulfilled_requests_on_clean(agenda, goal, constraintA, constraintB): """ Tests whether cleaning the stack removes requests that are already fulfilled in the goal. Args: agenda: Agenda object (given in conftest.py) goal: Goal object (given in conftest.py) constraintA (dict): an existing slot-value pair in the domain (given in conftest_<domain>.py) constraintB (dict): another existing slot-value pair in the domain (given in conftest_<domain>.py) """ fulfilled_request = UserAct(act_type=UserActionType.Request, slot=constraintA['slot'], value=constraintA['value']) stack = [ fulfilled_request, UserAct(act_type=UserActionType.Inform, slot=constraintB['slot'], value=constraintB['value']) ] agenda.stack = stack.copy() goal.requests = {fulfilled_request.slot: fulfilled_request.value} agenda.clean(goal) assert len(agenda.stack) < len(stack) assert fulfilled_request not in agenda.stack
def test_keep_order_in_stack_on_clean(agenda, goal, constraintA, constraintB): """ Tests whether cleaning the stack keeps the items in the same order. Args: agenda: Agenda object (given in conftest.py) goal: Goal object (given in conftest.py) constraintA (dict): an existing slot-value pair in the domain (given in conftest_<domain>.py) constraintB (dict): another existing slot-value pair in the domain (given in conftest_<domain>.py) """ stack = [ UserAct(act_type=UserActionType.Inform, slot=constraintA['slot'], value=constraintA['value']), UserAct(act_type=UserActionType.Confirm, slot=constraintB['slot'], value=constraintB['value']) ] agenda.stack = stack.copy() goal.requests = {constraintA['slot']: None} agenda.clean(goal) assert len(agenda.stack) == len(stack) assert agenda.stack == stack
def _receive_select(self, sys_act: SysAct): """Processes a select action from the system.""" # handle as request value_in_goal = False for slot, values in sys_act.slot_values.items(): for value in values: # do not consider 'dontcare' as any value if not self.goal.is_inconsistent_constraint_strict( Constraint(slot, value)): value_in_goal = True if value_in_goal: self._receive_request(sys_act) else: assert len(sys_act.slot_values.keys()) == 1,\ "There shall be only one slot in a select action." slot = list(sys_act.slot_values.keys())[0] # inform about correct value with some probability if common.random.random( ) < self.parameters['usermodel']['InformOnSelect']: self.agenda.push(UserAct(act_type=UserActionType.Inform, slot=slot,\ value=self.goal.get_constraint(slot), score=1.0)) for slot, values in sys_act.slot_values.items(): for value in values: self.agenda.push(UserAct(act_type=UserActionType.NegativeInform, slot=slot,\ value=value, score=1.0))
def test_remove_inconsistencies_on_clean(agenda, goal, constraintA, constraintB, constraintA_alt): """ Tests whether cleaning the stack removes inconsistent inform constraints. Args: agenda: Agenda object (given in conftest.py) goal: Goal object (given in conftest.py) constraintA (dict): an existing slot-value pair in the domain (given in conftest_<domain>.py) constraintB (dict): another existing slot-value pair in the domain (given in conftest_<domain>.py) constraintA_alt (dict): as constraint A, but with an alternative value (given in conftest_<domain>.py) """ inconsistent_item = UserAct(act_type=UserActionType.Inform, slot=constraintA['slot'], value=constraintA['value']) stack = [ inconsistent_item, UserAct(act_type=UserActionType.Confirm, slot=constraintB['slot'], value=constraintB['value']) ] agenda.stack = stack.copy() goal.constraints = [ Constraint(constraintA_alt['slot'], constraintA_alt['value']) ] agenda.clean(goal) assert len(agenda.stack) < len(stack) assert all(item.slot != inconsistent_item.slot and item.value != inconsistent_item.value for item in agenda.stack)
def test_get_actions_of_type_with_matching_action_without_considering_dontcare( agenda, constraintA, constraintB): """ Tests whether asking to get an action of specific type returns no matching actions in the stack that have the value 'dontcare' if specified. Args: agenda: Agenda object (given in conftest.py) constraintA (dict): an existing slot-value pair in the domain (given in conftest_<domain>.py) constraintB (dict): another existing slot-value pair in the domain (given in conftest_<domain>.py) """ act_type = UserActionType.Inform stack = [ UserAct(act_type=act_type, slot=constraintA['slot'], value='dontcare'), UserAct(act_type=act_type, slot=constraintB['slot'], value=constraintB['value']) ] agenda.stack = stack.copy() res = agenda.get_actions_of_type(act_type=act_type, consider_dontcare=False) res = list(res) assert len(res) < len(stack) assert res == [ act for act in stack if act.type == act_type and act.value != 'dontcare' ]
def test_get_actions_of_type_without_matching_action(agenda, constraintA, constraintB): """ Tests whether asking to get an action of specific type returns nothing if there are no matching actions in the stack. Args: agenda: Agenda object (given in conftest.py) constraintA (dict): an existing slot-value pair in the domain (given in conftest_<domain>.py) constraintB (dict): another existing slot-value pair in the domain (given in conftest_<domain>.py) """ stack = [ UserAct(act_type=UserActionType.Inform, slot=constraintA['slot'], value=constraintA['value']), UserAct(act_type=UserActionType.Confirm, slot=constraintB['slot'], value=constraintB['value']) ] agenda.stack = stack.copy() res = agenda.get_actions_of_type(act_type=UserActionType.Request, consider_dontcare=True) assert list(res) == []
def test_remove_actions_of_type_with_matching_action(agenda, constraintA, constraintB): """ Tests whether removing actions of a specific type will delete these actions if present in the stack. Args: agenda: Agenda object (given in conftest.py) constraintA (dict): an existing slot-value pair in the domain (given in conftest_<domain>.py) constraintB (dict): another existing slot-value pair in the domain (given in conftest_<domain>.py) """ act_type = UserActionType.Inform stack = [ UserAct(act_type=act_type, slot=constraintA['slot'], value=constraintA['value']), UserAct(act_type=UserActionType.Confirm, slot=constraintB['slot'], value=constraintB['value']) ] agenda.stack = stack.copy() agenda.remove_actions_of_type(act_type=act_type) assert len(agenda.stack) < len(stack) assert all(item.type != act_type for item in agenda.stack)
def test_remove_actions_of_type_without_matching_action( agenda, constraintA, constraintB): """ Tests whether removing actions of a specific type will not change the stack if no matching actions are in it. Args: agenda: Agenda object (given in conftest.py) constraintA (dict): an existing slot-value pair in the domain (given in conftest_<domain>.py) constraintB (dict): another existing slot-value pair in the domain (given in conftest_<domain>.py) """ stack = [ UserAct(act_type=UserActionType.Inform, slot=constraintA['slot'], value=constraintA['value']), UserAct(act_type=UserActionType.Confirm, slot=constraintB['slot'], value=constraintB['value']) ] agenda.stack = stack.copy() agenda.remove_actions_of_type(act_type=UserActionType.Request) assert len(agenda.stack) == len(stack) assert agenda.stack == stack
def test_remove_actions_with_value(agenda, constraintA, constraintB): """ Tests whether removing specific actions with stating their type, slot and value removes all matching actions from the stack. Args: agenda: Agenda object (given in conftest.py) constraintA (dict): an existing slot-value pair in the domain (given in conftest_<domain>.py) constraintB (dict): another existing slot-value pair in the domain (given in conftest_<domain>.py) """ act_type = UserActionType.Inform slot = constraintA['slot'] value = constraintA['value'] stack = [ UserAct(act_type=act_type, slot=slot, value=value), UserAct(act_type=UserActionType.Confirm, slot=constraintB['slot'], value=constraintB['value']) ] agenda.stack = stack.copy() agenda.remove_actions(act_type=act_type, slot=slot, value=value) assert len(agenda.stack) < len(stack) assert all( item.type != act_type and item.slot != slot and item.value != value for item in agenda.stack)
def _receive_confirm(self, sys_act: SysAct): """ Processes a confirm action from the system based on information in the user goal Args: sys_act (SysAct): the last system action """ for slot, _value in sys_act.slot_values.items(): value = _value[0] # there is always only one value if self.goal.is_inconsistent_constraint_strict( Constraint(slot, value)): # inform about correct value with some probability, otherwise deny value if common.random.random( ) < self.parameters['usermodel']['InformOnConfirm']: self.agenda.push( UserAct(act_type=UserActionType.Inform, slot=slot, value=self.goal.get_constraint(slot), score=1.0)) else: self.agenda.push( UserAct(act_type=UserActionType.NegativeInform, slot=slot, value=value, score=1.0)) else: # NOTE using inform currently since NLU currently does not support Affirm here and # NLU would tinker it into an Inform action anyway # self.agenda.push( # UserAct(act_type=UserActionType.Affirm, score=1.0)) self.agenda.push( UserAct(act_type=UserActionType.Inform, slot=slot, value=value, score=1.0))
def parse_user_utterance(self, user_utterance: str = None ) -> dict(user_acts=List[UserAct]): """Parses the user utterance. Responsible for detecting user acts with their respective slot-values from the user utterance by predicting relation, topic entities and the relation's direction. Args: user_utterance: the last user input as string Returns: A dictionary with the key "user_acts" and the value containing a list of user actions """ result = {} self.user_acts = [] user_utterance = user_utterance.strip() if not user_utterance: return {'user_acts': None} elif user_utterance.lower().replace(' ', '').endswith('bye'): return {'user_acts': [UserAct(user_utterance, UserActionType.Bye)]} if self.domain.get_keyword() in user_utterance.lower(): self.user_acts.append( UserAct(user_utterance, UserActionType.SelectDomain)) begin_idx = user_utterance.lower().find(self.domain.get_keyword()) user_utterance = user_utterance.lower().replace( self.domain.get_keyword(), "") if len(user_utterance) == 0: return {'user_acts': self.user_acts} tokens, embeddings = self._preprocess_utterance(user_utterance) relation_out = self._predict_relation(embeddings) entities_out = self._predict_topic_entities(embeddings) direction_out = self._predict_direction(embeddings) relation_pred = self._lookup_relation(relation_out) entities_pred = extract_entities(tokens[1:], entities_out[:, 0]) direction_pred = self._lookup_direction(direction_out) self.user_acts.extend([ UserAct(user_utterance, UserActionType.Inform, 'relation', relation_pred, 1.0), UserAct(user_utterance, UserActionType.Inform, 'direction', direction_pred, 1.0) ]) for t in entities_pred: self.user_acts.append( UserAct(user_utterance, UserActionType.Inform, 'topic', ' '.join(t), 1.0)) result['user_acts'] = self.user_acts self.debug_logger.dialog_turn("User Actions: %s" % str(self.user_acts)) return result
def extract_user_acts(self, user_utterance: str = None ) -> dict(user_acts=List[UserAct]): """Main function for detecting and publishing user acts. Args: user_utterance: the user input string Returns: dict with key 'user_acts' and list of user acts as value """ user_acts = [] if not user_utterance: return {'user_acts': None} user_utterance = ' '.join(user_utterance.lower().split()) for bye in ('bye', 'goodbye', 'byebye', 'seeyou'): if user_utterance.replace(' ', '').endswith(bye): return { 'user_acts': [UserAct(user_utterance, UserActionType.Bye)] } # check weather today for regex in WEATHER_DATE_TODAY_REGEXES: match = regex.search(user_utterance) if match: user_acts.append( UserAct(user_utterance, UserActionType.Inform, 'date', datetime.now())) break if len(user_acts) == 0: for regex in WEATHER_DATE_TOMORROW_REGEXES: match = regex.search(user_utterance) if match: tomorrow = datetime.now() + timedelta(days=1) date = datetime(tomorrow.year, tomorrow.month, tomorrow.day, hour=15) user_acts.append( UserAct(user_utterance, UserActionType.Inform, 'date', date)) break for regex in WEATHER_LOCATION_REGEXES: match = regex.search(user_utterance) if match: user_acts.append( UserAct(user_utterance, UserActionType.Inform, 'location', match.group(1))) self.debug_logger.dialog_turn("User Actions: %s" % str(user_acts)) return {'user_acts': user_acts}
def _finish_dialog(self, ungrateful=False): """ Pushes a bye action ontop of the agenda in order to end a dialog. Depending on the user model, a thankyou action might be added too. """ self.agenda.clear() # empty agenda # thank with some probability # NOTE bye has to be the topmost action on the agenda since we check for it in the # respond() method if not ungrateful and common.random.random( ) < self.parameters['usermodel']['Thank']: self.agenda.push(UserAct(act_type=UserActionType.Thanks, score=1.0)) self.agenda.push(UserAct(act_type=UserActionType.Bye, score=1.0))
def test_fill_agenda_on_initialization(agenda, goal, constraintA, constraintB): """ Tests whether the agenda is emptied and filled with the constraints of the given goal on initialization. Args: agenda: Agenda object (given in conftest.py) goal: Goal object (given in conftest.py) constraintA (dict): an existing slot-value pair in the domain (given in conftest_<domain>.py) constraintB (dict): another existing slot-value pair in the domain (given in conftest_<domain>.py) """ stored_action = UserAct(act_type=UserActionType.Inform, slot=constraintA['slot'], value=constraintA['value']) goal_constraint = Constraint(slot=constraintB['slot'], value=constraintB['value']) agenda.stack = [stored_action] goal.constraints = [goal_constraint] agenda.init(goal) assert len(agenda.stack) == len(goal.constraints) assert stored_action not in agenda.stack assert any(item.slot == goal_constraint.slot and item.value == goal_constraint.value and item.type == UserActionType.Inform for item in agenda.stack)
def dialog_start(self): """Resets the user model at the beginning of a dialog, e.g. draws a new goal and populates the agenda according to the goal.""" # self.goal = Goal(self.domain, self.parameters['goal']) self.goal.init() self.agenda.init(self.goal) if self.logger: self.logger.dialog_turn( "New goal has constraints {} and requests {}.".format( self.goal.constraints, self.goal.requests)) self.logger.dialog_turn("New agenda initialized: {}".format( self.agenda)) # add hello action with some probability if common.random.random() < self.parameters['usermodel']['Greeting']: self.agenda.push(UserAct(act_type=UserActionType.Hello, score=1.0)) # needed for possibility to reset patience if len(self.parameters['usermodel']['patience']) == 1: self.dialog_patience = self.parameters['usermodel']['patience'][0] else: self.dialog_patience = common.random.randint( *self.parameters['usermodel']['patience']) self.patience = self.dialog_patience self.last_user_actions = None self.last_system_action = None self.excluded_venues = [] self.turn = 0
def extract_user_acts(self, user_utterance: str = None) -> dict(user_acts=List[UserAct]): """ Responsible for detecting user acts with their respective slot-values from the user utterance through regular expressions. Args: user_utterance (BeliefState) - a BeliefState obejct representing current system knowledge Returns: dict of str: UserAct - a dictionary with the key "user_acts" and the value containing a list of user actions """ result = {} # Setting request everything to False at every turn self.req_everything = False self.user_acts = [] # slots_requested & slots_informed store slots requested and informed in this turn # they are used later for later disambiguation self.slots_requested, self.slots_informed = set(), set() if user_utterance is not None: user_utterance = user_utterance.strip() self._match_general_act(user_utterance) self._match_domain_specific_act(user_utterance) self._solve_informable_values() # If nothing else has been matched, see if the user chose a domain; otherwise if it's # not the first turn, it's a bad act if len(self.user_acts) == 0: if self.domain.get_keyword() in user_utterance: self.user_acts.append(UserAct(text=user_utterance if user_utterance else "", act_type=UserActionType.SelectDomain)) elif self.sys_act_info['last_act'] is not None: # start of dialogue or no regex matched self.user_acts.append(UserAct(text=user_utterance if user_utterance else "", act_type=UserActionType.Bad)) self._assign_scores() self.logger.dialog_turn("User Actions: %s" % str(self.user_acts)) result['user_acts'] = self.user_acts return result
def extract_user_acts(self, user_utterance: str = None, sys_act: SysAct = None, beliefstate: BeliefState = None) \ -> dict(user_acts=List[UserAct]): """Original code but adapted to automatically add a request(name) act""" result = {} # Setting request everything to False at every turn self.req_everything = False self.user_acts = [] # slots_requested & slots_informed store slots requested and informed in this turn # they are used later for later disambiguation self.slots_requested, self.slots_informed = set(), set() if user_utterance is not None: user_utterance = user_utterance.strip() self._match_general_act(user_utterance) self._match_domain_specific_act(user_utterance) # Solving ambiguities from regexes, especially with requests and informs happening # simultaneously on the same slot and two slots taking the same value self._disambiguate_co_occurrence(beliefstate) self._solve_informable_values() # If nothing else has been matched, see if the user chose a domain; otherwise if it's # not the first turn, it's a bad act if len(self.user_acts) == 0: if self.domain.get_keyword() in user_utterance: self.user_acts.append( UserAct(text=user_utterance if user_utterance else "", act_type=UserActionType.SelectDomain)) elif self.sys_act_info['last_act'] is not None: # start of dialogue or no regex matched self.user_acts.append( UserAct(text=user_utterance if user_utterance else "", act_type=UserActionType.Bad)) # the name should always be mentioned by the system, so we mock a request for it if self.user_acts: self.user_acts.append( UserAct(user_utterance, UserActionType.Request, 'name')) self._assign_scores() result['user_acts'] = self.user_acts self.logger.dialog_turn("User Actions: %s" % str(self.user_acts)) return result
def _request_alt(self, offer=None): # add current offer to exclusion list, reset current offer and request alternative if offer is not None: self.excluded_venues.append(offer) if self.goal.requests[self.domain.get_primary_key()] is not None: self.excluded_venues.append( self.goal.requests[self.domain.get_primary_key()]) self.goal.requests[self.domain.get_primary_key()] = None self.goal.reset() self.agenda.push(UserAct(act_type=UserActionType.RequestAlternatives))
def _receive_request(self, sys_act: SysAct): """ Processes a request action from the system by adding the corresponding answer based on the current simulator goal. Args: sys_act (SysAct): the last system action """ for slot, _ in sys_act.slot_values.items(): self.agenda.push(UserAct( act_type=UserActionType.Inform, slot=slot, value=self.goal.get_constraint(slot), score=1.0))
def test_clear_stack(agenda, constraintA, constraintB): """ Tests whether clearing the stack will remove all items from it. Args: agenda: Agenda object (given in conftest.py) constraintA (dict): an existing slot-value pair in the domain (given in conftest_<domain>.py) constraintB (dict): another existing slot-value pair in the domain (given in conftest_<domain>.py) """ stack = [ UserAct(act_type=UserActionType.Inform, slot=constraintA['slot'], value=constraintA['value']), UserAct(act_type=UserActionType.Confirm, slot=constraintB['slot'], value=constraintB['value']) ] agenda.stack = stack.copy() agenda.clear() assert len(agenda.stack) == 0
def fill_with_requests(self, goal: Goal, exclude_name: bool = True): """Adds all request actions to the agenda necessary to fulfill the *goal*. Args: goal (Goal): The current goal of the (simulated) user for which actions will be pushed to the agenda. exclude_name (bool): whehter or not to include an action to request an entities name. """ # add requests and make sure to add the name at the end (i.e. ask first for name) for key, value in goal.requests.items(): if ((key != 'name' and exclude_name) or not exclude_name) and value is None: self.stack.append( UserAct(act_type=UserActionType.Request, slot=key, value=value, score=1.0))
def test_get_all_usr_action_types(bst): """ Tests whether requesting user action types will return all of them. Args: bst: BST Object (given in conftest.py) """ user_action_types = [ UserActionType.Inform, UserActionType.Request, UserActionType.Hello, UserActionType.Thanks ] user_acts = [UserAct(act_type=act_type) for act_type in user_action_types] act_type_set = bst._get_all_usr_action_types(user_acts) assert act_type_set == set(user_action_types)
def test_handle_user_acts_for_user_request(bst): """ Tests whether the requests are set correctly when handling a user request. Args: bst: BST Object (given in conftest.py) """ slot = 'foo' score = 0.5 user_acts = [ UserAct(act_type=UserActionType.Request, slot=slot, score=score) ] bst._handle_user_acts(user_acts) assert slot in bst.bs['requests'] assert bst.bs['requests'][slot] == score
def test_push_several_items(agenda, constraintA, constraintB): """ Tests whether pushing several items on the stack increases the stack size by the number of items and inserts the items in the same order at the end of the stack. Args: agenda: Agenda object (given in conftest.py) constraintA (dict): an existing slot-value pair in the domain (given in conftest_<domain>.py) constraintB (dict): another existing slot-value pair in the domain (given in conftest_<domain>.py) """ items = [ UserAct(act_type=UserActionType.Inform, slot=constraintA['slot'], value=constraintA['value']), UserAct(act_type=UserActionType.Confirm, slot=constraintB['slot'], value=constraintB['value']) ] stack_size = len(agenda.stack) agenda.push(items) assert len(agenda.stack) == stack_size + len(items) assert agenda.stack[-len(items):] == items
def _add_request(self, user_utterance: str, slot: str): """ Creates the user request act and adds it to the user act list Args: user_utterance {str} -- text input from user slot {str} -- requested slot Returns: """ # New user act -- Request(slot) user_act = UserAct(text=user_utterance, act_type=UserActionType.Request, slot=slot) self.user_acts.append(user_act) # Storing user requested slots during the whole dialog self.slots_requested.add(slot)
def fill_with_constraints(self, goal): """ Adds all inform actions to the agenda necessary to fulfill the *goal*. Generally there is no need to add all constraints from the goal to the agenda apart from the initialisation. Args: goal: The current goal of the (simulated) user for which actions will be pushed to the agenda. """ # add informs from goal for constraint in goal.constraints: self.stack.append(UserAct(act_type=UserActionType.Inform, slot=constraint.slot,\ value=constraint.value, score=1.0))
def test_handle_user_acts_for_user_negative_inform(bst): """ Tests whether handling a negative inform by the user deletes the corresponding inform value. Args: bst: BST Object (given in conftest.py) """ slot = 'foo' value = 'bar' bst.bs['informs'][slot] = {value: 0.5} user_acts = [ UserAct(act_type=UserActionType.NegativeInform, slot=slot, value=value) ] bst._handle_user_acts(user_acts) assert value not in bst.bs['informs'][slot]