From eaf4576fdcace0f6bf96b55a5ccef896c1383286 Mon Sep 17 00:00:00 2001 From: DanielHighETH Date: Mon, 18 Nov 2024 03:45:00 +0400 Subject: [PATCH 1/7] Boredom score for long convos --- .../plugin-bootstrap/src/providers/boredom.ts | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/plugin-bootstrap/src/providers/boredom.ts b/packages/plugin-bootstrap/src/providers/boredom.ts index 1158cca46e4..9e6b9f42bff 100644 --- a/packages/plugin-bootstrap/src/providers/boredom.ts +++ b/packages/plugin-bootstrap/src/providers/boredom.ts @@ -290,6 +290,8 @@ const boredomProvider: Provider = { }); let boredomScore = 0; + const userMessageCounts: Record = {}; + const recentUserActivity: Set = new Set(); for (const recentMessage of recentMessages) { const messageText = recentMessage?.content?.text?.toLowerCase(); @@ -298,6 +300,8 @@ const boredomProvider: Provider = { } if (recentMessage.userId !== agentId) { + userMessageCounts[recentMessage.userId] = + (userMessageCounts[recentMessage.userId] || 0) + 1; // if message text includes any of the interest words, subtract 1 from the boredom score if (interestWords.some((word) => messageText.includes(word))) { boredomScore -= 1; @@ -326,6 +330,28 @@ const boredomProvider: Provider = { } } + const uniqueUsers = Object.keys(userMessageCounts); + const recentUsers = Array.from(recentUserActivity); + + // penalty for repetitive recent interactions + // probably change totalMessages value + if (recentUsers.length <= 2 && recentMessages.length > 10) { + boredomScore += 1; + } + + if (uniqueUsers.length < 3) { + // if less than 3 unique users, assume repetitive interaction + const totalMessages = Object.values(userMessageCounts).reduce( + (a, b) => a + b, + 0 + ); + + // probably change totalMessages value + if (totalMessages > 10) { + boredomScore += 1; + } + } + const boredomLevel = boredomLevels .filter((level) => boredomScore >= level.minScore) @@ -336,8 +362,6 @@ const boredomProvider: Provider = { ); const selectedMessage = boredomLevel.statusMessages[randomIndex]; return selectedMessage.replace("{{agentName}}", agentName); - - return ""; }, }; From 5876475d23e8ab2814a3ad2ace5e2fd175803f15 Mon Sep 17 00:00:00 2001 From: DanielHighETH Date: Mon, 18 Nov 2024 03:52:25 +0400 Subject: [PATCH 2/7] Refractored TwitterInteractionClient --- packages/client-twitter/src/interactions.ts | 390 +++++++++----------- 1 file changed, 168 insertions(+), 222 deletions(-) diff --git a/packages/client-twitter/src/interactions.ts b/packages/client-twitter/src/interactions.ts index 01669e28677..a90b3f4f690 100644 --- a/packages/client-twitter/src/interactions.ts +++ b/packages/client-twitter/src/interactions.ts @@ -74,288 +74,234 @@ IMPORTANT: {{agentName}} (aka @{{twitterUserName}}) is particularly sensitive ab export class TwitterInteractionClient extends ClientBase { onReady() { - const handleTwitterInteractionsLoop = () => { - this.handleTwitterInteractions(); - setTimeout( - handleTwitterInteractionsLoop, - (Math.floor(Math.random() * (5 - 2 + 1)) + 2) * 60 * 1000 - ); // Random interval between 2-5 minutes + const handleTwitterInteractionsLoop = async () => { + try { + await this.handleTwitterInteractions(); + } catch (error) { + console.error("Error in Twitter interaction loop:", error); + } finally { + setTimeout( + handleTwitterInteractionsLoop, + (Math.floor(Math.random() * (5 - 2 + 1)) + 2) * 60 * 1000 // Random interval between 2-5 minutes + ); + } }; handleTwitterInteractionsLoop(); } constructor(runtime: IAgentRuntime) { - super({ - runtime, - }); + super({ runtime }); } async handleTwitterInteractions() { - console.log("Checking Twitter interactions"); + console.log("Checking Twitter interactions..."); try { - // Check for mentions - const tweetCandidates = ( - await this.fetchSearchTweets( - `@${this.runtime.getSetting("TWITTER_USERNAME")}`, - 20, - SearchMode.Latest - ) - ).tweets; - - // de-duplicate tweetCandidates with a set - const uniqueTweetCandidates = [...new Set(tweetCandidates)]; - - // Sort tweet candidates by ID in ascending order - uniqueTweetCandidates - .sort((a, b) => a.id.localeCompare(b.id)) - .filter((tweet) => tweet.userId !== this.twitterUserId); - - // for each tweet candidate, handle the tweet - for (const tweet of uniqueTweetCandidates) { - if ( - !this.lastCheckedTweetId || - parseInt(tweet.id) > this.lastCheckedTweetId - ) { - const conversationId = - tweet.conversationId + "-" + this.runtime.agentId; - - const roomId = stringToUuid(conversationId); - - const userIdUUID = stringToUuid(tweet.userId as string); - - await this.runtime.ensureConnection( - userIdUUID, - roomId, - tweet.username, - tweet.name, - "twitter" - ); - - await buildConversationThread(tweet, this); - - const message = { - content: { text: tweet.text }, - agentId: this.runtime.agentId, - userId: userIdUUID, - roomId, - }; - - await this.handleTweet({ - tweet, - message, - }); - - // Update the last checked tweet ID after processing each tweet - this.lastCheckedTweetId = parseInt(tweet.id); - - try { - if (this.lastCheckedTweetId) { - fs.writeFileSync( - this.tweetCacheFilePath, - this.lastCheckedTweetId.toString(), - "utf-8" - ); - } - } catch (error) { - console.error( - "Error saving latest checked tweet ID to file:", - error - ); - } - } + const tweets = await this.fetchSearchTweets( + `@${this.runtime.getSetting("TWITTER_USERNAME")}`, + 20, + SearchMode.Latest + ); + const tweetCandidates = this.filterValidTweets(tweets.tweets); + + console.log( + `Found ${tweetCandidates.length} valid tweet(s) to process.` + ); + + const groupedTweets = + this.groupTweetsByConversation(tweetCandidates); + + for (const [conversationId, tweets] of Object.entries( + groupedTweets + )) { + await this.handleConversation(conversationId, tweets); } - // Save the latest checked tweet ID to the file - try { - if (this.lastCheckedTweetId) { - fs.writeFileSync( - this.tweetCacheFilePath, - this.lastCheckedTweetId.toString(), - "utf-8" - ); - } - } catch (error) { - console.error( - "Error saving latest checked tweet ID to file:", - error + console.log("Finished processing Twitter interactions."); + } catch (error) { + console.error("Error while handling Twitter interactions:", error); + } + } + + filterValidTweets(tweets: Tweet[]): Tweet[] { + const seenIds = new Set(); + return tweets + .filter((tweet) => tweet.userId !== this.twitterUserId) // Exclude bot's tweets + .filter((tweet) => tweet.text && !seenIds.has(tweet.id)) // Exclude empty or duplicate tweets + .map((tweet) => { + seenIds.add(tweet.id); + return tweet; + }); + } + + groupTweetsByConversation(tweets: Tweet[]): Record { + return tweets.reduce((acc, tweet) => { + const key = `${tweet.conversationId}-${this.runtime.agentId}`; + if (!acc[key]) acc[key] = []; + acc[key].push(tweet); + return acc; + }, {}); + } + + async handleConversation(conversationId: string, tweets: Tweet[]) { + console.log(`Processing conversation: ${conversationId}`); + try { + const roomId = stringToUuid(conversationId); + for (const tweet of tweets) { + const userIdUUID = stringToUuid(tweet.userId as string); + + await this.runtime.ensureConnection( + userIdUUID, + roomId, + tweet.username, + tweet.name, + "twitter" ); - } - console.log("Finished checking Twitter interactions"); + const message = { + content: { text: tweet.text }, + agentId: this.runtime.agentId, + userId: userIdUUID, + roomId, + }; + + await this.handleTweet({ tweet, message }); + this.updateLastCheckedTweetId(tweet.id); + } } catch (error) { - console.error("Error handling Twitter interactions:", error); + console.error( + `Error processing conversation ${conversationId}:`, + error + ); } } - private async handleTweet({ - tweet, - message, - }: { - tweet: Tweet; - message: Memory; - }) { + async handleTweet({ tweet, message }: { tweet: Tweet; message: Memory }) { if (tweet.username === this.runtime.getSetting("TWITTER_USERNAME")) { - console.log("skipping tweet from bot itself", tweet.id); - // Skip processing if the tweet is from the bot itself + console.log("Skipping tweet from bot itself:", tweet.id); return; } if (!message.content.text) { - console.log("skipping tweet with no text", tweet.id); - return { text: "", action: "IGNORE" }; + console.log("Skipping tweet with no text:", tweet.id); + return; } - console.log("handling tweet", tweet.id); - const formatTweet = (tweet: Tweet) => { - return ` ID: ${tweet.id} - From: ${tweet.name} (@${tweet.username}) - Text: ${tweet.text}`; - }; - const currentPost = formatTweet(tweet); - let homeTimeline = []; - // read the file if it exists - if (fs.existsSync("tweetcache/home_timeline.json")) { - homeTimeline = JSON.parse( - fs.readFileSync("tweetcache/home_timeline.json", "utf-8") - ); - } else { - homeTimeline = await this.fetchHomeTimeline(50); - fs.writeFileSync( - "tweetcache/home_timeline.json", - JSON.stringify(homeTimeline, null, 2) - ); + console.log("Handling tweet:", tweet.id); + const context = await this.composeTweetContext(tweet, message); + + const shouldRespond = await this.evaluateShouldRespond(context); + if (!shouldRespond) { + console.log("Decision: IGNORE tweet:", tweet.id); + return; } - const formattedHomeTimeline = - `# ${this.runtime.character.name}'s Home Timeline\n\n` + - homeTimeline - .map((tweet) => { - return `ID: ${tweet.id}\nFrom: ${tweet.name} (@${tweet.username})${tweet.inReplyToStatusId ? ` In reply to: ${tweet.inReplyToStatusId}` : ""}\nText: ${tweet.text}\n---\n`; - }) - .join("\n"); + const response = await this.generateResponse(context); + if (response?.text) { + await this.sendTweetResponse(tweet, response, message.roomId); + } + } + + async composeTweetContext(tweet: Tweet, message: Memory): Promise { + const formattedTimeline = await this.getFormattedTimeline(); + const currentPost = this.formatTweet(tweet); - let state = await this.runtime.composeState(message, { + return this.runtime.composeState(message, { twitterClient: this.twitterClient, twitterUserName: this.runtime.getSetting("TWITTER_USERNAME"), currentPost, - timeline: formattedHomeTimeline, + timeline: formattedTimeline, }); + } - // check if the tweet exists, save if it doesn't - const tweetId = stringToUuid(tweet.id + "-" + this.runtime.agentId); - const tweetExists = - await this.runtime.messageManager.getMemoryById(tweetId); - - if (!tweetExists) { - console.log("tweet does not exist, saving"); - const userIdUUID = stringToUuid(tweet.userId as string); - const roomId = stringToUuid(tweet.conversationId); - - const message = { - id: tweetId, - agentId: this.runtime.agentId, - content: { - text: tweet.text, - url: tweet.permanentUrl, - inReplyTo: tweet.inReplyToStatusId - ? stringToUuid( - tweet.inReplyToStatusId + - "-" + - this.runtime.agentId - ) - : undefined, - }, - userId: userIdUUID, - roomId, - createdAt: tweet.timestamp * 1000, - }; - this.saveRequestMessage(message, state); - } - - console.log("composeState done"); - + async evaluateShouldRespond(context: State): Promise { const shouldRespondContext = composeContext({ - state, + state: context, template: this.runtime.character.templates ?.twitterShouldRespondTemplate || - this.runtime.character?.templates?.shouldRespondTemplate || twitterShouldRespondTemplate, }); - const shouldRespond = await generateShouldRespond({ + const result = await generateShouldRespond({ runtime: this.runtime, context: shouldRespondContext, modelClass: ModelClass.SMALL, }); - if (!shouldRespond) { - console.log("Not responding to message"); - return { text: "", action: "IGNORE" }; - } + return result === "RESPOND"; + } - const context = composeContext({ - state, + async generateResponse(context: State): Promise { + const responseContext = composeContext({ + state: context, template: this.runtime.character.templates ?.twitterMessageHandlerTemplate || - this.runtime.character?.templates?.messageHandlerTemplate || twitterMessageHandlerTemplate, }); - const response = await generateMessageResponse({ + return await generateMessageResponse({ runtime: this.runtime, - context, + context: responseContext, modelClass: ModelClass.SMALL, }); + } - const stringId = stringToUuid(tweet.id + "-" + this.runtime.agentId); - - response.inReplyTo = stringId; - - if (response.text) { - try { - const callback: HandlerCallback = async (response: Content) => { - const memories = await sendTweet( - this, - response, - message.roomId, - this.runtime.getSetting("TWITTER_USERNAME"), - tweet.id - ); - return memories; - }; - - const responseMessages = await callback(response); - - state = (await this.runtime.updateRecentMessageState( - state - )) as State; + async sendTweetResponse(tweet: Tweet, response: Content, roomId: string) { + try { + const memories = await sendTweet( + this, + response, + roomId, + this.runtime.getSetting("TWITTER_USERNAME"), + tweet.id + ); - for (const responseMessage of responseMessages) { - await this.runtime.messageManager.createMemory( - responseMessage - ); - } + for (const memory of memories) { + await this.runtime.messageManager.createMemory(memory); + } + } catch (error) { + console.error( + `Error sending tweet response to ${tweet.id}:`, + error + ); + } + } - await this.runtime.evaluate(message, state); + formatTweet(tweet: Tweet): string { + return `ID: ${tweet.id}\nFrom: ${tweet.name} (@${tweet.username})\nText: ${tweet.text}`; + } - await this.runtime.processActions( - message, - responseMessages, - state + async getFormattedTimeline(): Promise { + try { + let homeTimeline = []; + const cacheFile = "tweetcache/home_timeline.json"; + + if (fs.existsSync(cacheFile)) { + homeTimeline = JSON.parse(fs.readFileSync(cacheFile, "utf-8")); + } else { + homeTimeline = await this.fetchHomeTimeline(50); + fs.writeFileSync( + cacheFile, + JSON.stringify(homeTimeline, null, 2) ); - const responseInfo = `Context:\n\n${context}\n\nSelected Post: ${tweet.id} - ${tweet.username}: ${tweet.text}\nAgent's Output:\n${response.text}`; - // f tweets folder dont exist, create - if (!fs.existsSync("tweets")) { - fs.mkdirSync("tweets"); - } - const debugFileName = `tweets/tweet_generation_${tweet.id}.txt`; - fs.writeFileSync(debugFileName, responseInfo); - await wait(); - } catch (error) { - console.error(`Error sending response tweet: ${error}`); } + + return homeTimeline + .map((tweet) => this.formatTweet(tweet)) + .join("\n---\n"); + } catch (error) { + console.error("Error fetching or formatting home timeline:", error); + return "Error loading timeline."; } } + + updateLastCheckedTweetId(tweetId: string) { + this.lastCheckedTweetId = parseInt(tweetId, 10); + fs.writeFileSync( + this.tweetCacheFilePath, + this.lastCheckedTweetId.toString(), + "utf-8" + ); + } } From 80dc4b659c72d070fd141f9ccf10a78a2fa75a42 Mon Sep 17 00:00:00 2001 From: DanielHighETH Date: Tue, 19 Nov 2024 03:09:40 +0400 Subject: [PATCH 3/7] removed console logs --- packages/client-twitter/src/interactions.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/client-twitter/src/interactions.ts b/packages/client-twitter/src/interactions.ts index a90b3f4f690..84a6894502d 100644 --- a/packages/client-twitter/src/interactions.ts +++ b/packages/client-twitter/src/interactions.ts @@ -94,7 +94,6 @@ export class TwitterInteractionClient extends ClientBase { } async handleTwitterInteractions() { - console.log("Checking Twitter interactions..."); try { const tweets = await this.fetchSearchTweets( `@${this.runtime.getSetting("TWITTER_USERNAME")}`, @@ -102,11 +101,6 @@ export class TwitterInteractionClient extends ClientBase { SearchMode.Latest ); const tweetCandidates = this.filterValidTweets(tweets.tweets); - - console.log( - `Found ${tweetCandidates.length} valid tweet(s) to process.` - ); - const groupedTweets = this.groupTweetsByConversation(tweetCandidates); @@ -115,8 +109,6 @@ export class TwitterInteractionClient extends ClientBase { )) { await this.handleConversation(conversationId, tweets); } - - console.log("Finished processing Twitter interactions."); } catch (error) { console.error("Error while handling Twitter interactions:", error); } @@ -143,7 +135,6 @@ export class TwitterInteractionClient extends ClientBase { } async handleConversation(conversationId: string, tweets: Tweet[]) { - console.log(`Processing conversation: ${conversationId}`); try { const roomId = stringToUuid(conversationId); for (const tweet of tweets) { @@ -177,21 +168,17 @@ export class TwitterInteractionClient extends ClientBase { async handleTweet({ tweet, message }: { tweet: Tweet; message: Memory }) { if (tweet.username === this.runtime.getSetting("TWITTER_USERNAME")) { - console.log("Skipping tweet from bot itself:", tweet.id); return; } if (!message.content.text) { - console.log("Skipping tweet with no text:", tweet.id); return; } - console.log("Handling tweet:", tweet.id); const context = await this.composeTweetContext(tweet, message); const shouldRespond = await this.evaluateShouldRespond(context); if (!shouldRespond) { - console.log("Decision: IGNORE tweet:", tweet.id); return; } From 9a8ac904c1bef1279dac5c40ff62a5d871fbf5a7 Mon Sep 17 00:00:00 2001 From: DanielHighETH Date: Tue, 19 Nov 2024 03:10:28 +0400 Subject: [PATCH 4/7] Filters combined to one --- packages/client-twitter/src/interactions.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/client-twitter/src/interactions.ts b/packages/client-twitter/src/interactions.ts index 84a6894502d..abd84055d3c 100644 --- a/packages/client-twitter/src/interactions.ts +++ b/packages/client-twitter/src/interactions.ts @@ -117,8 +117,12 @@ export class TwitterInteractionClient extends ClientBase { filterValidTweets(tweets: Tweet[]): Tweet[] { const seenIds = new Set(); return tweets - .filter((tweet) => tweet.userId !== this.twitterUserId) // Exclude bot's tweets - .filter((tweet) => tweet.text && !seenIds.has(tweet.id)) // Exclude empty or duplicate tweets + .filter( + (tweet) => + tweet.userId !== this.twitterUserId && // Exclude bot's tweets + tweet.text && // Exclude empty tweets + !seenIds.has(tweet.id) // Exclude duplicate tweets + ) .map((tweet) => { seenIds.add(tweet.id); return tweet; From 4050daff28742e0353b79ffbe7bb1fbcbfc70c0b Mon Sep 17 00:00:00 2001 From: DanielHighETH Date: Tue, 19 Nov 2024 03:34:19 +0400 Subject: [PATCH 5/7] include TODO comments for #408 Added TODO comments for #408 to review and update the `totalMessages` logic. --- packages/plugin-bootstrap/src/providers/boredom.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-bootstrap/src/providers/boredom.ts b/packages/plugin-bootstrap/src/providers/boredom.ts index 9e6b9f42bff..1af4c85912f 100644 --- a/packages/plugin-bootstrap/src/providers/boredom.ts +++ b/packages/plugin-bootstrap/src/providers/boredom.ts @@ -334,7 +334,7 @@ const boredomProvider: Provider = { const recentUsers = Array.from(recentUserActivity); // penalty for repetitive recent interactions - // probably change totalMessages value + // TODO (#408): change totalMessages value if (recentUsers.length <= 2 && recentMessages.length > 10) { boredomScore += 1; } @@ -346,7 +346,7 @@ const boredomProvider: Provider = { 0 ); - // probably change totalMessages value + // TODO (#408): change totalMessages value if (totalMessages > 10) { boredomScore += 1; } From 8a850bdda8844be421e02adbefa025987a2867a8 Mon Sep 17 00:00:00 2001 From: DanielHighETH Date: Wed, 20 Nov 2024 02:34:39 +0400 Subject: [PATCH 6/7] refractored newest commit --- packages/client-twitter/src/interactions.ts | 428 +++++++++++++++----- 1 file changed, 332 insertions(+), 96 deletions(-) diff --git a/packages/client-twitter/src/interactions.ts b/packages/client-twitter/src/interactions.ts index abd84055d3c..9d70b597ab3 100644 --- a/packages/client-twitter/src/interactions.ts +++ b/packages/client-twitter/src/interactions.ts @@ -1,14 +1,8 @@ import { SearchMode, Tweet } from "agent-twitter-client"; import fs from "fs"; -import { composeContext } from "@ai16z/eliza/src/context.ts"; -import { - generateMessageResponse, - generateShouldRespond, -} from "@ai16z/eliza/src/generation.ts"; -import { - messageCompletionFooter, - shouldRespondFooter, -} from "@ai16z/eliza/src/parsing.ts"; +import { composeContext, elizaLogger } from "@ai16z/eliza"; +import { generateMessageResponse, generateShouldRespond } from "@ai16z/eliza"; +import { messageCompletionFooter, shouldRespondFooter } from "@ai16z/eliza"; import { Content, HandlerCallback, @@ -17,9 +11,10 @@ import { ModelClass, State, } from "@ai16z/eliza"; -import { stringToUuid } from "@ai16z/eliza/src/uuid.ts"; +import { stringToUuid } from "@ai16z/eliza"; import { ClientBase } from "./base.ts"; import { buildConversationThread, sendTweet, wait } from "./utils.ts"; +import { embeddingZeroVector } from "@ai16z/eliza"; export const twitterMessageHandlerTemplate = `{{timeline}} @@ -44,15 +39,24 @@ Recent interactions between {{agentName}} and other users: {{recentPosts}} -# Task: Generate a post in the voice, style and perspective of {{agentName}} (@{{twitterUserName}}): + +# Task: Generate a post/reply in the voice, style and perspective of {{agentName}} (@{{twitterUserName}}) while using the thread of tweets as additional context: +Current Post: {{currentPost}} +Thread of Tweets You Are Replying To: +{{formattedConversation}} + +{{actions}} + +# Task: Generate a post in the voice, style and perspective of {{agentName}} (@{{twitterUserName}}). Include an action, if appropriate. {{actionNames}}: +{{currentPost}} ` + messageCompletionFooter; export const twitterShouldRespondTemplate = `# INSTRUCTIONS: Determine if {{agentName}} (@{{twitterUserName}}) should respond to the message and participate in the conversation. Do not comment. Just respond with "true" or "false". -Response options are RESPOND, IGNORE and STOP. +Response options are RESPOND, IGNORE and STOP . {{agentName}} should respond to messages that are directed at them, or participate in conversations that are interesting or relevant to their background, IGNORE messages that are irrelevant to them, and should STOP if the conversation is concluded. @@ -69,6 +73,10 @@ IMPORTANT: {{agentName}} (aka @{{twitterUserName}}) is particularly sensitive ab {{currentPost}} +Thread of Tweets You Are Replying To: + +{{formattedConversation}} + # INSTRUCTIONS: Respond with [RESPOND] if {{agentName}} should respond, or [IGNORE] if {{agentName}} should not respond to the last message and [STOP] if {{agentName}} should stop participating in the conversation. ` + shouldRespondFooter; @@ -90,17 +98,24 @@ export class TwitterInteractionClient extends ClientBase { } constructor(runtime: IAgentRuntime) { - super({ runtime }); + super({ + runtime, + }); } async handleTwitterInteractions() { + elizaLogger.log("Checking Twitter interactions"); try { - const tweets = await this.fetchSearchTweets( - `@${this.runtime.getSetting("TWITTER_USERNAME")}`, - 20, - SearchMode.Latest - ); - const tweetCandidates = this.filterValidTweets(tweets.tweets); + // Check for mentions + const tweets = ( + await this.fetchSearchTweets( + `@${this.runtime.getSetting("TWITTER_USERNAME")}`, + 20, + SearchMode.Latest + ) + ).tweets; + + const tweetCandidates = this.filterValidTweets(tweets); const groupedTweets = this.groupTweetsByConversation(tweetCandidates); @@ -152,6 +167,8 @@ export class TwitterInteractionClient extends ClientBase { "twitter" ); + const thread = await buildConversationThread(tweet, this); + const message = { content: { text: tweet.text }, agentId: this.runtime.agentId, @@ -159,140 +176,359 @@ export class TwitterInteractionClient extends ClientBase { roomId, }; - await this.handleTweet({ tweet, message }); + await this.handleTweet({ tweet, message, thread }); this.updateLastCheckedTweetId(tweet.id); } } catch (error) { - console.error( + elizaLogger.error( `Error processing conversation ${conversationId}:`, error ); } } - async handleTweet({ tweet, message }: { tweet: Tweet; message: Memory }) { + updateLastCheckedTweetId(tweetId: string) { + this.lastCheckedTweetId = parseInt(tweetId, 10); + fs.writeFileSync( + this.tweetCacheFilePath, + this.lastCheckedTweetId.toString(), + "utf-8" + ); + } + + private async handleTweet({ + tweet, + message, + thread, + }: { + tweet: Tweet; + message: Memory; + thread: Tweet[]; + }): Promise { if (tweet.username === this.runtime.getSetting("TWITTER_USERNAME")) { + // console.log("skipping tweet from bot itself", tweet.id); + // Skip processing if the tweet is from the bot itself return; } if (!message.content.text) { - return; + elizaLogger.log("skipping tweet with no text", tweet.id); + return { text: "", action: "IGNORE" }; } + elizaLogger.log("handling tweet", tweet.id); + const formatTweet = (tweet: Tweet) => { + return ` ID: ${tweet.id} + From: ${tweet.name} (@${tweet.username}) + Text: ${tweet.text}`; + }; + const currentPost = formatTweet(tweet); - const context = await this.composeTweetContext(tweet, message); - - const shouldRespond = await this.evaluateShouldRespond(context); - if (!shouldRespond) { - return; + let homeTimeline = []; + // read the file if it exists + if (fs.existsSync("tweetcache/home_timeline.json")) { + homeTimeline = JSON.parse( + fs.readFileSync("tweetcache/home_timeline.json", "utf-8") + ); + } else { + homeTimeline = await this.fetchHomeTimeline(50); + fs.writeFileSync( + "tweetcache/home_timeline.json", + JSON.stringify(homeTimeline, null, 2) + ); } - const response = await this.generateResponse(context); - if (response?.text) { - await this.sendTweetResponse(tweet, response, message.roomId); - } - } + elizaLogger.debug("Thread: ", thread); + const formattedConversation = thread + .map( + (tweet) => `@${tweet.username} (${new Date( + tweet.timestamp * 1000 + ).toLocaleString("en-US", { + hour: "2-digit", + minute: "2-digit", + month: "short", + day: "numeric", + })}): + ${tweet.text}` + ) + .join("\n\n"); + + elizaLogger.debug("formattedConversation: ", formattedConversation); - async composeTweetContext(tweet: Tweet, message: Memory): Promise { - const formattedTimeline = await this.getFormattedTimeline(); - const currentPost = this.formatTweet(tweet); + const formattedHomeTimeline = + `# ${this.runtime.character.name}'s Home Timeline\n\n` + + homeTimeline + .map((tweet) => { + return `ID: ${tweet.id}\nFrom: ${tweet.name} (@${tweet.username})${tweet.inReplyToStatusId ? ` In reply to: ${tweet.inReplyToStatusId}` : ""}\nText: ${tweet.text}\n---\n`; + }) + .join("\n"); - return this.runtime.composeState(message, { + let state = await this.runtime.composeState(message, { twitterClient: this.twitterClient, twitterUserName: this.runtime.getSetting("TWITTER_USERNAME"), currentPost, - timeline: formattedTimeline, + formattedConversation, + timeline: formattedHomeTimeline, }); - } - async evaluateShouldRespond(context: State): Promise { + // check if the tweet exists, save if it doesn't + const tweetId = stringToUuid(tweet.id + "-" + this.runtime.agentId); + const tweetExists = + await this.runtime.messageManager.getMemoryById(tweetId); + + if (!tweetExists) { + elizaLogger.log("tweet does not exist, saving"); + const userIdUUID = stringToUuid(tweet.userId as string); + const roomId = stringToUuid(tweet.conversationId); + + const message = { + id: tweetId, + agentId: this.runtime.agentId, + content: { + text: tweet.text, + url: tweet.permanentUrl, + inReplyTo: tweet.inReplyToStatusId + ? stringToUuid( + tweet.inReplyToStatusId + + "-" + + this.runtime.agentId + ) + : undefined, + }, + userId: userIdUUID, + roomId, + createdAt: tweet.timestamp * 1000, + }; + this.saveRequestMessage(message, state); + } + const shouldRespondContext = composeContext({ - state: context, + state, template: this.runtime.character.templates ?.twitterShouldRespondTemplate || + this.runtime.character?.templates?.shouldRespondTemplate || twitterShouldRespondTemplate, }); - const result = await generateShouldRespond({ + console.log("composeContext done"); + + const shouldRespond = await generateShouldRespond({ runtime: this.runtime, context: shouldRespondContext, - modelClass: ModelClass.SMALL, + modelClass: ModelClass.MEDIUM, }); - return result === "RESPOND"; - } + // Promise<"RESPOND" | "IGNORE" | "STOP" | null> { + if (shouldRespond !== "RESPOND") { + elizaLogger.log("Not responding to message"); + return []; + } - async generateResponse(context: State): Promise { - const responseContext = composeContext({ - state: context, + const context = composeContext({ + state, template: this.runtime.character.templates ?.twitterMessageHandlerTemplate || + this.runtime.character?.templates?.messageHandlerTemplate || twitterMessageHandlerTemplate, }); - return await generateMessageResponse({ + const response = await generateMessageResponse({ runtime: this.runtime, - context: responseContext, - modelClass: ModelClass.SMALL, + context, + modelClass: ModelClass.MEDIUM, }); - } - async sendTweetResponse(tweet: Tweet, response: Content, roomId: string) { - try { - const memories = await sendTweet( - this, - response, - roomId, - this.runtime.getSetting("TWITTER_USERNAME"), - tweet.id - ); + const removeQuotes = (str: string) => + str.replace(/^['"](.*)['"]$/, "$1"); + + const stringId = stringToUuid(tweet.id + "-" + this.runtime.agentId); + + response.inReplyTo = stringId; + + response.text = removeQuotes(response.text); - for (const memory of memories) { - await this.runtime.messageManager.createMemory(memory); + if (response.text) { + try { + const callback: HandlerCallback = async (response: Content) => { + const memories = await sendTweet( + this, + response, + message.roomId, + this.runtime.getSetting("TWITTER_USERNAME"), + tweet.id + ); + return memories; + }; + + const responseMessages = await callback(response); + + state = (await this.runtime.updateRecentMessageState( + state + )) as State; + + for (const responseMessage of responseMessages) { + if ( + responseMessage === + responseMessages[responseMessages.length - 1] + ) { + responseMessage.content.action = response.action; + } else { + responseMessage.content.action = "CONTINUE"; + } + await this.runtime.messageManager.createMemory( + responseMessage + ); + } + + await this.runtime.evaluate(message, state); + + await this.runtime.processActions( + message, + responseMessages, + state + ); + const responseInfo = `Context:\n\n${context}\n\nSelected Post: ${tweet.id} - ${tweet.username}: ${tweet.text}\nAgent's Output:\n${response.text}`; + // f tweets folder dont exist, create + if (!fs.existsSync("tweets")) { + fs.mkdirSync("tweets"); + } + const debugFileName = `tweets/tweet_generation_${tweet.id}.txt`; + fs.writeFileSync(debugFileName, responseInfo); + await wait(); + } catch (error) { + elizaLogger.error(`Error sending response tweet: ${error}`); } - } catch (error) { - console.error( - `Error sending tweet response to ${tweet.id}:`, - error - ); } } - formatTweet(tweet: Tweet): string { - return `ID: ${tweet.id}\nFrom: ${tweet.name} (@${tweet.username})\nText: ${tweet.text}`; - } + async buildConversationThread( + tweet: Tweet, + maxReplies: number = 10 + ): Promise { + const thread: Tweet[] = []; + const visited: Set = new Set(); + + async function processThread(currentTweet: Tweet, depth: number = 0) { + console.log("Processing tweet:", { + id: currentTweet.id, + inReplyToStatusId: currentTweet.inReplyToStatusId, + depth: depth, + }); - async getFormattedTimeline(): Promise { - try { - let homeTimeline = []; - const cacheFile = "tweetcache/home_timeline.json"; + if (!currentTweet) { + console.log("No current tweet found for thread building"); + return; + } - if (fs.existsSync(cacheFile)) { - homeTimeline = JSON.parse(fs.readFileSync(cacheFile, "utf-8")); - } else { - homeTimeline = await this.fetchHomeTimeline(50); - fs.writeFileSync( - cacheFile, - JSON.stringify(homeTimeline, null, 2) + if (depth >= maxReplies) { + console.log("Reached maximum reply depth", depth); + return; + } + + // Handle memory storage + const memory = await this.runtime.messageManager.getMemoryById( + stringToUuid(currentTweet.id + "-" + this.runtime.agentId) + ); + if (!memory) { + const roomId = stringToUuid( + currentTweet.conversationId + "-" + this.runtime.agentId ); + const userId = stringToUuid(currentTweet.userId); + + await this.runtime.ensureConnection( + userId, + roomId, + currentTweet.username, + currentTweet.name, + "twitter" + ); + + this.runtime.messageManager.createMemory({ + id: stringToUuid( + currentTweet.id + "-" + this.runtime.agentId + ), + agentId: this.runtime.agentId, + content: { + text: currentTweet.text, + source: "twitter", + url: currentTweet.permanentUrl, + inReplyTo: currentTweet.inReplyToStatusId + ? stringToUuid( + currentTweet.inReplyToStatusId + + "-" + + this.runtime.agentId + ) + : undefined, + }, + createdAt: currentTweet.timestamp * 1000, + roomId, + userId: + currentTweet.userId === this.twitterUserId + ? this.runtime.agentId + : stringToUuid(currentTweet.userId), + embedding: embeddingZeroVector, + }); } - return homeTimeline - .map((tweet) => this.formatTweet(tweet)) - .join("\n---\n"); - } catch (error) { - console.error("Error fetching or formatting home timeline:", error); - return "Error loading timeline."; + if (visited.has(currentTweet.id)) { + elizaLogger.log("Already visited tweet:", currentTweet.id); + return; + } + + visited.add(currentTweet.id); + thread.unshift(currentTweet); + + elizaLogger.debug("Current thread state:", { + length: thread.length, + currentDepth: depth, + tweetId: currentTweet.id, + }); + + if (currentTweet.inReplyToStatusId) { + console.log( + "Fetching parent tweet:", + currentTweet.inReplyToStatusId + ); + try { + const parentTweet = await this.twitterClient.getTweet( + currentTweet.inReplyToStatusId + ); + + if (parentTweet) { + console.log("Found parent tweet:", { + id: parentTweet.id, + text: parentTweet.text?.slice(0, 50), + }); + await processThread(parentTweet, depth + 1); + } else { + console.log( + "No parent tweet found for:", + currentTweet.inReplyToStatusId + ); + } + } catch (error) { + console.log("Error fetching parent tweet:", { + tweetId: currentTweet.inReplyToStatusId, + error, + }); + } + } else { + console.log("Reached end of reply chain at:", currentTweet.id); + } } - } - updateLastCheckedTweetId(tweetId: string) { - this.lastCheckedTweetId = parseInt(tweetId, 10); - fs.writeFileSync( - this.tweetCacheFilePath, - this.lastCheckedTweetId.toString(), - "utf-8" - ); + // Need to bind this context for the inner function + await processThread.bind(this)(tweet, 0); + + elizaLogger.debug("Final thread built:", { + totalTweets: thread.length, + tweetIds: thread.map((t) => ({ + id: t.id, + text: t.text?.slice(0, 50), + })), + }); + + return thread; } } From 6715d34b5ba7a47d3e809d11136150dfa9faad6e Mon Sep 17 00:00:00 2001 From: DanielHighETH Date: Wed, 20 Nov 2024 02:36:59 +0400 Subject: [PATCH 7/7] conflicts resolved --- packages/client-twitter/src/interactions.ts | 25 +++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/client-twitter/src/interactions.ts b/packages/client-twitter/src/interactions.ts index 9d70b597ab3..f25ddc2894b 100644 --- a/packages/client-twitter/src/interactions.ts +++ b/packages/client-twitter/src/interactions.ts @@ -44,11 +44,19 @@ Recent interactions between {{agentName}} and other users: Current Post: {{currentPost}} Thread of Tweets You Are Replying To: +<<<<<<< main {{formattedConversation}} {{actions}} +======= + +{{formattedConversation}} + +{{actions}} + +>>>>>>> main # Task: Generate a post in the voice, style and perspective of {{agentName}} (@{{twitterUserName}}). Include an action, if appropriate. {{actionNames}}: {{currentPost}} ` + messageCompletionFooter; @@ -204,7 +212,7 @@ export class TwitterInteractionClient extends ClientBase { tweet: Tweet; message: Memory; thread: Tweet[]; - }): Promise { + }) { if (tweet.username === this.runtime.getSetting("TWITTER_USERNAME")) { // console.log("skipping tweet from bot itself", tweet.id); // Skip processing if the tweet is from the bot itself @@ -321,7 +329,7 @@ export class TwitterInteractionClient extends ClientBase { // Promise<"RESPOND" | "IGNORE" | "STOP" | null> { if (shouldRespond !== "RESPOND") { elizaLogger.log("Not responding to message"); - return []; + return { text: "Response Decision:", action: shouldRespond }; } const context = composeContext({ @@ -400,6 +408,19 @@ export class TwitterInteractionClient extends ClientBase { elizaLogger.error(`Error sending response tweet: ${error}`); } } + + // Need to bind this context for the inner function + await processThread.bind(this)(tweet, 0); + + elizaLogger.debug("Final thread built:", { + totalTweets: thread.length, + tweetIds: thread.map((t) => ({ + id: t.id, + text: t.text?.slice(0, 50), + })), + }); + + return thread; } async buildConversationThread(