UNPKG

@defikitdotnet/x-ai-combat

Version:

XCombatAI - Social Media Engagement Template for the Agent Framework

465 lines 24.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BotResponderPlugin = void 0; const agent_twitter_client_1 = require("@defikitdotnet/agent-twitter-client"); const Post_1 = require("../models/Post"); const XAIAdapter_1 = require("../db/XAIAdapter"); const twitter_service_1 = require("../services/twitter-service"); const core_1 = require("@defikitdotnet/core"); const ai_utils_1 = require("../utils/ai-utils"); const redis_service_1 = require("../services/redis-service"); // Import RedisService const logger_1 = require("../utils/logger"); /** * BotResponder plugin * * Monitors posts mentioning the agent and replies to unlinked users * Uses a Redis queue to manage replies */ class BotResponderPlugin { constructor(runtime, twitterConfig) { this.name = "bot-responder"; this.version = "1.0.0"; this.description = "Monitors posts and replies to unlinked users using Redis queue"; this.checkInterval = null; this.checkIntervalMinutes = 15; this.isChecking = false; this.isProcessingQueue = false; this.lastCheckTime = null; this.adapter = new XAIAdapter_1.XAIAdapter(runtime.databaseAdapter); this.runtime = runtime; this.twitterConfig = twitterConfig; this.twitterService = new twitter_service_1.TwitterService(runtime, twitterConfig); this.agentId = runtime.agentId; this.redisService = new redis_service_1.RedisService(this.runtime.agentId); } /** * Initialize the plugin */ async initialize() { (0, logger_1.log)(`Initializing BotResponder Plugin for agent ${this.agentId}...`); try { // Check Redis connection via the service await this.redisService.ping(); (0, logger_1.log)("Successfully connected to Redis service"); // Check for incomplete processing from previous run const lastProcessingId = await this.redisService.getLastProcessingTweetId(); if (lastProcessingId) { (0, logger_1.warn)(`[${this.agentId}] BotResponder may have shut down uncleanly. Last processing tweet ID: ${lastProcessingId}. Consider verifying its status.`); // Optionally, clear it or handle recovery // await this.redisService.clearLastProcessingTweetId(); } // Load last check time from Redis via the service this.lastCheckTime = await this.redisService.getLastCheckTime(); if (this.lastCheckTime) { (0, logger_1.log)(`Loaded last check time from Redis: ${this.lastCheckTime.toISOString()}`); } else { const keys = this.redisService.getKeys(); // Get keys for logging (0, logger_1.log)(`No last check time found in Redis for key ${keys.lastCheckTimeKey}.`); } await this.twitterService.client.init(); (0, logger_1.log)("Successfully connected to Twitter service"); await this.loadRepliedTweets(); // Still loads from DB to Redis Set initially this.startChecker(); } catch (err) { (0, logger_1.error)("Failed to initialize BotResponder plugin:", err); // Attempt to close Redis connection via service on failure await this.redisService.quit(); } } /** * Load previously replied tweets from database into Redis Set via RedisService */ async loadRepliedTweets() { const keys = this.redisService.getKeys(); try { // Get replied interactions filtered by agentId const repliedInteractions = await this.adapter.getRepliedInteractions(this.agentId); if (repliedInteractions.length > 0) { const tweetIds = repliedInteractions .map(interaction => interaction.tweetId) .filter((id) => !!id); if (tweetIds.length > 0) { // Use RedisService method const addedCount = await this.redisService.addRepliedTweets(tweetIds); (0, logger_1.log)(`Added ${addedCount} previously replied tweets to Redis set '${keys.repliedTweetsKey}' from DB.`); } } // Use RedisService method const currentSetSize = await this.redisService.getRepliedTweetCount(); (0, logger_1.log)(`Redis set '${keys.repliedTweetsKey}' now contains ${currentSetSize} replied tweets.`); } catch (err) { (0, logger_1.error)(`Error loading replied tweets into Redis set '${keys.repliedTweetsKey}':`, err); } } /** * Start the checker, running immediately only if necessary based on lastCheckTime. */ startChecker() { if (this.checkInterval) { clearInterval(this.checkInterval); } const now = Date.now(); const intervalMillis = this.checkIntervalMinutes * 60 * 1000; let runImmediate = true; if (this.lastCheckTime) { const timeSinceLastCheck = now - this.lastCheckTime.getTime(); if (timeSinceLastCheck < intervalMillis) { (0, logger_1.log)(`Last check was at ${this.lastCheckTime.toISOString()} (${Math.round(timeSinceLastCheck / 1000)}s ago), less than interval (${this.checkIntervalMinutes} min). Skipping immediate check.`); runImmediate = false; } else { (0, logger_1.log)(`Last check was at ${this.lastCheckTime.toISOString()} (${Math.round(timeSinceLastCheck / 1000)}s ago), more than interval (${this.checkIntervalMinutes} min). Running immediate check.`); } } else { (0, logger_1.log)("No last check time recorded. Running immediate check."); } if (runImmediate) { this.checkForUnlinkedUsers().catch(err => (0, logger_1.error)("Error in initial unlinked users check:", err)); } this.checkInterval = setInterval(() => { if (!this.isChecking) { this.checkForUnlinkedUsers().catch(err => (0, logger_1.error)("Error in scheduled unlinked users check:", err)); } }, intervalMillis); (0, logger_1.log)(`BotResponder for agent ${this.agentId} scheduled to run every ${this.checkIntervalMinutes} minutes.`); } stopChecker() { if (this.checkInterval) { clearInterval(this.checkInterval); this.checkInterval = null; (0, logger_1.log)(`BotResponder for agent ${this.agentId} stopped`); } } setCheckInterval(minutes) { if (minutes < 5) { (0, logger_1.warn)("Check interval too short, setting to minimum of 5 minutes"); minutes = 5; } this.checkIntervalMinutes = minutes; (0, logger_1.log)(`Check interval updated to ${minutes} minutes for agent ${this.agentId}`); this.startChecker(); } async checkForUnlinkedUsers() { if (this.isChecking) { (0, logger_1.log)("Check already in progress, skipping"); return; } this.isChecking = true; const checkStartTime = new Date(); const keys = this.redisService.getKeys(); // Get keys for logging try { (0, logger_1.log)(`Starting unlinked users check for agent ${this.agentId}...`); const query = `@${this.twitterConfig.TWITTER_USERNAME}` || '@XCombatAI'; const tweetsGenerator = await this.twitterService.searchTweets(query, 100, agent_twitter_client_1.SearchMode.Latest); let tweetCount = 0; for await (const tweet of tweetsGenerator) { tweetCount++; await this.processTweetForUnlinkedUser(tweet); } (0, logger_1.log)(`Found ${tweetCount} tweets mentioning the agent ${this.agentId}`); // Trigger queue processing if not already running if (!this.isProcessingQueue) { this.processReplyQueue().catch(err => (0, logger_1.error)(`Error starting reply queue processing for agent ${this.agentId}:`, err)); } else { (0, logger_1.log)(`Queue is already being processed for agent ${this.agentId}, skipping additional processing`); } // Update lastCheckTime in memory and save to Redis via service this.lastCheckTime = checkStartTime; await this.redisService.setLastCheckTime(checkStartTime); (0, logger_1.log)(`Unlinked users check completed at ${checkStartTime.toISOString()} for agent ${this.agentId}. Saved last check time to Redis key '${keys.lastCheckTimeKey}'.`); } catch (err) { (0, logger_1.error)(`Error during unlinked users check for agent ${this.agentId}:`, err); try { const message = `⚠️ Error during BotResponder job for agent ${this.agentId}: ${err.message}`; await this.sendTelegramAlert(message); } catch (notifyError) { (0, logger_1.error)("Failed to send Telegram notification:", notifyError); } } finally { this.isChecking = false; } } async processTweetForUnlinkedUser(tweet) { const keys = this.redisService.getKeys(); try { if (tweet.username && this.twitterConfig.TWITTER_USERNAME && tweet.username.toLowerCase() === this.twitterConfig.TWITTER_USERNAME.toLowerCase()) { return; } if (!tweet.id || !tweet.userId) { (0, logger_1.log)("Tweet missing required properties", tweet); return; } // Use RedisService method const alreadyReplied = await this.redisService.isTweetReplied(tweet.id); if (alreadyReplied) { (0, logger_1.log)(`Skipping tweet ${tweet.id} - already replied to (checked Redis set '${keys.repliedTweetsKey}')`); return; } const twitterId = tweet.userId; // Find user by twitterId and agentId const user = await this.adapter.findUserByTwitterId(twitterId, this.agentId); if (user) { let post = await this.adapter.findPostByTweetId(tweet.id); if (!post) { const newPost = Post_1.PostResource.fromTweet(tweet); newPost.userId = user.id; newPost.agentId = this.agentId; // Set agentId for the new post await this.adapter.createPost(newPost); (0, logger_1.log)(`Stored tweet ${tweet.id} from linked user ${user.username} for agent ${this.agentId}`); } await this.queueReply(tweet.id, twitterId); (0, logger_1.log)(`User ${twitterId} is linked to agent ${this.agentId}, attempting to queue reply`); } else { (0, logger_1.log)(`User ${twitterId} is not linked to agent ${this.agentId}, attempting to queue reply without storing tweet`); await this.queueReply(tweet.id, twitterId); } } catch (err) { (0, logger_1.error)(`Error processing tweet ${tweet.id} for unlinked user ${tweet.userId} for agent ${this.agentId}:`, err); } } /** * Add a tweet to the Redis reply queue if not already replied. */ async queueReply(tweetId, userId) { const keys = this.redisService.getKeys(); try { // Use RedisService method const alreadyReplied = await this.redisService.isTweetReplied(tweetId); if (alreadyReplied) { (0, logger_1.log)(`Skipping reply queue for tweet ${tweetId}, already replied (checked Redis set '${keys.repliedTweetsKey}')`); return; } // Use RedisService method const queuedReply = { tweetId, userId }; await this.redisService.addToReplyQueue(queuedReply); (0, logger_1.log)(`Queued reply to user ${userId} for tweet ${tweetId} in Redis list '${keys.replyQueueKey}' for agent ${this.agentId}`); // Start processing the queue if not already running if (!this.isProcessingQueue) { this.processReplyQueue().catch(err => (0, logger_1.error)(`Error starting reply queue processing after queue addition for agent ${this.agentId}:`, err)); } } catch (err) { (0, logger_1.error)(`Error queuing reply for tweet ${tweetId}, user ${userId} in Redis (queue: '${keys.replyQueueKey}') for agent ${this.agentId}:`, err); } } /** * Process the reply queue with interval between replies */ async processReplyQueue() { if (this.isProcessingQueue) { (0, logger_1.log)(`Queue processing already in progress for agent ${this.agentId}, skipping`); return; } this.isProcessingQueue = true; const keys = this.redisService.getKeys(); try { (0, logger_1.log)(`Starting to process reply queue for agent ${this.agentId}`); // Get the reply interval from config with a minimum of 5 seconds const configIntervalSeconds = this.twitterConfig.XAICOMBAT_INTERVAL_REPLY || 5; const replyIntervalSeconds = Math.max(configIntervalSeconds, 5); // Ensure minimum of 5s (0, logger_1.log)(`Using reply interval of ${replyIntervalSeconds} seconds between replies for agent ${this.agentId}`); while (true) { const queuedItems = await this.redisService.getReplyQueue(); if (queuedItems.length === 0) { (0, logger_1.log)(`Redis reply queue '${keys.replyQueueKey}' is empty for agent ${this.agentId}, finishing processing`); break; } const nextItem = queuedItems[0]; (0, logger_1.log)(`Processing next item from Redis queue '${keys.replyQueueKey}' for agent ${this.agentId}: Tweet ID ${nextItem.tweetId}`); // Set last processing ID before attempting await this.redisService.setLastProcessingTweetId(nextItem.tweetId); try { const alreadyReplied = await this.redisService.isTweetReplied(nextItem.tweetId); if (alreadyReplied) { (0, logger_1.log)(`Skipping reply to tweet ${nextItem.tweetId}, already replied (final check in set '${keys.repliedTweetsKey}')`); } else { await this.sendReplyToUnlinkedUser(nextItem.tweetId, nextItem.userId); await this.redisService.addRepliedTweet(nextItem.tweetId); await this.adapter.markInteractionAsReplied(nextItem.tweetId, this.agentId); (0, logger_1.log)(`Successfully processed reply to tweet ${nextItem.tweetId} for agent ${this.agentId}`); } // Remove from queue and clear last processing ID on success await this.redisService.removeFromReplyQueue(); await this.redisService.clearLastProcessingTweetId(); } catch (err) { (0, logger_1.error)(`Error processing reply to tweet ${nextItem.tweetId} for agent ${this.agentId}:`, err); // Remove failed item, requeue it, and clear last processing ID await this.redisService.removeFromReplyQueue(); await this.redisService.addToReplyQueue(nextItem); await this.redisService.clearLastProcessingTweetId(); // Clear even on failure/requeue (0, logger_1.log)(`Requeued failed reply to tweet ${nextItem.tweetId} for agent ${this.agentId}`); } // Wait for the specified interval before processing the next item (0, logger_1.log)(`Waiting ${replyIntervalSeconds} seconds before processing next reply for agent ${this.agentId}`); await new Promise(resolve => setTimeout(resolve, replyIntervalSeconds * 1000)); } } catch (err) { (0, logger_1.error)(`Error processing Redis reply queue '${keys.replyQueueKey}' for agent ${this.agentId}:`, err); // Ensure last processing ID is cleared if the loop crashes await this.redisService.clearLastProcessingTweetId().catch(clearErr => (0, logger_1.error)(`Failed to clear last processing ID after main loop error for agent ${this.agentId}:`, clearErr)); } finally { this.isProcessingQueue = false; (0, logger_1.log)(`Finished processing reply queue for agent ${this.agentId}`); } } async sendReplyToUnlinkedUser(tweetId, twitterUserId) { // This method remains largely unchanged as it deals with Adapter and TwitterService const linkedUser = await this.adapter.findUserByTwitterId(twitterUserId, this.agentId); // Pass agentId let tweetContent = ""; if (linkedUser) { const post = await this.adapter.findPostByTweetId(tweetId); if (post) { tweetContent = post.content; } else { try { const tweet = await this.twitterService.client.getTweet(tweetId); if (tweet) { const tweetData = tweet; tweetContent = tweetData.text || tweetData.content || ""; } } catch (apiError) { (0, logger_1.error)(`Error fetching tweet content from API for tweet ${tweetId}:`, apiError); } } } const content = await this.generateReplyContent(twitterUserId, tweetContent, linkedUser !== null); const replyTweet = await this.twitterService.replyToTweet(tweetId, content); if (replyTweet) { (0, logger_1.log)(`Successfully replied to user ${twitterUserId} on tweet ${tweetId} for agent ${this.agentId}`); } else { (0, logger_1.error)(`Failed to reply to user ${twitterUserId} on tweet ${tweetId} for agent ${this.agentId}`); throw new Error(`Failed to send Twitter reply for tweet ${tweetId}`); } } async generateReplyContent(twitterUserId, tweetContent, isLinked) { const appUrl = process.env.APP_URL || 'https://xcombat.ai'; const invitationTemplates = [ `Thanks for reaching out! 🚀 To get personalized responses and track your fame points, please link your Twitter account at ${appUrl}.`, // ... other templates `XCombatAI works best with linked accounts! 🔗 Connect yours at ${appUrl} to unlock all features.` ]; if (!isLinked) { const randomIndex = Math.floor(Math.random() * invitationTemplates.length); return invitationTemplates[randomIndex]; } else { try { const linkedUser = await this.adapter.findUserByTwitterId(twitterUserId, this.agentId); // Pass agentId if (!linkedUser) { (0, logger_1.warn)(`Could not find linked user ${twitterUserId} for agent ${this.agentId} in DB for reply generation, using fallback.`); const randomIndex = Math.floor(Math.random() * invitationTemplates.length); return invitationTemplates[randomIndex]; // Fallback if user somehow not found } const { posts } = await this.adapter.getPostsByUserId(linkedUser.id, 1, 100, this.agentId); // Pass agentId const totalStats = { likes: 0, retweets: 0, quotes: 0, replies: 0, famePoints: 0 }; posts.forEach((post) => { if (post.stats) { totalStats.likes += post.stats.likes || 0; totalStats.retweets += post.stats.retweets || 0; totalStats.quotes += post.stats.quotes || 0; totalStats.replies += post.stats.replies || 0; totalStats.famePoints += post.stats.famePoints || 0; } }); // Prepare context for prompt building const promptContext = { username: linkedUser.username, userId: linkedUser.id, tweetContent: tweetContent, postCount: posts.length, stats: totalStats, // Pass aggregated stats famePoints: Math.round(totalStats.famePoints * 10) / 10, isLinked: true, agentId: this.agentId // Add agentId to the prompt context }; // Ensure runtime.character exists before using it if (!this.runtime.character) { throw new Error("Agent character definition is missing in runtime."); } // Build the system prompt using the utility function const systemPrompt = (0, ai_utils_1.buildSystemPrompt)(promptContext, this.runtime.character); // Generate reply using AI service const reply = await (0, core_1.generateText)({ runtime: this.runtime, context: systemPrompt, // Pass the generated system prompt modelClass: 'default' // Or determine dynamically if needed }); return reply; } catch (err) { (0, logger_1.error)(`Error generating reply content for linked user ${twitterUserId} for agent ${this.agentId}:`, err); // Provide a slightly more informative fallback using utility function const userName = (await this.adapter.findUserByTwitterId(twitterUserId, this.agentId))?.username; // Pass agentId // Assuming getFallbackReply exists and is imported from ai-utils // return getFallbackReply({ userId: twitterUserId, username: userName, isLinked: true, postCount: 0, stats: {likes:0, retweets:0, quotes:0, replies:0}, famePoints: 0 }); // Simple fallback if getFallbackReply isn't set up yet return `Thanks for your message, ${userName || 'friend'}! Having trouble generating a specific response right now, but keep engaging to climb the leaderboard! Check your rank at ${appUrl}`; } } } async sendTelegramAlert(message) { const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN; const telegramChatId = process.env.TELEGRAM_CHAT_ID; if (!telegramBotToken || !telegramChatId) { (0, logger_1.warn)("Telegram notifications not configured, skipping alert"); return; } try { const telegramUrl = `https://api.telegram.org/bot${telegramBotToken}/sendMessage`; const response = await fetch(telegramUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: telegramChatId, text: message, parse_mode: 'HTML', }), }); if (!response.ok) { const errorBody = await response.text(); throw new Error(`Telegram API response: ${response.status} ${response.statusText} - ${errorBody}`); } (0, logger_1.log)("Telegram notification sent successfully"); } catch (err) { (0, logger_1.error)("Error sending Telegram notification:", err); } } getLastCheckTime() { return this.lastCheckTime; } async runManualCheck() { if (this.isChecking) { (0, logger_1.log)(`A check is already in progress for agent ${this.agentId}`); return; } (0, logger_1.log)(`Starting manual unlinked users check for agent ${this.agentId}...`); await this.checkForUnlinkedUsers(); } async shutdown() { (0, logger_1.log)(`Shutting down BotResponder Plugin for agent ${this.agentId}...`); this.stopChecker(); // Use RedisService method await this.redisService.quit(); } } exports.BotResponderPlugin = BotResponderPlugin; //# sourceMappingURL=bot-responder-plugin.js.map