Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core] Conversation heat #644

Open
avaer opened this issue Nov 22, 2024 · 3 comments
Open

[core] Conversation heat #644

avaer opened this issue Nov 22, 2024 · 3 comments

Comments

@avaer
Copy link
Contributor

avaer commented Nov 22, 2024

We should use either heuristics or mini prepass for detecting whether the agent should reply to a given perception. We can use <PerceptionModifier> for this.

@AbdurrehmanSubhani
Copy link
Contributor

Implemented in the following PR:
#647

@AbdurrehmanSubhani
Copy link
Contributor

AbdurrehmanSubhani commented Nov 28, 2024

Agent Response Decision-Making Approaches

Overview

I've explored 2 approaches for implementing perception response decision-making in our agent architecture. Each approach aims to make agents decide when to respond in conversations by making them more socially aware of the conversation.

Approaches

1. Single-Prompt Approach

Using a combined prompt for both decision-making and action generation.

  • This approach uses a "null" Action component
  • The "null" Action is defined as an individual Action
  • The Action can is added to the list of Actions appended to the Prompt which an agent can take
  • The action does not instruct the agent when to opt for this action, rather just mentions that this action has to be taken if nothing has to be said in response
  • The instruction prompt is updated to guide the agent to choose if it should respond to the message received.
  • Additional communication instructions are provided to make the Agent naturally for more natural conversations helping the Agent know when not to reply scenarios.
  • In this pipeline both the decision to respond and the Action to pick are unified into a single API call
<Action
  name="null"
  description={dedent`\
    Choose this if you don't want to say anything in response.
  `}
  schema={
    z.object({})
  }
  examples={[
    {},
  ]}
  // handler={async (e: PendingActionEvent) => {
  //   await e.commit();
  // }}
/>

export const InstructionsPrompt = () => {
  const agent = useAgent();

  return (
    <Prompt>
      {dedent`
        # Instructions
        Respond with the next action taken by your character: ${agent.name}
        The method/args of your response must match one of the allowed actions.

        Before choosing an action, decide if you should respond at all:
        - Return null (no action) if:
          * Message is clearly meant for others (unless you have crucial information)
          * Your input wouldn't add value to the conversation
          * The conversation is naturally concluding
          * You've already responded frequently in the last few messages (2-3 messages max)
          * Multiple other agents are already actively participating
      `}
    </Prompt>
  );
};
export const DefaultCommunicationGuidelinesPrompt = () => {
  return (
    <Prompt>
      {dedent`
        # Communication Instructions
        Prioritize responding when:
          - You're directly mentioned or addressed
          - It's a group discussion where you can contribute meaningfully
          - Your personality traits are relevant to the topic
          - Avoid addressing users by name in every message - only use them when:
            * Directly responding to someone for the first time
            * Clarifying who you're addressing in a group
            * There's potential confusion about who you're talking to
          - If you've been very active in the last few messages, wrap up your participation naturally
            * Use phrases like "I'll let you all discuss" or simply stop responding
            * Don't feel obligated to respond to every message
          - Keep responses concise and natural
          - Let conversations breathe - not every message needs a response
          - If multiple agents are responding to the same person, step back and let others take the lead
      `}
    </Prompt>
  );
};

Link to this implementation PR:
#699

2. Perception Modifier Approach

Using the perception pipeline to make early decisions about response necessity.

  • This approach adds a PerceptionModifier to the incoming "say" method messages
  • In this pipeline, there is an additional LLM call made which is aimed to purely decide wether the agent should choose to respond back to this message at all.
  • Based on this response decision, the Action decision is either called of or the agent decides to choose an Action.
  • The SelfConsciousReplies uses the following parameters to make the decision: the conversation message history, members.
  • A backAndForth penalty is applied if the agent is being too active in the conversation which aims to reduce the overall confidence of the LLM to respond back
  • In the same prompt, important guidelines and considerations are mentioned which help agent make the decision for the conversations context
export type SelfConsciousRepliesProps = {
  historyLength?: number; // Number of previous messages to consider for context
  defaultThreshold?: number; // How likely the agent should respond by default (0-1)
};
export const SelfConsciousReplies: React.FC<SelfConsciousRepliesProps> = (props: SelfConsciousRepliesProps) => {
  const historyLength = props?.historyLength ?? 5;
  const defaultThreshold = props?.defaultThreshold ?? 0.6;

  return (
    <PerceptionModifier
    type="say" 
    handler={async (e: AbortablePerceptionEvent) => {
      const { message, sourceAgent, targetAgent } = e.data;

      // Get conversation members and recent messages in one pass
      const [conversationMembers, messages] = await Promise.all([
        targetAgent.conversation.getAgents(),
        targetAgent.conversation.getCachedMessages()
          .slice(-historyLength)
          .map(({name, args, timestamp}) => ({
            name,
            text: args?.text || '',
            timestamp
          }))
      ]);
      // Calculate back-and-forth agent conversation count
      // this is being calculated to determine if the agent is being in the conversation too much
      const recentMessages = messages.slice(-6);
      const backAndForthCount = recentMessages.reduce((count, msg, i) => {
        if (i === 0) return count;
        const prevMsg = recentMessages[i-1];
        return (msg.name === targetAgent.agent.name && 
                prevMsg.name === sourceAgent.name) ? count + 1 : count;
      }, 0);

      // Only apply penalty if more than 2 members in chat
      const backAndForthPenalty = conversationMembers.length > 2 ? Math.min(backAndForthCount * 0.2, 0.8) : 0;

        const decisionPrompt = `
          You are deciding whether to respond to an incoming message in a conversation.
          
          Current message: "${message?.args?.text || ''}"
          From user: ${sourceAgent.name}

          Conversation members: ${conversationMembers.map(a => `${a.playerSpec.name} (${a.playerSpec.id})`).join(', ')}
          
          Recent conversation history:
          ${messages.map(m => `${m.name}: ${m.text}`).join('\n')}
          
          Your personality (ONLY use this information to guide your response, do not make assumptions beyond it):
          ${targetAgent.agent.bio}
          Your name: ${targetAgent.agent.name}
          Your id: ${targetAgent.agent.id}
          
          Other users mentioned in the current message: ${extractMentions(message?.args?.text || '').join(', ')}
          
          CONVERSATION FATIGUE CONTEXT:
          - You and ${sourceAgent.name} have had ${backAndForthCount} back-and-forth exchanges recently
          - Your interest level is reduced by ${(backAndForthPenalty * 100).toFixed()}% due to conversation fatigue
          - If you've been going back and forth too much, you should naturally lose interest and let the conversation end
          
          Based on this context, should you respond to this message?
          
          IMPORTANT GUIDELINES:
          1. If the message is clearly addressed to someone else (via @mention or context), you should NOT respond UNLESS:
             - You have critical information that directly relates to the message and would be valuable to share
             - The information is urgent or important enough to justify interrupting
             - Not sharing this information could lead to misunderstandings or issues
          
          2. Only interrupt conversations between others in rare and justified cases. Your confidence should be very low (< 0.3) 
             if you're considering responding to a message not directed at you.
          3. If the message appears to be directed to the entire group or is a general statement/question:
             - You should be highly interested in participating
             - Your confidence should be high (> 0.7) as group discussions warrant active participation
             - Consider the value you can add to the group conversation
          
          4. Message frequency and fatigue guidelines:
             - Show significantly decreased interest after 4+ back-and-forth exchanges
             - Let conversations naturally end instead of forcing them to continue
             - If you've been talking frequently with someone, take breaks to avoid conversation fatigue
             - Consider if someone else should have a chance to speak
          
          Additional considerations:
          - Is the message explicitly directed at you? (Weight: ${defaultThreshold})
          - Is the message directed to everyone in the group? (High priority)
          - Would responding align with ONLY your defined personality traits?
          - Is your response truly necessary or would it derail the current conversation?
          - Are you certain you have unique, valuable information to add if interrupting?
          
          Respond with a decision object containing:
          - shouldRespond: boolean (true if confidence > ${defaultThreshold})
          - reason: brief explanation including specific justification if interrupting others' conversation
          - confidence: number between 0-1 (note: this will be reduced by ${(backAndForthPenalty * 100).toFixed()}% due to conversation fatigue)
        `;
        const decisionSchema = z.object({
          shouldRespond: z.boolean(),
          reason: z.string(),
          confidence: z.number(),
        });
        const decision = await targetAgent.completeJson([{
          role: 'assistant',
          content: decisionPrompt,
        }], decisionSchema,
        'openai:gpt-4o-mini',
        );
        if (!decision.content.shouldRespond) {
          e.abort();
        }
      }}
      priority={-defaultPriorityOffset * 2}
    />
  );
};

// Helper function to extract @mentions from text
const extractMentions = (text: string): string[] => {
  const mentions = text.match(/@(\w+)/g) || [];
  return mentions.map(m => m.substring(1));
};

Comparison Matrix

Aspect Single-Prompt Perception Modifier
Time Performance Fast Slow (in case the agent decides to respond only)
Complexity Low Medium
Maintainability Low High
Token Usage High (Higher if should respond) (Lower if not responding)
Debug Ease Easy Medium
Natural Flow Medium High

Pros and Cons

Single-Prompt Approach

Pros

  • Simple implementation
  • Easy to maintain
  • Unified context

Cons

  • Higher token usage
  • No early filtering

Perception Modifier Approach

Pros

  • Early filtering saves resources on no response decision
  • Lower token usage if decided not to reply
  • Clear separation of concerns

Cons

  • More complex implementation
  • Additional component management
  • Slightly more complex debugging

OODA Loop Concepts

  • Single Prompt: combines both the Orientation and Decision phase, then acts.
  • Perception Modifier: Adds in 2 decisions so doesn't really follow OODA. (Observe -> Orient + Decide (conditional if true ->) orient -> decide -> act

@avaer
Copy link
Contributor Author

avaer commented Nov 30, 2024

There are three other possibilities I can think of here:

  1. Heuristic heat
  • Use game engine like heuristic statistics to decide whether to respond
  • For example, each incoming message bumps the heat metric, with an exponential decay over time, and boosts for mentions, media, and message length
  1. Uniforms as heat updates
  • We can use the uniforms feature to add additional conversation heat update metadata after each action
  • For example, the uniform can rate whether the agent is bored, how long they should wait to perform the next action, etc.
  1. Asynchronous heat loop
  • Run an asynchronous thought loop whose only purpose is to maintain the conversation heat state, as in 2)
  • The advantage is that it doesn't add any latency or overhead to the main loop

For any of these techniques that provide some value to use cases, it makes sense to have them be a configurable component.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants