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, '')])] # Select intent intent = random.choice(self.intents) dact = DialogueAct(intent, []) # Select slot if intent in ['inform', 'request']: if intent == 'inform': # The Dialogue Manager will fill the slot's value slot = random.choice(self.ontology.ontology['requestable']) else: slot = \ random.choice(self.ontology.ontology['system_requestable']) dact.params = [DialogueActItem(slot, Operator.EQ, '')] return [dact]
def test_nlg(self): self.assertEqual( SlotFillingNLG().generate_output({ 'dacts': [DialogueAct('request', [DialogueActItem('pricerange', Operator.EQ, '')])], 'system': True} ), 'Which price range do you prefer? ' )
def test_nlu(self): self.assertEqual( SlotFillingNLU({'ontology': 'example/domains/CamRestaurants-rules.json', 'database': 'example/domains/CamRestaurants-dbase.db'}).process_input( 'looking for an expensive restaurant' ), [DialogueAct('inform', [DialogueActItem('pricerange', Operator.EQ, 'expensive')])] )
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 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
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 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, '') ]) ] # Default fall-back action print('Reinforce dialogue policy ({0}) policy action decoder warning: ' 'Selecting default action (index: {1})!'.format( self.agent_role, action_enc)) return [DialogueAct('bye', [])]
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 dialogue_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 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
def test_dialogue_act(self): self.assertEqual( str(DialogueAct('intent', [DialogueActItem('slot', Operator.EQ, 'value')])), 'intent(slot = value, )' )
def test_dialogue_act_item(self): self.assertEqual( str(DialogueActItem('slot', Operator.EQ, 'value')), 'slot = value' )
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! camrest_nlu 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! camrest_nlu did not understand slots or values ' 'for utterance: {0}\n'.format(utterance)) return dacts
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 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 generate(self, goal_slot_selection_weights=None): """ Generate a goal :param goal_slot_selection_weights: list of weights that bias the sampling of slots for constraints :return: a new goal """ # If a goals file has been provided if self.goals: return random.choice(self.goals) # Randomly pick an item from the database cursor = self.database.SQL_connection.cursor() sql_command = "SELECT * FROM " + self.db_table_name + \ " WHERE ROWID == (" + \ str(random.randint(1, self.db_row_count)) + ");" cursor.execute(sql_command) db_result = cursor.fetchone() attempt = 0 while attempt < 3 and not db_result: print('GoalGenerator: Database {0} appears to be empty!'.format( self.database)) print(f'Trying again (attempt {attempt} out of 3)...') sql_command = "SELECT * FROM " + self.db_table_name + \ " WHERE ROWID == (" + \ str(random.randint(1, self.db_row_count)) + ");" cursor.execute(sql_command) db_result = cursor.fetchone() attempt += 1 if not db_result: raise LookupError('GoalGenerator: Database {0} appears to be ' 'empty!'.format(self.database)) result = dict(zip(self.slot_names, db_result)) # Generate goal goal = Goal() # TODO: Sample from all available operators, not just '=' # (where applicable) # Sample constraints from informable slots if goal_slot_selection_weights: # If a list of weights has been provided then do weighted sampling # Make sure that all slots have some weight. Flatten the # dictionaries in the process slot_weights = [] half_min_weight = min(goal_slot_selection_weights.values()) / 2.0 for s in self.ontology.ontology['informable']: if s in goal_slot_selection_weights: slot_weights.append(goal_slot_selection_weights[s]) else: slot_weights.append(half_min_weight) inf_slots = \ self.weighted_random_sample_no_replacement( list(self.ontology.ontology['informable'].keys()), slot_weights, random.randint(2, len(self.ontology.ontology['informable']))) inf_slots.reverse() else: inf_slots = \ random.sample( list(self.ontology.ontology['informable'].keys()), random.randint(2, len(self.ontology.ontology['informable']))) # Sample requests from requestable slots req_slots = random.sample( self.ontology.ontology['requestable'], random.randint(0, len(self.ontology.ontology['requestable']))) # Remove slots for which the user places constraints # Note: 'name' may or may not be in inf_slots here, and this is # randomness is desirable for slot in inf_slots: if slot in req_slots: req_slots.remove(slot) # Never ask for specific name unless it is the only constraint # if 'name' in inf_slots and len(inf_slots) > 1: if 'name' in inf_slots: inf_slots.remove('name') # Shuffle informable and requestable slots to create some variety # when pushing into the agenda. random.shuffle(inf_slots) random.shuffle(req_slots) for slot in inf_slots: # Check that the slot has a value in the retrieved item if slot in result and result[slot]: goal.constraints[slot] = \ DialogueActItem(slot, Operator.EQ, result[slot]) for slot in req_slots: if slot in result: goal.requests[slot] = DialogueActItem(slot, Operator.EQ, '') return goal
def generate(self, goal_slot_selection_weights=None): """ Generate a new complex goal :param goal_slot_selection_weights: list of weights that bias the sampling of slots for constraints :return: a new goal """ if self.goals: return random.sample(self.goals) # Randomly pick an item from the database cursor = self.database.SQL_connection.cursor() sql_command = "SELECT * FROM " + self.db_table_name + \ " WHERE ROWID == (" + \ str(random.randint(1, self.db_row_count)) + ");" cursor.execute(sql_command) db_result = cursor.fetchone() global_key_value = '' global_attempt = 0 result = [] while global_attempt < 3 and not global_key_value: attempt = 0 while attempt < 3 and not db_result: print( 'GoalGenerator: Database {0} appears to be empty!'.format( self.database)) print(f'Trying again (attempt {attempt} out of 3)...') sql_command = "SELECT * FROM " + self.db_table_name + \ " WHERE ROWID == (" + \ str(random.randint(1, self.db_row_count)) + ");" cursor.execute(sql_command) db_result = cursor.fetchone() attempt += 1 if not db_result: raise LookupError('GoalGenerator: Database {0} appears to be ' 'empty!'.format(self.database)) result = dict(zip(self.slot_names, db_result)) if self.global_key in result: global_key_value = result[self.global_key] global_attempt += 1 if not result: raise LookupError(f'ComplexGoalGenerator cannot find an item with ' f'global key {self.global_key}') # Generate goal goal = Goal() # Sample "global" constraints and requests that all sub-goals share global_inf_slots = \ random.sample(self.global_slots, random.randint(2, len(self.global_slots))) # Fetch all items from the database that satisfy the constraints # sampled above sql_command = "SELECT * FROM " + self.db_table_name + \ " WHERE " + self.global_key + " = \"" + \ global_key_value + "\" AND " for gs in global_inf_slots: sql_command += gs + " = \"" + result[gs] + "\" AND " # Trim last 'AND ' sql_command = sql_command[:-4] + ";" cursor.execute(sql_command) db_results = cursor.fetchall() results = [dict(zip(self.slot_names, dbr)) for dbr in db_results] for slot in global_inf_slots: # Check that the slot has a value in the retrieved item if slot in result and result[slot]: if result[slot] not in ['None', 'Other']: goal.constraints[slot] = \ DialogueActItem(slot, Operator.EQ, result[slot]) # Sample requests global_req_slots = \ random.sample(self.global_slots, random.randint(0, len(self.global_slots))) print('DBG: Global Req Sampled') # Remove slots for which the user places constraints # Note: 'name' may or may not be in inf_slots here, and this is # randomness is desirable for slot in global_inf_slots: if slot in global_req_slots: global_req_slots.remove(slot) for slot in global_req_slots: if slot in result: goal.requests[slot] = DialogueActItem(slot, Operator.EQ, []) # Sample number of sub-goals num_sub_goals = \ random.choices(range(1, 5), weights=[0.15, 0.35, 0.35, 0.15])[0] # Make sure we don't attempt to sample more subgoals than items in the # results num_sub_goals = \ num_sub_goals if len(results) > num_sub_goals else len(results) subgoal_attempts = 0 # As there is no guarantee that the sampled slots for the sampled # subgoals exist (and we do not want empty subgoals), make three # attempts at sampling subgoals. while not goal.subgoals and subgoal_attempts < 3: print(f'DBG: Sampling Results {len(results)}, {num_sub_goals}') results = random.sample(results, num_sub_goals) subgoal_attempts += 1 # For each sub-goal, sample "local" constraints and requests # (must be on different slots than the global) for sg in range(num_sub_goals): local_inf_slots = \ random.sample(self.local_slots, random.randint(1, len(self.local_slots))) # Create new subgoal subgoal = Goal() for lif in local_inf_slots: # Check that the slot has indeed a value if results[sg][lif] and \ results[sg][lif] not in ['None', 'Other']: subgoal.constraints[lif] = \ DialogueActItem(lif, Operator.EQ, results[sg][lif]) if subgoal.constraints: goal.subgoals.append(subgoal) # TODO Check if a constraint exists in all subgoals, in which case # remove from all subgoals and put in global constr return goal
def respond(self): """ Consult the policy to retrieve nlg template and generate the response. :return: the DTL user Simulator's utterance (response) """ if self.curr_patience <= 0 or self.goal_met: return 'bye' if not self.input_system_acts: # Randomly sample from hello + responses to requests, as there is # where informs most likely live. sys_act_slot = \ random.choice([act for act in self.policy if 'request' in act]) replies = list(self.policy[sys_act_slot]['responses'].keys()) probs = \ [self.policy[sys_act_slot]['responses'][i] for i in replies] response = deepcopy(random.choices(replies, weights=probs)[0]) # Replace placeholders with values from goal for slot in self.ontology.ontology['informable']: if slot.upper() in response: if slot in self.goal.constraints: response = \ response.replace( '<' + slot.upper() + '>', self.goal.constraints[slot].value) else: # If there is no constraint, replace with slot 'any' response = \ response.replace( '<' + slot.upper() + '>', 'any') for slot in self.ontology.ontology['requestable']: # This check is necessary to know when to mark this as an # actual request if slot.upper() in response: response = response.replace('<' + slot.upper() + '>', slot) self.goal.actual_requests[slot] = \ DialogueActItem(slot, Operator.EQ, '') return random.choice([response, 'hello']) response_template = '' for system_act in self.input_system_acts: # 'bye' doesn't seem to appear in the CamRest data if system_act.intent == 'bye': response_template += 'thank you, goodbye' 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: print('Warning! DACT-nlg policy does not know what to do for ' '%s' % sys_act_slot) # return '' else: replies = list(self.policy[sys_act_slot]['responses'].keys()) probs = [ self.policy[sys_act_slot]['responses'][i] for i in replies ] response = deepcopy(random.choices(replies, weights=probs)[0]) # Replace placeholders with values from goal for slot in self.ontology.ontology['informable']: if slot.upper() in response: if slot in self.goal.constraints: response = \ response.replace( '<' + slot.upper() + '>', self.goal.constraints[slot].value) else: # If there is no constraint, replace with # slot 'any' response = \ response.replace( '<' + slot.upper() + '>', 'any') for slot in self.ontology.ontology['requestable']: # This check is necessary to know when to mark this as an # actual request if slot.upper() in response: if slot == 'addr': response = \ response.replace( '<' + slot.upper() + '>', 'address') elif slot == 'postcode': response = \ response.replace( '<' + slot.upper() + '>', 'post code') elif slot == 'pricerange': response = \ response.replace( '<' + slot.upper() + '>', 'price range') else: response = \ response.replace( '<' + slot.upper() + '>', slot) self.goal.actual_requests[slot] = \ DialogueActItem(slot, Operator.EQ, '') response_template += response + ' ' return response_template