def test_encoding_decoding_of_acts_for_user(self): intents = self.qp.dstc2_acts_usr for intent in intents: da = DialogueAct(intent=intent) da_as_int = self.qp.encode_action([da], system=False) da_decoded = self.qp.decode_action(da_as_int, system=False)[0] self.assertEqual(intent, da_decoded.intent)
def build_inform(d_state, new_sys_acts, sys_act): slots = [] if sys_act.params: # use the slots addressed by the inform act (slots selected by the policy) slots = [x.slot for x in sys_act.params] else: # use the requested slots, if any available if d_state.requested_slots: slots = [x for x in d_state.requested_slots] if not slots: # if we still have no slot(s) use one from the filled slots slots = [random.choice(list(d_state.slots_filled.keys()))] def get_value(slot): if not d_state.item_in_focus or (slot not in d_state.item_in_focus or not d_state.item_in_focus[slot]): value = "no info" else: value = d_state.item_in_focus[slot] return value act_items = [ DialogueActItem(slot, Operator.EQ, get_value(slot)) for slot in slots ] new_sys_acts.append(DialogueAct("inform", act_items))
def decode_action(self, step: AgentStep) -> DialogueAct: intent, slots = self.action_enc.decode(*step.action) slots = self._filter_slots(intent, slots) return DialogueAct( intent, params=[DialogueActItem(slot, Operator.EQ, "") for slot in slots], )
def create_random_dialog_act(domain:Domain,is_system=True): acts = [] if is_system: inform_slots = domain.requestable_slots request_slots = domain.system_requestable_slots else: inform_slots = domain.system_requestable_slots request_slots = domain.requestable_slots intent_p = random.choice(domain.acts_params) if intent_p is not None: if intent_p == 'inform': slots = pick_some(inform_slots,1,1) elif intent_p == 'request': slots = pick_some(request_slots,1,1) elif intent_p == 'expl-conf': slots = pick_some(request_slots,1,1) else: assert False act = DialogueAct(intent_p,params=[DialogueActItem(slot,Operator.EQ,None) for slot in slots]) acts.append(act) # if is_system: # intens_w = pick_some(domain.dstc2_acts_sys,0,3) # else: # intens_w = pick_some(domain.dstc2_acts_usr,0,3) # acts.extend([DialogueAct(i) for i in intens_w]) return acts
def receive_input(self, system_acts): """ Process input received and do some housekeeping. :param system_acts: list containing dialogue acts from the system :return: nothing """ if self.prev_sys_acts and self.prev_sys_acts == system_acts: self.curr_patience -= 1 else: self.curr_patience = self.patience self.prev_sys_acts = deepcopy(system_acts) self.input_system_acts = deepcopy(system_acts) # Check for goal satisfaction # Update user goal (in ABUS the state is factored into the goal and # the agenda) for system_act in system_acts: if system_act.intent == 'offer': self.offer_made = True # Reset past requests # TODO: Is this reasonable? self.goal.actual_requests = {} for item in self.goal.requests: self.goal.requests[item].value = '' # Gather all inform or offer params into one dialogue act inform_dact = DialogueAct('inform', []) for system_act in system_acts: if system_act.intent in ['inform', 'offer']: inform_dact.params += deepcopy(system_act.params) # Check for constraint satisfaction if self.offer_made: # Check that the venue provided meets the constraints meets_constraints = all( [i.value == self.goal.constraints[i.slot].value for i in inform_dact.params if i.slot in self.goal.constraints]) # If it meets the constraints, update the requests if meets_constraints: for item in inform_dact.params: if item.slot in self.goal.actual_requests: self.goal.actual_requests[item.slot].value = item.value if item.slot in self.goal.requests: self.goal.requests[item.slot].value = item.value # Use the true requests for asserting goal is met self.goal_met = True for slot in self.goal.requests: if not self.goal.requests[slot].value: self.goal_met = False break
def test_encoding_decoding_of_acts_for_system(self): intents = self.qp.dstc2_acts_sys for intent in intents: da = DialogueAct(intent=intent) da_as_int = self.qp.encode_action([da], system=True) self.assertNotEqual(-1, da_as_int) da_decoded = self.qp.decode_action(da_as_int, system=True)[0] self.assertEqual(intent, da_decoded.intent)
def build_offer_from_inform(d_state, new_sys_acts, slots): new_sys_acts.append( DialogueAct( "offer", [ DialogueActItem("name", Operator.EQ, d_state.item_in_focus["name"]) ], )) new_sys_acts.append( DialogueAct( "inform", [ DialogueActItem(slot, Operator.EQ, d_state.item_in_focus[slot]) for slot in slots if slot is not "name" ], ))
def test_encoding_decoding_of_request_for_user(self): slots = self.qp.requestable_slots for s in slots: da = DialogueAct(intent='request', params=[DialogueActItem(s, Operator.EQ, '')]) da_as_int = self.qp.encode_action([da], system=False) self.assertNotEqual(-1, da_as_int) da_decoded = self.qp.decode_action(da_as_int, system=False)[0] self.assertEqual('request', da_decoded.intent) slot_decoded = da_decoded.params[0].slot self.assertEqual(s, slot_decoded, 'Error in slot en- or decoding.')
def respond(self): """ Creates the response of the simulated user. :return: List of DialogueActs as a response """ if self.curr_patience == 0: if self.nlg: return self.nlg.generate_output( {'dacts': [DialogueAct('bye', [])], 'system': False}) else: return [DialogueAct('bye', [])] # Sample the number of acts to pop. # pop_distributions defines the weighted probability for the number of items to be pushed # the index in the list reflects the number, the value at this position the weight: # e.g. pop_distribution = [3.0, 2.0, 1.0] means, that in mean, in 3 of 6 choices one item will be popped, # in 2 of 6 choices two items, and in 1 of 6 choices three items. acts = [] # Use pop_distribution to determine the number of items (i.e. dialog acts) to be popped. # However, we can pop only as many items as on the agenda. pops = self._get_number_of_pop_items() for pop in range(pops): act = self.error_model.semantic_noise(self.agenda.pop()) # Keep track of actual requests made. These are used in reward and # success calculation if act.intent == 'request' and act.params: self.goal.actual_requests[act.params[0].slot] = act.params[0] acts.append(act) if self.nlg: acts = self.nlg.generate_output({'dacts': acts, 'system': False}) return acts
def respond(self): """ Creates the response of the simulated user. :return: List of DialogueActs as a response """ if self.curr_patience == 0: if self.nlg: return self.nlg.generate_output({ 'dacts': [DialogueAct('bye', [])], 'system': False }) else: return [DialogueAct('bye', [])] # Sample the number of acts to pop. acts = [] pops = min( random.choices(range(1, len(self.pop_distribution) + 1), weights=self.pop_distribution)[0], self.agenda.size()) for pop in range(pops): act = self.error_model.semantic_noise(self.agenda.pop()) # Keep track of actual requests made. These are used in reward and # success calculation if act.intent == 'request' and act.params: self.goal.actual_requests[act.params[0].slot] = act.params[0] acts.append(act) if self.nlg: acts = self.nlg.generate_output({'dacts': acts, 'system': False}) return acts
def build_offer(d_state, new_sys_acts, sys_act, sys_acts, sys_acts_copy): # TODO: What should happen if a policy selects offer, but there is no item in focus? if d_state.item_in_focus: # Remove the empty offer sys_acts_copy.remove(sys_act) new_sys_acts.append( DialogueAct( "offer", [ DialogueActItem("name", Operator.EQ, d_state.item_in_focus["name"]) ], )) assert len(sys_acts) == 1 # Only add these slots if no other acts were output # by the DM if len(sys_acts) == 1: for slot in d_state.slots_filled: if slot in d_state.item_in_focus: if (slot not in ["id", "name"] and slot not in d_state.requested_slots): new_sys_acts.append( DialogueAct( "inform", [ DialogueActItem( slot, Operator.EQ, d_state.item_in_focus[slot]) ], )) else: new_sys_acts.append( DialogueAct( "inform", [DialogueActItem(slot, Operator.EQ, "no info")]))
def consistency_check(self): """ Perform some basic checks to ensure that items in the agenda are consistent - i.e. not duplicate, not contradicting with current goal, etc. :return: Nothing """ # Remove all requests for slots that are filled in the goal if self.goal: for slot in self.goal.actual_requests: if self.goal.actual_requests[slot].value: self.remove( DialogueAct('request', [DialogueActItem(slot, Operator.EQ, '')])) else: print('Warning! Agenda consistency check called without goal. ' 'Did you forget to initialize?')
def build_explicit_confirm(d_state, new_sys_acts, sys_act, sys_acts_copy): slots = [] if sys_act.params: # use the slots addressed by the expl-conf act (slots selected by the policy) slots = [x.slot for x in sys_act.params] if len(slots) == 0: # if no slot was selected by the policy, do randomly select one random_slot = random.choices(list(d_state.slots_filled.keys())) slots.extend(random_slot) def _build_expl_confirm_act_item(slot): value = d_state.slots_filled[slot] if d_state.slots_filled[ slot] else "no info" item = DialogueActItem(slot, Operator.EQ, value) return item new_sys_acts.append( DialogueAct("expl-conf", [_build_expl_confirm_act_item(slot) for slot in slots])) # Remove the empty expl-conf sys_acts_copy.remove(sys_act)
def start_dialogue(self, args=None): """ Reset or initialize internal structures at the beginning of the dialogue. May issue first utterance if this agent has the initiative. :param args: :return: """ self.initialize() self.dialogue_turn = 0 # TODO: Get initial trigger from config if self.INTERACTION_MODE in ['speech', 'text', 'simulation']: self.prev_m_out = ConversationalFrame({'utterance': 'hello'}) else: self.prev_m_out = \ ConversationalFrame([DialogueAct('hello')]) self.continue_dialogue() return self.prev_m_out.content, '', self.agent_goal
def initialize(self, goal, us_has_initiative=False): """ Initialize the Agenda at the beginning of each dialogue :param goal: the new goal for the current dialogue :param us_has_initiative: if the simulator has the initiative at the first turn :return: nothing """ self.goal = goal self.clear() # Generate candidate actions dacts = [] # If there are sub-goals # Iterate from last to first because the acts will be popped in # reverse order. for i in range(len(self.goal.subgoals) - 1, -1, -1): sg = self.goal.subgoals[i] # Acknowledge completion of subgoal dacts.append(DialogueAct('ack_subgoal', [])) for constr in sg.constraints.values(): dacts.append(DialogueAct('inform', [constr])) for req in goal.requests.values(): dacts.append((DialogueAct('request', [req]))) for constr in goal.constraints.values(): dacts.append(DialogueAct('inform', [constr])) # Push actions into the agenda self.push(DialogueAct('bye', [])) for da in dacts: self.push(da, force=True) if us_has_initiative: self.push(DialogueAct('hello', []))
def receive_input_handcrafted(self, system_acts): """ Handle the input according to probabilistic rules :param system_acts: a list with the system's dialogue acts :return: Nothing """ # TODO: Revise these rules wrt other operators (i.e. not only EQ) if self.prev_system_acts and self.prev_system_acts == system_acts: self.curr_patience -= 1 else: self.curr_patience = self.patience self.prev_system_acts = deepcopy(system_acts) for system_act in system_acts: # Update user goal (in ABUS the state is factored into the goal # and the agenda) if system_act.intent == 'bye' or self.dialogue_turn > 15: self.agenda.clear() self.agenda.push(DialogueAct('bye', [])) elif system_act.intent in ['inform', 'offer']: # Check that the venue provided meets the constraints meets_constraints = True for item in system_act.params: if item.slot in self.goal.constraints and \ self.goal.constraints[item.slot].value != \ 'dontcare': # Remove the inform from the agenda, assuming the # value provided is correct. If it is not, the # act will be pushed again and will be on top of the # agenda (this way we avoid adding / removing # twice. dact = \ DialogueAct( 'inform', [DialogueActItem( deepcopy(item.slot), deepcopy( self.goal.constraints[item.slot].op), deepcopy( self.goal.constraints[ item.slot].value))]) # Remove and push to make sure the act is on top - # if it already exists self.agenda.remove(dact) if item.value != \ self.goal.constraints[item.slot].value: meets_constraints = False # For each violated constraint add an inform # TODO: Make this a deny-inform or change # operator to NE self.agenda.push(dact) # If it meets the constraints, update the requests if meets_constraints: for item in system_act.params: if item.slot in self.goal.actual_requests: self.goal.actual_requests[item.slot].value = \ item.value # Mark the value only if the slot has been # requested and is in the requests if item.slot in self.goal.requests: self.goal.requests[item.slot].value = \ item.value # Remove any requests from the agenda that ask # for that slot # TODO: Revise this for all operators self.agenda.remove( DialogueAct('request', [ DialogueActItem(item.slot, Operator.EQ, '') ])) # When the system makes a new offer, replace all requests in # the agenda if system_act.intent == 'offer': for r in self.goal.requests: req = deepcopy(self.goal.requests[r]) req_dact = DialogueAct('request', [req]) # The agenda will replace the old act first self.agenda.push(req_dact) # Push appropriate acts into the agenda elif system_act.intent == 'request': if system_act.params: for item in system_act.params: if item.slot in self.goal.constraints: self.agenda.push( DialogueAct('inform', [ DialogueActItem( deepcopy(item.slot), deepcopy(self.goal.constraints[ item.slot].op), deepcopy(self.goal.constraints[ item.slot].value)) ])) else: self.agenda.push( DialogueAct('inform', [ DialogueActItem(deepcopy(item.slot), Operator.EQ, 'dontcare') ]))
def receive_input_policy(self, system_acts): """ Handle the input according to a policy :param system_acts: a list with the system's dialogue acts :return: Nothing """ if self.prev_system_acts and self.prev_system_acts == system_acts: self.curr_patience -= 1 else: self.curr_patience = self.patience self.prev_system_acts = deepcopy(system_acts) for system_act in system_acts: # 'bye' doesn't seem to appear in the CamRest data if system_act.intent == 'bye' or self.curr_patience == 0 or \ self.dialogue_turn > 15: self.agenda.push(DialogueAct('bye', [])) return sys_act_slot = 'inform' if system_act.intent == 'offer' else \ system_act.intent if system_act.params and system_act.params[0].slot: sys_act_slot += '_' + system_act.params[0].slot # Attempt to recover if sys_act_slot not in self.policy: if sys_act_slot == 'inform_name': sys_act_slot = 'offer_name' if sys_act_slot not in self.policy: if system_act.intent == 'inform' and system_act.params and \ system_act.params[0].slot in self.goal.constraints: user_act_slots = ['inform_' + system_act.params[0].slot] else: print('Warning! ABUS policy does not know what to do for' ' %s' % sys_act_slot) return else: dacts = list(self.policy[sys_act_slot]['dacts'].keys()) probs = [self.policy[sys_act_slot]['dacts'][i] for i in dacts] user_act_slots = random.choices(dacts, weights=probs) for user_act_slot in user_act_slots: intent, slot = user_act_slot.split('_') if slot == 'this' and system_act.params and \ system_act.params[0].slot: slot = system_act.params[0].slot value = '' if intent == 'inform': if slot in self.goal.constraints: value = self.goal.constraints[slot].value else: value = 'dontcare' dact = \ DialogueAct(intent, [DialogueActItem(slot, Operator.EQ, value)]) self.agenda.remove(dact) self.agenda.push(dact)
def next_action(self, state): """ Consult the policy and produce the agent's response :param state: the current dialogue state :return: a list of actions, representing the agent's response """ sys_acts = [] state_enc = self.encode_state(state) if state not in self.policy: # TODO: Reactive policy. Fix this properly. state_enc = '' if state.user_acts: for sa in state.user_acts: state_enc = sa.intent # This is due to the DM rules and to be fair to the other # policies if sa.intent == 'offer': state_enc += '_name' elif sa.params: state_enc += '_' + sa.params[0].slot state_enc += ';' state_enc = state_enc[:-1] if state_enc in self.policy: sys_actions = list(self.policy[state_enc]['dacts'].keys()) probs = [self.policy[state_enc]['dacts'][i] for i in sys_actions] sys_act_slots = \ deepcopy(random.choices( sys_actions, weights=probs)[0]).split(';') for sys_act_slot in sys_act_slots: if not sys_act_slot: # Skip empty sys_act_slot strings (e.g. case where # there is ; at the end: inform_food;inform_area;) continue sys_act = DialogueAct('UNK') sys_act_slot_parts = sys_act_slot.split('_') sys_act.intent = sys_act_slot_parts[0] if len(sys_act_slot_parts) > 1: sys_act.params = \ [DialogueActItem( sys_act_slot_parts[1], Operator.EQ, '')] if sys_act.intent == 'offer': sys_act.params = [] elif sys_act.intent == 'canthelp.exception': sys_act.intent = 'canthelp' sys_acts.append(sys_act) else: print(f'Warning! {self.agent_role} Calculated DialoguePolicy: ' f'state not found, selecting random action.') sys_act = DialogueAct('UNK') if self.agent_role == 'system': sys_act.intent = \ random.choice(['welcomemsg', 'inform', 'request']) elif self.agent_role == 'user': sys_act.intent = random.choice(['hello', 'inform', 'request']) else: sys_act.intent = random.choice(['bye', 'inform', 'request']) sys_acts.append(sys_act) return sys_acts
def next_action(self, dialogue_state): """ Generate a response given which conditions are met by the current dialogue state. :param dialogue_state: :return: """ # Check for terminal state if dialogue_state.is_terminal_state: return [DialogueAct('bye', [DialogueActItem('', Operator.EQ, '')])] # Check if the user has made any requests elif dialogue_state.requested_slot: if dialogue_state.item_in_focus and \ dialogue_state.system_made_offer: requested_slot = dialogue_state.requested_slot # Reset request as we attempt to address it dialogue_state.requested_slot = '' value = 'not available' if requested_slot in dialogue_state.item_in_focus and \ dialogue_state.item_in_focus[requested_slot]: value = dialogue_state.item_in_focus[requested_slot] return \ [DialogueAct( 'inform', [DialogueActItem(requested_slot, Operator.EQ, value)])] # Else, if no item is in focus or no offer has been made, # ignore the user's request # Try to fill slots requestable_slots = \ deepcopy(self.ontology.ontology['system_requestable']) if not hasattr(dialogue_state, 'requestable_slot_entropies') or \ not dialogue_state.requestable_slot_entropies: slot = random.choice(requestable_slots) while dialogue_state.slots_filled[slot] and \ len(requestable_slots) > 1: requestable_slots.remove(slot) slot = random.choice(requestable_slots) else: slot = '' slots = \ [k for k, v in dialogue_state.requestable_slot_entropies.items() if v == max( dialogue_state.requestable_slot_entropies.values()) and v > 0 and k in requestable_slots] if slots: slot = random.choice(slots) while dialogue_state.slots_filled[slot] \ and dialogue_state.requestable_slot_entropies[ slot] > 0 \ and len(requestable_slots) > 1: requestable_slots.remove(slot) slots = \ [k for k, v in dialogue_state.requestable_slot_entropies.items() if v == max( dialogue_state.requestable_slot_entropies.values()) and k in requestable_slots] if slots: slot = random.choice(slots) else: break if slot and not dialogue_state.slots_filled[slot]: return [DialogueAct( 'request', [DialogueActItem(slot, Operator.EQ, '')])] elif dialogue_state.item_in_focus: name = dialogue_state.item_in_focus['name'] \ if 'name' in dialogue_state.item_in_focus \ else 'unknown' dacts = [DialogueAct( 'offer', [DialogueActItem('name', Operator.EQ, name)])] for slot in dialogue_state.slots_filled: if slot != 'requested' and dialogue_state.slots_filled[slot]: if slot in dialogue_state.item_in_focus: if slot not in ['id', 'name']: dacts.append( DialogueAct( 'inform', [DialogueActItem( slot, Operator.EQ, dialogue_state.item_in_focus[slot])])) else: dacts.append(DialogueAct( 'inform', [DialogueActItem( slot, Operator.EQ, 'no info')])) return dacts else: # Fallback action - cannot help! # Note: We can have this check (no item in focus) at the beginning, # but this would assume that the system # queried a database before coming in here. return [DialogueAct('canthelp', [])]
def next_action(self, dialogue_state): """ Generate a response given which conditions are met by the current dialogue state. :param dialogue_state: :return: """ # Check for terminal state if dialogue_state.is_terminal_state: return [DialogueAct('bye', [DialogueActItem('', Operator.EQ, '')])] # Check if the user denies and the system has sent an expl-conf in the previous turn elif dialogue_state.user_denied_last_sys_acts and \ dialogue_state.last_sys_acts and \ 'expl-conf' in [x.intent for x in dialogue_state.last_sys_acts]: # If the user denies an explicit confirmation, request the slots to be confirmed again act: DialogueAct request_act = DialogueAct('request', []) for act in dialogue_state.last_sys_acts: # search the explicit confirmation act in the system acts from the previous turn item: DialogueActItem if act.intent == 'expl-conf' and act.params: for item in act.params: new_item = DialogueActItem(slot=item.slot, op=Operator.EQ, value=None) request_act.params.append(new_item) return [request_act] # Check if the user has made any requests elif len(dialogue_state.requested_slots) > 0: if dialogue_state.item_in_focus and \ dialogue_state.system_made_offer: requested_slots = dialogue_state.requested_slots # Reset request as we attempt to address it dialogue_state.requested_slots = [] items = [] for rs in requested_slots: value = 'not available' if rs in dialogue_state.item_in_focus and \ dialogue_state.item_in_focus[rs]: value = dialogue_state.item_in_focus[rs] items.append(DialogueActItem(rs, Operator.EQ, value)) return \ [DialogueAct( 'inform', items)] # Else, if no item is in focus or no offer has been made, # ignore the user's request # Try to fill slots requestable_slots = \ deepcopy(self.ontology.ontology['system_requestable']) if not hasattr(dialogue_state, 'requestable_slot_entropies') or \ not dialogue_state.requestable_slot_entropies: slot = random.choice(requestable_slots) while dialogue_state.slots_filled[slot] and \ len(requestable_slots) > 1: requestable_slots.remove(slot) slot = random.choice(requestable_slots) else: slot = '' slots = \ [k for k, v in dialogue_state.requestable_slot_entropies.items() if v == max( dialogue_state.requestable_slot_entropies.values()) and v > 0 and k in requestable_slots] if slots: slot = random.choice(slots) while dialogue_state.slots_filled[slot] \ and dialogue_state.requestable_slot_entropies[ slot] > 0 \ and len(requestable_slots) > 1: requestable_slots.remove(slot) slots = \ [k for k, v in dialogue_state.requestable_slot_entropies.items() if v == max( dialogue_state.requestable_slot_entropies.values()) and k in requestable_slots] if slots: slot = random.choice(slots) else: break if slot and not dialogue_state.slots_filled[slot]: return [ DialogueAct('request', [DialogueActItem(slot, Operator.EQ, '')]) ] elif dialogue_state.item_in_focus: name = dialogue_state.item_in_focus['name'] \ if 'name' in dialogue_state.item_in_focus \ else 'unknown' dacts = [ DialogueAct('offer', [DialogueActItem('name', Operator.EQ, name)]) ] for slot in dialogue_state.slots_filled: if slot != 'requested' and dialogue_state.slots_filled[slot]: if slot in dialogue_state.item_in_focus: if slot not in ['id', 'name']: dacts.append( DialogueAct('inform', [ DialogueActItem( slot, Operator.EQ, dialogue_state.item_in_focus[slot]) ])) else: dacts.append( DialogueAct('inform', [ DialogueActItem(slot, Operator.EQ, 'no info') ])) return dacts # if not all filled slot confirmed, try confirmation elif not all([v for k, v in dialogue_state.slots_confirmed.items()]): # get unconfirmed slots unconfirmed_slots = [ k for k, v in dialogue_state.slots_confirmed.items() if not v ] # match match unconfirmed slots with filled slots, as we ask only for confirmation of already filled slots confirmation_candidates = [] for us in unconfirmed_slots: if dialogue_state.slots_filled[us]: confirmation_candidates.append(us) first_unconfirmed_slot = confirmation_candidates[0] return [ DialogueAct('expl-conf', [ DialogueActItem( first_unconfirmed_slot, Operator.EQ, dialogue_state.slots_filled[first_unconfirmed_slot]) ]) ] else: # Fallback action - cannot help! # Note: We can have this check (no item in focus) at the beginning, # but this would assume that the system # queried a database before coming in here. return [DialogueAct('canthelp', [])]
def process_input(self, utterance, dialogue_state=None): """ Process the utterance and see if any intent pattern matches. :param utterance: a string, the utterance to be recognised :param dialogue_state: the current dialogue state, if available :return: a list of recognised dialogue acts """ dacts = [] dact = DialogueAct('UNK', []) # TODO: Remove this once NLG is updated utterance = utterance.replace('<PAD>', '') if not utterance: return [dact] last_sys_act = \ dialogue_state.last_sys_acts[0] \ if dialogue_state and dialogue_state.last_sys_acts else None utterance = utterance.rstrip().lower() utterance = utterance.translate(self.punctuation_remover) # Replace synonyms utterance = utterance.replace('location', 'area') utterance = utterance.replace('part of town', 'area') utterance = utterance.replace('center', 'centre') utterance = utterance.replace('cheaply', 'cheap') utterance = utterance.replace('moderately', 'moderate') utterance = utterance.replace('expensively', 'expensive') utterance = utterance.replace('address', 'addr') utterance = utterance.replace('telephone', 'phone') utterance = utterance.replace('postal code', 'postcode') utterance = utterance.replace('post code', 'postcode') utterance = utterance.replace('zip code', 'postcode') utterance = utterance.replace('price range', 'pricerange') # First check if the user doesn't care if last_sys_act and last_sys_act.intent in ['request', 'expl-conf']: for p in self.dontcare_pattern: # Look for exact matches here only (i.e. user just says # 'i don't care') if p == utterance: dact.intent = 'inform' dact.params.append( DialogueActItem(last_sys_act.params[0].slot, Operator.EQ, 'dontcare')) return [dact] # Look for slot keyword and corresponding value words = utterance.split(' ') for p in self.ack_pattern: if p == utterance: dact.intent = 'ack' break for p in self.deny_pattern: if p == utterance: dact.intent = 'deny' break for p in self.affirm_pattern: if p == utterance: dact.intent = 'affirm' break # Check for dialogue ending for p in self.bye_pattern: match = re.search(r'\b{0}\b'.format(p), utterance) if match: dact.intent = 'bye' break # Search for 'welcome' first because it may contain 'hello' if dact.intent == 'UNK': for p in self.welcome_pattern: match = re.search(r'\b{0}\b'.format(p), utterance) if match: dact.intent = 'welcomemsg' break if dact.intent == 'UNK': for p in self.hi_pattern: match = re.search(r'\b{0}\b'.format(p), utterance) if match: dact.intent = 'hello' break if dact.intent == 'UNK': for p in self.reqalts_pattern: match = re.search(r'\b{0}\b'.format(p), utterance) if match: dact.intent = 'reqalts' break if dact.intent == 'UNK': for p in self.reqmore_pattern: match = re.search(r'\b{0}\b'.format(p), utterance) if match: dact.intent = 'reqmore' break if dact.intent == 'UNK': for p in self.repeat_pattern: match = re.search(r'\b{0}\b'.format(p), utterance) if match: dact.intent = 'repeat' break if dact.intent == 'UNK': for p in self.restart_pattern: match = re.search(r'\b{0}\b'.format(p), utterance) if match: dact.intent = 'restart' break if dact.intent == 'UNK': for p in self.thankyou_pattern: match = re.search(r'\b{0}\b'.format(p), utterance) if match: dact.intent = 'thankyou' break if dact.intent == 'UNK': for p in self.request_pattern: match = re.search(r'\b{0}\b'.format(p), utterance) if match: dact.intent = 'request' break if dact.intent == 'UNK': for p in self.select_pattern: match = re.search(r'\b{0}\b'.format(p), utterance) if match: dact.intent = 'select' break if dact.intent == 'UNK': for p in self.confirm_pattern: match = re.search(r'\b{0}\b'.format(p), utterance) if match: dact.intent = 'confirm' break if dact.intent == 'UNK': for p in self.expl_conf_pattern: match = re.search(r'\b{0}\b'.format(p), utterance) if match: dact.intent = 'expl-conf' break if dact.intent == 'UNK': for p in self.cant_help_pattern: match = re.search(r'\b{0}\b'.format(p), utterance) if match: dact.intent = 'canthelp' dact.params = [] return [dact] if dact.intent == 'UNK': dact.intent = 'inform' # Check if there is no information about the slot if 'no info' in utterance: # Search for a slot name in the utterance for slot in self.ontology.ontology['requestable']: if slot in utterance: dact.params.append( DialogueActItem(slot, Operator.EQ, 'no info')) return [dact] # Else try to grab slot name from the other agent's # previous act if last_sys_act and \ last_sys_act.intent in ['request', 'expl-conf']: dact.params.append( DialogueActItem(last_sys_act.params[0].slot, Operator.EQ, 'dontcare')) return [dact] # Else do nothing, and see if anything matches below if dact.intent in ['inform', 'request']: for word in words: # Check for requests. Requests for informable slots are # captured below if word in self.requestable_only_slots: if dact.intent == 'request': dact.params.append( DialogueActItem(word, Operator.EQ, '')) break elif word != 'name': if 'is' not in utterance and 'its' not in utterance: dact.intent = 'request' dact.params.append( DialogueActItem(word, Operator.EQ, '')) break # For any other kind of intent, we have no way of # determining the slot's value, since such # information is not in the ontology. # Check for informs (most intensive) if word in self.ontology.ontology['informable']: # If a request intent has already been recognized, # do not search for slot values if dact.intent == 'request': dact.params.append( DialogueActItem(word, Operator.EQ, '')) break found = False for p in self.ontology.ontology['informable'][word]: match = re.search(r'\b{0}\b'.format(p), utterance) if match: if word == 'name': dact.intent = 'offer' else: dact.intent = 'inform' dact.params.append( DialogueActItem(word, Operator.EQ, p)) found = True break if not found: # Search for dontcare (e.g. I want any area) for p in self.dontcare_pattern: match = re.search(r'\b{0}\b'.format(p), utterance) if match: dact.intent = 'inform' dact.params.append( DialogueActItem(word, Operator.EQ, 'dontcare')) return [dact] dact.intent = 'request' dact.params.append( DialogueActItem(word, Operator.EQ, '')) # If nothing was recognised, do an even more brute-force search if dact.intent in ['UNK', 'inform'] and not dact.params: slot_vals = self.ontology.ontology['informable'] if self.slot_values: slot_vals = self.slot_values for slot in slot_vals: for value in slot_vals[slot]: if value and \ value.lower().translate(self.punctuation_remover) \ in utterance: if slot == 'name': dact.intent = 'offer' di = DialogueActItem(slot, Operator.EQ, value) if di not in dact.params: dact.params.append(di) # Check if something has been missed (e.g. utterance is dont care and # there's no previous sys act) if dact.intent == 'inform': # Check to see that all slots have an identified value if dact.params: for dact_item in dact.params: if not dact_item.slot or not dact_item.value: dact.params.remove(dact_item) if not dact.params: dact.intent = 'UNK' break # Else, break up the inform into several acts elif dact_item.slot == 'name': dacts.append(DialogueAct('offer', [dact_item])) else: dacts.append(DialogueAct('inform', [dact_item])) else: # Try to see if the utterance refers to a slot in the # requestable ones (e.g. 'I prefer any phone') for slot in self.ontology.ontology['requestable']: if slot in utterance: # We can only handle 'dontcare' kind of values here, # as we do not know values of req. slots. for p in self.dontcare_pattern: match = re.search(r'\b{0}\b'.format(p), utterance) if match: dact.params = \ [DialogueActItem( slot, Operator.EQ, 'dontcare')] dact.intent = 'UNK' else: dacts.append(dact) return dacts
def continue_dialogue(self, args): """ Perform next dialogue turn. :return: this agent's output """ if 'other_input_raw' not in args: raise ValueError( 'ConversationalMultiAgent called without raw input!') other_input_raw = args['other_input_raw'] other_input_dact = None if 'other_input_dact' in args: other_input_dact = args['other_input_dact'] goal = None if 'goal' in args: goal = args['goal'] if goal: self.agent_goal = goal sys_utterance = '' other_input_nlu = deepcopy(other_input_raw) if self.nlu and isinstance(other_input_raw, str): # Process the other agent's utterance other_input_nlu = self.nlu.process_input( other_input_raw, self.dialogue_manager.get_state()) elif other_input_dact: # If no utterance provided, use the dacts other_input_nlu = other_input_dact # print( # '{0} Recognised Input: {1}'.format( # self.agent_role.upper(), # '; '.join([str(ui) for ui in other_input_nlu]) # ) # ) self.dialogue_manager.receive_input(other_input_nlu) # Keep track of prev_state, for the DialogueEpisodeRecorder # Store here because this is the state that the dialogue # manager will use to make a decision. self.curr_state = deepcopy(self.dialogue_manager.get_state()) # Update goal's ground truth if self.agent_role == 'system': self.agent_goal.ground_truth = deepcopy( self.curr_state.item_in_focus) if self.dialogue_turn < self.MAX_TURNS: response = self.dialogue_manager.generate_output() self.agent_goal = self.dialogue_manager.DSTracker.DState.user_goal else: # Force dialogue stop # print('{0}: terminating dialogue due to too many turns'. # format(self.agent_role)) response = [DialogueAct('bye', [])] rew, success, task_success = self.reward_func.calculate( self.dialogue_manager.get_state(), response, goal=self.agent_goal, agent_role=self.agent_role) if self.USE_NLG: sys_utterance = self.nlg.generate_output( { 'dacts': response, 'system': self.agent_role == 'system', 'last_sys_utterance': other_input_raw }) + ' ' print('{0} > {1}'.format(self.agent_role.upper(), sys_utterance)) if self.USE_SPEECH: tts = gTTS(text=sys_utterance, lang='en') tts.save('sys_output.mp3') os.system('afplay sys_output.mp3') else: print('{0} > {1} \n'.format( self.agent_role.upper(), '; '.join([str(sr) for sr in response]))) if self.prev_state: self.recorder.record(self.prev_state, self.curr_state, self.prev_action, self.prev_reward, self.prev_success, input_utterance=other_input_raw, output_utterance=sys_utterance, task_success=self.prev_task_success) self.dialogue_turn += 1 self.prev_state = deepcopy(self.curr_state) self.prev_usr_utterance = deepcopy(other_input_raw) self.prev_sys_utterance = deepcopy(sys_utterance) self.prev_action = deepcopy(response) self.prev_reward = rew self.prev_success = success self.prev_task_success = task_success return sys_utterance, response, self.agent_goal
def end_dialogue(self): """ Perform final dialogue turn. Train and ave models if applicable. :return: """ if self.dialogue_episode % \ self.train_switch_trainable_agents_every == 0: self.train_system = not self.train_system # Record final state if not self.curr_state.is_terminal_state: self.curr_state.is_terminal_state = True self.prev_reward, self.prev_success, self.prev_task_success = \ self.reward_func.calculate( self.curr_state, [DialogueAct('bye', [])], goal=self.agent_goal, agent_role=self.agent_role ) self.recorder.record(self.curr_state, self.curr_state, self.prev_action, self.prev_reward, self.prev_success, input_utterance=self.prev_usr_utterance, output_utterance=self.prev_sys_utterance, task_success=self.prev_task_success, force_terminate=True) if self.dialogue_manager.is_training(): if not self.train_alternate_training or \ (self.train_system and self.agent_role == 'system' or not self.train_system and self.agent_role == 'user'): if (self.dialogue_episode+1) % self.train_interval == 0 and \ len(self.recorder.dialogues) >= self.minibatch_length: for epoch in range(self.train_epochs): print('{0}: Training epoch {1} of {2}'.format( self.agent_role, (epoch + 1), self.train_epochs)) # Sample minibatch minibatch = random.sample(self.recorder.dialogues, self.minibatch_length) self.dialogue_manager.train(minibatch) self.dialogue_episode += 1 self.cumulative_rewards += \ self.recorder.dialogues[-1][-1]['cumulative_reward'] if self.dialogue_turn > 0: self.total_dialogue_turns += self.dialogue_turn if self.dialogue_episode % 10000 == 0: self.dialogue_manager.save() # Count successful dialogues if self.recorder.dialogues[-1][-1]['success']: print('{0} SUCCESS! (reward: {1})'.format( self.agent_role, sum([t['reward'] for t in self.recorder.dialogues[-1]]))) self.num_successful_dialogues += \ int(self.recorder.dialogues[-1][-1]['success']) else: print('{0} FAILURE. (reward: {1})'.format( self.agent_role, sum([t['reward'] for t in self.recorder.dialogues[-1]]))) if self.recorder.dialogues[-1][-1]['task_success']: self.num_task_success += int( self.recorder.dialogues[-1][-1]['task_success'])
def start_dialogue(self, goal=None): """ Perform initial dialogue turn. :param goal: an optional Goal :return: """ self.dialogue_turn = 0 if self.agent_role == 'user': self.agent_goal = self.goal_generator.generate() self.dialogue_manager.update_goal(self.agent_goal) print('DEBUG > Usr goal:') for c in self.agent_goal.constraints: print(f'\t\tConstr({self.agent_goal.constraints[c].slot}=' f'{self.agent_goal.constraints[c].value})') print('\t\t-------------') for r in self.agent_goal.requests: print(f'\t\tReq({self.agent_goal.requests[r].slot})') print('\n') elif goal: # No deep copy here so that all agents see the same goal. self.agent_goal = goal else: raise ValueError('ConversationalMultiAgent - no goal provided ' 'for agent {0}!'.format(self.agent_role)) self.dialogue_manager.restart({'goal': self.agent_goal}) response = [DialogueAct('welcomemsg', [])] response_utterance = '' # The above forces the DM's initial utterance to be the welcomemsg act. # The code below can be used to push an empty list of DialogueActs to # the DM and get a response from the model. # self.dialogue_manager.receive_input([]) # response = self.dialogue_manager.respond() if self.agent_role == 'system': response = [DialogueAct('welcomemsg', [])] if self.USE_NLG: response_utterance = self.nlg.generate_output({ 'dacts': response, 'system': self.agent_role == 'system', 'last_sys_utterance': '' }) print('{0} > {1}'.format(self.agent_role.upper(), response_utterance)) if self.USE_SPEECH: tts = gTTS(text=response_utterance, lang='en') tts.save('sys_output.mp3') os.system('afplay sys_output.mp3') else: print('{0} > {1}'.format( self.agent_role.upper(), '; '.join([str(sr) for sr in response]))) # TODO: Generate output depending on initiative - i.e. # have users also start the dialogue self.prev_state = None # Re-initialize these for good measure self.curr_state = None self.prev_usr_utterance = None self.prev_sys_utterance = None self.prev_action = None self.prev_reward = None self.prev_success = None return response_utterance, response, self.agent_goal
def continue_dialogue(self): """ Perform next dialogue turn. :return: nothing """ usr_utterance = '' sys_utterance = '' if self.USE_USR_SIMULATOR: usr_input = self.user_simulator.respond() # TODO: THIS FIRST IF WILL BE HANDLED BY ConversationalAgentGeneric # -- SHOULD NOT LIVE HERE if isinstance(self.user_simulator, DTLUserSimulator): print('USER (NLG) > %s \n' % usr_input) usr_input = self.nlu.process_input( usr_input, self.dialogue_manager.get_state()) elif self.USER_SIMULATOR_NLG: print('USER > %s \n' % usr_input) if self.nlu: usr_input = self.nlu.process_input(usr_input) # Otherwise it will just print the user's NLG but use the # simulator's output DActs to proceed. else: print('USER (DACT) > %s \n' % usr_input[0]) else: if self.USE_SPEECH: # Listen for input from the microphone with speech_rec.Microphone() as source: print('(listening...)') audio = self.asr.listen(source, phrase_time_limit=3) try: # This uses the default key usr_utterance = self.asr.recognize_google(audio) print("Google ASR: " + usr_utterance) except speech_rec.UnknownValueError: print("Google ASR did not understand you") except speech_rec.RequestError as e: print("Google ASR request error: {0}".format(e)) else: usr_utterance = input('USER > ') # Process the user's utterance if self.nlu: usr_input = self.nlu.process_input( usr_utterance, self.dialogue_manager.get_state()) else: raise EnvironmentError( 'ConversationalAgent: No NLU defined for ' 'text-based interaction!') # DEBUG print # print( # '\nSYSTEM NLU > %s ' % '; '.join([str(ui) for ui in usr_input]) # ) self.dialogue_manager.receive_input(usr_input) # Keep track of prev_state, for the DialogueEpisodeRecorder # Store here because this is the state that the dialogue manager # will use to make a decision. self.curr_state = deepcopy(self.dialogue_manager.get_state()) # print('\nDEBUG> '+str(self.dialogue_manager.get_state()) + '\n') if self.dialogue_turn < self.MAX_TURNS: sys_response = self.dialogue_manager.generate_output() else: # Force dialogue stop # print( # '{0}: terminating dialogue due to too ' # 'many turns'.format(self.agent_role) # ) sys_response = [DialogueAct('bye', [])] if self.USE_NLG: sys_utterance = self.nlg.generate_output({'dacts': sys_response}) print('SYSTEM > %s ' % sys_utterance) if self.USE_SPEECH: try: tts = gTTS(text=sys_utterance, lang='en') tts.save('sys_output.mp3') os.system('afplay sys_output.mp3') except: print('WARNING: gTTS encountered an error. ' 'Falling back to Sys TTS.') os.system('say ' + sys_utterance) else: print('SYSTEM > %s ' % '; '.join([str(sr) for sr in sys_response])) if self.USE_USR_SIMULATOR: usim_input = sys_response if self.USER_SIMULATOR_NLU and self.USE_NLG: usim_input = \ self.user_simulator.nlu.process_input(sys_utterance) print('USER NLU ' '> %s ' % '; '.join([str(ui) for ui in usim_input])) self.user_simulator.receive_input(usim_input) rew, success, task_success = \ self.reward_func.calculate( self.dialogue_manager.get_state(), sys_response, self.user_simulator.goal ) else: rew, success, task_success = 0, None, None if self.prev_state: self.recorder.record(self.prev_state, self.curr_state, self.prev_action, self.prev_reward, self.prev_success, input_utterance=usr_utterance, output_utterance=sys_utterance) self.dialogue_turn += 1 self.prev_state = deepcopy(self.curr_state) self.prev_action = deepcopy(sys_response) self.prev_usr_utterance = deepcopy(usr_utterance) self.prev_sys_utterance = deepcopy(sys_utterance) self.prev_reward = rew self.prev_success = success self.prev_task_success = task_success
def start_dialogue(self, args=None): """ Perform initial dialogue turn. :param args: optional args :return: """ self.dialogue_turn = 0 sys_utterance = '' if self.USE_USR_SIMULATOR: self.user_simulator.initialize(self.user_simulator_args) print('DEBUG > Usr goal:') print(self.user_simulator.goal) if self.agent_role == 'user': self.agent_goal = self.goal_generator.generate() self.dialogue_manager.restart({'goal': self.agent_goal}) else: self.dialogue_manager.restart({}) if not self.USER_HAS_INITIATIVE: # sys_response = self.dialogue_manager.respond() sys_response = [DialogueAct('welcomemsg', [])] if self.USE_NLG: sys_utterance = self.nlg.generate_output( {'dacts': sys_response}) print('SYSTEM > %s ' % sys_utterance) if self.USE_SPEECH: try: tts = gTTS(sys_utterance) tts.save('sys_output.mp3') os.system('afplay sys_output.mp3') except Exception as e: print('WARNING: gTTS encountered an error: {0}. ' 'Falling back to Sys TTS.'.format(e)) os.system('say ' + sys_utterance) else: print('SYSTEM > %s ' % '; '.join([str(sr) for sr in sys_response])) if self.USE_USR_SIMULATOR: usim_input = sys_response if self.USER_SIMULATOR_NLU and self.USE_NLG: usim_input = self.user_simulator.nlu.process_input( sys_utterance) self.user_simulator.receive_input(usim_input) rew, success, task_success = self.reward_func.calculate( self.dialogue_manager.get_state(), sys_response, self.user_simulator.goal) else: rew, success, task_success = 0, None, None self.recorder.record(deepcopy(self.dialogue_manager.get_state()), self.dialogue_manager.get_state(), sys_response, rew, success, task_success, output_utterance=sys_utterance) self.dialogue_turn += 1 self.prev_state = None # Re-initialize these for good measure self.curr_state = None self.prev_usr_utterance = None self.prev_sys_utterance = None self.prev_action = None self.prev_reward = None self.prev_success = None self.prev_task_success = None self.continue_dialogue()
def decode_action(self, action_enc, system=True): """ Decode the action, given the role. Note that does not have to match the agent's role, as the agent may be decoding another agent's action (e.g. a system decoding the previous user act). :param action_enc: action encoding to be decoded :param system: whether the role whose action we are decoding is a 'system' :return: the decoded action """ if system: if action_enc < len(self.dstc2_acts_sys): return [DialogueAct(self.dstc2_acts_sys[action_enc], [])] if action_enc < len(self.dstc2_acts_sys) + \ len(self.system_requestable_slots): return [ DialogueAct('request', [ DialogueActItem( self.system_requestable_slots[ action_enc - len(self.dstc2_acts_sys)], Operator.EQ, '') ]) ] if action_enc < len(self.dstc2_acts_sys) + \ len(self.system_requestable_slots) + \ len(self.requestable_slots): index = \ action_enc - len(self.dstc2_acts_sys) - \ len(self.system_requestable_slots) return [ DialogueAct('inform', [ DialogueActItem(self.requestable_slots[index], Operator.EQ, '') ]) ] else: if action_enc < len(self.dstc2_acts_usr): return [DialogueAct(self.dstc2_acts_usr[action_enc], [])] if action_enc < len(self.dstc2_acts_usr) + \ len(self.requestable_slots): return [ DialogueAct('request', [ DialogueActItem( self.requestable_slots[action_enc - len(self.dstc2_acts_usr)], Operator.EQ, '') ]) ] if action_enc < len(self.dstc2_acts_usr) + \ 2 * len(self.requestable_slots): return [ DialogueAct('inform', [ DialogueActItem( self.requestable_slots[ action_enc - len(self.dstc2_acts_usr) - len(self.requestable_slots)], Operator.EQ, '') ]) ]
def process_input(self, utterance, dialogue_state=None): """ Query the Ludwig model with the given utterance to obtain predictions for IOB tags and intent. Then parse the results and package them into a list of Dialogue Acts. :param utterance: a string, the utterance to be recognised :param dialogue_state: the current dialogue state, if available :return: a list of dialogue acts containing the recognised intents """ # Pre-process utterance utterance = utterance.rstrip().lower() utterance = utterance.translate(self.punctuation_remover) # Warning: Make sure the same tokenizer that was used to train the # model is used during prediction result = \ self.model.predict( pd.DataFrame( data={'transcript': [utterance]}), return_type=dict) dacts = [] # Only keep the first act for now last_sys_act = dialogue_state.last_sys_acts[0] \ if dialogue_state and dialogue_state.last_sys_acts else None utterance_parts = utterance.split(' ') iob_tags = [tag for tag in result['iob']['predictions'][0]] for intent in result['intent']['predictions'][0]: intent_parts = intent.split('_') intent = intent_parts[0] if intent == 'request' and len(intent_parts) > 1: dacts.append( DialogueAct( intent, [DialogueActItem(intent_parts[1], Operator.EQ, '')])) elif intent == 'dontcare' and len(intent_parts) > 1: if intent_parts[1] == 'this': if dialogue_state and last_sys_act and last_sys_act.params: dacts.append( DialogueAct('inform', [ DialogueActItem(last_sys_act.params[0].slot, Operator.EQ, 'dontcare') ])) else: dacts.append( DialogueAct('inform', [ DialogueActItem(intent_parts[1], Operator.EQ, 'dontcare') ])) # Exclude acts that take slot-value arguments # (will be handled below) elif intent not in ['inform', 'confirm', 'offer']: dacts.append(DialogueAct(intent, [])) # If no act was recognised if not dacts: # Search for tags intent = '' slot = '' value = '' # iob_tags is a fixed length but the utterance may be shorter. for t in range(len(utterance_parts)): # If however we encounter input longer than the IOB tags, # we can't handle it. if t >= len(iob_tags): print('Warning! CamRestNLU cannot handle such a long ' 'sequence. Returning partial result.') break if iob_tags[t][0] == 'B': # Case where we have B-slot1 I-slot1 I-slot1 B-slot2 ... if value: # Correct offer / inform mis-prediction if slot == 'name': dacts.append( DialogueAct('offer', [ DialogueActItem(slot, Operator.EQ, value) ])) else: dacts.append( DialogueAct('inform', [ DialogueActItem(slot, Operator.EQ, value) ])) else: tag_parts = iob_tags[t].split('-') intent = tag_parts[1] slot = tag_parts[2] value = utterance_parts[t] elif iob_tags[t][0] == 'I': # Case where NLU doesn't work perfectly well and the first # tag is I-... instead of B-... if not value or not slot or not intent: tag_parts = iob_tags[t].split('-') intent = tag_parts[1] slot = tag_parts[2] value = utterance_parts[t] value += ' ' + utterance_parts[t] elif iob_tags[t] == 'O' and value: if slot == 'name': dacts.append( DialogueAct( 'offer', [DialogueActItem(slot, Operator.EQ, value)])) else: dacts.append( DialogueAct( 'inform', [DialogueActItem(slot, Operator.EQ, value)])) # Reset intent, slot, and value intent = '' slot = '' value = '' # Special case for when a tag is at the end of the utterance if value and intent: # Save the recognised slot value dacts.append( DialogueAct(intent, [DialogueActItem(slot, Operator.EQ, value)])) # If still no act is recognised if not dacts: print('WARNING! CamRestNLU did not understand slots or values for ' 'utterance: {0}\n'.format(utterance)) # Print recognized IOB tags # print('IOB: {0}\n'.format(iob_tags)) return dacts
def decode_action(self, action_enc, system=True): """ Decode the action, given the role. Note that does not have to match the agent's role, as the agent may be decoding another agent's action (e.g. a system decoding the previous user act). :param action_enc: action encoding to be decoded :param system: whether the role whose action we are decoding is a 'system' :return: the decoded action """ if system: if action_enc < len(self.dstc2_acts_sys): return [DialogueAct(self.dstc2_acts_sys[action_enc], [])] if action_enc < len(self.dstc2_acts_sys) + \ len(self.system_requestable_slots): return [ DialogueAct('request', [ DialogueActItem( self.system_requestable_slots[ action_enc - len(self.dstc2_acts_sys)], Operator.EQ, '') ]) ] if action_enc < len(self.dstc2_acts_sys) + \ len(self.system_requestable_slots) + \ len(self.requestable_slots): index = \ action_enc - len(self.dstc2_acts_sys) - \ len(self.system_requestable_slots) return [ DialogueAct('inform', [ DialogueActItem(self.requestable_slots[index], Operator.EQ, '') ]) ] else: if action_enc < len(self.dstc2_acts_usr): return [DialogueAct(self.dstc2_acts_usr[action_enc], [])] if action_enc < len(self.dstc2_acts_usr) + \ len(self.requestable_slots): return [ DialogueAct('request', [ DialogueActItem( self.requestable_slots[action_enc - len(self.dstc2_acts_usr)], Operator.EQ, '') ]) ] if action_enc < len(self.dstc2_acts_usr) + \ len(self.requestable_slots) + \ len(self.system_requestable_slots): return [ DialogueAct('inform', [ DialogueActItem( self.system_requestable_slots[ action_enc - len(self.dstc2_acts_usr) - len(self.requestable_slots)], Operator.EQ, '') ]) ] # Default fall-back action print('MinimaxQ DialoguePolicy ({0}) policy action decoder warning: ' 'Selecting repeat() action ' '(index: {1})!'.format(self.agent_role, action_enc)) return [DialogueAct('repeat', [])]
def generate_output(self, args=None): """ Consult the current policy to generate a response. :return: List of DialogueAct representing the system's output. """ d_state = self.DSTracker.get_state() sys_acts = self.policy.next_action(d_state) # Copy the sys_acts to be able to iterate over all sys_acts while also # replacing some acts sys_acts_copy = deepcopy(sys_acts) new_sys_acts = [] # Safeguards to support policies that make decisions on intents only # (i.e. do not output slots or values) for sys_act in sys_acts: if sys_act.intent == 'canthelp' and not sys_act.params: slots = \ [ s for s in d_state.slots_filled if d_state.slots_filled[s] ] if slots: slot = random.choice(slots) # Remove the empty canthelp sys_acts_copy.remove(sys_act) new_sys_acts.append( DialogueAct('canthelp', [ DialogueActItem(slot, Operator.EQ, d_state.slots_filled[slot]) ])) else: print('DialogueManager Warning! No slot provided by ' 'policy for canthelp and cannot find a reasonable ' 'one!') if sys_act.intent == 'offer' and not sys_act.params: # Remove the empty offer sys_acts_copy.remove(sys_act) if d_state.item_in_focus: new_sys_acts.append( DialogueAct('offer', [ DialogueActItem('name', Operator.EQ, d_state.item_in_focus['name']) ])) # Only add these slots if no other acts were output # by the DM if len(sys_acts) == 1: for slot in d_state.slots_filled: if slot in d_state.item_in_focus: if slot not in ['id', 'name'] and \ slot != d_state.requested_slot: new_sys_acts.append( DialogueAct('inform', [ DialogueActItem( slot, Operator.EQ, d_state.item_in_focus[slot]) ])) else: new_sys_acts.append( DialogueAct('inform', [ DialogueActItem( slot, Operator.EQ, 'no info') ])) elif sys_act.intent == 'inform': if self.agent_role == 'system': if sys_act.params and sys_act.params[0].value: continue if sys_act.params: slot = sys_act.params[0].slot else: slot = d_state.requested_slot if not slot: slot = random.choice(list(d_state.slots_filled.keys())) if d_state.item_in_focus: if slot not in d_state.item_in_focus or \ not d_state.item_in_focus[slot]: new_sys_acts.append( DialogueAct('inform', [ DialogueActItem(slot, Operator.EQ, 'no info') ])) else: if slot == 'name': new_sys_acts.append( DialogueAct('offer', [ DialogueActItem( slot, Operator.EQ, d_state.item_in_focus[slot]) ])) else: new_sys_acts.append( DialogueAct('inform', [ DialogueActItem( slot, Operator.EQ, d_state.item_in_focus[slot]) ])) else: new_sys_acts.append( DialogueAct('inform', [ DialogueActItem(slot, Operator.EQ, 'no info') ])) elif self.agent_role == 'user': if sys_act.params: slot = sys_act.params[0].slot # Do nothing if the slot is already filled if sys_act.params[0].value: continue elif d_state.last_sys_acts and d_state.user_acts and \ d_state.user_acts[0].intent == 'request': slot = d_state.user_acts[0].params[0].slot else: slot = \ random.choice( list(d_state.user_goal.constraints.keys())) # Populate the inform with a slot from the user goal if d_state.user_goal: # Look for the slot in the user goal if slot in d_state.user_goal.constraints: value = d_state.user_goal.constraints[slot].value else: value = 'dontcare' new_sys_acts.append( DialogueAct( 'inform', [DialogueActItem(slot, Operator.EQ, value)])) # Remove the empty inform sys_acts_copy.remove(sys_act) elif sys_act.intent == 'request': # If the policy did not select a slot if not sys_act.params: found = False if self.agent_role == 'system': # Select unfilled slot for slot in d_state.slots_filled: if not d_state.slots_filled[slot]: found = True new_sys_acts.append( DialogueAct('request', [ DialogueActItem(slot, Operator.EQ, '') ])) break elif self.agent_role == 'user': # Select request from goal if d_state.user_goal: for req in d_state.user_goal.requests: if not d_state.user_goal.requests[req].value: found = True new_sys_acts.append( DialogueAct('request', [ DialogueActItem( req, Operator.EQ, '') ])) break if not found: # All slots are filled new_sys_acts.append( DialogueAct('request', [ DialogueActItem( random.choice( list( d_state.slots_filled.keys())[:-1]), Operator.EQ, '') ])) # Remove the empty request sys_acts_copy.remove(sys_act) # Append unique new sys acts for sa in new_sys_acts: if sa not in sys_acts_copy: sys_acts_copy.append(sa) self.DSTracker.update_state_sysact(sys_acts_copy) return sys_acts_copy