def get_prompt(self, state: State) -> PromptResult: """This method is the same as get_response. Only difference is that 1. we do not get a list of reasons (because there is not enough time) 2. we only chose to solicit an opinion if user didn't have an opinion on it before, or 3. we will solicit a reason if user told us before that they like or dislike a phrase :param state: the current state :type state: State :return: a result chirpy can use :rtype: PromptResult """ self.initialize_turn() if state.last_turn_prompt: return emptyPrompt(state) phrase, priority, transition_phrase = self.select_phrase_for_prompt(state) if phrase is None: return emptyPrompt(state) utterance = self.state_manager.current_state.text additional_features = self.populate_features(state, utterance) additional_features.detected_phrases = tuple([phrase for phrase in additional_features.detected_phrases if phrase not in state.phrases_done]) # Then need to advance the state using the utterance state_p = state_actions.next_state(state, utterance, additional_features) if state_p is None: # Special handling for prompt: Add phrase and sentiment state_p = state.reset_state() state_p.cur_phrase = phrase user_sentiments_history = dict(state_p.user_sentiment_history) state_p.cur_sentiment = user_sentiments_history[phrase] if phrase in user_sentiments_history else 2 if state_p.cur_sentiment != 2: action = Action(solicit_reason=True) else: action = Action(solicit_opinion=True) # Then need to utterancify the action text, phrase, reason = fancy_utterancify_prompt(state_p, action, [], [], [], state_p.cur_phrase not in state.detected_opinionated_phrases, self.state_manager.current_state.choose_least_repetitive) # type: ignore # Then need to fill the rest of the fields of state_p (through mutation) state_p = state_actions.fill_state_on_action(state_p, action, text, phrase, additional_features, reason, self.opinionable_phrases, self.opinionable_entities) state_p.last_turn_prompt, state_p.last_turn_select = True, False wiki_entity = None if self.opinionable_phrases[phrase].good_for_wiki: wiki_entity = get_entity_by_wiki_name(self.opinionable_phrases[phrase].wiki_entity_name) state.last_turn_prompt, state.last_turn_select = False, False text = ' '.join((transition_phrase, text)) return PromptResult(text, priority, state, wiki_entity, conditional_state=state_p)
def get_action(self, state: State, action_space: List[Action], additional_features: AdditionalFeatures) -> Action: """On a high level, this policy follows the following fixed trajectory 1. Ask the user if they like the phrase or not (skip if we already have that info) 2. Agree with the user, give a reason (if possible) and ask user's reason 3. Give another reason (if possible) and ask if user agrees 4. Switch to a different entity (if possible), and start over at stage 1 :param state: the current state :type state: State :param action_space: a list of available actions :type action_space: List[Action] :param additional_features: additional features the policy can use :type additional_features: AdditionalFeatures :return: a specific action within the confines of the action spaces :rtype: Action """ number_of_switches = len([ action for action in state.action_history if action.suggest_alternative ]) action = Action(exit=True) if len(state.action_history) == 0: # First turn user_sentiment_history = dict(state.user_sentiment_history) if state.cur_phrase not in user_sentiment_history: action = Action(solicit_opinion=True) else: action = Action(sentiment=4 - state.cur_sentiment, give_agree=True, solicit_reason=True) state.cur_sentiment = user_sentiment_history[state.cur_phrase] elif additional_features.detected_user_sentiment_switch and not state.action_history[ -1].solicit_disambiguate: action = Action(exit=True) elif state.action_history[ -1].solicit_disambiguate and additional_features.detected_no and state.cur_sentiment == 2: action = Action(exit=True) elif state.action_history[-1].solicit_opinion or state.action_history[-1].suggest_alternative \ or state.action_history[-1].solicit_disambiguate: if state.cur_sentiment == 2: action = Action(exit=True) elif number_of_switches == 0: if additional_features.detected_user_gave_reason: action = self.agree_solicit_agree(state, action_space) else: action = self.disagree_solicit_reason(state, action_space) else: if additional_features.detected_user_gave_reason: action = self.agree_solicit_agree(state, action_space) else: action = self.agree_reason_reason(state, action_space) elif state.action_history[-1].solicit_reason: if not additional_features.detected_user_disinterest: # Check if user is still interested action = self.agree_solicit_agree(state, action_space) elif state.action_history[-1].solicit_agree: if number_of_switches == 0: # Check if user is still interested action = self.agree_suggest_alternative(state, action_space) return action
def respond_neg_nav(self, state : State, wiki_entity : Optional[WikiEntity]) -> ResponseGeneratorResult: """This method generates the result when user says "change the subject" :param state: the current state :type state: State :param wiki_entity: the current WIKI entity that we are using :type wiki_entity: Optional[WikiEntity] :return: a result that can be directly returned from the get_response function :rtype: ResponseGeneratorResult """ self.logger.primary_info('NavigationalIntent is negative, so doing a hard switch out of OPINION') # type: ignore conditional_state = state.reset_state() conditional_state.last_turn_select = True return ResponseGeneratorResult( text=get_neural_fallback_handoff(self.state_manager.current_state) or "Ok, cool.", priority=ResponsePriority.WEAK_CONTINUE, needs_prompt=True, state=state, cur_entity=None, conditional_state=conditional_state)
def init_state(self) -> State: return State()
def update_state_if_not_chosen(self, state: State, conditional_state : Optional[State]) -> State: new_state = state.reset_state() new_state.num_turns_since_long_policy += 1 return new_state
def get_response(self, state : State) -> ResponseGeneratorResult: """This function defines the stages that we go through to generate the result. The procedure is 1. First populate the "additional_features" 2. Incorporate unconditional information such as user's likes and dislikes, phrases that were detected 3. Advance the state to the next state depending on the user's utterance and additional features 4. Define the action space for the policy 5. Select an action using a policy 6. Utterancify the action chosen using additional information like lists of reasons and alternatives 7. Post process the state conditioned on the action :param state: the current state :type state: State :return: a result that can be used for chirpy :rtype: ResponseGeneratorResult """ self.initialize_turn() neg_intent = self.state_manager.current_state.navigational_intent.neg_intent # type: ignore if neg_intent and self.state_manager.last_state_active_rg == 'OPINION': # type: ignore return self.respond_neg_nav(state, None) utterance = self.state_manager.current_state.text additional_features = self.populate_features(state, utterance) high_prec = self.state_manager.current_state.entity_linker.high_prec # type: ignore # should_evaluate = len(state.action_history) > 4 and not state.evaluated \ # and not state.first_episode and state.cur_policy != '' and state.cur_policy != repr(OneTurnAgreePolicy()) should_evaluate = False # Turning off evaluation question. if self.state_manager.current_state.entity_linker.high_prec and state.cur_phrase != '': # type: ignore cur_entity_name = self.opinionable_phrases[state.cur_phrase].wiki_entity_name if (cur_entity_name is None and state.cur_phrase not in [linked_span.span for linked_span in high_prec]) \ or (cur_entity_name is not None and cur_entity_name not in [linked_span.top_ent.name for linked_span in high_prec]): # If the above condition passes, it means that the linked entity is not the currently opinionating phrase. if len(additional_features.detected_phrases) == 0: # User no longer want to talk about an opinionable phrase return self.respond_neg_nav(state, random.choice(self.state_manager.current_state.entity_linker.high_prec).top_ent) # type: ignore if state.last_turn_prompt or state.last_turn_select: priority = ResponsePriority.STRONG_CONTINUE elif len(high_prec) > 0 and \ not any(linked_span.span in self.opinionable_phrases \ or linked_span.top_ent.name in self.opinionable_entities for linked_span in high_prec): # type: ignore self.logger.primary_info(f'Opinion realized that there is a high precision entity, will not CAN_START our conversation') # type: ignore priority = ResponsePriority.NO # if WhatsYourOpinion().execute(utterance) is not None: # self.logger.primary_info(f"Opinion detected user is asking for our opinion, raising priority to FORCE_START") # type: ignore # priority = ResponsePriority.FORCE_START else: priority = ResponsePriority.CAN_START if len(state.action_history) > 0 and state.action_history[-1].exit: self.logger.primary_info(f'Opinion detected our previous action is to exit and we were not selected, will reset the state before this turn starts') # type: ignore state = state.reset_state() priority = ResponsePriority.CAN_START # Drop the priority to CAN_START because we already ended a convo before # First need to incorporate the unconditional information learned from this turn state.detected_opinionated_phrases += additional_features.detected_phrases if len(additional_features.detected_phrases) > 0: # Here we only use regex since sentiment analysis may not always do well if utils.is_like(utterance)[0]: state.user_sentiment_history += tuple((phrase, 4) for phrase in additional_features.detected_phrases) elif utils.is_not_like(utterance)[0]: state.user_sentiment_history += tuple((phrase, 0) for phrase in additional_features.detected_phrases) additional_features.detected_phrases = tuple([phrase for phrase in additional_features.detected_phrases if phrase not in state.phrases_done]) # Then need to advance the state using the utterance state_p = state_actions.next_state(state, utterance, additional_features) if state_p is None or state_p.cur_phrase is None: return emptyResult(state.reset_state()) reasons_used = dict(state.reasons_used) phrase_reasons_used = set(reasons_used[state_p.cur_phrase]) if state_p.cur_phrase in reasons_used else [] pos_reasons, neg_reasons = utils.get_reasons(state_p.cur_phrase) pos_reasons = [reason for reason in pos_reasons if reason not in phrase_reasons_used] neg_reasons = [reason for reason in neg_reasons if reason not in phrase_reasons_used] related_entities = [phrase.text for phrase in self.opinionable_phrases.values() \ if phrase.category is not None and phrase.category == self.opinionable_phrases[state_p.cur_phrase].category \ and phrase.wiki_entity_name != self.opinionable_phrases[state_p.cur_phrase].wiki_entity_name] related_entities = [e for e in related_entities if e not in state_p.phrases_done] # Then need to define the action space action_space = self.get_action_space(state_p, pos_reasons, neg_reasons, related_entities) # Then need to select a policy if we don't have one ab_test_policy = self.state_manager.current_state.experiments.look_up_experiment_value('opinion_policy') # type: ignore if state_p.cur_policy != '': self.logger.primary_info(f'OPINION is using current policy {state_p.cur_policy} to respond to the user') # type: ignore elif ab_test_policy != 'random' or ab_test_policy == 'not_defined': state_p.cur_policy = ab_test_policy self.logger.primary_info(f'Opinion detected a/b test policy is {ab_test_policy}, will set current episode accordingly ') # type: ignore elif state_p.num_turns_since_long_policy < 20: policies, weights = zip(*self.short_policy_rates) state_p.cur_policy = random.choices(policies, weights, k=1)[0] # type: ignore self.logger.primary_info(f'Opinion had a long conversation {state_p.num_turns_since_long_policy} < 20 turns ago. Will use policy {state_p.cur_policy}') # type: ignore else: if state_p.last_policy in set([p for p, _ in self.disagree_policy_rates]): policies, weights = zip(*self.agree_policies_rates) elif state_p.last_policy in set([p for p, _ in self.agree_policies_rates]): policies, weights = zip(*self.agree_policies_rates) else: policies, weights = zip(*self.policy_rates) state_p.cur_policy = random.choices(policies, weights, k=1)[0] # type: ignore self.logger.primary_info(f'OPINION have no current policy, randomly picked {state_p.cur_policy} to respond to the user, resetting turn count') # type: ignore state_p.last_policy = state_p.cur_policy policy = self.policies[state_p.cur_policy] # type: ignore # Then need to get the action from a policy action = policy.get_action(state_p, action_space, additional_features) self.logger.primary_info(f'OPINION\'s strategy chose action {action}') # type: ignore action_space = self.get_action_space(state_p, pos_reasons, neg_reasons, related_entities) # Redefine action space for checks in case cur_phrase changed if action not in action_space: self.logger.error(f'OPINION policy {repr(policy)} generated an action {action} that is not in the action space {action_space}. Check policy implementation.') new_state = state.reset_state() return emptyResult(new_state) # Then need to utterancify the action text, phrase, reason = fancy_utterancify(state_p, action, pos_reasons, neg_reasons, related_entities, should_evaluate, self.state_manager.current_state.choose_least_repetitive) # type: ignore # Then need to fill the rest of the fields of state_p (through mutation) state_p = state_actions.fill_state_on_action(state_p, action, text, phrase, additional_features, reason, self.opinionable_phrases, self.opinionable_entities) state_p.last_turn_select = True state_p.last_turn_prompt = False user_sentiment_history_dict = dict(state.user_sentiment_history) wiki_entity = None if phrase != '' and phrase in user_sentiment_history_dict and user_sentiment_history_dict[phrase] > 2 \ and self.opinionable_phrases[phrase].good_for_wiki: wiki_entity = get_entity_by_wiki_name(self.opinionable_phrases[phrase].wiki_entity_name) state.last_turn_prompt, state.last_turn_select = False, False needs_prompt = False if action.exit: if len(state_p.action_history) > 6: self.logger.primary_info(f"Opinion had a conversation of length {len(state_p.action_history)}, will reset long_policy count") # type: ignore state_p.num_turns_since_long_policy = 0 if not should_evaluate: needs_prompt = True if len(state_p.action_history) < 4: self.logger.primary_info(f"Opinion only had 4 turns. Will WEAK_CONTINUE the conversation") # type: ignore priority = ResponsePriority.WEAK_CONTINUE state_p.first_episode = False return ResponseGeneratorResult(text, priority, needs_prompt, state, wiki_entity, conditional_state=state_p)