def _convert_inform_by_primkey(self, q_results: iter, sys_act: SysAct, belief_state: BeliefState): """ Helper function that adds the values for slots to a SysAct object when the system is answering a request for information about an entity from the user Args: q_results (iterable): list of query results from the database sys_act (SysAct): current raw sys_act to be filled in belief_state (BeliefState) """ sys_act.type = SysActionType.InformByName if q_results: result = q_results[0] # currently return just the first result keys = list(result.keys() ) # should represent all user specified constraints # add slots + values (where available) to the sys_act for k in keys: res = result[k] if result[k] else 'not available' sys_act.add_value(k, res) # Name might not be a constraint in request queries, so add it if self.domain_key not in keys: name = self._get_name(belief_state) sys_act.add_value(self.domain_key, name) # Add default Inform slots for slot in self.domain.get_default_inform_slots(): if slot not in sys_act.slot_values: sys_act.add_value(slot, result[slot]) else: sys_act.add_value(self.domain_key, 'none')
def _convert_inform_by_constraints(self, q_results: iter, sys_act: SysAct, belief_state: BeliefState): """ Helper function for filling in slots and values of a raw inform act when the system is ready to make the user an offer Args: q_results (iter): the results from the databse query sys_act (SysAct): the raw infor act to be filled in belief_state (BeliefState): the current system beliefs """ # altered to add all results to the SysAct if list(q_results): self.current_suggestions = [] self.s_index = 0 for result in q_results: sys_act.add_value(self.domain_key, result[self.domain_key]) else: sys_act.add_value(self.domain_key, 'none') sys_act.type = SysActionType.InformByName constraints, dontcare = self._get_constraints(belief_state) for c in constraints: # Using constraints here rather than results to deal with empty # results sets (eg. user requests something impossible) --LV sys_act.add_value(c, constraints[c]) # altered to the changes above if list(q_results): for slot in belief_state['requests']: if slot not in sys_act.slot_values: for result in q_results: sys_act.add_value(slot, result[slot])
def _receive_confirmrequest(self, sys_act): """Processes a confirmrequest action from the system.""" # first slot is confirm, second slot is request for slot, value in sys_act.slot_values.items(): if value is None: # system's request action self._receive_request( SysAct(act_type=SysActionType.Request, slot_values={slot: None})) else: # system's confirm action self._receive_confirm( SysAct(act_type=SysActionType.Confirm, slot_values={slot: [value]}))
def test_convert_inform_by_alternatives_adds_constraints( policy, beliefstate, entryA, constraintA, constraintB): """ Tests whether converting the inform system action by a request for alternatives adds the constraints as slot-value pairs to the system action. Args: policy: Policy Object (given in conftest.py) beliefstate: BeliefState object (given in conftest.py) entryA (dict): slot-value pairs for a complete entry in the domain (given in conftest_<domain>.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) """ beliefstate['informs'] = { constraintA['slot']: { constraintA['value']: 0.5 }, constraintB['slot']: { constraintB['value']: 0.3 } } sys_act = SysAct() policy._convert_inform_by_alternatives(sys_act, [entryA], beliefstate) assert any(slot == constraintA['slot'] and constraintA['value'] in values for slot, values in sys_act.slot_values.items()) assert any(slot == constraintB['slot'] and constraintB['value'] in values for slot, values in sys_act.slot_values.items())
def _convert_inform_by_constraints(self, q_results: iter, sys_act: SysAct, belief_state: BeliefState): """ Helper function for filling in slots and values of a raw inform act when the system is ready to make the user an offer Args: q_results (iter): the results from the databse query sys_act (SysAct): the raw infor act to be filled in belief_state (BeliefState): the current system beliefs """ # TODO: Do we want some way to allow users to scroll through # result set other than to type 'alternatives'? --LV if q_results: self.current_suggestions = [] self.s_index = 0 for result in q_results: self.current_suggestions.append(result) result = self.current_suggestions[0] sys_act.add_value(self.domain_key, result[self.domain_key]) else: sys_act.add_value(self.domain_key, 'none') sys_act.type = SysActionType.InformByName constraints, dontcare = self._get_constraints(belief_state) for c in constraints: # Using constraints here rather than results to deal with empty # results sets (eg. user requests something impossible) --LV sys_act.add_value(c, constraints[c])
def _convert_inform_by_alternatives(self, sys_act: SysAct, q_res: iter, belief_state: BeliefState): """ Helper Function, scrolls through the list of alternative entities which match the user's specified constraints and uses the next item in the list to fill in the raw inform act. When the end of the list is reached, currently continues to give last item in the list as a suggestion Args: sys_act (SysAct): the raw inform to be filled in belief_state (BeliefState): current system belief state () """ if q_res and not self.current_suggestions: self.current_suggestions = [] self.s_index = -1 for result in q_res: self.current_suggestions.append(result) self.s_index += 1 # here we should scroll through possible offers presenting one each turn the user asks # for alternatives if self.s_index <= len(self.current_suggestions) - 1: # the first time we inform, we should inform by name, so we use the right template if self.s_index == 0: sys_act.type = SysActionType.InformByName else: sys_act.type = SysActionType.InformByAlternatives result = self.current_suggestions[self.s_index] # Inform by alternatives according to our current templates is # just a normal inform apparently --LV sys_act.add_value(self.domain_key, result[self.domain_key]) else: sys_act.type = SysActionType.InformByAlternatives # default to last suggestion in the list self.s_index = len(self.current_suggestions) - 1 sys_act.add_value(self.domain.get_primary_key(), 'none') # in addition to the name, add the constraints the user has specified, so they know the # offer is relevant to them constraints, dontcare = self._get_constraints(belief_state) for c in constraints: sys_act.add_value(c, constraints[c])
def _raw_action(self, q_res: iter, beliefstate: BeliefState) -> SysAct: """Based on the output of the db query and the method, choose whether next action should be request or inform Args: q_res (list): rows (list of dicts) returned by the issued sqlite3 query method (str): the type of user action ('byprimarykey', 'byconstraints', 'byalternatives') Returns: (SysAct): SysAct object of appropriate type --LV """ sys_act = SysAct() # if there is more than one result if len(q_res) > 1: constraints, dontcare = self._get_constraints(beliefstate) # Gather all the results for each column temp = {key: [] for key in q_res[0].keys()} # If any column has multiple values, ask for clarification for result in q_res: for key in result.keys(): if key != self.domain_key: temp[key].append(result[key]) next_req = self._gen_next_request(temp, beliefstate) if next_req: sys_act.type = SysActionType.Request sys_act.add_value(next_req) return sys_act # Otherwise action type will be inform, so return an empty inform (to be filled in later) sys_act.type = SysActionType.InformByName return sys_act
def test_convert_inform_by_constraints_without_results(policy, beliefstate): """ Tests whether the system converts the inform system action to an inform by name action with no specific entity name if querying the database returned no results. Args: policy: Policy Object (given in conftest.py) beliefstate: BeliefState object (given in conftest.py) """ primkey = policy.domain.get_primary_key() sys_act = SysAct() policy._convert_inform_by_constraints([], sys_act, beliefstate) assert sys_act.type == SysActionType.InformByName assert 'none' in sys_act.slot_values[primkey]
def test_convert_inform_with_given_constraints(policy, beliefstate, entryA): """ Tests whether the system selects an inform by name action if the inform is converted with other constraints than the primary key or a request for alternatives. Args: policy: Policy Object (given in conftest.py) beliefstate: BeliefState object (given in conftest.py) entryA (dict): slot-value pairs for a complete entry in the domain (given in conftest_<domain>.py) """ beliefstate['user_acts'] = [] beliefstate['requests'] = [] sys_act = SysAct() policy._convert_inform([entryA], sys_act, beliefstate) assert sys_act.type == SysActionType.InformByName assert sys_act.slot_values != {}
def test_convert_inform_by_primkey_with_results(policy, beliefstate, entryA): """ Tests whether the system converts the inform system action to an inform by name action with a specified name if querying the database returned results. Args: policy: Policy Object (given in conftest.py) beliefstate: BeliefState object (given in conftest.py) entryA (dict): slot-value pairs for a complete entry in the domain (given in conftest_<domain>.py) """ primkey = policy.domain.get_primary_key() beliefstate['informs'] = {primkey: {entryA[primkey]: 0.5}} sys_act = SysAct() policy._convert_inform_by_primkey([entryA], sys_act, beliefstate) assert sys_act.type == SysActionType.InformByName assert primkey in sys_act.slot_values assert entryA[primkey] in sys_act.slot_values[primkey]
def test_convert_inform_for_request_alternatives(policy, beliefstate, entryA): """ Tests whether the system selects either an inform by name action or an inform by alternatives action if the inform is converted with a given request for alternatives by the user. Args: policy: Policy Object (given in conftest.py) beliefstate: BeliefState object (given in conftest.py) entryA (dict): slot-value pairs for a complete entry in the domain (given in conftest_<domain>.py) """ beliefstate['user_acts'] = [UserActionType.RequestAlternatives] beliefstate['requests'] = [] sys_act = SysAct() policy._convert_inform([entryA], sys_act, beliefstate) assert sys_act.type in (SysActionType.InformByName, SysActionType.InformByAlternatives) assert sys_act.slot_values != {}
def parse_action(self, intent, slot, value): try: if not self.user: return SysAct(act_type=SysActionType(intent), slot_values={slot: [value] if value else []} if slot else None) else: return UserAct(act_type=UserActionType(intent), slot=slot, value=value) except ValueError: # intent is probably not a valid SysActionType if not self.user: print("Intent must be one of {}".format( list(map(lambda x: x.value, SysActionType)))) else: print("Intent must be one of {}".format( list(map(lambda x: x.value, UserActionType))))
def test_convert_inform_by_alternatives_without_suggestions( policy, beliefstate, entryA): """ Tests whether the system converts the inform system action to an inform by name action if it is the first inform of the dialog (i.e. the list of current suggestions is empty). Args: policy: Policy Object (given in conftest.py) beliefstate: BeliefState object (given in conftest.py) entryA (dict): slot-value pairs for a complete entry in the domain (given in conftest_<domain>.py) """ policy.current_suggestions = [] policy.s_index = 0 sys_act = SysAct() policy._convert_inform_by_alternatives(sys_act, [entryA], beliefstate) assert sys_act.type == SysActionType.InformByName assert policy.current_suggestions != [] assert policy.s_index == 0 assert sys_act.slot_values != {}
def test_convert_inform_with_primary_key(policy, beliefstate, primkey_constraint, entryA): """ Tests whether the system selects an inform by name action if the inform is converted with a given primary key. Args: policy: Policy Object (given in conftest.py) beliefstate: BeliefState object (given in conftest.py) primkey_constraint (dict): slot-value pair for a primary key constraint (given in conftest_<domain>.py) entryA (dict): slot-value pairs for a complete entry in the domain (given in conftest_<domain>.py) """ beliefstate['informs'] = { primkey_constraint['slot']: { primkey_constraint['value']: 0.5 } } sys_act = SysAct() policy._convert_inform([entryA], sys_act, beliefstate) assert sys_act.type == SysActionType.InformByName assert sys_act.slot_values != {}
def test_convert_inform_by_constraints_with_results(policy, beliefstate, entryA, entryB): """ Tests whether the system converts the inform system action to an inform by name action with specific entity name if querying the database returned results. Args: policy: Policy Object (given in conftest.py) beliefstate: BeliefState object (given in conftest.py) entryA (dict): slot-value pairs for a complete entry in the domain (given in conftest_<domain>.py) entryB (dict): slot-value pairs for another complete entry in the domain (given in conftest_<domain>.py) """ primkey = policy.domain.get_primary_key() policy.current_suggestions = [entryA] policy.s_index = 1 sys_act = SysAct() policy._convert_inform_by_constraints([entryB], sys_act, beliefstate) assert sys_act.type == SysActionType.InformByName assert policy.current_suggestions == [entryB] assert policy.s_index == 0 assert entryB[primkey] in sys_act.slot_values[primkey]
def test_convert_inform_by_alternatives_with_suggestions( policy, beliefstate, entryA, entryB): """ Tests whether the system converts the inform system action to an inform by alternatives action if it is not the first inform of the dialog (i.e. the list of current suggestions is not empty). Args: policy: Policy Object (given in conftest.py) beliefstate: BeliefState object (given in conftest.py) entryA (dict): slot-value pairs for a complete entry in the domain (given in conftest_<domain>.py) entryB (dict): slot-value pairs for another complete entry in the domain (given in conftest_<domain>.py) """ primkey = policy.domain.get_primary_key() policy.current_suggestions = [entryA, entryB] policy.s_index = 0 sys_act = SysAct() policy._convert_inform_by_alternatives(sys_act, [], beliefstate) assert sys_act.type == SysActionType.InformByAlternatives assert policy.s_index == 1 assert sys_act.slot_values != {} assert entryB[primkey] in sys_act.slot_values[primkey]
def test_convert_inform_by_alternatives_with_invalid_index( policy, beliefstate, entryA, entryB): """ Tests whether the system converts the inform system action to an inform by name action without a specific entity name if the index for the current suggestion list is not valid (e.g. the index is higher than the number of current suggestions). Args: policy: Policy Object (given in conftest.py) beliefstate: BeliefState object (given in conftest.py) entryA (dict): slot-value pairs for a complete entry in the domain (given in conftest_<domain>.py) entryB (dict): slot-value pairs for another complete entry in the domain (given in conftest_<domain>.py) """ primkey = policy.domain.get_primary_key() policy.current_suggestions = [entryA] policy.s_index = 1 sys_act = SysAct() policy._convert_inform_by_alternatives(sys_act, [entryB], beliefstate) assert sys_act.type == SysActionType.InformByAlternatives assert policy.s_index == 0 assert sys_act.slot_values != {} assert 'none' in sys_act.slot_values[primkey]
def generate_sys_acts(self, user_acts: List[UserAct] = None) -> dict(sys_acts=List[SysAct]): """Generates system acts by looking up answers to the given user question. Args: user_acts: The list of user acts containing information about the predicted relation, topic entities and relation direction Returns: dict with 'sys_acts' as key and list of system acts as value """ if user_acts is None: return { 'sys_acts': [SysAct(SysActionType.Welcome)]} elif any([user_act.type == UserActionType.Bye for user_act in user_acts]): return { 'sys_acts': [SysAct(SysActionType.Bye)] } elif not user_acts: return { 'sys_acts': [SysAct(SysActionType.Bad)] } user_acts = [user_act for user_act in user_acts if user_act.type != UserActionType.SelectDomain] if len(user_acts) == 0: return { 'sys_acts': [SysAct(SysActionType.Welcome)]} relation = [user_act.value for user_act in user_acts \ if user_act.type == UserActionType.Inform and user_act.slot == 'relation'][0] topics = [user_act.value for user_act in user_acts \ if user_act.type == UserActionType.Inform and user_act.slot == 'topic'] direction = [user_act.value for user_act in user_acts \ if user_act.type == UserActionType.Inform and user_act.slot == 'direction'][0] if not topics: return { 'sys_acts': [SysAct(SysActionType.Bad)] } # currently, short answers are used for world knowledge answers = self._get_short_answers(relation, topics, direction) sys_acts = [SysAct(SysActionType.InformByName, slot_values=answer) for answer in answers] self.debug_logger.dialog_turn("System Action: " + '; '.join( [str(sys_act) for sys_act in sys_acts])) return {'sys_acts': sys_acts}
def forward(self, dialog_graph, beliefstate: BeliefState = None, user_acts: List[u.UserAct] = None, sys_act: SysAct = None, **kwargs) -> dict(sys_act=SysAct): """ Responsible for walking the policy through a single turn. Uses the current user action and system belief state to determine what the next system action should be. To implement an alternate policy, this method may need to be overwritten Args: dialog_graph (DialogSystem): the graph to which the policy belongs belief_state (BeliefState): a BeliefState obejct representing current system knowledge user_acts (list): a list of UserAct objects mapped from the user's last utterance sys_act (SysAct): this should be None Returns: (dict): a dictionary with the key "sys_act" and the value that of the systems next action """ belief_state = beliefstate # variables for general (non-domain specific) actions self.turn = dialog_graph.num_turns self.prev_sys_act = sys_act self._check_for_gen_actions(user_acts) # do nothing on the first turn --LV if user_acts is None and self.turn == 0: sys_act = SysAct() sys_act.type = SysActionType.Welcome return sys_act # if there is no user act, and it's not the first turn, this is bad elif not self.act_types_lst and self.turn > 0: # if not self.is_user_act and self.turn > 0: sys_act = SysAct() sys_act.type = SysActionType.Bad # if the user has not said anything which can be parsed elif u.UserActionType.Bad in self.act_types_lst: sys_act = SysAct() sys_act.type = SysActionType.Bad # if the action is 'bye' tell system to end dialog elif u.UserActionType.Bye in self.act_types_lst: sys_act = SysAct() sys_act.type = SysActionType.Bye # if user only says thanks, ask if they want anything else elif u.UserActionType.Thanks in self.act_types_lst: sys_act = SysAct() sys_act.type = SysActionType.RequestMore # If user only says hello, request a random slot to move dialog along elif u.UserActionType.Hello in self.act_types_lst: sys_act = SysAct() sys_act.type = SysActionType.Request sys_act.add_value(self._get_open_slot(belief_state)) # handle domain specific actions else: sys_act = self._next_action(belief_state) self.logger.dialog_turn("System Action: " + str(sys_act)) return {'sys_act': sys_act}
def choose_sys_act(self, beliefstate: BeliefState = None, sys_act: SysAct = None)\ -> dict(sys_act=SysAct): """ Responsible for walking the policy through a single turn. Uses the current user action and system belief state to determine what the next system action should be. To implement an alternate policy, this method may need to be overwritten Args: belief_state (BeliefState): a BeliefState object representing current system knowledge user_acts (list): a list of UserAct objects mapped from the user's last utterance sys_act (SysAct): this should be None Returns: (dict): a dictionary with the key "sys_act" and the value that of the systems next action """ # variables for general (non-domain specific) actions # self.turn = dialog_graph.num_turns self.prev_sys_act = sys_act self._remove_gen_actions(beliefstate) sys_state = {} # do nothing on the first turn --LV if self.first_turn and not beliefstate['user_acts']: self.first_turn = False sys_act = SysAct() sys_act.type = SysActionType.Welcome return {'sys_act': sys_act} elif UserActionType.Bad in beliefstate["user_acts"]: sys_act = SysAct() sys_act.type = SysActionType.Bad # if the action is 'bye' tell system to end dialog elif UserActionType.Bye in beliefstate["user_acts"]: sys_act = SysAct() sys_act.type = SysActionType.Bye # if user only says thanks, ask if they want anything else elif UserActionType.Thanks in beliefstate["user_acts"]: sys_act = SysAct() sys_act.type = SysActionType.RequestMore # If user only says hello, request a random slot to move dialog along elif UserActionType.Hello in beliefstate[ "user_acts"] or UserActionType.SelectDomain in beliefstate[ "user_acts"]: sys_act = SysAct() sys_act.type = SysActionType.Request slot = self._get_open_slot(beliefstate) sys_act.add_value(slot) # prepare sys_state info sys_state['lastRequestSlot'] = slot # If we switch to the domain, start a new dialog if UserActionType.SelectDomain in beliefstate["user_acts"]: self.dialog_start() self.first_turn = False # handle domain specific actions else: sys_act, sys_state = self._next_action(beliefstate) self.logger.dialog_turn("System Action: " + str(sys_act)) if 'last_act' not in sys_state: sys_state['last_act'] = sys_act return {'sys_act': sys_act, 'sys_state': sys_state}
def choose_sys_act(self, beliefstate: BeliefState = None) \ -> dict(sys_act=SysAct): """ Responsible for walking the policy through a single turn. Uses the current user action and system belief state to determine what the next system action should be. To implement an alternate policy, this method may need to be overwritten Args: belief_state (BeliefState): a BeliefState obejct representing current system knowledge Returns: (dict): a dictionary with the key "sys_act" and the value that of the systems next action """ self.turns += 1 # do nothing on the first turn --LV sys_state = {} if self.first_turn and not beliefstate['user_acts']: self.first_turn = False sys_act = SysAct() sys_act.type = SysActionType.Welcome sys_state["last_act"] = sys_act return {'sys_act': sys_act, "sys_state": sys_state} if self.turns >= self.max_turns: sys_act = SysAct() sys_act.type = SysActionType.Bye sys_state["last_act"] = sys_act return {'sys_act': sys_act, "sys_state": sys_state} # removes hello and thanks if there are also domain specific actions self._remove_gen_actions(beliefstate) if UserActionType.Bad in beliefstate["user_acts"]: sys_act = SysAct() sys_act.type = SysActionType.Bad # if the action is 'bye' tell system to end dialog elif UserActionType.Bye in beliefstate["user_acts"]: sys_act = SysAct() sys_act.type = SysActionType.Bye # if user only says thanks, ask if they want anything else elif UserActionType.Thanks in beliefstate["user_acts"]: sys_act = SysAct() sys_act.type = SysActionType.RequestMore # If user only says hello, request a random slot to move dialog along elif UserActionType.Hello in beliefstate[ "user_acts"] or UserActionType.SelectDomain in beliefstate[ "user_acts"]: sys_act = SysAct() sys_act.type = SysActionType.Request slot = self._get_open_slot(beliefstate) sys_act.add_value(slot) # If we switch to the domain, start a new dialog if UserActionType.SelectDomain in beliefstate["user_acts"]: self.dialog_start() self.first_turn = False # handle domain specific actions else: sys_act, sys_state = self._next_action(beliefstate) if self.logger: self.logger.dialog_turn("System Action: " + str(sys_act)) if "last_act" not in sys_state: sys_state["last_act"] = sys_act return {'sys_act': sys_act, "sys_state": sys_state}
def _next_action(self, beliefstate: BeliefState): """Determines the next system action based on the current belief state and previous action. When implementing a new type of policy, this method MUST be rewritten Args: belief_state (HandCraftedBeliefState): system values on liklihood of each possible state Return: (SysAct): the next system action --LV """ sys_state = {} # Assuming this happens only because domain is not actually active --LV """if UserActionType.Bad in beliefstate['user_acts'] or beliefstate['requests'] \ and not self._get_name(beliefstate): sys_act = SysAct() sys_act.type = SysActionType.Bad return sys_act, {'last_action': sys_act}""" if not self._mandatory_requests_fulfilled(beliefstate): sys_act = SysAct() sys_act.type = SysActionType.Request sys_act.slot_values = { self._get_open_mandatory_slot(beliefstate): None } return sys_act, {'last_action': sys_act} elif UserActionType.RequestAlternatives in beliefstate['user_acts'] \ and not self._get_constraints(beliefstate)[0]: sys_act = SysAct() sys_act.type = SysActionType.Bad return sys_act, {'last_action': sys_act} elif self.domain.get_primary_key() in beliefstate['informs'] \ and not beliefstate['requests']: sys_act = SysAct() sys_act.type = SysActionType.InformByName sys_act.add_value(self.domain.get_primary_key(), self._get_name(beliefstate)) return sys_act, {'last_action': sys_act} # Otherwise we need to query the db to determine next action results = self._query_db(beliefstate) sys_act = self._raw_action(results, beliefstate) # requests are fairly easy, if it's a request, return it directly if sys_act.type == SysActionType.Request: if len(list(sys_act.slot_values.keys())) > 0: # update the belief state to reflec the slot we just asked about sys_state['lastRequestSlot'] = list( sys_act.slot_values.keys())[0] # belief_state['system']['lastRequestSlot'] = list(sys_act.slot_values.keys())[0] # otherwise we need to convert a raw inform into a one with proper slots and values elif sys_act.type == SysActionType.InformByName: self._convert_inform(results, sys_act, beliefstate) # update belief state to reflect the offer we just made values = sys_act.get_values(self.domain.get_primary_key()) if values: # belief_state['system']['lastInformedPrimKeyVal'] = values[0] sys_state['lastInformedPrimKeyVal'] = values[0] else: sys_act.add_value(self.domain.get_primary_key(), 'none') sys_state['last_act'] = sys_act return (sys_act, sys_state)