diff --git a/packages/client-twitter/src/base.ts b/packages/client-twitter/src/base.ts index feedf94c4f..0ba693eee6 100644 --- a/packages/client-twitter/src/base.ts +++ b/packages/client-twitter/src/base.ts @@ -17,12 +17,27 @@ import { } from "agent-twitter-client"; import { EventEmitter } from "events"; +/** + * Extracts the answer from the given text. + * + * @param {string} text - The text from which to extract the answer. + * @returns {string} - The extracted answer. + */ export function extractAnswer(text: string): string { const startIndex = text.indexOf("Answer: ") + 8; const endIndex = text.indexOf("<|endoftext|>", 11); return text.slice(startIndex, endIndex); } +/** + * Represents a Twitter user profile. + * @typedef {Object} TwitterProfile + * @property {string} id - The unique identifier of the user. + * @property {string} username - The username of the user. + * @property {string} screenName - The display name of the user. + * @property {string} bio - The biography of the user. + * @property {string[]} nicknames - An array of nicknames associated with the user. + */ type TwitterProfile = { id: string; username: string; @@ -31,10 +46,20 @@ type TwitterProfile = { nicknames: string[]; }; +/** + * Represents a queue of requests that are processed in order with exponential backoff and random delay. + */ class RequestQueue { private queue: (() => Promise)[] = []; private processing: boolean = false; +/** + * Asynchronously adds a new request to the queue for processing. + * + * @template T + * @param {() => Promise} request - The request to be added to the queue. + * @returns {Promise} - A promise that resolves with the result of the request or rejects with an error. + */ async add(request: () => Promise): Promise { return new Promise((resolve, reject) => { this.queue.push(async () => { @@ -49,6 +74,13 @@ class RequestQueue { }); } +/** + * Asynchronously processes the queue of requests one by one. + * If there is an error while processing a request, it will log the error, re-add the failed request to the front of the queue, + * and wait using exponential backoff before attempting the request again. + * Additionally, a random delay is added after each request processing. + * This method returns a Promise that resolves once all requests in the queue have been processed. + */ private async processQueue(): Promise { if (this.processing || this.queue.length === 0) { return; @@ -70,17 +102,31 @@ class RequestQueue { this.processing = false; } +/** + * Perform exponential backoff for retrying a process. + * @param {number} retryCount - The number of times to retry the process + * @returns {Promise} A promise that resolves after the specified delay + */ private async exponentialBackoff(retryCount: number): Promise { const delay = Math.pow(2, retryCount) * 1000; await new Promise((resolve) => setTimeout(resolve, delay)); } +/** + * Asynchronously waits for a random delay between 1500ms and 3500ms. + * @returns {Promise} A Promise that resolves after the random delay. + */ private async randomDelay(): Promise { const delay = Math.floor(Math.random() * 2000) + 1500; await new Promise((resolve) => setTimeout(resolve, delay)); } } +/** + * A base class for a Twitter client that extends EventEmitter. + * Handles caching of tweets, fetching of tweets, initializing the client, + * fetching user posts, home timeline, search tweets, and timeline population. + */ export class ClientBase extends EventEmitter { static _twitterClients: { [accountIdentifier: string]: Scraper } = {}; twitterClient: Scraper; @@ -94,6 +140,12 @@ export class ClientBase extends EventEmitter { profile: TwitterProfile | null; +/** + * Caches a Tweet object in the cache manager. + * + * @param {Tweet} tweet - The Tweet object to cache. + * @returns {Promise} A Promise that resolves when the Tweet is successfully cached. + */ async cacheTweet(tweet: Tweet): Promise { if (!tweet) { console.warn("Tweet is undefined, skipping cache"); @@ -103,6 +155,12 @@ export class ClientBase extends EventEmitter { this.runtime.cacheManager.set(`twitter/tweets/${tweet.id}`, tweet); } +/** + * Asynchronously retrieves a cached Tweet object with the specified tweet ID. + * + * @param {string} tweetId - The ID of the tweet to retrieve from the cache. + * @returns {Promise} A Promise that resolves to the cached tweet object, or undefined if not found. + */ async getCachedTweet(tweetId: string): Promise { const cached = await this.runtime.cacheManager.get( `twitter/tweets/${tweetId}` @@ -111,6 +169,13 @@ export class ClientBase extends EventEmitter { return cached; } +/** + * Retrieves a tweet with the specified tweet ID. If the tweet is found in the cache, it is returned directly. + * Otherwise, the tweet is fetched from the Twitter client using a request queue and then cached for future use. + * + * @param {string} tweetId The ID of the tweet to retrieve. + * @returns {Promise} A promise that resolves to the fetched tweet. + */ async getTweet(tweetId: string): Promise { const cachedTweet = await this.getCachedTweet(tweetId); @@ -128,12 +193,21 @@ export class ClientBase extends EventEmitter { callback: (self: ClientBase) => any = null; +/** + * Function to be called when the object is ready. + * + * @throws {Error} Not implemented in base class, please call from subclass + */ onReady() { throw new Error( "Not implemented in base class, please call from subclass" ); } +/** + * Constructor for initializing a new instance of ClientBase. + * @param {IAgentRuntime} runtime - The runtime object for the agent. + */ constructor(runtime: IAgentRuntime) { super(); this.runtime = runtime; @@ -152,6 +226,14 @@ export class ClientBase extends EventEmitter { this.runtime.character.style.post.join(); } +/** +* Asynchronously initializes the Twitter bot by logging in with the specified credentials. +* Retrieves settings such as username, password, email, retry limit, and 2FA secret. +* Logs in to Twitter with retries, caching cookies if available. +* Retrieves Twitter profile information, stores it for responses, and populates the timeline. +* +* @returns {Promise} A Promise that resolves once the initialization process is complete. +*/ async init() { const username = this.runtime.getSetting("TWITTER_USERNAME"); const password = this.runtime.getSetting("TWITTER_PASSWORD"); @@ -237,6 +319,11 @@ export class ClientBase extends EventEmitter { await this.populateTimeline(); } +/** + * Fetches the specified number of own posts from Twitter. + * @param {number} count - The number of posts to fetch + * @returns {Promise} The array of own tweets fetched + */ async fetchOwnPosts(count: number): Promise { elizaLogger.debug("fetching own posts"); const homeTimeline = await this.twitterClient.getUserTweets( @@ -246,6 +333,12 @@ export class ClientBase extends EventEmitter { return homeTimeline.tweets; } +/** + * Asynchronously fetches the home timeline with the specified count. + * + * @param {number} count - The number of tweets to fetch. + * @returns {Promise} - A promise that resolves to an array of processed tweets. + */ async fetchHomeTimeline(count: number): Promise { elizaLogger.debug("fetching home timeline"); const homeTimeline = await this.twitterClient.fetchHomeTimeline( @@ -306,6 +399,11 @@ export class ClientBase extends EventEmitter { return processedTimeline; } +/** + * Fetches the timeline for actions based on the given count. + * @param {number} count - The number of tweets to fetch + * @returns {Promise} - A promise that resolves with an array of Tweet objects representing the timeline for actions + */ async fetchTimelineForActions(count: number): Promise { elizaLogger.debug("fetching timeline for actions"); const homeTimeline = await this.twitterClient.fetchHomeTimeline( @@ -338,6 +436,15 @@ export class ClientBase extends EventEmitter { })); } +/** + * Fetch search tweets based on the provided query, max number of tweets, search mode, and optional cursor. + * + * @param {string} query - The search query for tweets. + * @param {number} maxTweets - The maximum number of tweets to fetch. + * @param {SearchMode} searchMode - The search mode to use for fetching tweets. + * @param {string} [cursor] - Optional cursor for pagination. + * @returns {Promise} A promise that resolves with the response containing the fetched tweets. + */ async fetchSearchTweets( query: string, maxTweets: number, @@ -375,6 +482,12 @@ export class ClientBase extends EventEmitter { } } +/** + * Asynchronously populates the timeline by fetching and processing tweets. + * It checks the cached timeline, reads it from file, gets existing memories from the database, + * filters out existing tweets, saves missing tweets as memories, and then combines with the home timeline. + * Handles mentions, interactions, and new tweets by creating memories for them. + */ private async populateTimeline() { elizaLogger.debug("populating timeline..."); @@ -614,6 +727,19 @@ export class ClientBase extends EventEmitter { await this.cacheMentions(mentionsAndInteractions.tweets); } +/** + * Sets cookies from an array of cookie objects. + * + * @param {Object[]} cookiesArray - An array of cookie objects. + * @param {string} cookiesArray[].key - The key of the cookie. + * @param {string} cookiesArray[].value - The value of the cookie. + * @param {string} cookiesArray[].domain - The domain of the cookie. + * @param {string} cookiesArray[].path - The path of the cookie. + * @param {boolean} [cookiesArray[].secure] - Whether the cookie is secure or not. + * @param {boolean} [cookiesArray[].httpOnly] - Whether the cookie is HttpOnly or not. + * @param {string} [cookiesArray[].sameSite] - The SameSite attribute of the cookie, defaults to 'Lax'. + * @returns {Promise} - A Promise that resolves when the cookies are set. + */ async setCookiesFromArray(cookiesArray: any[]) { const cookieStrings = cookiesArray.map( (cookie) => @@ -626,6 +752,13 @@ export class ClientBase extends EventEmitter { await this.twitterClient.setCookies(cookieStrings); } +/** + * Asynchronously saves a request message in memory and evaluates it. + * + * @param {Memory} message - The message to be saved in memory. + * @param {State} state - The current state of the application. + * @returns {Promise} - A Promise that resolves once the message is saved and evaluated. + */ async saveRequestMessage(message: Memory, state: State) { if (message.content.text) { const recentMessage = await this.runtime.messageManager.getMemories( @@ -655,6 +788,12 @@ export class ClientBase extends EventEmitter { } } +/** + * Asynchronously loads the latest checked tweet ID from cache. + * If the ID is found in the cache, it converts it to a BigInt + * and assigns it to the 'lastCheckedTweetId' property of the class instance. + * @returns {Promise} + */ async loadLatestCheckedTweetId(): Promise { const latestCheckedTweetId = await this.runtime.cacheManager.get( @@ -666,6 +805,9 @@ export class ClientBase extends EventEmitter { } } +/** + * Asynchronously caches the latest checked tweet ID for the user's Twitter profile. + */ async cacheLatestCheckedTweetId() { if (this.lastCheckedTweetId) { await this.runtime.cacheManager.set( @@ -675,12 +817,22 @@ export class ClientBase extends EventEmitter { } } +/** + * Asynchronously retrieves the cached timeline for the user's profile. + * + * @returns A Promise that resolves with an array of Tweet objects representing the timeline of the user's profile, or undefined if the timeline is not cached. + */ async getCachedTimeline(): Promise { return await this.runtime.cacheManager.get( `twitter/${this.profile.username}/timeline` ); } +/** + * Caches the timeline of a user. + * @param {Tweet[]} timeline - The timeline of tweets to cache. + * @returns {Promise} - A Promise that resolves once the timeline is cached. + */ async cacheTimeline(timeline: Tweet[]) { await this.runtime.cacheManager.set( `twitter/${this.profile.username}/timeline`, @@ -689,6 +841,12 @@ export class ClientBase extends EventEmitter { ); } +/** + * Caches the provided array of Tweet mentions for the Twitter profile. + * + * @param {Tweet[]} mentions - The array of Tweet mentions to cache. + * @returns {Promise} - A Promise that resolves once the mentions are successfully cached. + */ async cacheMentions(mentions: Tweet[]) { await this.runtime.cacheManager.set( `twitter/${this.profile.username}/mentions`, @@ -697,12 +855,25 @@ export class ClientBase extends EventEmitter { ); } +/** + * Retrieve cached cookies for a specific user from the cacheManager. + * + * @param {string} username - The username of the user for whom cookies are being retrieved. + * @returns {Promise} - A Promise that resolves to an array of cookies for the specified user. + */ async getCachedCookies(username: string) { return await this.runtime.cacheManager.get( `twitter/${username}/cookies` ); } +/** + * Caches the provided cookies for a specific user in the runtime cache manager. + * + * @param {string} username - The username of the user for whom the cookies are being cached. + * @param {any[]} cookies - The cookies to be cached. + * @returns {Promise} - A Promise that resolves once the cookies are cached. + */ async cacheCookies(username: string, cookies: any[]) { await this.runtime.cacheManager.set( `twitter/${username}/cookies`, @@ -710,12 +881,23 @@ export class ClientBase extends EventEmitter { ); } +/** + * Asynchronously retrieves the cached Twitter profile for a given username. + * + * @param {string} username - The username for which to retrieve the profile. + * @returns {Promise} A Promise that resolves to the cached Twitter profile object. + */ async getCachedProfile(username: string) { return await this.runtime.cacheManager.get( `twitter/${username}/profile` ); } +/** +* Caches the Twitter profile for a specific user in the cache manager. +* @param {TwitterProfile} profile - The Twitter profile to cache. +* @returns {Promise} - A promise that resolves once the profile is cached. +*/ async cacheProfile(profile: TwitterProfile) { await this.runtime.cacheManager.set( `twitter/${profile.username}/profile`, @@ -723,6 +905,11 @@ export class ClientBase extends EventEmitter { ); } +/** + * Fetches the Twitter profile of a user. + * @param {string} username - The username of the Twitter user. + * @returns {Promise} The Twitter profile of the user. + */ async fetchProfile(username: string): Promise { const cached = await this.getCachedProfile(username); diff --git a/packages/client-twitter/src/environment.ts b/packages/client-twitter/src/environment.ts index 8adabce3b8..15e565c426 100644 --- a/packages/client-twitter/src/environment.ts +++ b/packages/client-twitter/src/environment.ts @@ -16,8 +16,17 @@ export const twitterEnvSchema = z.object({ .default(DEFAULT_MAX_TWEET_LENGTH.toString()), }); +/** + * Represents the configuration for Twitter settings. + * This type is inferred from the 'twitterEnvSchema'. + */ export type TwitterConfig = z.infer; +/** + * Validates the Twitter configuration settings provided by the runtime and environment variables. + * @param {IAgentRuntime} runtime - The runtime object containing the settings. + * @returns {Promise} - A promise that resolves with the validated Twitter configuration. + */ export async function validateTwitterConfig( runtime: IAgentRuntime ): Promise { diff --git a/packages/client-twitter/src/index.ts b/packages/client-twitter/src/index.ts index 2476afd6d0..aa93f410e6 100644 --- a/packages/client-twitter/src/index.ts +++ b/packages/client-twitter/src/index.ts @@ -5,11 +5,21 @@ import { TwitterInteractionClient } from "./interactions.ts"; import { TwitterPostClient } from "./post.ts"; import { TwitterSearchClient } from "./search.ts"; +/** + * Class representing a Twitter manager for interacting with Twitter API. + * @class + */ class TwitterManager { client: ClientBase; post: TwitterPostClient; search: TwitterSearchClient; interaction: TwitterInteractionClient; +/** + * Constructor for creating a new Twitter client with specified runtime and search functionality. + * + * @param {IAgentRuntime} runtime - The runtime environment for the client. + * @param {boolean} enableSearch - Flag to enable search functionality. + */ constructor(runtime: IAgentRuntime, enableSearch: boolean) { this.client = new ClientBase(runtime); this.post = new TwitterPostClient(this.client, runtime); diff --git a/packages/client-twitter/src/interactions.ts b/packages/client-twitter/src/interactions.ts index bd71684bf4..6f559c5474 100644 --- a/packages/client-twitter/src/interactions.ts +++ b/packages/client-twitter/src/interactions.ts @@ -86,14 +86,26 @@ Thread of Tweets You Are Replying To: # 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; +/** + * Represents a client for interacting with Twitter, handling interactions and responding to tweets. + */ export class TwitterInteractionClient { client: ClientBase; runtime: IAgentRuntime; +/** + * Constructor for creating a new instance of a class. + * + * @param {ClientBase} client - The client object to interact with. + * @param {IAgentRuntime} runtime - The runtime object for managing the agent. + */ constructor(client: ClientBase, runtime: IAgentRuntime) { this.client = client; this.runtime = runtime; } +/** + * Asynchronously starts the Twitter interactions loop. + */ async start() { const handleTwitterInteractionsLoop = () => { this.handleTwitterInteractions(); @@ -107,6 +119,10 @@ export class TwitterInteractionClient { handleTwitterInteractionsLoop(); } +/** + * Asynchronously handles Twitter interactions including checking mentions and tweets from target users. + * @returns {Promise} A Promise that resolves when the Twitter interactions have been processed + */ async handleTwitterInteractions() { elizaLogger.log("Checking Twitter interactions"); // Read from environment variable, fallback to default list if not set @@ -299,6 +315,15 @@ export class TwitterInteractionClient { } } +/** + * Handles a tweet message and generates appropriate responses. + * + * @param {Object} param0 - The parameters object + * @param {Tweet} param0.tweet - The tweet object + * @param {Memory} param0.message - The message object + * @param {Tweet[]} param0.thread - The thread of tweets + * @returns {Promise} - The response object containing text and action + */ private async handleTweet({ tweet, message, @@ -494,6 +519,13 @@ export class TwitterInteractionClient { } } +/** + * Asynchronously builds a conversation thread starting from a given tweet. + * + * @param {Tweet} tweet - The tweet to start building the thread from. + * @param {number} [maxReplies=10] - The maximum number of replies to include in the thread. + * @returns {Promise} The list of tweets representing the conversation thread. + */ async buildConversationThread( tweet: Tweet, maxReplies: number = 10 diff --git a/packages/client-twitter/src/post.ts b/packages/client-twitter/src/post.ts index 50e00d8f05..4d491f88f6 100644 --- a/packages/client-twitter/src/post.ts +++ b/packages/client-twitter/src/post.ts @@ -60,7 +60,13 @@ Tweet: # Respond with qualifying action tags only.` + postActionResponseFooter; /** - * Truncate text to fit within the Twitter character limit, ensuring it ends at a complete sentence. + * Truncates a given text to fit within the maximum tweet length while maintaining complete sentences. + * If possible, truncates at the last period within the limit. If no period is found, truncates to the + * nearest whitespace. If all else fails, hard truncates and adds ellipsis. + * + * @param {string} text - The input text to be truncated. + * @param {number} maxTweetLength - The maximum length allowed for a tweet. + * @returns {string} The truncated text that fits within the max tweet length. */ function truncateToCompleteSentence( text: string, diff --git a/packages/client-twitter/src/search.ts b/packages/client-twitter/src/search.ts index 0e1662fd74..96591e9651 100644 --- a/packages/client-twitter/src/search.ts +++ b/packages/client-twitter/src/search.ts @@ -42,22 +42,36 @@ Your response should not contain any questions. Brief, concise statements only. ` + messageCompletionFooter; +/** + * Class representing a Twitter search client. + */ export class TwitterSearchClient { client: ClientBase; runtime: IAgentRuntime; twitterUsername: string; private respondedTweets: Set = new Set(); +/** + * Initialize a new instance of the TwitterService class. + * @param {ClientBase} client - The client used to communicate with Twitter API. + * @param {IAgentRuntime} runtime - The agent runtime environment. + */ constructor(client: ClientBase, runtime: IAgentRuntime) { this.client = client; this.runtime = runtime; this.twitterUsername = runtime.getSetting("TWITTER_USERNAME"); } +/** + * Asynchronously starts the search term engagement loop. + */ async start() { this.engageWithSearchTermsLoop(); } +/** + * Continuously engages with search terms at random intervals between 60-120 minutes. + */ private engageWithSearchTermsLoop() { this.engageWithSearchTerms(); setTimeout( @@ -66,6 +80,15 @@ export class TwitterSearchClient { ); } +/** + * Asynchronously engages with search terms to fetch tweets, select the most interesting tweet, + * and generate a response to engage with users on Twitter. The process involves fetching recent + * tweets based on a random search term, selecting a tweet to reply to, building a conversation + * thread, composing a response message, sending the response tweet, updating state and memories, + * and caching the tweet and response information. If no suitable tweets are found or if an error + * occurs during the process, appropriate error messages are logged. This method utilizes various + * helper functions and services to facilitate the engagement process. + */ private async engageWithSearchTerms() { console.log("Engaging with search terms"); try { diff --git a/packages/client-twitter/src/utils.ts b/packages/client-twitter/src/utils.ts index 526e2e12a1..194fb07a5a 100644 --- a/packages/client-twitter/src/utils.ts +++ b/packages/client-twitter/src/utils.ts @@ -30,6 +30,14 @@ export const isValidTweet = (tweet: Tweet): boolean => { ); }; +/** + * Asynchronously builds a conversation thread starting from a given Tweet. + * + * @param {Tweet} tweet - The initial tweet to build the conversation thread from. + * @param {ClientBase} client - The client object used for interacting with the Twitter API. + * @param {number} [maxReplies=10] - The maximum number of replies to include in the thread. + * @returns {Promise} The conversation thread as an array of Tweets. + */ export async function buildConversationThread( tweet: Tweet, client: ClientBase, @@ -165,6 +173,16 @@ export async function buildConversationThread( return thread; } +/** + * Sends a tweet with the specified content, attachments, and in reply to a tweet. + * + * @param {ClientBase} client - The client used to send the tweet. + * @param {Content} content - The content of the tweet. + * @param {UUID} roomId - The ID of the room where the tweet will be sent. + * @param {string} twitterUsername - The username of the Twitter account sending the tweet. + * @param {string} inReplyTo - The ID of the tweet to which this tweet is replying to. + * @returns {Promise} - An array of memories representing the sent tweets. + */ export async function sendTweet( client: ClientBase, content: Content, @@ -276,6 +294,12 @@ export async function sendTweet( return memories; } +/** + * Splits the given content into tweet-sized chunks based on the specified maxLength. + * @param {string} content - The content to be split into tweet-sized chunks. + * @param {number} maxLength - The maximum length that a single tweet can have. + * @returns {string[]} An array of tweet-sized chunks from the content. + */ function splitTweetContent(content: string, maxLength: number): string[] { const paragraphs = content.split("\n\n").map((p) => p.trim()); const tweets: string[] = []; @@ -312,6 +336,12 @@ function splitTweetContent(content: string, maxLength: number): string[] { return tweets; } +/** + * Splits a given paragraph into chunks based on a specified maximum length. + * @param paragraph The paragraph to split into chunks. + * @param maxLength The maximum length of each chunk. + * @returns An array of strings, each representing a chunk of the paragraph. + */ function splitParagraph(paragraph: string, maxLength: number): string[] { // eslint-disable-next-line const sentences = paragraph.match(/[^\.!\?]+[\.!\?]+|[^\.!\?]+$/g) || [