def test_fill_with_constraints(agenda, goal, constraintA, constraintB): """ Tests whether filling the stack with constraints will add all constraints from the goal as user actions of type inform to the stack. 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) """ goal.constraints = [ Constraint(slot=constraintA['slot'], value=constraintA['value']), Constraint(slot=constraintB['slot'], value=constraintB['value']) ] agenda_size = len(agenda.stack) agenda.fill_with_constraints(goal) assert len(agenda.stack) == agenda_size + len(goal.constraints) assert any( item.type == UserActionType.Inform and item.slot == constraintA['slot'] and item.value == constraintA['value'] for item in agenda.stack) assert any( item.type == UserActionType.Inform and item.slot == constraintB['slot'] and item.value == constraintB['value'] for item in agenda.stack)
def _alter_constraints(self, constraints, count): """ Alters *count* constraints from the given constraints by choosing a new value (could be also 'dontcare'). """ constraints_candidates = constraints[:] # copy list if not constraints_candidates: for _constraint in self.goal.constraints: if _constraint.value != 'dontcare': constraints_candidates.append( Constraint(_constraint.slot, _constraint.value)) else: # any constraint from the current system actions has to be taken into consideration # make sure that constraints are part of the goal since noise could have influenced the # dialog -> given constraints must conform to the current goal constraints_candidates = list( filter( lambda x: not self.goal.is_inconsistent_constraint_strict( x), constraints_candidates)) if not constraints_candidates: return [] constraints_to_alter = common.numpy.random.choice( constraints_candidates, count, replace=False) new_constraints = [] for _constraint in constraints_to_alter: self.goal.excluded_inf_slot_values[_constraint.slot].add( _constraint.value) possible_values = self.goal.inf_slot_values[_constraint.slot][:] for _value in self.goal.excluded_inf_slot_values[_constraint.slot]: # remove values which have been tried already # NOTE values in self.excluded_inf_slot_values should always be in possible_values # because the same source is used for both and to initialize the goal possible_values.remove(_value) if not possible_values: # add 'dontcare' as last option possible_values.append('dontcare') # 'dontcare' value with some probability if common.random.random( ) < self.parameters['usermodel']['DontcareIfNoVenue']: value = 'dontcare' else: value = common.numpy.random.choice(possible_values) if not self.goal.update_constraint(_constraint.slot, value): # NOTE: this case should never happen! print( "The given constraints (probably by the system) are not part of the goal!" ) new_constraints.append(Constraint(_constraint.slot, value)) self.logger.dialog_turn("Goal altered! {} -> {}.".format( constraints_to_alter, new_constraints)) return new_constraints
def test_is_inconsistent_constraint_for_consistent_constraint(goal): """ Tests whether a consistent constraint is recognized as such by the base function. Args: goal: Goal object (given in conftest.py) """ goal.constraints = [Constraint('foo', 'bar')] is_inconsistent = goal.is_inconsistent_constraint(Constraint('foo', 'bar')) assert is_inconsistent is False
def test_is_inconsistent_constraint_strict_for_unknown_constraint(goal): """ Tests whether an unknown constraint is not recognized as inconsistent by the strict function. Args: goal: Goal object (given in conftest.py) """ goal.constraints = [Constraint('foo', 'bar')] is_inconsistent = goal.is_inconsistent_constraint_strict( Constraint('bar', 'foo')) assert is_inconsistent is True
def test_is_inconsistent_constraint_for_dontcare_constraint(goal): """ Tests whether an inconsistent constraint is not recognized as such by the base function if the value for the corresponding goal constraint is 'dontcare'. Args: goal: Goal object (given in conftest.py) """ goal.constraints = [Constraint('foo', 'dontcare')] is_inconsistent = goal.is_inconsistent_constraint(Constraint('foo', 'bar')) assert is_inconsistent is False
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 _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 _receive_select(self, sys_act: SysAct): """ Processes a select action from the system based on the simulation goal Args: sys_act (SysAct): the last system action """ # 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 respond(self): """ Gets n actions from the agenda, where n is drawn depending on the agenda or a pdf. """ # get some actions from the agenda assert len(self.agenda) > 0, "Agenda is empty, this must not happen at this point!" if self.num_actions_next_turn > 0: # use and reset self.num_actions_next_turn if set num_actions = self.num_actions_next_turn self.num_actions_next_turn = -1 elif self.agenda.stack[-1].type == UserActionType.Bye: # pop all actions from agenda since agenda can only contain thanks (optional) and # bye action num_actions = -1 else: # draw amount of actions num_actions = min(len(self.agenda), common.numpy.random.choice( [1, 2, 3], p=[.6, .3, .1])) # hardcoded pdf # get actions from agenda user_actions = self.agenda.get_actions(num_actions) # copy needed for repeat action since they might be changed in other modules self.last_user_actions = copy.deepcopy(user_actions) for action in user_actions: if action.type == UserActionType.Inform: _constraint = Constraint(action.slot, action.value) # if _constraint in self.goal.constraints: if action in self.goal.missing_informs: self.goal.missing_informs.remove(action) return user_actions
def clean(self, goal: Goal): """Cleans the agenda, i.e. makes sure that actions are consistent with goal and in the correct order. Args: goal (Goal): The goal which is needed to determine the consistent actions. """ cleaned_stack = [] # reverse order since most recent actions are on top of agenda for action in self.stack[::-1]: if action not in cleaned_stack: # NOTE sufficient if there is only one slot per (request) action # remove accomplished requests if (action.type is not UserActionType.Request or (action.slot in goal.requests and goal.requests[action.slot] is None) or action.slot not in goal.requests): # make sure to remove "old" inform actions if action.type is UserActionType.Inform: if not goal.is_inconsistent_constraint( Constraint(action.slot, action.value)): cleaned_stack.insert(0, action) else: cleaned_stack.insert(0, action) self.stack = cleaned_stack
def test_update_unknown_constraint(goal): """ Tests whether updating a constraint is skipped if the constraint is not present in the goal's constraints. Args: goal: Goal object (given in conftest.py) """ goal.constraints = [Constraint('foo', 'bar')] update_successful = goal.update_constraint('bar', 'foo') assert update_successful is False
def test_get_unknown_constraint(goal): """ Tests whether the get_constraint function returns a 'dontcare' if the goal has no constraint with that slot. Args: goal: Goal object (given in conftest.py) """ goal.constraints = [Constraint('foo', 'bar')] constraint_value = goal.get_constraint('baz') assert constraint_value == 'dontcare'
def test_get_known_constraint(goal): """ Tests whether the get_constraint function returns the corresponding value if the goal has a constraint with that slot. Args: goal: Goal object (given in conftest.py) """ slot = 'foo' value = 'bar' goal.constraints = [Constraint(slot, value)] constraint_value = goal.get_constraint(slot) assert constraint_value == value
def test_update_known_constraint(goal): """ Tests whether updating a constraint works correctly if the constraint is actually present in the goal's constraints. Args: goal: Goal object (given in conftest.py) """ slot = 'foo' value = 'baz' goal.constraints = [Constraint(slot, 'bar')] update_successful = goal.update_constraint(slot, value) matching_constraint = next(constraint for constraint in goal.constraints if constraint.slot == slot) assert update_successful is True assert matching_constraint.value == value
def _receive_informbyname(self, sys_act: SysAct): """ Processes an informbyname action from the system; checks if the inform matches the goal constraints and if yes, will add unanswered requests to the agenda Args: sys_act (SysAct): the last system action """ # check all system informs for offer inform_list = [] offers = [] for slot, value_list in sys_act.slot_values.items(): for value in value_list: if slot == 'name': offers.append(value) else: inform_list.append(Constraint(slot, value)) # check offer if offers: if self._check_offer(offers, inform_list): # valid offer for slot, value in inform_list: self.goal.fulfill_request(slot, value) # needed to make sure that not informed constraints (which have been turned into requests) # will be asked first (before ending the dialog too early) req_actions_not_in_goal = [] for action in self.agenda.get_actions_of_type(UserActionType.Request): if action.slot not in self.goal.requests: req_actions_not_in_goal.append(copy.deepcopy(action)) # Emotiongoal might be fulfilled now if (self.goal.is_fulfilled() and not self.agenda.contains_action_of_type(UserActionType.Inform) and not req_actions_not_in_goal): emotionadd(self.emotion_list, 1) self._finish_dialog() else: emotionadd(self.emotion_list, 0)
assert goal.constraints != [] assert goal.requests != {} assert len(goal.constraints) >= min_constraints assert len(goal.constraints) <= max_constraints assert len(goal.requests) >= min_requests assert len(goal.requests ) <= max_requests + 1 # primary key slot is added separately @pytest.mark.parametrize( 'constraints', [ lambda x, y: [(x['slot'], x['value']), (y['slot'], y['value'])], # as list of tuples lambda x, y: [Constraint(x['slot'], x['value']), Constraint(y['slot'], y['value'])], # as list of Constraints lambda x, y: { x['slot']: x['value'], y['slot']: y['value'] } # as dict ]) @pytest.mark.parametrize( 'requests', [ lambda x, y: [x['slot'], y['slot']], # as list of strings (slots) lambda x, y: { x['slot']: None, y['slot']: None } # as dict ])
def _check_offer(self, offers, informed_constraints_by_system): """ Checks for an offer and returns True if the offer is valid. """ if not self._check_informs(informed_constraints_by_system): # reset offer in goal since inconsistencies have been detected and covered self.goal.requests[self.domain.get_primary_key()] = None return False # TODO maybe check for current offer first since alternative with name='none' by system # would trigger goal change -> what is the correct action in this case? if offers: if 'none' not in offers: # offer was given # convert informs of values != 'dontcare' to requests actions_to_convert = list( self.agenda.get_actions_of_type(UserActionType.Inform, consider_dontcare=False)) if len(self.goal.constraints) > 1 and len( actions_to_convert) == len(self.goal.constraints): # penalise too early offers self._repeat_last_actions() self.num_actions_next_turn = len(self.last_user_actions) return False # ask for values of remaining inform slots on agenda - this has two purposes: # 1. making sure that offer is consistent with goal # 2. making sure that inconsistent offers prolongate a dialog for action in actions_to_convert: self.agenda.push( UserAct(act_type=UserActionType.Request, slot=action.slot, value=None, score=1.0)) self.agenda.remove_actions_of_type(UserActionType.Inform) if self.goal.requests[ self.domain.get_primary_key()] is not None: if self.goal.requests[ self.domain.get_primary_key()] in offers: # offer is the same, don't change anything but treat offer as valid return True else: # offer is not the same, but did not request a new one # NOTE with current bst do not (negative) inform about the offer, because # it will only set the proability to zero -> will not be excluded # self.agenda.push(UserAct(act_type=UserActionType.NegativeInform,\ # slot=self.domain.get_primary_key(), value=offers[0])) return False else: for _offer in offers: if _offer not in self.excluded_venues: # offer is not on the exclusion list (e.g. from reqalt action) and # there is no current offer # sometimes ask for alternative if common.random.random( ) < self.parameters['usermodel']['ReqAlt']: self._request_alt(_offer) return False else: self.goal.requests[ self.domain.get_primary_key()] = _offer for _action in self.goal.missing_informs: # informed constraints by system are definitely consistent with # goal at this point if Constraint( _action.slot, _action.value ) not in informed_constraints_by_system: self.agenda.push( UserAct(act_type=UserActionType. Request, slot=_action.slot, value=None)) return True # no valid offer was given self._request_alt() return False else: # no offer was given # TODO add probability to choose number of alternations altered_constraints = self._alter_constraints( informed_constraints_by_system, 1) # reset goal push new actions on top of agenda self.goal.reset() self.goal.missing_informs = [ UserAct(act_type=UserActionType.Inform, slot=_constraint.slot, value=_constraint.value) for _constraint in self.goal.constraints ] for _constraint in altered_constraints: self.agenda.push( UserAct(act_type=UserActionType.Inform, slot=_constraint.slot, value=_constraint.value, score=1.0)) self.agenda.clean(self.goal) return False return False