From 65ec7e4f8595bacde4cfe8a9c854939253c574f8 Mon Sep 17 00:00:00 2001 From: azep-ninja Date: Thu, 12 Dec 2024 11:01:08 -0700 Subject: [PATCH] Fixed PR review, optimized Discord, and Telegram fix. --- packages/client-discord/src/constants.ts | 85 ++++ packages/client-discord/src/messages.ts | 424 +++++++++++----- packages/client-discord/src/utils.ts | 81 ++- packages/client-telegram/src/constants.ts | 38 ++ .../client-telegram/src/messageManager.ts | 467 +++++++++++++++++- packages/client-telegram/src/utils.ts | 97 ++++ packages/core/src/types.ts | 2 + 7 files changed, 1062 insertions(+), 132 deletions(-) create mode 100644 packages/client-discord/src/constants.ts create mode 100644 packages/client-telegram/src/constants.ts create mode 100644 packages/client-telegram/src/utils.ts diff --git a/packages/client-discord/src/constants.ts b/packages/client-discord/src/constants.ts new file mode 100644 index 0000000000..2bc2346410 --- /dev/null +++ b/packages/client-discord/src/constants.ts @@ -0,0 +1,85 @@ +export const TEAM_COORDINATION = { + KEYWORDS: [ + 'team', + 'everyone', + 'all agents', + 'team update', + 'gm team', + 'hello team', + 'hey team', + 'hi team', + 'morning team', + 'evening team', + 'night team', + 'update team', + ] +} as const; + +export const MESSAGE_CONSTANTS = { + MAX_MESSAGES: 10, + RECENT_MESSAGE_COUNT: 3, + CHAT_HISTORY_COUNT: 5, + INTEREST_DECAY_TIME: 5 * 60 * 1000, // 5 minutes + PARTIAL_INTEREST_DECAY: 3 * 60 * 1000, // 3 minutes + DEFAULT_SIMILARITY_THRESHOLD: 0.3, + DEFAULT_SIMILARITY_THRESHOLD_FOLLOW_UPS: 0.20, +} as const; + +export const MESSAGE_LENGTH_THRESHOLDS = { + LOSE_INTEREST: 100, + SHORT_MESSAGE: 10, + VERY_SHORT_MESSAGE: 2, + IGNORE_RESPONSE: 4, +} as const; + +export const TIMING_CONSTANTS = { + LEADER_RESPONSE_TIMEOUT: 3000, + TEAM_MEMBER_DELAY: 1500, + LEADER_DELAY_MIN: 3000, + LEADER_DELAY_MAX: 4000, + TEAM_MEMBER_DELAY_MIN: 1000, + TEAM_MEMBER_DELAY_MAX: 3000, +} as const; + +export const RESPONSE_CHANCES = { + AFTER_LEADER: 0.5, // 50% chance + FREQUENT_CHATTER: 0.5, // Base chance for frequent responders +} as const; + +export const LOSE_INTEREST_WORDS = [ + "shut up", + "stop", + "please shut up", + "shut up please", + "dont talk", + "silence", + "stop talking", + "be quiet", + "hush", + "wtf", + "chill", + "stfu", + "stupid bot", + "dumb bot", + "stop responding", + "god damn it", + "god damn", + "goddamnit", + "can you not", + "can you stop", + "be quiet", + "hate you", + "hate this", + "fuck up", +] as const; + +export const IGNORE_RESPONSE_WORDS = [ + "lol", + "nm", + "uh", + "wtf", + "stfu", + "dumb", + "jfc", + "omg", +] as const; \ No newline at end of file diff --git a/packages/client-discord/src/messages.ts b/packages/client-discord/src/messages.ts index 85b4d2940f..d82ed169cf 100644 --- a/packages/client-discord/src/messages.ts +++ b/packages/client-discord/src/messages.ts @@ -28,13 +28,21 @@ import { discordShouldRespondTemplate, discordMessageHandlerTemplate, } from "./templates.ts"; -import { sendMessageInChunks, canSendMessage } from "./utils.ts"; +import { IGNORE_RESPONSE_WORDS, LOSE_INTEREST_WORDS, MESSAGE_CONSTANTS, MESSAGE_LENGTH_THRESHOLDS, RESPONSE_CHANCES, TEAM_COORDINATION, TIMING_CONSTANTS } from './constants'; +import { sendMessageInChunks, canSendMessage, cosineSimilarity } from "./utils.ts"; + +interface MessageContext { + content: string; + timestamp: number; +} export type InterestChannels = { [key: string]: { - currentHandler: any; + currentHandler: string | undefined; lastMessageSent: number; messages: { userId: UUID; userName: string; content: Content }[]; + previousContext?: MessageContext; + contextSimilarityThreshold?: number; }; }; @@ -71,24 +79,57 @@ export class MessageManager { return; } - const isDirectlyMentioned = this._isMessageForMe(message); - // Check for mentions-only mode - if (this.runtime.character.clientConfig?.discord?.shouldRespondOnlyToMentions && !isDirectlyMentioned) { + if (this.runtime.character.clientConfig?.discord?.shouldRespondOnlyToMentions) { + if(!this._isMessageForMe(message)) { + return; + } + } + + if ( + this.runtime.character.clientConfig?.discord + ?.shouldIgnoreDirectMessages && + message.channel.type === ChannelType.DM + ) { return; } + const userId = message.author.id as UUID; + const userName = message.author.username; + const name = message.author.displayName; + const channelId = message.channel.id; + const isDirectlyMentioned = this._isMessageForMe(message); + const hasInterest = this._checkInterest(message.channelId); + // Team handling if (this.runtime.character.clientConfig?.discord?.isPartOfTeam && !this.runtime.character.clientConfig?.discord?.shouldRespondOnlyToMentions) { const authorId = this._getNormalizedUserId(message.author.id); - const hasInterest = this._checkInterest(message.channelId); + if (!this._isTeamLeader() && this._isRelevantToTeamMember(message.content, channelId)) { + this.interestChannels[message.channelId] = { + currentHandler: this.client.user?.id, + lastMessageSent: Date.now(), + messages: [] + }; + } + const isTeamRequest = this._isTeamCoordinationRequest(message.content); const isLeader = this._isTeamLeader(); // After team-wide responses, check if we should maintain interest if (hasInterest && !isDirectlyMentioned) { - const isRelevant = this._isRelevantToTeamMember(message.content); + const lastSelfMemories = await this.runtime.messageManager.getMemories({ + roomId: stringToUuid(channelId + "-" + this.runtime.agentId), + unique: false, + count: 5 + }); + + const lastSelfSortedMemories = lastSelfMemories?.filter(m => m.userId === this.runtime.agentId).sort((a, b) => + (b.createdAt || 0) - (a.createdAt || 0) + ); + + const isRelevant = this._isRelevantToTeamMember(message.content, channelId, lastSelfSortedMemories?.[0]); + if (!isRelevant) { // Clearing interest - conversation not relevant to team member delete this.interestChannels[message.channelId]; @@ -104,9 +145,6 @@ export class MessageManager { messages: [] }; } else { - // Non-leader team members should respond to team requests - const isFirstResponse = !this.interestChannels[message.channelId]?.currentHandler; - // Set temporary interest for this response this.interestChannels[message.channelId] = { currentHandler: this.client.user?.id, @@ -149,17 +187,13 @@ export class MessageManager { lastMessageSent: Date.now(), messages: [] }; - } else if (!isTeamRequest && !hasInterest || this.runtime.character.clientConfig?.discord?.shouldRespondOnlyToMentions) { + } else if (!isTeamRequest && !hasInterest) { return; } // Bot-specific checks if (message.author.bot) { if (this._isTeamMember(authorId) && !isDirectlyMentioned) { - elizaLogger.log('Ignoring teammate bot message - not mentioned', { - authorId, - agentId: this.runtime.agentId - }); return; } else if (this.runtime.character.clientConfig.discord.shouldIgnoreBotMessages) { return; @@ -167,19 +201,6 @@ export class MessageManager { } } - if ( - this.runtime.character.clientConfig?.discord - ?.shouldIgnoreDirectMessages && - message.channel.type === ChannelType.DM - ) { - return; - } - - const userId = message.author.id as UUID; - const userName = message.author.username; - const name = message.author.displayName; - const channelId = message.channel.id; - try { const { processedContent, attachments } = await this.processMessageMedia(message); @@ -247,6 +268,21 @@ export class MessageManager { if (content.text) { await this.runtime.messageManager.addEmbeddingToMemory(memory); await this.runtime.messageManager.createMemory(memory); + + if (this.interestChannels[message.channelId]) { + // Add new message + this.interestChannels[message.channelId].messages.push({ + userId: userIdUUID, + userName: userName, + content: content + }); + + // Trim to keep only recent messages + if (this.interestChannels[message.channelId].messages.length > MESSAGE_CONSTANTS.MAX_MESSAGES) { + this.interestChannels[message.channelId].messages = + this.interestChannels[message.channelId].messages.slice(-MESSAGE_CONSTANTS.MAX_MESSAGES); + } + } } let state = await this.runtime.composeState(userMessage, { @@ -272,7 +308,6 @@ export class MessageManager { if (shouldIgnore) { return; } - const hasInterest = this._checkInterest(channelId); const agentUserState = await this.runtime.databaseAdapter.getParticipantUserState( @@ -542,46 +577,29 @@ export class MessageManager { } private _isTeamCoordinationRequest(content: string): boolean { - const coordinationKeywords = [ - 'team', - 'coordinate', - 'everyone', - 'all agents', - 'team update', - 'status update', - 'report', - 'gm team', - 'gm all', - 'hello team', - 'hey team', - 'hi team', - 'morning team', - 'evening team', - 'night team', - 'update team', - 'anyone', - 'anybody', - 'rest of', - 'others', - 'you guys', - 'you all', - "y'all", - 'yall' - ]; - const contentLower = content.toLowerCase(); - return coordinationKeywords.some(keyword => + return TEAM_COORDINATION.KEYWORDS?.some(keyword => contentLower.includes(keyword.toLowerCase()) ); } - private _isRelevantToTeamMember(content: string): boolean { - // Team leader always maintains general conversation - if (this._isTeamLeader()) { - return true; + private _isRelevantToTeamMember(content: string, channelId: string, lastAgentMemory: Memory | null = null): boolean { + const teamConfig = this.runtime.character.clientConfig?.discord; + + if (this._isTeamLeader() && lastAgentMemory?.content.text) { + const timeSinceLastMessage = Date.now() - lastAgentMemory.createdAt; + if (timeSinceLastMessage > MESSAGE_CONSTANTS.INTEREST_DECAY_TIME) { + return false; // Memory too old, not relevant + } + + const similarity = cosineSimilarity( + content.toLowerCase(), + lastAgentMemory.content.text.toLowerCase() + ); + + return similarity >= MESSAGE_CONSTANTS.DEFAULT_SIMILARITY_THRESHOLD_FOLLOW_UPS; } - const teamConfig = this.runtime.character.clientConfig?.discord; // If no keywords defined, only leader maintains conversation if (!teamConfig?.teamMemberInterestKeywords) { return false; @@ -593,7 +611,7 @@ export class MessageManager { } private _isMessageForMe(message: DiscordMessage): boolean { - const isMentioned = message.mentions.has(this.client.user?.id as string); + const isMentioned = message.mentions.users?.has(this.client.user?.id as string); const guild = message.guild; const member = guild?.members.cache.get(this.client.user?.id as string); const nickname = member?.nickname; @@ -607,28 +625,115 @@ export class MessageManager { return false; } - return isMentioned || + return isMentioned || (!this.runtime.character.clientConfig?.discord?.shouldRespondOnlyToMentions && ( message.content.toLowerCase().includes(this.client.user?.username.toLowerCase() as string) || message.content.toLowerCase().includes(this.client.user?.tag.toLowerCase() as string) || - (nickname && message.content.toLowerCase().includes(nickname.toLowerCase())); + (nickname && message.content.toLowerCase().includes(nickname.toLowerCase())))); } - private _checkInterest(channelId: string): boolean { - //return !!this.interestChannels[channelId]; + private async _analyzeContextSimilarity(currentMessage: string, previousContext?: MessageContext, agentLastMessage?: string): Promise { + if (!previousContext) return 1; // No previous context to compare against + + // If more than 5 minutes have passed, reduce similarity weight + const timeDiff = Date.now() - previousContext.timestamp; + const timeWeight = Math.max(0, 1 - (timeDiff / (5 * 60 * 1000))); // 5 minutes threshold + + // Calculate content similarity + const similarity = cosineSimilarity( + currentMessage.toLowerCase(), + previousContext.content.toLowerCase(), + agentLastMessage?.toLowerCase() + ); + + // Weight the similarity by time factor + const weightedSimilarity = similarity * timeWeight; + + return weightedSimilarity; + } + + private async _shouldRespondBasedOnContext(message: DiscordMessage, channelState: InterestChannels[string]): Promise { + // Always respond if directly mentioned + if (this._isMessageForMe(message)) return true; + + // If we're not the current handler, don't respond + if (channelState?.currentHandler !== this.client.user?.id) return false; + + // Check if we have messages to compare + if (!channelState.messages?.length) return false; + + // Get last user message (not from the bot) + const lastUserMessage = [...channelState.messages] + .reverse() + .find((m, index) => + index > 0 && // Skip first message (current) + m.userId !== this.runtime.agentId + ); + + if (!lastUserMessage) return false; + + const lastSelfMemories = await this.runtime.messageManager.getMemories({ + roomId: stringToUuid(message.channel.id + "-" + this.runtime.agentId), + unique: false, + count: 5 + }); + + const lastSelfSortedMemories = lastSelfMemories?.filter(m => m.userId === this.runtime.agentId).sort((a, b) => + (b.createdAt || 0) - (a.createdAt || 0) + ); + + // Calculate context similarity + const contextSimilarity = await this._analyzeContextSimilarity( + message.content, + { + content: lastUserMessage.content.text || '', + timestamp: Date.now() + }, + lastSelfSortedMemories?.[0]?.content?.text + ); + + const similarityThreshold = + this.runtime.character.clientConfig?.discord?.messageSimilarityThreshold || + channelState.contextSimilarityThreshold || + MESSAGE_CONSTANTS.DEFAULT_SIMILARITY_THRESHOLD; + return contextSimilarity >= similarityThreshold; + } + + private _checkInterest(channelId: string): boolean { const channelState = this.interestChannels[channelId]; if (!channelState) return false; + const lastMessage = channelState.messages[channelState.messages.length - 1]; // If it's been more than 5 minutes since last message, reduce interest const timeSinceLastMessage = Date.now() - channelState.lastMessageSent; - if (timeSinceLastMessage > 5 * 60 * 1000) { // 5 minutes + + if (timeSinceLastMessage > MESSAGE_CONSTANTS.INTEREST_DECAY_TIME) { delete this.interestChannels[channelId]; return false; + } else if (timeSinceLastMessage > MESSAGE_CONSTANTS.PARTIAL_INTEREST_DECAY) { + // Require stronger relevance for continued interest + return this._isRelevantToTeamMember(lastMessage.content.text || '', channelId) + } + + // If team leader and messages exist, check for topic changes and team member responses + if (this._isTeamLeader() && channelState.messages.length > 0) { + // If leader's keywords don't match and another team member has responded, drop interest + if (!this._isRelevantToTeamMember(lastMessage.content.text || '', channelId)) { + const recentTeamResponses = channelState.messages.slice(-3).some(m => + m.userId !== this.client.user?.id && + this._isTeamMember(m.userId) + ); + + if (recentTeamResponses) { + delete this.interestChannels[channelId]; + return false; + } + } } // Check if conversation has shifted to a new topic if (channelState.messages.length > 0) { - const recentMessages = channelState.messages.slice(-3); // Look at last 3 messages + const recentMessages = channelState.messages.slice(-MESSAGE_CONSTANTS.RECENT_MESSAGE_COUNT); const differentUsers = new Set(recentMessages.map(m => m.userId)).size; // If multiple users are talking and we're not involved, reduce interest @@ -654,29 +759,64 @@ export class MessageManager { if (this.runtime.character.clientConfig?.discord?.isPartOfTeam) { const authorId = this._getNormalizedUserId(message.author.id); - // Team leader specific logic - if (this._isTeamLeader() && this._isTeamCoordinationRequest(message.content)) { + if (this._isTeamLeader()) { + if(this._isTeamCoordinationRequest(message.content)) { + return false; + } + // Ignore if message is only about team member interests and not directed to leader + if (!this._isMessageForMe(message)) { + const otherMemberInterests = this.runtime.character.clientConfig?.discord?.teamMemberInterestKeywords || []; + const hasOtherInterests = otherMemberInterests.some(keyword => + message.content.toLowerCase().includes(keyword.toLowerCase()) + ); + if (hasOtherInterests) { + return true; + } + } + } else if (this._isTeamCoordinationRequest(message.content)) { + const randomDelay = Math.floor(Math.random() * (TIMING_CONSTANTS.TEAM_MEMBER_DELAY_MAX - TIMING_CONSTANTS.TEAM_MEMBER_DELAY_MIN)) + + TIMING_CONSTANTS.TEAM_MEMBER_DELAY_MIN; // 1-3 second random delay + await new Promise(resolve => setTimeout(resolve, randomDelay)); return false; } if (this._isTeamMember(authorId)) { if (!this._isMessageForMe(message)) { + // If message contains our interests, don't ignore + if (this._isRelevantToTeamMember(message.content, message.channelId)) { + return false; + } return true; } } - // Check if another team agent is handling the conversation + // Check if we're in an active conversation based on context const channelState = this.interestChannels[message.channelId]; + if (channelState?.currentHandler) { - if (!this._isMessageForMe(message) && - !this._isTeamCoordinationRequest(message.content)) { - // Randomly ignore some messages to make conversation more natural - const shouldIgnoreRandom = Math.random() > 0.3; // 30% chance to respond - if (shouldIgnoreRandom) { - return true; + + // If we're the current handler, check context + if (channelState.currentHandler === this.client.user?.id) { + //If it's our keywords, bypass context check + if (this._isRelevantToTeamMember(message.content, message.channelId)) { + return false; } + + const shouldRespondContext = await this._shouldRespondBasedOnContext( + message, + channelState + ); + + // If context is different, ignore. If similar, don't ignore + return !shouldRespondContext; } - } + + // If another team member is handling and we're not mentioned or coordinating + else if (!this._isMessageForMe(message) && + !this._isTeamCoordinationRequest(message.content)) { + return true; + } + } } let messageContent = message.content.toLowerCase(); @@ -698,36 +838,10 @@ export class MessageManager { // strip all special characters messageContent = messageContent.replace(/[^a-zA-Z0-9\s]/g, ""); - // short responses where ruby should stop talking and disengage unless mentioned again - const loseInterestWords = [ - "shut up", - "stop", - "please shut up", - "shut up please", - "dont talk", - "silence", - "stop talking", - "be quiet", - "hush", - "wtf", - "chill", - "stfu", - "stupid bot", - "dumb bot", - "stop responding", - "god damn it", - "god damn", - "goddamnit", - "can you not", - "can you stop", - "be quiet", - "hate you", - "hate this", - "fuck up", - ]; + // short responses where eliza should stop talking and disengage unless mentioned again if ( - messageContent.length < 100 && - loseInterestWords.some((word) => messageContent.includes(word)) + messageContent.length < MESSAGE_LENGTH_THRESHOLDS.LOSE_INTEREST && + LOSE_INTEREST_WORDS.some((word) => messageContent.includes(word)) ) { delete this.interestChannels[message.channelId]; return true; @@ -735,7 +849,7 @@ export class MessageManager { // If we're not interested in the channel and it's a short message, ignore it if ( - messageContent.length < 10 && + messageContent.length < MESSAGE_LENGTH_THRESHOLDS.SHORT_MESSAGE && !this.interestChannels[message.channelId] ) { return true; @@ -765,24 +879,14 @@ export class MessageManager { // if the message is short, ignore but maintain interest if ( !this.interestChannels[message.channelId] && - messageContent.length < 2 + messageContent.length < MESSAGE_LENGTH_THRESHOLDS.VERY_SHORT_MESSAGE ) { return true; } - const ignoreResponseWords = [ - "lol", - "nm", - "uh", - "wtf", - "stfu", - "dumb", - "jfc", - "omg", - ]; if ( - message.content.length < 4 && - ignoreResponseWords.some((word) => + message.content.length < MESSAGE_LENGTH_THRESHOLDS.IGNORE_RESPONSE && + IGNORE_RESPONSE_WORDS.some((word) => message.content.toLowerCase().includes(word) ) ) { @@ -803,6 +907,15 @@ export class MessageManager { return this._isMessageForMe(message); } + const channelState = this.interestChannels[message.channelId]; + + // Check if team member has direct interest first + if (this.runtime.character.clientConfig?.discord?.isPartOfTeam && + !this._isTeamLeader() && + this._isRelevantToTeamMember(message.content, message.channelId)) { + return true; + } + try { // Team-based response logic if (this.runtime.character.clientConfig?.discord?.isPartOfTeam) { @@ -813,6 +926,48 @@ export class MessageManager { return true; } + if (!this._isTeamLeader() && this._isRelevantToTeamMember(message.content, message.channelId)) { + // Add small delay for non-leader responses + await new Promise(resolve => setTimeout(resolve, TIMING_CONSTANTS.TEAM_MEMBER_DELAY)); //1.5 second delay + + // If leader has responded in last few seconds, reduce chance of responding + + if (channelState?.messages?.length) { + const recentMessages = channelState.messages.slice(-MESSAGE_CONSTANTS.RECENT_MESSAGE_COUNT); + const leaderResponded = recentMessages.some(m => + m.userId === this.runtime.character.clientConfig?.discord?.teamLeaderId && + Date.now() - channelState.lastMessageSent < 3000 + ); + + if (leaderResponded) { + // 50% chance to respond if leader just did + return Math.random() > RESPONSE_CHANCES.AFTER_LEADER; + } + } + + return true; + } + + // If I'm the leader but message doesn't match my keywords, add delay and check for team responses + if (this._isTeamLeader() && !this._isRelevantToTeamMember(message.content, message.channelId)) { + const randomDelay = Math.floor(Math.random() * (TIMING_CONSTANTS.LEADER_DELAY_MAX - TIMING_CONSTANTS.LEADER_DELAY_MIN)) + + TIMING_CONSTANTS.LEADER_DELAY_MIN; // 2-4 second random delay + await new Promise(resolve => setTimeout(resolve, randomDelay)); + + // After delay, check if another team member has already responded + if (channelState?.messages?.length) { + const recentResponses = channelState.messages.slice(-MESSAGE_CONSTANTS.RECENT_MESSAGE_COUNT); + const otherTeamMemberResponded = recentResponses.some(m => + m.userId !== this.client.user?.id && + this._isTeamMember(m.userId) + ); + + if (otherTeamMemberResponded) { + return false; + } + } + } + // Update current handler if we're mentioned if (this._isMessageForMe(message)) { const channelState = this.interestChannels[message.channelId]; @@ -824,7 +979,6 @@ export class MessageManager { } // Don't respond if another teammate is handling the conversation - const channelState = this.interestChannels[message.channelId]; if (channelState?.currentHandler) { if (channelState.currentHandler !== this.client.user?.id && this._isTeamMember(channelState.currentHandler)) { @@ -835,7 +989,7 @@ export class MessageManager { // Natural conversation cadence if (!this._isMessageForMe(message) && channelState) { // Count our recent messages - const recentMessages = channelState.messages.slice(-5); + const recentMessages = channelState.messages.slice(-MESSAGE_CONSTANTS.CHAT_HISTORY_COUNT); const ourMessageCount = recentMessages.filter(m => m.userId === this.client.user?.id ).length; @@ -859,6 +1013,18 @@ export class MessageManager { }); } + // Otherwise do context check + if (channelState?.previousContext) { + const shouldRespondContext = await this._shouldRespondBasedOnContext( + message, + channelState + ); + if (!shouldRespondContext) { + delete this.interestChannels[message.channelId]; + return false; + } + } + if (message.mentions.has(this.client.user?.id as string)) return true; const guild = message.guild; @@ -899,9 +1065,11 @@ export class MessageManager { }); if (response === "RESPOND") { - // Randomness to responses for more natural flow - if (!this._isMessageForMe(message) && Math.random() > 0.7) { - return false; + if (channelState) { + channelState.previousContext = { + content: message.content, + timestamp: Date.now() + }; } return true; @@ -966,4 +1134,4 @@ export class MessageManager { const data = await response.json(); return data.username; } -} +} \ No newline at end of file diff --git a/packages/client-discord/src/utils.ts b/packages/client-discord/src/utils.ts index 053c3d4ccb..6ee1e98871 100644 --- a/packages/client-discord/src/utils.ts +++ b/packages/client-discord/src/utils.ts @@ -50,11 +50,11 @@ export async function generateSummary( text = trimTokens(text, 100000, "gpt-4o-mini"); // TODO: clean this up const prompt = `Please generate a concise summary for the following text: - + Text: """ ${text} """ - + Respond with a JSON object in the following format: \`\`\`json { @@ -221,3 +221,80 @@ export function canSendMessage(channel) { : null, }; } + +export function cosineSimilarity(text1: string, text2: string, text3?: string): number { + const preprocessText = (text: string) => text + .toLowerCase() + .replace(/[^\w\s'_-]/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + + const getWords = (text: string) => { + return text.split(' ').filter(word => word.length > 1); + }; + + const words1 = getWords(preprocessText(text1)); + const words2 = getWords(preprocessText(text2)); + const words3 = text3 ? getWords(preprocessText(text3)) : []; + + const freq1: { [key: string]: number } = {}; + const freq2: { [key: string]: number } = {}; + const freq3: { [key: string]: number } = {}; + + words1.forEach(word => freq1[word] = (freq1[word] || 0) + 1); + words2.forEach(word => freq2[word] = (freq2[word] || 0) + 1); + if (words3.length) { + words3.forEach(word => freq3[word] = (freq3[word] || 0) + 1); + } + + const uniqueWords = new Set([...Object.keys(freq1), ...Object.keys(freq2), ...(words3.length ? Object.keys(freq3) : [])]); + + let dotProduct = 0; + let magnitude1 = 0; + let magnitude2 = 0; + let magnitude3 = 0; + + uniqueWords.forEach(word => { + const val1 = freq1[word] || 0; + const val2 = freq2[word] || 0; + const val3 = freq3[word] || 0; + + if (words3.length) { + // For three-way, calculate pairwise similarities + const sim12 = val1 * val2; + const sim23 = val2 * val3; + const sim13 = val1 * val3; + + // Take maximum similarity between any pair + dotProduct += Math.max(sim12, sim23, sim13); + } else { + dotProduct += val1 * val2; + } + + magnitude1 += val1 * val1; + magnitude2 += val2 * val2; + if (words3.length) { + magnitude3 += val3 * val3; + } + }); + + magnitude1 = Math.sqrt(magnitude1); + magnitude2 = Math.sqrt(magnitude2); + magnitude3 = words3.length ? Math.sqrt(magnitude3) : 1; + + if (magnitude1 === 0 || magnitude2 === 0 || (words3.length && magnitude3 === 0)) return 0; + + // For two texts, use original calculation + if (!words3.length) { + return dotProduct / (magnitude1 * magnitude2); + } + + // For three texts, use max magnitude pair to maintain scale + const maxMagnitude = Math.max( + magnitude1 * magnitude2, + magnitude2 * magnitude3, + magnitude1 * magnitude3 + ); + + return dotProduct / maxMagnitude; +} \ No newline at end of file diff --git a/packages/client-telegram/src/constants.ts b/packages/client-telegram/src/constants.ts new file mode 100644 index 0000000000..f377019e1a --- /dev/null +++ b/packages/client-telegram/src/constants.ts @@ -0,0 +1,38 @@ +export const MESSAGE_CONSTANTS = { + MAX_MESSAGES: 50, + RECENT_MESSAGE_COUNT: 5, + CHAT_HISTORY_COUNT: 10, + DEFAULT_SIMILARITY_THRESHOLD: 0.6, + DEFAULT_SIMILARITY_THRESHOLD_FOLLOW_UPS: 0.4, + INTEREST_DECAY_TIME: 5 * 60 * 1000, // 5 minutes + PARTIAL_INTEREST_DECAY: 3 * 60 * 1000, // 3 minutes +} as const; + +export const TIMING_CONSTANTS = { + TEAM_MEMBER_DELAY: 1500, // 1.5 seconds + TEAM_MEMBER_DELAY_MIN: 1000, // 1 second + TEAM_MEMBER_DELAY_MAX: 3000, // 3 seconds + LEADER_DELAY_MIN: 2000, // 2 seconds + LEADER_DELAY_MAX: 4000 // 4 seconds +} as const; + +export const RESPONSE_CHANCES = { + AFTER_LEADER: 0.5, // 50% chance to respond after leader +} as const; + +export const TEAM_COORDINATION = { + KEYWORDS: [ + 'team', + 'everyone', + 'all agents', + 'team update', + 'gm team', + 'hello team', + 'hey team', + 'hi team', + 'morning team', + 'evening team', + 'night team', + 'update team', + ] +} as const; \ No newline at end of file diff --git a/packages/client-telegram/src/messageManager.ts b/packages/client-telegram/src/messageManager.ts index 8eb9154f24..f0f0421b75 100644 --- a/packages/client-telegram/src/messageManager.ts +++ b/packages/client-telegram/src/messageManager.ts @@ -18,6 +18,14 @@ import { stringToUuid } from "@ai16z/eliza"; import { generateMessageResponse, generateShouldRespond } from "@ai16z/eliza"; import { messageCompletionFooter, shouldRespondFooter } from "@ai16z/eliza"; +import { cosineSimilarity } from "./utils"; +import { + MESSAGE_CONSTANTS, + TIMING_CONSTANTS, + RESPONSE_CHANCES, + TEAM_COORDINATION +} from "./constants"; + const MAX_MESSAGE_LENGTH = 4096; // Telegram's max message length const telegramShouldRespondTemplate = @@ -133,13 +141,223 @@ Thread of Tweets You Are Replying To: {{formattedConversation}} ` + messageCompletionFooter; +interface MessageContext { + content: string; + timestamp: number; +} + +export type InterestChats = { + [key: string]: { + currentHandler: string | undefined; + lastMessageSent: number; + messages: { userId: UUID; userName: string; content: Content }[]; + previousContext?: MessageContext; + contextSimilarityThreshold?: number; + }; +}; + export class MessageManager { public bot: Telegraf; private runtime: IAgentRuntime; + private interestChats: InterestChats = {}; + private teamMemberUsernames: Map = new Map(); constructor(bot: Telegraf, runtime: IAgentRuntime) { this.bot = bot; this.runtime = runtime; + + this.initializeTeamMemberUsernames().catch(error => + elizaLogger.error("Error initializing team member usernames:", error) + ); + } + + private async initializeTeamMemberUsernames(): Promise { + if (!this.runtime.character.clientConfig?.telegram?.isPartOfTeam) return; + + const teamAgentIds = this.runtime.character.clientConfig.telegram.teamAgentIds || []; + + for (const id of teamAgentIds) { + try { + const chat = await this.bot.telegram.getChat(id); + if ('username' in chat && chat.username) { + this.teamMemberUsernames.set(id, chat.username); + elizaLogger.info(`Cached username for team member ${id}: ${chat.username}`); + } + } catch (error) { + elizaLogger.error(`Error getting username for team member ${id}:`, error); + } + } + } + + private getTeamMemberUsername(id: string): string | undefined { + return this.teamMemberUsernames.get(id); + } + + private _getNormalizedUserId(id: string | number): string { + return id.toString().replace(/[^0-9]/g, ''); + } + + private _isTeamMember(userId: string | number): boolean { + const teamConfig = this.runtime.character.clientConfig?.telegram; + if (!teamConfig?.isPartOfTeam || !teamConfig.teamAgentIds) return false; + + const normalizedUserId = this._getNormalizedUserId(userId); + return teamConfig.teamAgentIds.some(teamId => + this._getNormalizedUserId(teamId) === normalizedUserId + ); + } + + private _isTeamLeader(): boolean { + return this.bot.botInfo?.id.toString() === this.runtime.character.clientConfig?.telegram?.teamLeaderId; + } + + private _isTeamCoordinationRequest(content: string): boolean { + const contentLower = content.toLowerCase(); + return TEAM_COORDINATION.KEYWORDS?.some(keyword => + contentLower.includes(keyword.toLowerCase()) + ); + } + + private _isRelevantToTeamMember(content: string, chatId: string, lastAgentMemory: Memory | null = null): boolean { + const teamConfig = this.runtime.character.clientConfig?.telegram; + + // Check leader's context based on last message + if (this._isTeamLeader() && lastAgentMemory?.content.text) { + const timeSinceLastMessage = Date.now() - lastAgentMemory.createdAt; + if (timeSinceLastMessage > MESSAGE_CONSTANTS.INTEREST_DECAY_TIME) { + return false; + } + + const similarity = cosineSimilarity( + content.toLowerCase(), + lastAgentMemory.content.text.toLowerCase() + ); + + return similarity >= MESSAGE_CONSTANTS.DEFAULT_SIMILARITY_THRESHOLD_FOLLOW_UPS; + } + + // Check team member keywords + if (!teamConfig?.teamMemberInterestKeywords?.length) { + return false; // If no keywords defined, only leader maintains conversation + } + + // Check if content matches any team member keywords + return teamConfig.teamMemberInterestKeywords.some(keyword => + content.toLowerCase().includes(keyword.toLowerCase()) + ); + } + + private async _analyzeContextSimilarity(currentMessage: string, previousContext?: MessageContext, agentLastMessage?: string): Promise { + if (!previousContext) return 1; + + const timeDiff = Date.now() - previousContext.timestamp; + const timeWeight = Math.max(0, 1 - (timeDiff / (5 * 60 * 1000))); + + const similarity = cosineSimilarity( + currentMessage.toLowerCase(), + previousContext.content.toLowerCase(), + agentLastMessage?.toLowerCase() + ); + + return similarity * timeWeight; + } + + private async _shouldRespondBasedOnContext(message: Message, chatState: InterestChats[string]): Promise { + const messageText = 'text' in message ? message.text : + 'caption' in message ? (message as any).caption : ''; + + if (!messageText) return false; + + // Always respond if mentioned + if (this._isMessageForMe(message)) return true; + + // If we're not the current handler, don't respond + if (chatState?.currentHandler !== this.bot.botInfo?.id.toString()) return false; + + // Check if we have messages to compare + if (!chatState.messages?.length) return false; + + // Get last user message (not from the bot) + const lastUserMessage = [...chatState.messages] + .reverse() + .find((m, index) => + index > 0 && // Skip first message (current) + m.userId !== this.runtime.agentId + ); + + if (!lastUserMessage) return false; + + const lastSelfMemories = await this.runtime.messageManager.getMemories({ + roomId: stringToUuid(message.chat.id.toString() + "-" + this.runtime.agentId), + unique: false, + count: 5 + }); + + const lastSelfSortedMemories = lastSelfMemories?.filter(m => m.userId === this.runtime.agentId) + .sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)); + + // Calculate context similarity + const contextSimilarity = await this._analyzeContextSimilarity( + messageText, + { + content: lastUserMessage.content.text || '', + timestamp: Date.now() + }, + lastSelfSortedMemories?.[0]?.content?.text + ); + + const similarityThreshold = + this.runtime.character.clientConfig?.telegram?.messageSimilarityThreshold || + chatState.contextSimilarityThreshold || + MESSAGE_CONSTANTS.DEFAULT_SIMILARITY_THRESHOLD; + + return contextSimilarity >= similarityThreshold; + } + + private _isMessageForMe(message: Message): boolean { + const botUsername = this.bot.botInfo?.username; + if (!botUsername) return false; + + const messageText = 'text' in message ? message.text : + 'caption' in message ? (message as any).caption : ''; + if (!messageText) return false; + + const isMentioned = messageText.includes(`@${botUsername}`); + const hasUsername = messageText.toLowerCase().includes(botUsername.toLowerCase()); + + return isMentioned || (!this.runtime.character.clientConfig?.telegram?.shouldRespondOnlyToMentions && hasUsername); + } + + private _checkInterest(chatId: string): boolean { + const chatState = this.interestChats[chatId]; + if (!chatState) return false; + + const lastMessage = chatState.messages[chatState.messages.length - 1]; + const timeSinceLastMessage = Date.now() - chatState.lastMessageSent; + + if (timeSinceLastMessage > MESSAGE_CONSTANTS.INTEREST_DECAY_TIME) { + delete this.interestChats[chatId]; + return false; + } else if (timeSinceLastMessage > MESSAGE_CONSTANTS.PARTIAL_INTEREST_DECAY) { + return this._isRelevantToTeamMember(lastMessage?.content.text || '', chatId); + } + + // Team leader specific checks + if (this._isTeamLeader() && chatState.messages.length > 0) { + if (!this._isRelevantToTeamMember(lastMessage?.content.text || '', chatId)) { + const recentTeamResponses = chatState.messages.slice(-3).some(m => + m.userId !== this.runtime.agentId && + this._isTeamMember(m.userId.toString()) + ); + + if (recentTeamResponses) { + delete this.interestChats[chatId]; + return false; + } + } + } + + return true; } // Process image messages and generate descriptions @@ -149,6 +367,8 @@ export class MessageManager { try { let imageUrl: string | null = null; + elizaLogger.info(`Telegram Message: ${message}`) + if ("photo" in message && message.photo?.length > 0) { const photo = message.photo[message.photo.length - 1]; const fileLink = await this.bot.telegram.getFileLink( @@ -186,11 +406,17 @@ export class MessageManager { message: Message, state: State ): Promise { + + if (this.runtime.character.clientConfig?.telegram?.shouldRespondOnlyToMentions) { + return this._isMessageForMe(message); + } + // Respond if bot is mentioned if ( "text" in message && message.text?.includes(`@${this.bot.botInfo?.username}`) ) { + elizaLogger.info(`Bot mentioned`) return true; } @@ -208,6 +434,123 @@ export class MessageManager { return false; } + const chatId = message.chat.id.toString(); + const chatState = this.interestChats[chatId]; + const messageText = 'text' in message ? message.text : + 'caption' in message ? (message as any).caption : ''; + + // Check if team member has direct interest first + if (this.runtime.character.clientConfig?.discord?.isPartOfTeam && + !this._isTeamLeader() && + this._isRelevantToTeamMember(messageText, chatId)) { + + return true; + } + + // Team-based response logic + if (this.runtime.character.clientConfig?.telegram?.isPartOfTeam) { + // Team coordination + if(this._isTeamCoordinationRequest(messageText)) { + if (this._isTeamLeader()) { + return true; + } else { + const randomDelay = Math.floor(Math.random() * (TIMING_CONSTANTS.TEAM_MEMBER_DELAY_MAX - TIMING_CONSTANTS.TEAM_MEMBER_DELAY_MIN)) + + TIMING_CONSTANTS.TEAM_MEMBER_DELAY_MIN; // 1-3 second random delay + await new Promise(resolve => setTimeout(resolve, randomDelay)); + return true; + } + } + + if (!this._isTeamLeader() && this._isRelevantToTeamMember(messageText, chatId)) { + // Add small delay for non-leader responses + await new Promise(resolve => setTimeout(resolve, TIMING_CONSTANTS.TEAM_MEMBER_DELAY)); //1.5 second delay + + // If leader has responded in last few seconds, reduce chance of responding + if (chatState.messages?.length) { + const recentMessages = chatState.messages.slice(-MESSAGE_CONSTANTS.RECENT_MESSAGE_COUNT); + const leaderResponded = recentMessages.some(m => + m.userId === this.runtime.character.clientConfig?.telegram?.teamLeaderId && + Date.now() - chatState.lastMessageSent < 3000 + ); + + if (leaderResponded) { + // 50% chance to respond if leader just did + return Math.random() > RESPONSE_CHANCES.AFTER_LEADER; + } + } + + return true; + } + + // If I'm the leader but message doesn't match my keywords, add delay and check for team responses + if (this._isTeamLeader() && !this._isRelevantToTeamMember(messageText, chatId)) { + const randomDelay = Math.floor(Math.random() * (TIMING_CONSTANTS.LEADER_DELAY_MAX - TIMING_CONSTANTS.LEADER_DELAY_MIN)) + + TIMING_CONSTANTS.LEADER_DELAY_MIN; // 2-4 second random delay + await new Promise(resolve => setTimeout(resolve, randomDelay)); + + // After delay, check if another team member has already responded + if (chatState?.messages?.length) { + const recentResponses = chatState.messages.slice(-MESSAGE_CONSTANTS.RECENT_MESSAGE_COUNT); + const otherTeamMemberResponded = recentResponses.some(m => + m.userId !== this.runtime.agentId && + this._isTeamMember(m.userId) + ); + + if (otherTeamMemberResponded) { + return false; + } + } + } + + // Update current handler if we're mentioned + if (this._isMessageForMe(message)) { + const channelState = this.interestChats[chatId]; + if (channelState) { + channelState.currentHandler = this.bot.botInfo?.id.toString() + channelState.lastMessageSent = Date.now(); + } + return true; + } + + // Don't respond if another teammate is handling the conversation + if (chatState?.currentHandler) { + if (chatState.currentHandler !== this.bot.botInfo?.id.toString() && + this._isTeamMember(chatState.currentHandler)) { + return false; + } + } + + // Natural conversation cadence + if (!this._isMessageForMe(message) && this.interestChats[chatId]) { + + const recentMessages = this.interestChats[chatId].messages + .slice(-MESSAGE_CONSTANTS.CHAT_HISTORY_COUNT); + const ourMessageCount = recentMessages.filter(m => + m.userId === this.runtime.agentId + ).length; + + if (ourMessageCount > 2) { + + const responseChance = Math.pow(0.5, ourMessageCount - 2); + if (Math.random() > responseChance) { + return; + } + } + } + + } + + // Check context-based response for team conversations + if (chatState?.currentHandler) { + const shouldRespondContext = await this._shouldRespondBasedOnContext(message, chatState); + + if (!shouldRespondContext) { + return false; + } + + } + + // Use AI to decide for text or captions if ("text" in message || ("caption" in message && message.caption)) { const shouldRespondContext = composeContext({ @@ -329,6 +672,124 @@ export class MessageManager { } const message = ctx.message; + const chatId = ctx.chat?.id.toString(); + const messageText = 'text' in message ? message.text : + 'caption' in message ? (message as any).caption : ''; + + // Add team handling at the start + if (this.runtime.character.clientConfig?.telegram?.isPartOfTeam && + !this.runtime.character.clientConfig?.telegram?.shouldRespondOnlyToMentions) { + + const isDirectlyMentioned = this._isMessageForMe(message); + const hasInterest = this._checkInterest(chatId); + + + // Non-leader team member showing interest based on keywords + if (!this._isTeamLeader() && this._isRelevantToTeamMember(messageText, chatId)) { + + this.interestChats[chatId] = { + currentHandler: this.bot.botInfo?.id.toString(), + lastMessageSent: Date.now(), + messages: [] + }; + } + + const isTeamRequest = this._isTeamCoordinationRequest(messageText); + const isLeader = this._isTeamLeader(); + + + // Check for continued interest + if (hasInterest && !isDirectlyMentioned) { + const lastSelfMemories = await this.runtime.messageManager.getMemories({ + roomId: stringToUuid(chatId + "-" + this.runtime.agentId), + unique: false, + count: 5 + }); + + const lastSelfSortedMemories = lastSelfMemories?.filter(m => m.userId === this.runtime.agentId) + .sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)); + + const isRelevant = this._isRelevantToTeamMember( + messageText, + chatId, + lastSelfSortedMemories?.[0] + ); + + if (!isRelevant) { + delete this.interestChats[chatId]; + return; + } + } + + // Handle team coordination requests + if (isTeamRequest) { + if (isLeader) { + this.interestChats[chatId] = { + currentHandler: this.bot.botInfo?.id.toString(), + lastMessageSent: Date.now(), + messages: [] + }; + } else { + this.interestChats[chatId] = { + currentHandler: this.bot.botInfo?.id.toString(), + lastMessageSent: Date.now(), + messages: [] + }; + + if (!isDirectlyMentioned) { + this.interestChats[chatId].lastMessageSent = 0; + } + + } + } + + // Check for other team member mentions using cached usernames + const otherTeamMembers = this.runtime.character.clientConfig.telegram.teamAgentIds.filter( + id => id !== this.bot.botInfo?.id.toString() + ); + + const mentionedTeamMember = otherTeamMembers.find(id => { + const username = this.getTeamMemberUsername(id); + return username && messageText?.includes(`@${username}`); + }); + + // If another team member is mentioned, clear our interest + if (mentionedTeamMember) { + if (hasInterest || this.interestChats[chatId]?.currentHandler === this.bot.botInfo?.id.toString()) { + delete this.interestChats[chatId]; + + // Only return if we're not the mentioned member + if (!isDirectlyMentioned) { + return; + } + } + } + + // Set/maintain interest only if we're mentioned or already have interest + if (isDirectlyMentioned) { + this.interestChats[chatId] = { + currentHandler: this.bot.botInfo?.id.toString(), + lastMessageSent: Date.now(), + messages: [] + }; + } else if (!isTeamRequest && !hasInterest) { + return; + } + + // Update message tracking + if (this.interestChats[chatId]) { + this.interestChats[chatId].messages.push({ + userId: stringToUuid(ctx.from.id.toString()), + userName: ctx.from.username || ctx.from.first_name || "Unknown User", + content: { text: messageText, source: "telegram" } + }); + + if (this.interestChats[chatId].messages.length > MESSAGE_CONSTANTS.MAX_MESSAGES) { + this.interestChats[chatId].messages = + this.interestChats[chatId].messages.slice(-MESSAGE_CONSTANTS.MAX_MESSAGES); + } + } + } try { // Convert IDs to UUIDs @@ -459,8 +920,10 @@ export class MessageManager { "-" + this.runtime.agentId ), - agentId, - userId, + // agentId, + // userId, + agentId: this.runtime.agentId, + userId: this.runtime.agentId, roomId, content: { ...content, diff --git a/packages/client-telegram/src/utils.ts b/packages/client-telegram/src/utils.ts new file mode 100644 index 0000000000..86f0278f0e --- /dev/null +++ b/packages/client-telegram/src/utils.ts @@ -0,0 +1,97 @@ +export function cosineSimilarity(text1: string, text2: string, text3?: string): number { + const preprocessText = (text: string) => text + .toLowerCase() + .replace(/[^\w\s'_-]/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + + const getWords = (text: string) => { + return text.split(' ').filter(word => word.length > 1); + }; + + const words1 = getWords(preprocessText(text1)); + const words2 = getWords(preprocessText(text2)); + const words3 = text3 ? getWords(preprocessText(text3)) : []; + + const freq1: { [key: string]: number } = {}; + const freq2: { [key: string]: number } = {}; + const freq3: { [key: string]: number } = {}; + + words1.forEach(word => freq1[word] = (freq1[word] || 0) + 1); + words2.forEach(word => freq2[word] = (freq2[word] || 0) + 1); + if (words3.length) { + words3.forEach(word => freq3[word] = (freq3[word] || 0) + 1); + } + + const uniqueWords = new Set([...Object.keys(freq1), ...Object.keys(freq2), ...(words3.length ? Object.keys(freq3) : [])]); + + let dotProduct = 0; + let magnitude1 = 0; + let magnitude2 = 0; + let magnitude3 = 0; + + uniqueWords.forEach(word => { + const val1 = freq1[word] || 0; + const val2 = freq2[word] || 0; + const val3 = freq3[word] || 0; + + if (words3.length) { + // For three-way, calculate pairwise similarities + const sim12 = val1 * val2; + const sim23 = val2 * val3; + const sim13 = val1 * val3; + + // Take maximum similarity between any pair + dotProduct += Math.max(sim12, sim23, sim13); + } else { + dotProduct += val1 * val2; + } + + magnitude1 += val1 * val1; + magnitude2 += val2 * val2; + if (words3.length) { + magnitude3 += val3 * val3; + } + }); + + magnitude1 = Math.sqrt(magnitude1); + magnitude2 = Math.sqrt(magnitude2); + magnitude3 = words3.length ? Math.sqrt(magnitude3) : 1; + + if (magnitude1 === 0 || magnitude2 === 0 || (words3.length && magnitude3 === 0)) return 0; + + // For two texts, use original calculation + if (!words3.length) { + return dotProduct / (magnitude1 * magnitude2); + } + + // For three texts, use max magnitude pair to maintain scale + const maxMagnitude = Math.max( + magnitude1 * magnitude2, + magnitude2 * magnitude3, + magnitude1 * magnitude3 + ); + + return dotProduct / maxMagnitude; +} + +/** + * Splits a message into chunks that fit within Telegram's message length limit + */ +export function splitMessage(text: string, maxLength: number = 4096): string[] { + const chunks: string[] = []; + let currentChunk = ""; + + const lines = text.split("\n"); + for (const line of lines) { + if (currentChunk.length + line.length + 1 <= maxLength) { + currentChunk += (currentChunk ? "\n" : "") + line; + } else { + if (currentChunk) chunks.push(currentChunk); + currentChunk = line; + } + } + + if (currentChunk) chunks.push(currentChunk); + return chunks; +} \ No newline at end of file diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3dc89333d7..63734a6eae 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -710,6 +710,7 @@ export type Character = { shouldIgnoreBotMessages?: boolean; shouldIgnoreDirectMessages?: boolean; shouldRespondOnlyToMentions?: boolean; + messageSimilarityThreshold?: number; isPartOfTeam?: boolean; teamAgentIds?: string[]; teamLeaderId?: string; @@ -719,6 +720,7 @@ export type Character = { shouldIgnoreBotMessages?: boolean; shouldIgnoreDirectMessages?: boolean; shouldRespondOnlyToMentions?: boolean; + messageSimilarityThreshold?: number; isPartOfTeam?: boolean; teamAgentIds?: string[]; teamLeaderId?: string;