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 next_action(self, state): """ Consult the dialogue 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 dialogue 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 dialogue policy: ' 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