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)
Exemplo n.º 2
0
    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)