From 848e70a100c61402d377d7b693557a4c7b279527 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Sat, 4 Jan 2025 14:43:04 -0500 Subject: [PATCH 1/8] add MAX_ACTIONS_PROCESSING variable --- packages/client-twitter/src/base.ts | 99 ++++++++++++---------- packages/client-twitter/src/environment.ts | 7 ++ packages/client-twitter/src/post.ts | 25 ++++-- 3 files changed, 79 insertions(+), 52 deletions(-) diff --git a/packages/client-twitter/src/base.ts b/packages/client-twitter/src/base.ts index 85267da722..d63d06564f 100644 --- a/packages/client-twitter/src/base.ts +++ b/packages/client-twitter/src/base.ts @@ -136,7 +136,7 @@ export class ClientBase extends EventEmitter { ); } - constructor(runtime: IAgentRuntime, twitterConfig:TwitterConfig) { + constructor(runtime: IAgentRuntime, twitterConfig: TwitterConfig) { super(); this.runtime = runtime; this.twitterConfig = twitterConfig; @@ -159,7 +159,7 @@ export class ClientBase extends EventEmitter { const username = this.twitterConfig.TWITTER_USERNAME; const password = this.twitterConfig.TWITTER_PASSWORD; const email = this.twitterConfig.TWITTER_EMAIL; - let retries = this.twitterConfig.TWITTER_RETRY_LIMIT + let retries = this.twitterConfig.TWITTER_RETRY_LIMIT; const twitter2faSecret = this.twitterConfig.TWITTER_2FA_SECRET; if (!username) { @@ -176,7 +176,8 @@ export class ClientBase extends EventEmitter { elizaLogger.log("Waiting for Twitter login"); while (retries > 0) { try { - if (await this.twitterClient.isLoggedIn()) { // cookies are valid, no login required + if (await this.twitterClient.isLoggedIn()) { + // cookies are valid, no login required elizaLogger.info("Successfully logged in."); break; } else { @@ -186,7 +187,8 @@ export class ClientBase extends EventEmitter { email, twitter2faSecret ); - if (await this.twitterClient.isLoggedIn()) { // fresh login, store new cookies + if (await this.twitterClient.isLoggedIn()) { + // fresh login, store new cookies elizaLogger.info("Successfully logged in."); elizaLogger.info("Caching cookies"); await this.cacheCookies( @@ -251,7 +253,10 @@ export class ClientBase extends EventEmitter { /** * Fetch timeline for twitter account, optionally only from followed accounts */ - async fetchHomeTimeline(count: number, following?: boolean): Promise { + async fetchHomeTimeline( + count: number, + following?: boolean + ): Promise { elizaLogger.debug("fetching home timeline"); const homeTimeline = following ? await this.twitterClient.fetchFollowingTimeline(count, []) @@ -288,13 +293,14 @@ export class ClientBase extends EventEmitter { hashtags: tweet.hashtags ?? tweet.legacy?.entities.hashtags, mentions: tweet.mentions ?? tweet.legacy?.entities.user_mentions, - photos: tweet.legacy?.entities?.media?.filter( - (media) => media.type === "photo" - ).map(media => ({ - id: media.id_str, - url: media.media_url_https, // Store media_url_https as url - alt_text: media.alt_text - })) || [], + photos: + tweet.legacy?.entities?.media + ?.filter((media) => media.type === "photo") + .map((media) => ({ + id: media.id_str, + url: media.media_url_https, // Store media_url_https as url + alt_text: media.alt_text, + })) || [], thread: tweet.thread || [], urls: tweet.urls ?? tweet.legacy?.entities.urls, videos: @@ -311,41 +317,44 @@ export class ClientBase extends EventEmitter { return processedTimeline; } - async fetchTimelineForActions(count: number): Promise { + async fetchTimelineForActions(): Promise { elizaLogger.debug("fetching timeline for actions"); - const agentUsername = this.twitterConfig.TWITTER_USERNAME - const homeTimeline = await this.twitterClient.fetchHomeTimeline( - count, - [] - ); - - return homeTimeline.map((tweet) => ({ - id: tweet.rest_id, - name: tweet.core?.user_results?.result?.legacy?.name, - username: tweet.core?.user_results?.result?.legacy?.screen_name, - text: tweet.legacy?.full_text, - inReplyToStatusId: tweet.legacy?.in_reply_to_status_id_str, - timestamp: new Date(tweet.legacy?.created_at).getTime() / 1000, - userId: tweet.legacy?.user_id_str, - conversationId: tweet.legacy?.conversation_id_str, - permanentUrl: `https://twitter.com/${tweet.core?.user_results?.result?.legacy?.screen_name}/status/${tweet.rest_id}`, - hashtags: tweet.legacy?.entities?.hashtags || [], - mentions: tweet.legacy?.entities?.user_mentions || [], - photos: tweet.legacy?.entities?.media?.filter( - (media) => media.type === "photo" - ).map(media => ({ - id: media.id_str, - url: media.media_url_https, // Store media_url_https as url - alt_text: media.alt_text - })) || [], - thread: tweet.thread || [], - urls: tweet.legacy?.entities?.urls || [], - videos: - tweet.legacy?.entities?.media?.filter( - (media) => media.type === "video" - ) || [], - })).filter(tweet => tweet.username !== agentUsername); // do not perform action on self-tweets + const agentUsername = this.twitterConfig.TWITTER_USERNAME; + const homeTimeline = await this.twitterClient.fetchHomeTimeline(20, []); + + const processedTweets = homeTimeline + .map((tweet) => ({ + id: tweet.rest_id, + name: tweet.core?.user_results?.result?.legacy?.name, + username: tweet.core?.user_results?.result?.legacy?.screen_name, + text: tweet.legacy?.full_text, + inReplyToStatusId: tweet.legacy?.in_reply_to_status_id_str, + timestamp: new Date(tweet.legacy?.created_at).getTime() / 1000, + userId: tweet.legacy?.user_id_str, + conversationId: tweet.legacy?.conversation_id_str, + permanentUrl: `https://twitter.com/${tweet.core?.user_results?.result?.legacy?.screen_name}/status/${tweet.rest_id}`, + hashtags: tweet.legacy?.entities?.hashtags || [], + mentions: tweet.legacy?.entities?.user_mentions || [], + photos: + tweet.legacy?.entities?.media + ?.filter((media) => media.type === "photo") + .map((media) => ({ + id: media.id_str, + url: media.media_url_https, // Store media_url_https as url + alt_text: media.alt_text, + })) || [], + thread: tweet.thread || [], + urls: tweet.legacy?.entities?.urls || [], + videos: + tweet.legacy?.entities?.media?.filter( + (media) => media.type === "video" + ) || [], + })) + .filter((tweet) => tweet.username !== agentUsername); // do not perform action on self-tweets + + const shuffledTweets = processedTweets.sort(() => Math.random() - 0.5); + return shuffledTweets; } async fetchSearchTweets( diff --git a/packages/client-twitter/src/environment.ts b/packages/client-twitter/src/environment.ts index 2c54cb0f92..05b3fafaae 100644 --- a/packages/client-twitter/src/environment.ts +++ b/packages/client-twitter/src/environment.ts @@ -61,6 +61,7 @@ export const twitterEnvSchema = z.object({ ACTION_INTERVAL: z.number().int(), POST_IMMEDIATELY: z.boolean(), TWITTER_SPACES_ENABLE: z.boolean().default(false), + MAX_ACTIONS_PROCESSING: z.number().int(), }); export type TwitterConfig = z.infer; @@ -199,6 +200,12 @@ export async function validateTwitterConfig( runtime.getSetting("TWITTER_SPACES_ENABLE") || process.env.TWITTER_SPACES_ENABLE ) ?? false, + + MAX_ACTIONS_PROCESSING: safeParseInt( + runtime.getSetting("MAX_ACTIONS_PROCESSING") || + process.env.MAX_ACTIONS_PROCESSING, + 1 + ), }; return twitterEnvSchema.parse(twitterConfig); diff --git a/packages/client-twitter/src/post.ts b/packages/client-twitter/src/post.ts index 1c176cb719..47eb41b17a 100644 --- a/packages/client-twitter/src/post.ts +++ b/packages/client-twitter/src/post.ts @@ -62,7 +62,8 @@ Actions (respond only with tags): Tweet: {{currentTweet}} -# Respond with qualifying action tags only. Default to NO action unless extremely confident of relevance.` + postActionResponseFooter; +# Respond with qualifying action tags only. Default to NO action unless extremely confident of relevance.` + + postActionResponseFooter; /** * Truncate text to fit within the Twitter character limit, ensuring it ends at a complete sentence. @@ -111,7 +112,7 @@ export class TwitterPostClient { this.client = client; this.runtime = runtime; this.twitterUsername = this.client.twitterConfig.TWITTER_USERNAME; - this.isDryRun = this.client.twitterConfig.TWITTER_DRY_RUN + this.isDryRun = this.client.twitterConfig.TWITTER_DRY_RUN; // Log configuration on initialization elizaLogger.log("Twitter Client Configuration:"); @@ -188,8 +189,9 @@ export class TwitterPostClient { `Next action processing scheduled in ${actionInterval} minutes` ); // Wait for the full interval before next processing - await new Promise((resolve) => - setTimeout(resolve, actionInterval * 60 * 1000) // now in minutes + await new Promise( + (resolve) => + setTimeout(resolve, actionInterval * 60 * 1000) // now in minutes ); } } catch (error) { @@ -215,7 +217,10 @@ export class TwitterPostClient { elizaLogger.log("Tweet generation loop disabled (dry run mode)"); } - if (this.client.twitterConfig.ENABLE_ACTION_PROCESSING && !this.isDryRun) { + if ( + this.client.twitterConfig.ENABLE_ACTION_PROCESSING && + !this.isDryRun + ) { processActionsLoop().catch((error) => { elizaLogger.error( "Fatal error in process actions loop:", @@ -480,7 +485,7 @@ export class TwitterPostClient { } // Truncate the content to the maximum tweet length specified in the environment settings, ensuring the truncation respects sentence boundaries. - const maxTweetLength = this.client.twitterConfig.MAX_TWEET_LENGTH + const maxTweetLength = this.client.twitterConfig.MAX_TWEET_LENGTH; if (maxTweetLength) { cleanedContent = truncateToCompleteSentence( cleanedContent, @@ -620,10 +625,15 @@ export class TwitterPostClient { "twitter" ); - const homeTimeline = await this.client.fetchTimelineForActions(15); + const homeTimeline = await this.client.fetchTimelineForActions(); const results = []; + let maxActionsProcessing = + this.client.twitterConfig.MAX_ACTIONS_PROCESSING; for (const tweet of homeTimeline) { + if (maxActionsProcessing >= 2) { + break; + } try { // Skip if we've already processed this tweet const memory = @@ -915,6 +925,7 @@ export class TwitterPostClient { parsedActions: actionResponse, executedActions, }); + maxActionsProcessing++; } catch (error) { elizaLogger.error( `Error processing tweet ${tweet.id}:`, From 7ad924bc306eb4f7ebae84fb5647fe700062c079 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Sat, 4 Jan 2025 14:45:51 -0500 Subject: [PATCH 2/8] Limit processing to maximum defined by maxActionsProcessing --- packages/client-twitter/src/post.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/client-twitter/src/post.ts b/packages/client-twitter/src/post.ts index 47eb41b17a..13ab4ecf79 100644 --- a/packages/client-twitter/src/post.ts +++ b/packages/client-twitter/src/post.ts @@ -627,11 +627,12 @@ export class TwitterPostClient { const homeTimeline = await this.client.fetchTimelineForActions(); const results = []; - let maxActionsProcessing = + let count = 0; + const maxActionsProcessing = this.client.twitterConfig.MAX_ACTIONS_PROCESSING; for (const tweet of homeTimeline) { - if (maxActionsProcessing >= 2) { + if (count >= maxActionsProcessing) { break; } try { @@ -925,7 +926,7 @@ export class TwitterPostClient { parsedActions: actionResponse, executedActions, }); - maxActionsProcessing++; + count++; } catch (error) { elizaLogger.error( `Error processing tweet ${tweet.id}:`, From 1ea3020c01070a1babfe93683e08b7fb4a8a1b82 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Sat, 4 Jan 2025 14:50:51 -0500 Subject: [PATCH 3/8] rename --- packages/client-twitter/src/post.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client-twitter/src/post.ts b/packages/client-twitter/src/post.ts index 13ab4ecf79..b6c04db119 100644 --- a/packages/client-twitter/src/post.ts +++ b/packages/client-twitter/src/post.ts @@ -627,12 +627,12 @@ export class TwitterPostClient { const homeTimeline = await this.client.fetchTimelineForActions(); const results = []; - let count = 0; + let processedTweets = 0; const maxActionsProcessing = this.client.twitterConfig.MAX_ACTIONS_PROCESSING; for (const tweet of homeTimeline) { - if (count >= maxActionsProcessing) { + if (processedTweets >= maxActionsProcessing) { break; } try { @@ -926,7 +926,7 @@ export class TwitterPostClient { parsedActions: actionResponse, executedActions, }); - count++; + processedTweets++; } catch (error) { elizaLogger.error( `Error processing tweet ${tweet.id}:`, From c50549ea5015b1385c5e0ffb756a4d4391f493db Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Sat, 4 Jan 2025 15:01:13 -0500 Subject: [PATCH 4/8] clean code --- packages/client-twitter/src/base.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/client-twitter/src/base.ts b/packages/client-twitter/src/base.ts index d63d06564f..578b01c453 100644 --- a/packages/client-twitter/src/base.ts +++ b/packages/client-twitter/src/base.ts @@ -323,7 +323,7 @@ export class ClientBase extends EventEmitter { const agentUsername = this.twitterConfig.TWITTER_USERNAME; const homeTimeline = await this.twitterClient.fetchHomeTimeline(20, []); - const processedTweets = homeTimeline + return homeTimeline .map((tweet) => ({ id: tweet.rest_id, name: tweet.core?.user_results?.result?.legacy?.name, @@ -351,10 +351,8 @@ export class ClientBase extends EventEmitter { (media) => media.type === "video" ) || [], })) - .filter((tweet) => tweet.username !== agentUsername); // do not perform action on self-tweets - - const shuffledTweets = processedTweets.sort(() => Math.random() - 0.5); - return shuffledTweets; + .filter((tweet) => tweet.username !== agentUsername) // do not perform action on self-tweets + .sort(() => Math.random() - 0.5); } async fetchSearchTweets( From 63e69c6c72a12f24bb6fbcad4db729846efb4860 Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Sat, 4 Jan 2025 21:48:45 -0500 Subject: [PATCH 5/8] sort timelines based on the llm score --- packages/client-twitter/src/base.ts | 3 +- packages/client-twitter/src/post.ts | 54 ++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 10 deletions(-) diff --git a/packages/client-twitter/src/base.ts b/packages/client-twitter/src/base.ts index 578b01c453..f6c3431ab1 100644 --- a/packages/client-twitter/src/base.ts +++ b/packages/client-twitter/src/base.ts @@ -351,8 +351,7 @@ export class ClientBase extends EventEmitter { (media) => media.type === "video" ) || [], })) - .filter((tweet) => tweet.username !== agentUsername) // do not perform action on self-tweets - .sort(() => Math.random() - 0.5); + .filter((tweet) => tweet.username !== agentUsername); // do not perform action on self-tweets } async fetchSearchTweets( diff --git a/packages/client-twitter/src/post.ts b/packages/client-twitter/src/post.ts index b6c04db119..8fe3f5d7c5 100644 --- a/packages/client-twitter/src/post.ts +++ b/packages/client-twitter/src/post.ts @@ -627,14 +627,11 @@ export class TwitterPostClient { const homeTimeline = await this.client.fetchTimelineForActions(); const results = []; - let processedTweets = 0; const maxActionsProcessing = this.client.twitterConfig.MAX_ACTIONS_PROCESSING; + const processedTimelines = []; for (const tweet of homeTimeline) { - if (processedTweets >= maxActionsProcessing) { - break; - } try { // Skip if we've already processed this tweet const memory = @@ -685,9 +682,52 @@ export class TwitterPostClient { ); continue; } + processedTimelines.push({ + tweet: tweet, + actionResponse: actionResponse, + tweetState: tweetState, + roomId: roomId, + }); + } catch (error) { + elizaLogger.error( + `Error processing tweet ${tweet.id}:`, + error + ); + continue; + } + } + + const sortProcessedTimeline = (arr: typeof processedTimelines) => { + return arr.sort((a, b) => { + // Count the number of true values in the actionResponse object + const countTrue = (obj: typeof a.actionResponse) => + Object.values(obj).filter(Boolean).length; + + const countA = countTrue(a.actionResponse); + const countB = countTrue(b.actionResponse); + + // Primary sort by number of true values + if (countA !== countB) { + return countB - countA; + } - const executedActions: string[] = []; + // Secondary sort by the "like" property + if (a.actionResponse.like !== b.actionResponse.like) { + return a.actionResponse.like ? -1 : 1; + } + + // Tertiary sort keeps the remaining objects with equal weight + return 0; + }); + }; + const sortedTimelines = sortProcessedTimeline( + processedTimelines + ).slice(0, maxActionsProcessing); + for (const timeline of sortedTimelines) { + const { actionResponse, tweetState, roomId, tweet } = timeline; + try { + const executedActions: string[] = []; // Execute actions if (actionResponse.like) { try { @@ -923,10 +963,9 @@ export class TwitterPostClient { results.push({ tweetId: tweet.id, - parsedActions: actionResponse, + actionResponse: actionResponse, executedActions, }); - processedTweets++; } catch (error) { elizaLogger.error( `Error processing tweet ${tweet.id}:`, @@ -935,7 +974,6 @@ export class TwitterPostClient { continue; } } - return results; // Return results array to indicate completion } catch (error) { elizaLogger.error("Error in processTweetActions:", error); From c1502516dbe52305ff4c59874512a8cea7cf520c Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Sun, 5 Jan 2025 08:35:23 -0500 Subject: [PATCH 6/8] clean code and add comment --- packages/client-twitter/src/post.ts | 469 +++++++++++++++------------- 1 file changed, 246 insertions(+), 223 deletions(-) diff --git a/packages/client-twitter/src/post.ts b/packages/client-twitter/src/post.ts index 8fe3f5d7c5..535b176518 100644 --- a/packages/client-twitter/src/post.ts +++ b/packages/client-twitter/src/post.ts @@ -16,6 +16,8 @@ import { IImageDescriptionService, ServiceType } from "@elizaos/core"; import { buildConversationThread } from "./utils.ts"; import { twitterMessageHandlerTemplate } from "./interactions.ts"; import { DEFAULT_MAX_TWEET_LENGTH } from "./environment.ts"; +import { State } from "@elizaos/core"; +import { ActionResponse } from "@elizaos/core"; const twitterPostTemplate = ` # Areas of Expertise @@ -625,8 +627,10 @@ export class TwitterPostClient { "twitter" ); + // TODO: Once the 'count' parameter is fixed in the 'fetchTimeline' method of the 'agent-twitter-client', + // we should enable the ability to control the number of items fetched here. + // Related issue: https://github.com/elizaOS/agent-twitter-client/issues/43 const homeTimeline = await this.client.fetchTimelineForActions(); - const results = []; const maxActionsProcessing = this.client.twitterConfig.MAX_ACTIONS_PROCESSING; const processedTimelines = []; @@ -720,267 +724,286 @@ export class TwitterPostClient { return 0; }); }; + // Sort the timeline based on the action decision score, + // then slice the results according to the environment variable to limit the number of actions per cycle. const sortedTimelines = sortProcessedTimeline( processedTimelines ).slice(0, maxActionsProcessing); - for (const timeline of sortedTimelines) { - const { actionResponse, tweetState, roomId, tweet } = timeline; - try { - const executedActions: string[] = []; - // Execute actions - if (actionResponse.like) { - try { - if (this.isDryRun) { - elizaLogger.info( - `Dry run: would have liked tweet ${tweet.id}` - ); - executedActions.push("like (dry run)"); - } else { - await this.client.twitterClient.likeTweet( - tweet.id - ); - executedActions.push("like"); - elizaLogger.log(`Liked tweet ${tweet.id}`); - } - } catch (error) { - elizaLogger.error( - `Error liking tweet ${tweet.id}:`, - error + return this.processTimelineActions(sortedTimelines); // Return results array to indicate completion + } catch (error) { + elizaLogger.error("Error in processTweetActions:", error); + throw error; + } finally { + this.isProcessing = false; + } + } + + /** + * Processes a list of timelines by executing the corresponding tweet actions. + * Each timeline includes the tweet, action response, tweet state, and room context. + * Results are returned for tracking completed actions. + * + * @param timelines - Array of objects containing tweet details, action responses, and state information. + * @returns A promise that resolves to an array of results with details of executed actions. + */ + private async processTimelineActions( + timelines: { + tweet: Tweet; + actionResponse: ActionResponse; + tweetState: State; + roomId: UUID; + }[] + ): Promise< + { + tweetId: string; + actionResponse: ActionResponse; + executedActions: string[]; + }[] + > { + const results = []; + for (const timeline of timelines) { + const { actionResponse, tweetState, roomId, tweet } = timeline; + try { + const executedActions: string[] = []; + // Execute actions + if (actionResponse.like) { + try { + if (this.isDryRun) { + elizaLogger.info( + `Dry run: would have liked tweet ${tweet.id}` ); + executedActions.push("like (dry run)"); + } else { + await this.client.twitterClient.likeTweet(tweet.id); + executedActions.push("like"); + elizaLogger.log(`Liked tweet ${tweet.id}`); } + } catch (error) { + elizaLogger.error( + `Error liking tweet ${tweet.id}:`, + error + ); } + } - if (actionResponse.retweet) { - try { - if (this.isDryRun) { - elizaLogger.info( - `Dry run: would have retweeted tweet ${tweet.id}` - ); - executedActions.push("retweet (dry run)"); - } else { - await this.client.twitterClient.retweet( - tweet.id - ); - executedActions.push("retweet"); - elizaLogger.log(`Retweeted tweet ${tweet.id}`); - } - } catch (error) { - elizaLogger.error( - `Error retweeting tweet ${tweet.id}:`, - error + if (actionResponse.retweet) { + try { + if (this.isDryRun) { + elizaLogger.info( + `Dry run: would have retweeted tweet ${tweet.id}` ); + executedActions.push("retweet (dry run)"); + } else { + await this.client.twitterClient.retweet(tweet.id); + executedActions.push("retweet"); + elizaLogger.log(`Retweeted tweet ${tweet.id}`); } + } catch (error) { + elizaLogger.error( + `Error retweeting tweet ${tweet.id}:`, + error + ); } + } - if (actionResponse.quote) { - try { - // Check for dry run mode - if (this.isDryRun) { - elizaLogger.info( - `Dry run: would have posted quote tweet for ${tweet.id}` - ); - executedActions.push("quote (dry run)"); - continue; - } - - // Build conversation thread for context - const thread = await buildConversationThread( - tweet, - this.client + if (actionResponse.quote) { + try { + // Check for dry run mode + if (this.isDryRun) { + elizaLogger.info( + `Dry run: would have posted quote tweet for ${tweet.id}` ); - const formattedConversation = thread - .map( - (t) => - `@${t.username} (${new Date(t.timestamp * 1000).toLocaleString()}): ${t.text}` - ) - .join("\n\n"); + executedActions.push("quote (dry run)"); + continue; + } - // Generate image descriptions if present - const imageDescriptions = []; - if (tweet.photos?.length > 0) { - elizaLogger.log( - "Processing images in tweet for context" - ); - for (const photo of tweet.photos) { - const description = await this.runtime - .getService( - ServiceType.IMAGE_DESCRIPTION - ) - .describeImage(photo.url); - imageDescriptions.push(description); - } + // Build conversation thread for context + const thread = await buildConversationThread( + tweet, + this.client + ); + const formattedConversation = thread + .map( + (t) => + `@${t.username} (${new Date(t.timestamp * 1000).toLocaleString()}): ${t.text}` + ) + .join("\n\n"); + + // Generate image descriptions if present + const imageDescriptions = []; + if (tweet.photos?.length > 0) { + elizaLogger.log( + "Processing images in tweet for context" + ); + for (const photo of tweet.photos) { + const description = await this.runtime + .getService( + ServiceType.IMAGE_DESCRIPTION + ) + .describeImage(photo.url); + imageDescriptions.push(description); } + } - // Handle quoted tweet if present - let quotedContent = ""; - if (tweet.quotedStatusId) { - try { - const quotedTweet = - await this.client.twitterClient.getTweet( - tweet.quotedStatusId - ); - if (quotedTweet) { - quotedContent = `\nQuoted Tweet from @${quotedTweet.username}:\n${quotedTweet.text}`; - } - } catch (error) { - elizaLogger.error( - "Error fetching quoted tweet:", - error + // Handle quoted tweet if present + let quotedContent = ""; + if (tweet.quotedStatusId) { + try { + const quotedTweet = + await this.client.twitterClient.getTweet( + tweet.quotedStatusId ); + if (quotedTweet) { + quotedContent = `\nQuoted Tweet from @${quotedTweet.username}:\n${quotedTweet.text}`; } - } - - // Compose rich state with all context - const enrichedState = - await this.runtime.composeState( - { - userId: this.runtime.agentId, - roomId: stringToUuid( - tweet.conversationId + - "-" + - this.runtime.agentId - ), - agentId: this.runtime.agentId, - content: { - text: tweet.text, - action: "QUOTE", - }, - }, - { - twitterUserName: this.twitterUsername, - currentPost: `From @${tweet.username}: ${tweet.text}`, - formattedConversation, - imageContext: - imageDescriptions.length > 0 - ? `\nImages in Tweet:\n${imageDescriptions.map((desc, i) => `Image ${i + 1}: ${desc}`).join("\n")}` - : "", - quotedContent, - } + } catch (error) { + elizaLogger.error( + "Error fetching quoted tweet:", + error ); + } + } - const quoteContent = - await this.generateTweetContent(enrichedState, { - template: - this.runtime.character.templates - ?.twitterMessageHandlerTemplate || - twitterMessageHandlerTemplate, - }); + // Compose rich state with all context + const enrichedState = await this.runtime.composeState( + { + userId: this.runtime.agentId, + roomId: stringToUuid( + tweet.conversationId + + "-" + + this.runtime.agentId + ), + agentId: this.runtime.agentId, + content: { + text: tweet.text, + action: "QUOTE", + }, + }, + { + twitterUserName: this.twitterUsername, + currentPost: `From @${tweet.username}: ${tweet.text}`, + formattedConversation, + imageContext: + imageDescriptions.length > 0 + ? `\nImages in Tweet:\n${imageDescriptions.map((desc, i) => `Image ${i + 1}: ${desc}`).join("\n")}` + : "", + quotedContent, + } + ); - if (!quoteContent) { - elizaLogger.error( - "Failed to generate valid quote tweet content" - ); - return; + const quoteContent = await this.generateTweetContent( + enrichedState, + { + template: + this.runtime.character.templates + ?.twitterMessageHandlerTemplate || + twitterMessageHandlerTemplate, } + ); - elizaLogger.log( - "Generated quote tweet content:", - quoteContent + if (!quoteContent) { + elizaLogger.error( + "Failed to generate valid quote tweet content" ); + return; + } - // Send the tweet through request queue - const result = await this.client.requestQueue.add( - async () => - await this.client.twitterClient.sendQuoteTweet( - quoteContent, - tweet.id - ) - ); + elizaLogger.log( + "Generated quote tweet content:", + quoteContent + ); - const body = await result.json(); + // Send the tweet through request queue + const result = await this.client.requestQueue.add( + async () => + await this.client.twitterClient.sendQuoteTweet( + quoteContent, + tweet.id + ) + ); - if ( - body?.data?.create_tweet?.tweet_results?.result - ) { - elizaLogger.log( - "Successfully posted quote tweet" - ); - executedActions.push("quote"); + const body = await result.json(); - // Cache generation context for debugging - await this.runtime.cacheManager.set( - `twitter/quote_generation_${tweet.id}.txt`, - `Context:\n${enrichedState}\n\nGenerated Quote:\n${quoteContent}` - ); - } else { - elizaLogger.error( - "Quote tweet creation failed:", - body - ); - } - } catch (error) { - elizaLogger.error( - "Error in quote tweet generation:", - error - ); - } - } + if (body?.data?.create_tweet?.tweet_results?.result) { + elizaLogger.log("Successfully posted quote tweet"); + executedActions.push("quote"); - if (actionResponse.reply) { - try { - await this.handleTextOnlyReply( - tweet, - tweetState, - executedActions + // Cache generation context for debugging + await this.runtime.cacheManager.set( + `twitter/quote_generation_${tweet.id}.txt`, + `Context:\n${enrichedState}\n\nGenerated Quote:\n${quoteContent}` ); - } catch (error) { + } else { elizaLogger.error( - `Error replying to tweet ${tweet.id}:`, - error + "Quote tweet creation failed:", + body ); } + } catch (error) { + elizaLogger.error( + "Error in quote tweet generation:", + error + ); } + } - // Add these checks before creating memory - await this.runtime.ensureRoomExists(roomId); - await this.runtime.ensureUserExists( - stringToUuid(tweet.userId), - tweet.username, - tweet.name, - "twitter" - ); - await this.runtime.ensureParticipantInRoom( - this.runtime.agentId, - roomId - ); + if (actionResponse.reply) { + try { + await this.handleTextOnlyReply( + tweet, + tweetState, + executedActions + ); + } catch (error) { + elizaLogger.error( + `Error replying to tweet ${tweet.id}:`, + error + ); + } + } - // Then create the memory - await this.runtime.messageManager.createMemory({ - id: stringToUuid(tweet.id + "-" + this.runtime.agentId), - userId: stringToUuid(tweet.userId), - content: { - text: tweet.text, - url: tweet.permanentUrl, - source: "twitter", - action: executedActions.join(","), - }, - agentId: this.runtime.agentId, - roomId, - embedding: getEmbeddingZeroVector(), - createdAt: tweet.timestamp * 1000, - }); + // Add these checks before creating memory + await this.runtime.ensureRoomExists(roomId); + await this.runtime.ensureUserExists( + stringToUuid(tweet.userId), + tweet.username, + tweet.name, + "twitter" + ); + await this.runtime.ensureParticipantInRoom( + this.runtime.agentId, + roomId + ); - results.push({ - tweetId: tweet.id, - actionResponse: actionResponse, - executedActions, - }); - } catch (error) { - elizaLogger.error( - `Error processing tweet ${tweet.id}:`, - error - ); - continue; - } + // Then create the memory + await this.runtime.messageManager.createMemory({ + id: stringToUuid(tweet.id + "-" + this.runtime.agentId), + userId: stringToUuid(tweet.userId), + content: { + text: tweet.text, + url: tweet.permanentUrl, + source: "twitter", + action: executedActions.join(","), + }, + agentId: this.runtime.agentId, + roomId, + embedding: getEmbeddingZeroVector(), + createdAt: tweet.timestamp * 1000, + }); + + results.push({ + tweetId: tweet.id, + actionResponse: actionResponse, + executedActions, + }); + } catch (error) { + elizaLogger.error(`Error processing tweet ${tweet.id}:`, error); + continue; } - return results; // Return results array to indicate completion - } catch (error) { - elizaLogger.error("Error in processTweetActions:", error); - throw error; - } finally { - this.isProcessing = false; } + + return results; } /** From c1cf36230fc7c331e590e729babe4132207c27be Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Sun, 5 Jan 2025 08:48:20 -0500 Subject: [PATCH 7/8] add MAX_ACTIONS_PROCESSING --- .env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.example b/.env.example index 2c0bdbe3bd..3125b25582 100644 --- a/.env.example +++ b/.env.example @@ -85,6 +85,7 @@ POST_IMMEDIATELY= # Twitter action processing configuration ACTION_INTERVAL= # Interval in minutes between action processing runs (default: 5 minutes) ENABLE_ACTION_PROCESSING=false # Set to true to enable the action processing loop +MAX_ACTIONS_PROCESSING= # Maximum number of actions (e.g., retweets, likes) to process in a single cycle. Helps prevent excessive or uncontrolled actions. # Feature Flags IMAGE_GEN= # Set to TRUE to enable image generation From 857673f0e3ccf2b40ef6c04c783c2bd95d787ccc Mon Sep 17 00:00:00 2001 From: Ting Chien Meng Date: Mon, 6 Jan 2025 11:28:40 -0500 Subject: [PATCH 8/8] add followinf option for timeline action --- .env.example | 3 ++- packages/client-twitter/src/base.ts | 7 ++++++- packages/client-twitter/src/environment.ts | 13 ++++++++++++- packages/core/src/types.ts | 5 +++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 3125b25582..a871759f4c 100644 --- a/.env.example +++ b/.env.example @@ -85,7 +85,8 @@ POST_IMMEDIATELY= # Twitter action processing configuration ACTION_INTERVAL= # Interval in minutes between action processing runs (default: 5 minutes) ENABLE_ACTION_PROCESSING=false # Set to true to enable the action processing loop -MAX_ACTIONS_PROCESSING= # Maximum number of actions (e.g., retweets, likes) to process in a single cycle. Helps prevent excessive or uncontrolled actions. +MAX_ACTIONS_PROCESSING=1 # Maximum number of actions (e.g., retweets, likes) to process in a single cycle. Helps prevent excessive or uncontrolled actions. +ACTION_TIMELINE_TYPE=foryou # Type of timeline to interact with. Options: "foryou" or "following". Default: "foryou" # Feature Flags IMAGE_GEN= # Set to TRUE to enable image generation diff --git a/packages/client-twitter/src/base.ts b/packages/client-twitter/src/base.ts index f6c3431ab1..d8a800636d 100644 --- a/packages/client-twitter/src/base.ts +++ b/packages/client-twitter/src/base.ts @@ -8,6 +8,7 @@ import { getEmbeddingZeroVector, elizaLogger, stringToUuid, + ActionTimelineType, } from "@elizaos/core"; import { QueryTweetsResponse, @@ -321,7 +322,11 @@ export class ClientBase extends EventEmitter { elizaLogger.debug("fetching timeline for actions"); const agentUsername = this.twitterConfig.TWITTER_USERNAME; - const homeTimeline = await this.twitterClient.fetchHomeTimeline(20, []); + const homeTimeline = + this.twitterConfig.ACTION_TIMELINE_TYPE === + ActionTimelineType.Following + ? await this.twitterClient.fetchFollowingTimeline(20, []) + : await this.twitterClient.fetchHomeTimeline(20, []); return homeTimeline .map((tweet) => ({ diff --git a/packages/client-twitter/src/environment.ts b/packages/client-twitter/src/environment.ts index 05b3fafaae..cdb57b02c2 100644 --- a/packages/client-twitter/src/environment.ts +++ b/packages/client-twitter/src/environment.ts @@ -1,4 +1,8 @@ -import { parseBooleanFromText, IAgentRuntime } from "@elizaos/core"; +import { + parseBooleanFromText, + IAgentRuntime, + ActionTimelineType, +} from "@elizaos/core"; import { z, ZodError } from "zod"; export const DEFAULT_MAX_TWEET_LENGTH = 280; @@ -62,6 +66,9 @@ export const twitterEnvSchema = z.object({ POST_IMMEDIATELY: z.boolean(), TWITTER_SPACES_ENABLE: z.boolean().default(false), MAX_ACTIONS_PROCESSING: z.number().int(), + ACTION_TIMELINE_TYPE: z + .nativeEnum(ActionTimelineType) + .default(ActionTimelineType.ForYou), }); export type TwitterConfig = z.infer; @@ -206,6 +213,10 @@ export async function validateTwitterConfig( process.env.MAX_ACTIONS_PROCESSING, 1 ), + + ACTION_TIMELINE_TYPE: + runtime.getSetting("ACTION_TIMELINE_TYPE") || + process.env.ACTION_TIMELINE_TYPE, }; return twitterEnvSchema.parse(twitterConfig); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3687ded5e0..2f60e8c908 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1328,3 +1328,8 @@ export enum TranscriptionProvider { Deepgram = "deepgram", Local = "local", } + +export enum ActionTimelineType { + ForYou = "foryou", + Following = "following", +}