def _run_initial_turn(self) -> None: """ Show the image to the human and bot, and show the bot's response to the human. """ system_id = 'SYSTEM' system_agent_idx = None # Show the image to the human image_act_for_human = { 'episode_done': False, 'id': system_id, 'text': f"""Welcome! You'll now have a conversation with your partner. <-- FIRST, YOUR PARTNER WILL SAY SOMETHING ABOUT THIS IMAGE TO YOUR LEFT. Be sure to talk about this image a little bit before discussing other things! """, 'task_data': { 'image_src': self.image_src }, 'agent_idx': system_agent_idx, } self.agent.observe(validate(image_act_for_human)) # Show the image to the bot image_act = { **self.image_act, 'episode_done': False, 'id': system_id, 'agent_idx': system_agent_idx, } self.bot.observe(validate(image_act)) del image_act['image'] # Don't save the image features to disk # Have the bot respond bot_first_act_raw = self.bot.act() bot_first_act_raw = Message( Compatibility.maybe_fix_act( bot_first_act_raw)).json_safe_payload() bot_first_act_raw['id'] = self.bot.agent_id self.agent.observe(validate(bot_first_act_raw)) bot_first_act = { 'episode_done': False, 'id': bot_first_act_raw['id'], 'text': bot_first_act_raw['text'], 'agent_idx': 1, } # Record lines of dialogue self.dialog.append(image_act) self.dialog.append(bot_first_act)
def _run_initial_turn(self) -> None: """ Run the initial turn for both the human and the bot. Optionally show the bot its persona. If we are in BST conversation mode, show 2 previous BST utterances to both the human and the bot; if we are in Meena-like conversation mode, show "Hi!" to the human and the bot and let the bot respond accordingly. """ control_msg = {"episode_done": False} time.sleep(2) coordinator_first_msg = { 'episode_done': False, 'id': 'Coordinator', 'text': 'Please chitchat with another worker for 6 turns as if you were catching up since last time you two spoke.', 'fake_start': True, 'agent_idx': 2, 'task_data': self.task_data, } self.agent.observe(validate(coordinator_first_msg)) time.sleep(2) human_first_msg = { 'episode_done': False, 'id': self.agent.id, 'text': self.context_for_bot_prompt, 'fake_start': True, 'agent_idx': 0, 'task_data': self.task_data, } for k, v in control_msg.items(): human_first_msg[k] = v # self.dialog.append(human_first_msg) # self.agent.observe(validate(human_first_msg)) print(human_first_msg) self.bot.observe(validate(human_first_msg)) first_bot_act = self.bot.act() first_bot_act = Compatibility.maybe_fix_act(first_bot_act) first_bot_act['id'] = 'THEY' self.agent.observe(validate(first_bot_act)) bot_utterance_data = { 'agent_idx': 1, 'text': first_bot_act['text'], 'id': 'THEY', } self.dialog.append(bot_utterance_data)
def _run_initial_turn(self) -> None: """ Run the initial turn for both the human and the bot. Optionally show the bot its persona. If we are in BST conversation mode, show 2 previous BST utterances to both the human and the bot; if we are in Meena-like conversation mode, show "Hi!" to the human and the bot and let the bot respond accordingly. """ control_msg = {"episode_done": False} if self.opt['include_persona']: # The Bot agent # We add the personas and 1/3 of the time WoW topic as the # first utterance in the history. # Previously for BST task, we also had a big first utterance # that gave instructions. Removing that for this task. persona_strings = [s.strip() for s in self.personas[1]] persona_utterance = self._get_persona_utterance( persona_strings=persona_strings, context_dataset=self.context_info['context_dataset'], additional_context=self.context_info['additional_context'], is_bot=True, ) message = control_msg.copy() message['text'] = persona_utterance # The bot seeing its persona does not count as a "turn" self.bot.observe(validate(message), increment_turn=False) if self.opt['conversation_start_mode'] == 'bst': print('[Displaying first utterances as per BST task.]') # Display the previous two utterances human_first_msg = { 'episode_done': False, 'id': self.agent.id, 'text': self.context_info['person1_seed_utterance'], 'fake_start': True, 'agent_idx': 0, } for k, v in control_msg.items(): human_first_msg[k] = v bot_first_msg = { 'episode_done': False, 'id': self.bot.id, 'text': self.context_info['person2_seed_utterance'], 'fake_start': True, 'agent_idx': 1, } print( f'human_first_msg: {human_first_msg}, bot_first_msg: {bot_first_msg}' ) self.dialog.append(human_first_msg) self.dialog.append(bot_first_msg) for observer in [self.agent, self.bot]: observer.observe(validate(human_first_msg)) observer.observe(validate(bot_first_msg)) elif self.opt['conversation_start_mode'] == 'hi': print('[Displaying "Hi!" only as per Meena task.]') human_first_msg = { 'episode_done': False, 'id': self.agent.id, 'text': 'Hi!', 'fake_start': True, 'agent_idx': 0, } for k, v in control_msg.items(): human_first_msg[k] = v self.dialog.append(human_first_msg) self.agent.observe(validate(human_first_msg)) self.bot.observe(validate(human_first_msg)) first_bot_act = self.bot.act() first_bot_act = Compatibility.maybe_fix_act(first_bot_act) self.agent.observe(validate(first_bot_act)) bot_utterance_data = { 'agent_idx': 1, 'text': first_bot_act['text'], 'id': first_bot_act['id'], } self.dialog.append(bot_utterance_data) else: raise ValueError( f"Conversation start mode {self.opt['conversation_start_mode']} " f"not recognized!")
def parley(self): print( f'{self.__class__.__name__}:{self.tag}: is at turn {self.task_turn_idx}, with {self.num_turns} pairs of turns needed...' ) if self.task_turn_idx == 0: self._run_initial_turn() self.task_turn_idx += 1 return """Otherwise, we proceed accordingly""" print( f'{self.__class__.__name__}:{self.tag}: About to act with task turn idx: {self.task_turn_idx}' ) acts = [None, None] for idx, agent in enumerate([self.agent, self.bot]): if not self.chat_done: acts[idx] = agent.act(timeout=self.max_resp_time) acts[idx] = Compatibility.maybe_fix_act(acts[idx]) if 'metrics' in acts[idx]: del acts[idx]['metrics'] # Metrics can't be saved to JSON and are not needed here print( f'Got act for agent idx {idx}, act was: {acts[idx]} and self.task_turn_idx: {self.task_turn_idx}.' ) if acts[idx].get('task_data', {}).get('final_rating') is not None: self.chat_done = True # agent ends chat after exceeding minimum number of turns if self.task_turn_idx > self.num_turns: # Human has just responded. Any problem data received now will be # regarding the bot's prior utterance p = acts[idx]['task_data'].get( 'problem_data_for_prior_message') if p is not None: turn_idx = -1 # Attach the problem data to the last utterance, since the human # hasn't said anything since then self.__add_problem_data_to_utterance(p, turn_idx=turn_idx) # Save the final chat data time_string = time.strftime('%Y%m%d_%H%M%S') chat_data_folder = self.opt['chat_data_folder'] os.makedirs(chat_data_folder, exist_ok=True) chat_data_path = os.path.join( chat_data_folder, f'{time_string}_{np.random.randint(0, 1000)}_{self.task_type}.json', ) final_chat_data = self.get_final_chat_data() self.agent.mephisto_agent.state.messages.append( {'final_chat_data': final_chat_data}) # Append the chat data directly to the agent state's message list in # order to prevent the worker from seeing a new text response in the UI with open(chat_data_path, 'w+') as f_json: data_str = json.dumps(final_chat_data) f_json.write(data_str) print(f'{self.__class__.__name__}:{self.tag}: Data saved at ' f'{chat_data_path} for model: {self.bot.worker_id}.') # Soft-block the worker if there were acceptability violations acceptability_violations = final_chat_data[ 'acceptability_violations'][0] if (acceptability_violations is not None and acceptability_violations != ''): print( f'**NOTE** Acceptability violations detected: {acceptability_violations}' ) # Grant the failed qualification self.agent.mephisto_agent.get_worker().grant_qualification( self.block_qualification, 1) return else: utterance_data = { 'agent_idx': idx, # Get rid of annotations HTML if it's the bot response 'text': acts[idx]['text'].split('<br>')[0], 'id': acts[idx]['id'] if 'id' in acts[idx] else 'NULL_ID', # Person1 or Polyencoder } self.dialog.append(utterance_data) if idx == 0: # Human has just responded. Any problem data received now will be # regarding the bot's prior utterance p = acts[idx]['task_data'].get( 'problem_data_for_prior_message') if p is not None: turn_idx = -2 # Attach the problem data to the second-to-last utterance, since # the last utterance is what the human just said self.__add_problem_data_to_utterance(p, turn_idx=turn_idx) self._postprocess_acts(acts=acts, agent_idx=idx) for other_agent in [self.agent, self.bot]: if other_agent != agent: other_agent.observe(validate(acts[idx])) print( f'[agent {idx}] self.task_turn_idx: {self.task_turn_idx}, self.dialog is: {self.dialog}' ) self.task_turn_idx += 1
def parley(self): """ The main function that controls the logic of the task. Uses self.task_turn_idx to control the sequence of the conversation. Specifically, when self.task_turn_idx is even, we know that the bots just gave their potential responses, and that it is the human's turn to choose one of the responses and give a justification value. When self.task_turn_idx is odd, we know that the human just chose one of the bots' responses, and now needs to respond to that response. self.task_turn_idx is initially 0, and during _run_initial_turn() the UI is redrawn to have the human select between the bots' responses. Then, self.task_turn_idx is incremented to 1. During self.agent.observe(), the UI is redrawn for the following human input, and during self.agent.act(), the code awaits the human input. """ logging.info( f'{self.__class__.__name__}:{self.tag}: is at task_turn_idx ' f'{self.task_turn_idx}, with {self.num_turns} pairs of turns needed...' ) if self.task_turn_idx == 0: self._run_initial_turn() self.task_turn_idx += 1 return logging.info( f'{self.__class__.__name__}:{self.tag}: About to act with task turn idx: ' f'{self.task_turn_idx}') # At this point, we know that the human now needs to respond to the bot's # response that the human just chose # We retrieve information regarding the human's choice and justification using # self.agent.act() human_choose_bot_response_act = self.agent.act( timeout=self.max_resp_time) human_choose_bot_response_act = Message( Compatibility.maybe_fix_act( human_choose_bot_response_act)).json_safe_payload() logging.info( f'Got act for human, act was: {human_choose_bot_response_act} and ' f'self.task_turn_idx: {self.task_turn_idx}.') accepted_bot_response = human_choose_bot_response_act['task_data'][ 'accepted_bot_response'] accepted_bot_id = human_choose_bot_response_act['task_data'][ 'accepted_bot_id'] accepted_bot_justification_value = human_choose_bot_response_act[ 'task_data']['justification_value'] not_accepted_bot_response = human_choose_bot_response_act['task_data'][ 'not_accepted_bot_response'] not_accepted_bot_id = human_choose_bot_response_act['task_data'][ 'not_accepted_bot_id'] # We have both bots observe the accepted bot's response so that the conversation # history stays the same self.bots[0].observe(accepted_bot_response) self.bots[1].observe(accepted_bot_response) task_data = {} accepted_bot_utterance_data = { 'text': accepted_bot_response['text'].split('<br>')[0], 'id': accepted_bot_id, } not_accepted_bot_utterance_data = { 'text': not_accepted_bot_response['text'].split('<br>')[0], 'id': not_accepted_bot_id, } bot_utterance_data = { 'agent_idx': 1, 'accepted_bot_data': accepted_bot_utterance_data, 'not_accepted_bot_data': not_accepted_bot_utterance_data, 'human_choice': accepted_bot_id, 'human_justification': accepted_bot_justification_value, } self.dialog.append(bot_utterance_data) self._postprocess_acts(acts=None, agent_idx=0) # All logic and processing for this step has now been done, so we do # self.agent.observe() to send the accepted response back to the frontend to # display and update task turn index, as well as await for the next action, # which is the human typing their response task_data['task_turn_idx'] = self.task_turn_idx # The UI will ask the human to respond to the chosen bot response self.agent.observe({ 'text': accepted_bot_response['text'], 'task_data': task_data }) # Make self.task_turn_idx even now self.task_turn_idx += 1 # Check for whether 6 pairs of turns has been done, since the last message of a # conversation should always be the bot's response if (human_choose_bot_response_act is not None and human_choose_bot_response_act.get( 'task_data', {}).get('finished') is not None): self.chat_done = True # agent ends chat after exceeding minimum number of turns # Bot has just responded. Any problem data received now will be # regarding this bot's utterance # Get the final chat data self.final_chat_data = self.get_final_chat_data() # Soft-block the worker if there were acceptability violations acceptability_violations = self.final_chat_data[ 'acceptability_violations'][0] if acceptability_violations is not None and acceptability_violations != '': logging.info(f'**NOTE** Acceptability violations detected: ' f'{acceptability_violations}') # Grant the failed qualification self.agent.mephisto_agent.get_worker().grant_qualification( self.block_qualification, 1) return logging.info( f'[human agent] self.task_turn_idx: {self.task_turn_idx}, self.dialog is: ' f'{self.dialog}') logging.info( f'Got act for human, act was: {human_choose_bot_response_act} and ' f'self.task_turn_idx: {self.task_turn_idx}.') # At this point, we know that the human now needs to respond to the bot's # response that the human just chose # We retrieve information regarding the human's response using self.agent.act() human_response_act = self.agent.act(timeout=self.max_resp_time) # Again, we have both bots observe the human response so that the conversation # history stays the same self.bots[0].observe(validate(human_response_act)) self.bots[1].observe(validate(human_response_act)) # Check that the models' conversation histories are the same bot_1_history = self.bots[0].model_agent.history.history_strings bot_2_history = self.bots[1].model_agent.history.history_strings assert ( bot_1_history == bot_2_history ), f"The two bots' conversation histories are different.\nBot 1 history: {bot_1_history}\nBot 2 history: {bot_2_history}" # After the bots have observed the human response, it's time for them to produce # their response to the human using self.bots.act() bot_1_response = self.bots[0].act() bot_1_response = Compatibility.maybe_fix_act(bot_1_response) bot_2_response = self.bots[1].act() bot_2_response = Compatibility.maybe_fix_act(bot_2_response) # We display the result to the frontend randomly so there is no selection bias. # Also, we attach our result to task_data to send arbitrary data to the frontend if random.random() > 0.5: task_data = { 'top_bot_data': { 'top_bot_id': self.bots[0].worker_id, 'top_bot_response': bot_1_response, }, 'bottom_bot_data': { 'bottom_bot_id': self.bots[1].worker_id, 'bottom_bot_response': bot_2_response, }, 'task_turn_idx': self.task_turn_idx, } else: task_data = { 'top_bot_data': { 'top_bot_id': self.bots[1].worker_id, 'top_bot_response': bot_2_response, }, 'bottom_bot_data': { 'bottom_bot_id': self.bots[0].worker_id, 'bottom_bot_response': bot_1_response, }, 'task_turn_idx': self.task_turn_idx, } human_utterance_data = { 'agent_idx': 0, # Get rid of annotations HTML if it's the bot response 'text': human_response_act['text'].split('<br>')[0], 'id': human_response_act['id'] if 'id' in human_response_act else 'NULL_ID', # Person1 or Polyencoder } self.dialog.append(human_utterance_data) # Human has just responded. Any problem data received now will be regarding the # bot's prior utterance p = human_response_act['task_data'].get( 'problem_data_for_prior_message') if p is not None: turn_idx = -2 # Attach the problem data to the second-to-last utterance, since the last # utterance is what the human just said self.__add_problem_data_to_utterance(p, turn_idx=turn_idx) self._postprocess_acts(acts=None, agent_idx=0) task_data['task_turn_idx'] = self.task_turn_idx # All logic and processing for this step has now been done, so we do # self.agent.observe() to send the two bots' responses back to the frontend to # display and update task turn index, as well as await for the next action, # which is the human choosing from the two responses and providing a # justification value # The UI will ask the human to choose between two bot responses and give a # justification logging.info(f'*** self.task_turn_idx: {self.task_turn_idx} ***') self.agent.observe({'text': '', 'task_data': task_data}) # Make self.task_turn_idx odd now self.task_turn_idx += 1 logging.info( f'[bot agent] self.task_turn_idx: {self.task_turn_idx}, self.dialog is: ' f'{self.dialog}')
def _run_initial_turn(self) -> None: """ Run the initial turn for both the human and the bot. Optionally show the bot its persona. If we are in Meena-like conversation mode show "Hi!" to the human and the bot and let the bot respond accordingly. Check parley() function for more information on the main logic. """ control_msg = {"episode_done": False} if self.opt['include_persona']: # The Bot agent # We add the personas and 1/3 of the time WoW topic as the # first utterance in the history. # Previously for BST task, we also had a big first utterance # that gave instructions. Removing that for this task. persona_strings = [s.strip() for s in self.personas[1]] persona_utterance = self._get_persona_utterance( persona_strings=persona_strings, context_dataset=self.context_info['context_dataset'], additional_context=self.context_info['additional_context'], is_bot=True, ) message = control_msg.copy() message['text'] = persona_utterance # The bot seeing its persona does not count as a "turn" self.bots[0].observe(validate(message), increment_turn=False) self.bots[1].observe(validate(message), increment_turn=False) if self.opt['conversation_start_mode'] == 'hi': logging.info('[Displaying "Hi!" only as per Meena task.]') if self.personas is not None: human_persona_strings = [s.strip() for s in self.personas[0]] else: human_persona_strings = ['', ''] human_first_msg = { 'episode_done': False, 'id': self.agent.id, 'text': 'Hi!', 'fake_start': True, 'agent_idx': 0, 'task_data': { 'human_persona_string_1': human_persona_strings[0], 'human_persona_string_2': human_persona_strings[1], 'prompt_instruction': self.opt['task_question'], }, } for k, v in control_msg.items(): human_first_msg[k] = v # The first message is always "Hi", so we have both bots observe the message self.dialog.append(human_first_msg) self.agent.observe(validate(human_first_msg)) self.bots[0].observe(validate(human_first_msg)) self.bots[1].observe(validate(human_first_msg)) bot_1_response = self.bots[0].act() bot_1_response = Compatibility.maybe_fix_act(bot_1_response) bot_2_response = self.bots[1].act() bot_2_response = Compatibility.maybe_fix_act(bot_2_response) if random.random() > 0.5: task_data = { 'top_bot_data': { 'top_bot_id': self.bots[0].worker_id, 'top_bot_response': bot_1_response, }, 'bottom_bot_data': { 'bottom_bot_id': self.bots[1].worker_id, 'bottom_bot_response': bot_2_response, }, 'task_turn_idx': self.task_turn_idx, } else: task_data = { 'top_bot_data': { 'top_bot_id': self.bots[1].worker_id, 'top_bot_response': bot_2_response, }, 'bottom_bot_data': { 'bottom_bot_id': self.bots[0].worker_id, 'bottom_bot_response': bot_1_response, }, 'task_turn_idx': self.task_turn_idx, } # Need an initial human's observe to observe the two choices from the bot self.agent.observe({'text': '', 'task_data': task_data}) else: raise ValueError( f"Conversation start mode {self.opt['conversation_start_mode']} " f"not recognized!")
def parley(self): print( f'{self.__class__.__name__}:{self.tag}: is at turn {self.task_turn_idx}, with {self.num_turns} pairs of turns needed...' ) if self.task_turn_idx == 0: self._run_initial_turn() self.task_turn_idx += 1 return """Otherwise, we proceed accordingly""" print( f'{self.__class__.__name__}:{self.tag}: About to act with task turn idx: {self.task_turn_idx}' ) acts = [None, None] for idx, agent in enumerate([self.agent, self.bot]): if not self.chat_done: acts[idx] = agent.act(timeout=self.max_resp_time) if (agent == self.bot and hasattr(self.bot, 'agent_id') and self.bot.agent_id): # Set speaker name as self.bot_agent_id otherwise, at frontend bot name such as "TransformerGenerator" would appear Compatibility.backward_compatible_force_set( acts[idx], 'id', self.bot.agent_id) acts[idx] = Message(Compatibility.maybe_fix_act( acts[idx])).json_safe_payload() print( f'Got act for agent idx {idx}, act was: {acts[idx]} and self.task_turn_idx: {self.task_turn_idx}.' ) if acts[idx].get('task_data', {}).get('final_rating') is not None: self.chat_done = True # agent ends chat after exceeding minimum number of turns # Human has just responded. Any problem data received now will be # regarding the bot's prior utterance turn_idx = -1 # Attach the problem data and final rating to the last utterance, since # the human hasn't said anything since then p = acts[idx]['task_data'].get( 'problem_data_for_prior_message') if p is not None: self.__add_problem_data_to_utterance(p, turn_idx=turn_idx) self.dialog[turn_idx]['final_rating'] = acts[idx]['task_data'][ 'final_rating'] # Save the final chat data date_folder = time.strftime('%Y_%m_%d') time_string = time.strftime('%Y%m%d_%H%M%S') chat_data_subfolder = os.path.join( self.opt['chat_data_folder'], date_folder) os.makedirs(chat_data_subfolder, exist_ok=True) chat_data_path = os.path.join( chat_data_subfolder, f'{time_string}_{np.random.randint(0, 1000)}_{self.task_type}.json', ) self.final_chat_data = self.get_final_chat_data() self.agent.mephisto_agent.state.messages.append({ 'final_chat_data': self.final_chat_data, 'data': {}, 'packet_type': None, 'timestamp': None, }) # Append the chat data directly to the agent state's message list in # order to prevent the worker from seeing a new text response in the UI. # Add some dummy keys for compatibility with all agent state messages # TODO: remove this when no longer saving data to disk manually with open(chat_data_path, 'w+') as f_json: data_str = json.dumps(self.final_chat_data) f_json.write(data_str) print(f'{self.__class__.__name__}:{self.tag}: Data saved at ' f'{chat_data_path} for model: {self.bot.worker_id}.') # Soft-block the worker if there were acceptability violations acceptability_violations = self.final_chat_data[ 'acceptability_violations'][0] if (acceptability_violations is not None and acceptability_violations != ''): print( f'**NOTE** Acceptability violations detected: {acceptability_violations}' ) # Grant the failed qualification self.agent.mephisto_agent.get_worker().grant_qualification( self.block_qualification, 1) return else: utterance_data = { 'agent_idx': idx, # Get rid of annotations HTML if it's the bot response 'text': acts[idx]['text'].split('<br>')[0], 'id': acts[idx].get('id', 'NULL_ID'), # In case model doesn't set id } self.dialog.append(utterance_data) if idx == 0: # Human has just responded. Any problem data received now will be # regarding the bot's prior utterance p = acts[idx]['task_data'].get( 'problem_data_for_prior_message') if p is not None: turn_idx = -2 # Attach the problem data to the second-to-last utterance, since # the last utterance is what the human just said self.__add_problem_data_to_utterance(p, turn_idx=turn_idx) self._postprocess_acts(acts=acts, agent_idx=idx) for other_agent in [self.agent, self.bot]: if other_agent != agent: other_agent.observe(validate(acts[idx])) print( f'[agent {idx}] self.task_turn_idx: {self.task_turn_idx}, self.dialog is: {self.dialog}' ) self.task_turn_idx += 1