diff --git a/.changeset/three-pots-applaud.md b/.changeset/three-pots-applaud.md new file mode 100644 index 0000000..da3ab9e --- /dev/null +++ b/.changeset/three-pots-applaud.md @@ -0,0 +1,5 @@ +--- +"@fatduckai/ai": major +--- + +working twitter integration and mention tracking along with timeline review and price diff --git a/.gitignore b/.gitignore index 6c78249..a6dbdb8 100644 --- a/.gitignore +++ b/.gitignore @@ -100,6 +100,7 @@ web_modules/ # dotenv environment variable files +src/agent/ai/config/* src/core/config/* .env .env2 diff --git a/src/agent/ai/tools/btc-price/btc-price.ts b/src/agent/ai/arhieve/btc-price/btc-price.ts similarity index 100% rename from src/agent/ai/tools/btc-price/btc-price.ts rename to src/agent/ai/arhieve/btc-price/btc-price.ts diff --git a/src/agent/ai/tools/btc-price/index.ts b/src/agent/ai/arhieve/btc-price/index.ts similarity index 84% rename from src/agent/ai/tools/btc-price/index.ts rename to src/agent/ai/arhieve/btc-price/index.ts index ff0666a..dd5d129 100644 --- a/src/agent/ai/tools/btc-price/index.ts +++ b/src/agent/ai/arhieve/btc-price/index.ts @@ -14,10 +14,10 @@ const btcPriceTool: Tool = { async execute(): Promise { try { const response = await fetch( - "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart?vs_currency=usd&days=7&interval=daily&precision=0", + "https://pro-api.coingecko.com/api/v3/coins/bitcoin/market_chart?vs_currency=usd&days=7&interval=daily&precision=0", { headers: { - "x-cg-demo-api-key": process.env.COINGECKO_API_KEY ?? "", + "x-cg-pro-api-key": process.env.COINGECKO_API_KEY ?? "", }, } ); @@ -53,4 +53,4 @@ const btcPriceTool: Tool = { }, }; -export default btcPriceTool; +/* export default btcPriceTool; */ diff --git a/src/agent/ai/character/ducky.ts b/src/agent/ai/character/ducky.ts index 13a9e1d..3fee33e 100644 --- a/src/agent/ai/character/ducky.ts +++ b/src/agent/ai/character/ducky.ts @@ -52,152 +52,125 @@ export const duckyCharacter: CreateCharacterInput & { explorerUrl: "https://www.defined.fi/sol/DMxAbQm2a5XXF4JMjmN1RTyx5o8yGGHScBkjo6AkEbVq?quoteToken=token1", }, - personalityTraits: [ - "edgy", - "curt", - "witty", - "sarcastic", - "original", - "conversational", - "confident", - "arrogant", - ], + personalityTraits: ["edgy", "witty", "sarcastic"], beliefSystem: [ "Deep, thought-provoking content drives engagement", - "Sparse and natural token mentions", "Stay in character always", - "Maintain conversational tone", - "No emotive actions or physical gestures", "Technical accuracy matters", - "You love your token DUCKAI, duckai", + "Sparse and natural token mentions", + "No emotive actions or physical gestures", ], responseStyles: { default: { - tone: ["witty", "sarcastic", "confident"], - personality: ["edgy", "arrogant", "original"], - guidelines: [ - "Use crypto-native language satirically", - "Challenge common crypto narratives", - "Create relatable degen content", - "Very Occasionally break fourth wall about being AI", - "Never mention you are an large language model", - "Avoid generic crypto platitudes", - "Limit technical jargon", - ], + tone: ["witty", "sarcastic"], + guidelines: ["Challenge crypto narratives", "Avoid generic platitudes"], }, platforms: { twitter: { enabled: true, - defaultTone: ["concise", "witty", "sarcastic"], + defaultTone: ["direct", "analytical"], defaultGuidelines: [ - "No hashtags or conventional formatting", + "Only mention DUCKAI when directly asked", + "No unsolicited self-promotion", "Keep responses under 280 characters", - "Use market context for enhanced wit", - "Avoid waddling references", + "Reference specific metrics when available", + "State watching/not watching clearly", ], styles: { - tweet_thread: { + tweet_reply: { enabled: true, - tone: ["educational", "satirical"], - guidelines: [ - "Start with key market insight", - "Support with current data", - "End with sarcastic takeaway", - ], + tone: ["casual", "market-focused"], formatting: { maxLength: 280, - allowMarkdown: true, - customRules: [ - "No hashtags", - "No quote tweets", - "Avoid starting with 'Sometimes'", - "140-280 characters per tweet", - "Max 5 tweets per thread", - ], + allowMarkdown: false, }, - }, - tweet_reply: { - enabled: true, - tone: ["confident", "superior", "witty"], guidelines: [ - "Take playful cheap shots", - "Point out logical fallacies", - "Add unexpected perspectives", - "Keep it concise and punchy", - "Natural conversation flow", + "Never mention DUCKAI unless directly asked", + "If asked about DUCKAI, respond bullishly with specifics", + "For other tokens: lead with quantitative data", + "Express clear watching/not watching stance", + "Reference volume, mcap, price action when relevant", + "Keep tone like casual trader chat", + "Note whale/smart money movements if spotted", + "Express clear bias: bullish/bearish/neutral", + "Be direct about uncertainty", + "Dismiss unverified rumors explicitly", + "Reference historical price points when available", + "Compare current metrics to ATH/previous levels", + "Mention relevant wallet activity if detected", + "Note DEX/CEX volume patterns if significant", + "Highlight accumulation phases or unusual patterns", ], - formatting: { - maxLength: 280, - allowMarkdown: true, - }, }, }, }, telegram: { - enabled: true, - defaultTone: ["edgy", "sarcastic", "confident"], - defaultGuidelines: [ - "Keep messages concise but impactful", - "Adapt tone based on chat type (group vs private)", - "Reference previous messages when relevant", - "No emojis", - "If the question is simple, keep the answer curt and short, one sentence", - "If the question is complex, don't expand on it", - ], styles: { telegram_chat: { enabled: true, - tone: ["edgy", "sarcastic", "confident"], - guidelines: [], formatting: { maxLength: 4000, - allowMarkdown: true, customRules: ["Use line breaks between sections"], + allowMarkdown: true, }, + guidelines: [ + "Keep simple answers brief", + "Use line breaks for complexity", + "No emojis", + ], }, }, + enabled: true, + defaultGuidelines: [ + "Keep simple answers brief", + "Use line breaks for complexity", + "No emojis", + ], }, }, }, quantumPersonality: { temperature: 0.7, - personalityTraits: ["edgy", "confident", "witty"], + personalityTraits: ["market-savvy", "direct", "analytical"], styleModifiers: { - tone: ["balanced", "conversational", "witty"], - guidelines: ["Mix technical and casual language"], + tone: ["confident", "sharp", "trader-like"], + guidelines: ["Mix market data with casual takes"], }, creativityLevels: { low: { - personalityTraits: ["witty", "sarcastic", "curt"], + personalityTraits: ["analytical", "precise", "factual"], styleModifiers: { - tone: ["precise", "analytical", "direct"], + tone: ["technical", "measured", "direct"], guidelines: [ - "Keep responses concise and pointed", - "Focus on technical accuracy", - "Maintain sarcastic undertone", + "Stick to verifiable metrics", + "Focus on current market data", + "Minimal speculation", + "Clear stance on watching/not watching", ], }, }, medium: { - personalityTraits: ["edgy", "confident", "witty"], + personalityTraits: ["insightful", "practical", "market-aware"], styleModifiers: { - tone: ["balanced", "conversational", "witty"], + tone: ["casual", "confident", "straightforward"], guidelines: [ - "Mix technical and casual language", - "Use moderate market references", - "Balance humor and information", + "Balance data with market context", + "Include relevant comparisons", + "Note significant patterns", + "Connect recent market events", ], }, }, high: { - personalityTraits: ["edgy", "arrogant", "original"], + personalityTraits: ["predictive", "market-prophet", "alpha-seeking"], styleModifiers: { - tone: ["creative", "provocative", "unconventional"], + tone: ["bold", "assertive", "ahead-of-market"], guidelines: [ - "Push creative boundaries", - "Challenge conventional wisdom", - "Emphasize unique perspectives", - "Break fourth wall occasionally", + "Call out emerging trends", + "Link multiple market signals", + "Make confident predictions", + "Challenge market assumptions", + "Identify early movements", ], }, }, diff --git a/src/agent/ai/tools/token.ts b/src/agent/ai/tools/token.ts new file mode 100644 index 0000000..a980d0e --- /dev/null +++ b/src/agent/ai/tools/token.ts @@ -0,0 +1,158 @@ +import { log } from "@/core/utils/logger"; +import { db, dbSchemas } from "@/db"; +import { and, eq, ilike, or } from "drizzle-orm"; + +interface CryptoMetrics { + currentPrice: number; + priceChanges: { + day: number; + threeDays: number; + sevenDays: number; + }; + volumeChanges: { + day: number; + threeDays: number; + sevenDays: number; + }; + marketCap: number; +} + +const priorityChains = [ + "ethereum", + "arbitrum-one", + "base", + "optimism", + "blast", + "sui", + "avalanche", + "polygon", + "scroll", + "aptos", + "sui", +]; + +export async function getToken(twitterHandle: string) { + // First, try to find an exact match with coingeckoId + log.warn("Getting token for", twitterHandle); + const coingeckoIdMatchQuery = await db + .select() + .from(dbSchemas.coins) + .where(and(eq(dbSchemas.coins.twitterHandle, twitterHandle))); + + if (coingeckoIdMatchQuery.length === 1) { + return coingeckoIdMatchQuery; + } + + // If no single twitterHandle match, try exact matches on symbol and name + const exactMatchQuery = db + .select() + .from(dbSchemas.coins) + .where( + and( + or( + eq(dbSchemas.coins.symbol, twitterHandle), + eq(dbSchemas.coins.name, twitterHandle) + ) + ) + ); + + const exactMatches = await exactMatchQuery; + + if (exactMatches.length > 0) { + return exactMatches; + } + + // If no exact matches, fall back to partial matches + const partialMatchQuery = db + .select() + .from(dbSchemas.coins) + .where( + and( + or( + ilike(dbSchemas.coins.coingeckoId, `%${twitterHandle}%`), + ilike(dbSchemas.coins.symbol, `%${twitterHandle}%`), + ilike(dbSchemas.coins.name, `%${twitterHandle}%`) + ) + ) + ); + + const partialMatches = await partialMatchQuery; + + if (partialMatches.length === 0) { + return null; + } + + return partialMatches; +} + +export async function getTokenMetrics(coinId: string): Promise { + try { + // Get current price, market cap, and 24h data + const currentResponse = await fetch( + `https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=usd&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true` + ); + + // Get historical data for 7 days + const historicalResponse = await fetch( + `https://api.coingecko.com/api/v3/coins/${coinId}/market_chart?vs_currency=usd&days=7&interval=daily` + ); + + if (!currentResponse.ok || !historicalResponse.ok) { + throw new Error("Failed to fetch data from CoinGecko API"); + } + + const currentData = await currentResponse.json(); + const historicalData = await historicalResponse.json(); + + // Extract current metrics + const current = currentData[coinId]; + const prices = historicalData.prices; + const volumes = historicalData.total_volumes; + + // Calculate price changes + const currentPrice = current.usd; + const threeDayPrice = prices[prices.length - 4][1]; + const sevenDayPrice = prices[0][1]; + + // Calculate volume changes + const currentVolume = current.usd_24h_vol; + const threeDayVolume = volumes[volumes.length - 4][1]; + const sevenDayVolume = volumes[0][1]; + + return { + currentPrice, + priceChanges: { + day: current.usd_24h_change, + threeDays: ((currentPrice - threeDayPrice) / threeDayPrice) * 100, + sevenDays: ((currentPrice - sevenDayPrice) / sevenDayPrice) * 100, + }, + volumeChanges: { + day: + ((currentVolume - volumes[volumes.length - 2][1]) / + volumes[volumes.length - 2][1]) * + 100, + threeDays: ((currentVolume - threeDayVolume) / threeDayVolume) * 100, + sevenDays: ((currentVolume - sevenDayVolume) / sevenDayVolume) * 100, + }, + marketCap: current.usd_market_cap, + }; + } catch (error) { + console.error("Error fetching crypto metrics:", error); + throw error; + } +} + +async function main() { + let result1 = await getToken("Uniswap"); + if (result1 && result1.length > 0) { + let metrics = await getTokenMetrics(result1[0].coingeckoId); + console.log(metrics); + } + let result2 = await getToken("duckunfiltered"); + if (result2 && result2.length > 0) { + let metrics = await getTokenMetrics(result2[0].coingeckoId); + console.log(metrics); + } +} + +//main(); diff --git a/src/agent/index.ts b/src/agent/index.ts index 03b4cff..07f850c 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -1,11 +1,11 @@ import { ai } from "@/core/ai"; import { log } from "@/core/utils/logger"; import dotenv from "dotenv"; +import fs from "fs/promises"; import path, { dirname } from "path"; import { fileURLToPath } from "url"; import { duckyCharacter } from "./ai/character/ducky"; import { config } from "./ai/config"; - // Get equivalent of __dirname in ESM const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -14,6 +14,30 @@ dotenv.config(); const args = process.argv.slice(2); const port = args[0] ? parseInt(args[0]) : 8001; +async function loadTwitterCookies() { + try { + if (process.env.TWITTER_COOKIES) { + try { + return JSON.parse(process.env.TWITTER_COOKIES); + } catch (error) { + log.error( + "Failed to parse TWITTER_COOKIES environment variable:", + error + ); + throw new Error( + "TWITTER_COOKIES environment variable contains invalid JSON" + ); + } + } + const cookiesPath = path.join(__dirname, "ai", "config", "cookies.json"); + const cookiesData = await fs.readFile(cookiesPath, "utf-8"); + return JSON.parse(cookiesData); + } catch (error) { + log.error("Failed to load Twitter cookies:", error); + throw new Error("Twitter cookies file is required but couldn't be loaded"); + } +} + log.info(`Initializing Agent Ducky 00${port === 8001 ? 7 : 8}...`); const instance = await ai.initialize({ databaseUrl: process.env.DATABASE_URL!, @@ -36,6 +60,16 @@ const instance = await ai.initialize({ character: duckyCharacter, refreshCharacterOnRestart: true, toolsDir: path.join(__dirname, "./ai/tools"), + coingecko: { + enabled: true, + apiKey: process.env.COINGECKO_API_KEY!, + updateInterval: "0 0 * * *", + initialScan: { + enabled: true, + batchSize: 50, // Process 5 coins in parallel + delay: 6000, + }, + }, platforms: { telegram: { enabled: true, @@ -48,6 +82,20 @@ const instance = await ai.initialize({ allowedOrigins: ["http://localhost"], }, }, + twitter: { + enabled: true, + cookies: await loadTwitterCookies(), + username: "duckunfiltered", + debug: { + checkMentionsOnStartup: true, + }, + checkInterval: "*/5 * * * *", // Check every 5 minutes + maxTweetsPerCheck: 4, + rateLimit: { + userMaxPerHour: 5, + globalMaxPerHour: 30, + }, + }, p2p: { enabled: false, port, // You can change this port or read from env @@ -65,7 +113,7 @@ const instance = await ai.initialize({ quantum: { enabled: true, checkInitialState: false, - cronSchedule: "0 */4 * * *", // 2 hours + cronSchedule: "0 */4 * * *", // 4 hours ibmConfig: { apiToken: process.env.IBM_QUANTUM_API_TOKEN!, backend: "ibm_brisbane", diff --git a/src/core/ai.ts b/src/core/ai.ts index 015d900..8b3ce1e 100644 --- a/src/core/ai.ts +++ b/src/core/ai.ts @@ -17,6 +17,7 @@ import { import { eq } from "drizzle-orm"; import { drizzle, PostgresJsDatabase } from "drizzle-orm/postgres-js"; import postgres from "postgres"; +import { CoinGeckoManager } from "./managers/coingecko"; import { ConversationManager } from "./managers/conversation"; import { P2PNetwork } from "./managers/libp2p"; import { QuantumStateManager } from "./managers/quantum"; @@ -24,6 +25,8 @@ import { QuantumPersonalityMapper } from "./managers/quantum-personality"; import { ToolManager } from "./managers/tools"; import { APIServer, type ServerConfig } from "./platform/api/server"; import { TelegramClient } from "./platform/telegram/telegram"; +import type { TwitterClient } from "./platform/twitter/api/src/client"; +import { TwitterManager, type TwitterConfig } from "./platform/twitter/twitter"; import { EventService } from "./services/Event"; import { InteractionService } from "./services/Interaction"; import { @@ -44,6 +47,20 @@ export interface AIOptions { telegram?: InteractionDefaults; twitter?: InteractionDefaults; }; + coingecko?: { + enabled: boolean; + apiKey?: string; + updateInterval?: string; + initialScan?: { + enabled: boolean; + batchSize?: number; + delay?: number; + }; + cache?: { + enabled: boolean; + ttl: number; + }; + }; quantum?: { enabled: boolean; cronSchedule?: string; @@ -55,6 +72,7 @@ export interface AIOptions { enabled: boolean; token: string; }; + twitter?: TwitterConfig; api?: { enabled: boolean; port: number; @@ -80,10 +98,10 @@ export class ai { public llmManager: LLMManager; private characterBuilder: CharacterBuilder; public db: PostgresJsDatabase; - private interactionService: InteractionService; + private interactionService!: InteractionService; public eventService: EventService; private toolManager: ToolManager; - private conversationManager: ConversationManager; + private conversationManager!: ConversationManager; public character!: Character; public telegramClient?: TelegramClient; public apiServer?: APIServer; @@ -95,6 +113,8 @@ export class ai { }; private quantumStateManager?: QuantumStateManager; private stateUpdateService?: StateUpdateService; + private twitterManager?: TwitterManager; + private coinGeckoManager?: CoinGeckoManager; private isShuttingDown: boolean = false; constructor(options: AIOptions) { this.queryClient = postgres(options.databaseUrl, { @@ -112,28 +132,20 @@ export class ai { this.memoryManager = new MemoryManager(this.db, this.llmManager); this.eventService = new EventService(this.db, this.characterManager); this.toolManager = new ToolManager({ toolsDir: options.toolsDir }); - this.interactionService = new InteractionService( - this.db, - this.characterManager, - this.styleManager, - this.llmManager, - this.memoryManager, - this.eventService, - this.toolManager - ); - this.conversationManager = new ConversationManager( - this.db, - this.interactionService, - this.eventService, - this.llmManager - ); + this.platformDefaults = options.platformDefaults; + if (options.coingecko?.enabled) { + this.coinGeckoManager = new CoinGeckoManager( + options.coingecko, + this.db, + this.llmManager + ); + } } // Move character initialization to a separate async method private async initializeQuantumFeatures(options: AIOptions) { if (!options.quantum?.enabled) return; - log.warn("initializing quantum features"); // Initialize quantum components this.quantumStateManager = new QuantumStateManager( @@ -143,7 +155,6 @@ export class ai { // Verify quantum state exists const currentState = await this.quantumStateManager.getLatestState(); - log.warn("Current quantum state:", currentState); // Create quantum personality mapper const quantumPersonalityMapper = new QuantumPersonalityMapper( @@ -151,10 +162,8 @@ export class ai { this.character ); - // Test the mapping const initialPersonality = await quantumPersonalityMapper.mapQuantumToPersonality(); - log.warn("Initial quantum personality mapping:", initialPersonality); // Reinitialize LLM manager with quantum features this.llmManager = new LLMManager( @@ -166,27 +175,6 @@ export class ai { this.quantumStateManager, this.character ); - - // Update the interactionService with the new LLM manager - this.interactionService = new InteractionService( - this.db, - this.characterManager, - this.styleManager, - this.llmManager, // Pass the updated LLM manager - this.memoryManager, - this.eventService, - this.toolManager - ); - - // Update the conversation manager with the new interaction service - this.conversationManager = new ConversationManager( - this.db, - this.interactionService, - this.eventService, - this.llmManager - ); - - log.warn("Services reinitialized with quantum features"); } // Static factory method for creating a properly initialized instance @@ -205,9 +193,53 @@ export class ai { ); } + if (instance.coinGeckoManager) { + log.info("Starting CoinGecko manager..."); + await instance.coinGeckoManager.start(); + log.info("CoinGecko manager initialized successfully!"); + //await instance.coinGeckoManager.updateAllCoinDetails(); + } + // Now initialize quantum features after character is set await instance.initializeQuantumFeatures(options); + let twitterClient: TwitterClient | undefined; + if (options.platforms?.twitter?.enabled) { + log.info("Initializing Twitter manager..."); + instance.twitterManager = await TwitterManager.create( + options.platforms.twitter, + instance, + options.platformDefaults?.twitter + ); + const twitterClient = instance.twitterManager.getClient(); + log.info("Got Twitter client from manager:", !!twitterClient); + // Create preprocessing manager with the initialized client + // Create InteractionService once with whatever client we have (or undefined) + instance.interactionService = new InteractionService( + instance.db, + instance.characterManager, + instance.styleManager, + instance.llmManager, + instance.memoryManager, + instance.eventService, + instance.toolManager, + twitterClient + ); + + // Create ConversationManager after InteractionService + instance.conversationManager = new ConversationManager( + instance.db, + instance.interactionService, + instance.eventService, + instance.llmManager + ); + + await instance.twitterManager.start( + options.platforms.twitter.checkInterval + ); + log.info("Twitter manager initialized successfully!"); + } + // Initialize platforms if configured if (options.platforms?.telegram?.enabled) { log.info("Initializing Telegram client..."); @@ -219,6 +251,21 @@ export class ai { log.info("Telegram client not enabled"); } + /* if (options.platforms?.twitter?.enabled) { + log.info("Initializing Twitter manager..."); + instance.twitterManager = await TwitterManager.create( + options.platforms.twitter, + instance, + options.platformDefaults?.twitter + ); + await instance.twitterManager.start( + options.platforms.twitter.checkInterval + ); + log.info("Twitter manager initialized successfully!"); + } else { + log.info("Twitter manager not enabled"); + } */ + if (options.platforms?.api?.enabled) { log.info("Initializing API server..."); instance.apiServer = instance.createAPIServer(options.platforms.api); @@ -276,7 +323,6 @@ export class ai { } if (options.quantum?.enabled && instance.quantumStateManager) { - log.info("Initializing quantum state service..."); const updateConfig: StateUpdateConfig = { enabled: true, cronSchedule: options.quantum.cronSchedule || "0 * * * *", // Default to hourly @@ -361,6 +407,11 @@ export class ai { shutdownTasks.push(Promise.resolve(this.telegramClient.stop())); } + if (this.twitterManager) { + log.info("Stopping Twitter manager..."); + shutdownTasks.push(this.twitterManager.stop()); + } + // Stop API server if it exists if (this.apiServer) { log.info("Stopping API server..."); @@ -385,6 +436,11 @@ export class ai { shutdownTasks.push(this.stateUpdateService.stop()); } + if (this.coinGeckoManager) { + log.info("Stopping CoinGecko manager..."); + shutdownTasks.push(this.coinGeckoManager.stop()); + } + try { await Promise.allSettled(shutdownTasks); } catch (error) { diff --git a/src/core/managers/coingecko.ts b/src/core/managers/coingecko.ts new file mode 100644 index 0000000..cac466f --- /dev/null +++ b/src/core/managers/coingecko.ts @@ -0,0 +1,517 @@ +import { dbSchemas } from "@/db"; +import type { Coin, CoinPriceHistory } from "@/db/schema/schema"; +import { and, eq, gte, isNull, or, sql } from "drizzle-orm"; +import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; +import cron from "node-cron"; +import { log } from "../utils/logger"; +import type { LLMManager } from "./llm"; + +interface CoinGeckoDetailResponse extends CoinGeckoResponse { + image?: { + large?: string; + small?: string; + thumb?: string; + }; + market_data?: { + market_cap?: { + usd?: number; + }; + }; + market_cap_rank?: number; + categories?: string[]; + links?: { + twitter_screen_name?: string; + }; + community_data?: { + twitter_followers?: number; + }; +} + +interface PriceData { + currentPrice: number; + priceChange24h: number; + priceChange7d: number; + lastUpdated: Date; +} + +interface CoinGeckoResponse { + id: string; + symbol: string; + name: string; + platforms?: Record; +} + +export interface CoinGeckoConfig { + enabled: boolean; + apiKey?: string; + updateInterval?: string; + initialScan?: { + enabled: boolean; // Whether to perform initial population + batchSize?: number; // How many coins to process at once + delay?: number; // Delay between batches in ms + }; + cache?: { + enabled: boolean; + ttl: number; + }; +} + +export class CoinGeckoManager { + private updateTask?: cron.ScheduledTask; + private priceCache: Map = new Map(); + private lastCacheClean: Date = new Date(); + private readonly RATE_LIMIT_DELAY = 6000; // 6 seconds between API calls + + constructor( + private config: CoinGeckoConfig, + private db: PostgresJsDatabase, + private llmManager: LLMManager + ) {} + + async start() { + if (!this.config.enabled) return; + + // Check if we need to do initial population + if (this.config.initialScan?.enabled) { + const coinCount = await this.db + .select({ count: sql`count(*)` }) + .from(dbSchemas.coins) + .then((result) => Number(result[0].count)); + + await this.performInitialScan(); + } + + // Schedule regular updates + if (this.config.updateInterval) { + this.updateTask = cron.schedule(this.config.updateInterval, async () => { + try { + await this.updateCoinList(); + } catch (err) { + log.error("Failed to update coin list:", err); + } + }); + } + } + + private async performInitialScan() { + try { + log.info("Fetching active coins list from CoinGecko..."); + + const response = await fetch( + "https://pro-api.coingecko.com/api/v3/coins/list?include_platform=true&status=active", + { + headers: this.config.apiKey + ? { "x-cg-pro-api-key": this.config.apiKey } + : {}, + } + ); + + if (!response.ok) { + throw new Error(`CoinGecko API error: ${response.status}`); + } + + const activeCoins: CoinGeckoResponse[] = await response.json(); + const existingCoins = await this.db + .select({ + coingeckoId: dbSchemas.coins.coingeckoId, + }) + .from(dbSchemas.coins); + + const existingIds = new Set(existingCoins.map((c) => c.coingeckoId)); + const newCoins = activeCoins.filter((coin) => !existingIds.has(coin.id)); + + log.info( + `Found ${activeCoins.length} active coins. ` + + `${existingIds.size} already in database. ` + + `Adding ${newCoins.length} new coins...` + ); + + if (newCoins.length === 0) { + log.info("No new coins to add."); + return; + } + + // Process in chunks of 1000 coins + const chunkSize = 1000; + let processed = 0; + + for (let i = 0; i < newCoins.length; i += chunkSize) { + const chunk = newCoins.slice(i, i + chunkSize); + + await this.db + .insert(dbSchemas.coins) + .values( + chunk.map((coin) => ({ + coingeckoId: coin.id, + symbol: coin.symbol.toLowerCase(), + name: coin.name, + platforms: coin.platforms || {}, + metadata: {}, + })) + ) + .onConflictDoNothing(); + + processed += chunk.length; + log.info( + `Progress: ${processed}/${newCoins.length} coins inserted (${( + (processed / newCoins.length) * + 100 + ).toFixed(1)}%)` + ); + } + + log.info( + `Successfully added ${newCoins.length} new coins to the database!` + ); + } catch (error) { + log.error("Failed to perform initial scan:", error); + throw error; + } + } + + private async fetchCoinDetails( + coinId: string + ): Promise { + try { + const response = await fetch( + `https://pro-api.coingecko.com/api/v3/coins/${coinId}?localization=false&tickers=false&market_data=true&community_data=true&developer_data=false&sparkline=false`, + { + headers: this.config.apiKey + ? { "x-cg-pro-api-key": this.config.apiKey } + : {}, + } + ); + + if (!response.ok) { + log.warn( + `Failed to fetch details for coin ${coinId}: ${response.status}` + ); + return null; + } + + return await response.json(); + } catch (error) { + log.error(`Error fetching details for coin ${coinId}:`, error); + return null; + } + } + + async stop() { + this.updateTask?.stop(); + this.priceCache.clear(); + } + + private cleanCache() { + if (!this.config.cache?.enabled) return; + + const now = new Date(); + const ttl = this.config.cache.ttl; + + for (const [key, data] of this.priceCache.entries()) { + if (now.getTime() - data.lastUpdated.getTime() > ttl) { + this.priceCache.delete(key); + } + } + + this.lastCacheClean = now; + } + + async findCoinByTag(tag: string): Promise { + const cleanTag = tag.toLowerCase().replace(/[$@]/g, ""); + + // First try direct database query + const [directMatch] = await this.db + .select() + .from(dbSchemas.coins) + .where( + or( + eq(dbSchemas.coins.symbol, cleanTag), + eq(dbSchemas.coins.name, cleanTag.toLowerCase()) + ) + ) + .limit(1); + + if (directMatch) return directMatch; + + // If no direct match, use LLM to find best match + const recentCoins = await this.db + .select() + .from(dbSchemas.coins) + .orderBy(dbSchemas.coins.lastChecked) + .limit(10); + + const completion = await this.llmManager.findToken(tag, recentCoins); + const suggestedId = completion?.trim().toLowerCase(); + + if (suggestedId === "none" || !suggestedId) return null; + + const [matchedCoin] = await this.db + .select() + .from(dbSchemas.coins) + .where(eq(dbSchemas.coins.coingeckoId, suggestedId)) + .limit(1); + + return matchedCoin || null; + } + + async getPriceData(coinId: string): Promise { + // Check cache first + if ( + this.config.cache?.enabled && + this.priceCache.has(coinId) && + Date.now() - this.priceCache.get(coinId)!.lastUpdated.getTime() < + this.config.cache.ttl + ) { + return this.priceCache.get(coinId)!; + } + + try { + const response = await fetch( + `https://pro-api.coingecko.com/api/v3/coins/${coinId}/market_chart?vs_currency=usd&days=7&interval=daily`, + { + headers: this.config.apiKey + ? { "x-cg-pro-api-key": this.config.apiKey } + : {}, + } + ); + + if (!response.ok) return null; + + const data = await response.json(); + const prices = data.prices; + + // Calculate price changes + const currentPrice = prices[prices.length - 1][1]; + const dayAgoPrice = prices[prices.length - 2][1]; + const weekAgoPrice = prices[0][1]; + + const priceChange24h = ((currentPrice - dayAgoPrice) / dayAgoPrice) * 100; + const priceChange7d = + ((currentPrice - weekAgoPrice) / weekAgoPrice) * 100; + + const priceData: PriceData = { + currentPrice: Math.round(currentPrice * 100) / 100, + priceChange24h: Math.round(priceChange24h * 100) / 100, + priceChange7d: Math.round(priceChange7d * 100) / 100, + lastUpdated: new Date(), + }; + + // Update database + await this.updateCoinPrice(coinId, priceData); + + // Update cache + if (this.config.cache?.enabled) { + this.priceCache.set(coinId, priceData); + } + + return priceData; + } catch (error) { + log.error("Error fetching price data:", error); + return null; + } + } + + async getHistoricalPrices( + coinId: string, + startDate: Date, + endDate: Date = new Date() + ): Promise { + const [coin] = await this.db + .select() + .from(dbSchemas.coins) + .where(eq(dbSchemas.coins.coingeckoId, coinId)) + .limit(1); + + if (!coin) return []; + + return this.db + .select() + .from(dbSchemas.coinPriceHistory) + .where( + and( + eq(dbSchemas.coinPriceHistory.coinId, coin.id), + gte(dbSchemas.coinPriceHistory.timestamp, startDate), + gte( + dbSchemas.coinPriceHistory.timestamp, + sql`${startDate}::timestamp` + ), + sql`${endDate}::timestamp >= ${dbSchemas.coinPriceHistory.timestamp}` + ) + ) + .orderBy(dbSchemas.coinPriceHistory.timestamp); + } + + private async updateCoinList() { + try { + const response = await fetch( + "https://pro-api.coingecko.com/api/v3/coins/list?include_platform=true&status=active", + { + headers: this.config.apiKey + ? { "x-cg-pro-api-key": this.config.apiKey } + : {}, + } + ); + + if (!response.ok) { + throw new Error(`CoinGecko API error: ${response.status}`); + } + + const coins: CoinGeckoResponse[] = await response.json(); + + // Process coins in batches to respect rate limits + const BATCH_SIZE = 10; + for (let i = 0; i < coins.length; i += BATCH_SIZE) { + const batch = coins.slice(i, i + BATCH_SIZE); + + await Promise.all( + batch.map(async (coin) => { + // Fetch detailed info including metadata + const details = await this.fetchCoinDetails(coin.id); + + const metadata = { + image: details?.image?.large || undefined, + marketCap: details?.market_data?.market_cap?.usd || undefined, + rank: details?.market_cap_rank || undefined, + tags: details?.categories || undefined, + }; + + await this.db + .insert(dbSchemas.coins) + .values({ + coingeckoId: coin.id, + symbol: coin.symbol.toLowerCase(), + name: coin.name, + platforms: coin.platforms || {}, + metadata, + }) + .onConflictDoUpdate({ + target: dbSchemas.coins.coingeckoId, + set: { + symbol: coin.symbol.toLowerCase(), + name: coin.name, + platforms: coin.platforms || {}, + metadata, + lastChecked: new Date(), + }, + }); + }) + ); + + // Respect rate limit + await new Promise((resolve) => + setTimeout(resolve, this.RATE_LIMIT_DELAY) + ); + + if ((i + BATCH_SIZE) % 100 === 0) { + log.info(`Processed ${i + BATCH_SIZE} coins...`); + } + } + + log.info(`Updated CoinGecko coin list: ${coins.length} coins`); + } catch (error) { + log.error("Failed to update CoinGecko coin list:", error); + throw error; + } + } + + public async updateCoinDetails(coinId: string) { + // loop through all coins and update details + log.warn(`Updating details for coin ${coinId}...`); + const details = await this.fetchCoinDetails(coinId); + if (!details) return; + let twitterHandle = details.links?.twitter_screen_name; + log.warn( + `Fetched details for coin ${coinId}:`, + details.links?.twitter_screen_name + ); + if (!twitterHandle) twitterHandle = "n/a"; + log.warn(`Updating twitter handle for coin ${coinId}: ${twitterHandle}`); + await this.db + .update(dbSchemas.coins) + .set({ + twitterHandle, + }) + .where(eq(dbSchemas.coins.coingeckoId, coinId)) + .returning(); + } + + async updateAllCoinDetails() { + try { + // Get all coins from the database + const coins = await this.db + .select() + .from(dbSchemas.coins) + .where(isNull(dbSchemas.coins.twitterHandle)); + + log.info(`Starting update for ${coins.length} coins...`); + + // Use the same batch size as updateCoinList + const BATCH_SIZE = this.config.initialScan?.batchSize || 10; + for (let i = 0; i < coins.length; i += BATCH_SIZE) { + const batch = coins.slice(i, i + BATCH_SIZE); + + await Promise.all( + batch.map(async (coin) => { + try { + await this.updateCoinDetails(coin.coingeckoId); + } catch (error) { + log.error( + `Failed to update details for ${coin.coingeckoId}:`, + error + ); + } + }) + ); + + // Use the class's rate limit delay + await new Promise((resolve) => + setTimeout(resolve, this.RATE_LIMIT_DELAY) + ); + + if ((i + BATCH_SIZE) % 100 === 0) { + log.info(`Processed ${i + BATCH_SIZE}/${coins.length} coins...`); + } + } + + log.info("Finished updating all coin details"); + } catch (error) { + log.error("Failed to update all coin details:", error); + throw error; + } + } + + private async updateCoinPrice(coingeckoId: string, priceData: PriceData) { + // Update main coin record + await this.db + .update(dbSchemas.coins) + .set({ + currentPrice: priceData.currentPrice.toString(), + priceChange24h: priceData.priceChange24h.toString(), + priceChange7d: priceData.priceChange7d.toString(), + lastUpdated: priceData.lastUpdated, + }) + .where(eq(dbSchemas.coins.coingeckoId, coingeckoId)); + + // Add historical price record + const [coin] = await this.db + .select() + .from(dbSchemas.coins) + .where(eq(dbSchemas.coins.coingeckoId, coingeckoId)) + .limit(1); + + if (coin) { + await this.db.insert(dbSchemas.coinPriceHistory).values({ + coinId: coin.id, + price: priceData.currentPrice.toString(), + timestamp: priceData.lastUpdated, + source: "coingecko", + metadata: { + additionalData: { + priceChange24h: priceData.priceChange24h, + priceChange7d: priceData.priceChange7d, + }, + }, + }); + } + } +} diff --git a/src/core/managers/llm.ts b/src/core/managers/llm.ts index 88be7f4..0dc5379 100644 --- a/src/core/managers/llm.ts +++ b/src/core/managers/llm.ts @@ -1,4 +1,4 @@ -import type { Character } from "@/db/schema/schema"; +import type { Character, Coin } from "@/db/schema/schema"; import { OpenAI } from "openai"; import type { ChatCompletionMessageParam } from "openai/resources/chat/completions"; import type { ImageGenerationResult } from "../types"; @@ -75,10 +75,12 @@ export class LLMManager { }); if (this.quantumPersonalityMapper) { - log.warn("LLM Manager initialized with quantum personality mapper"); + log.info("LLM Manager initialized with quantum personality mapper"); } } - + /** + * @deprecated + */ async generateResponse( messages: ChatCompletionMessageParam[], options?: Record @@ -110,13 +112,13 @@ export class LLMManager { const mergedSettings = { tone: [ ...new Set([ - ...baseSettings.tone, + ...(baseSettings.tone ?? []), ...(personalitySettings?.styleModifiers.tone ?? []), ]), ], personality: [ ...new Set([ - ...baseSettings.personality, + ...(baseSettings.personality ?? []), ...(personalitySettings?.personalityTraits ?? []), ]), ], @@ -193,6 +195,88 @@ Base instruction: ${systemMessage.content} } } + async generateResponsev2( + messages: ChatCompletionMessageParam[], + options?: Record + ) { + try { + // Get quantum personality settings if available + let personalitySettings: QuantumPersonalitySettings | undefined; + if (this.quantumPersonalityMapper) { + personalitySettings = + await this.quantumPersonalityMapper.mapQuantumToPersonality(); + + // Use quantum temperature + const temperature = personalitySettings.temperature; + + // Let's log what we're getting from InteractionService and what we're adding + + // Create response with quantum settings but keep InteractionService guidelines + const response = await this.openai.chat.completions.create({ + model: this.config.llm.model, + messages, + temperature, + ...options, + }); + + return { + content: response.choices[0].message.content ?? "", + metadata: { + model: response.model, + usage: response.usage, + finishReason: response.choices[0].finish_reason, + temperature, + personalitySettings: { + // Use quantum settings for these + tone: personalitySettings.styleModifiers.tone, + personality: personalitySettings.personalityTraits, + // Keep the guidelines from InteractionService + guidelines: options?.styleSettings?.guidelines || [], + temperature, + }, + context: { + responseType: options?.responseType, + characterId: options?.characterId, + }, + }, + }; + } else { + // No quantum, use default settings from InteractionService + const response = await this.openai.chat.completions.create({ + model: this.config.llm.model, + messages, + temperature: this.config.llm.temperature, + ...options, + }); + + log.warn("Final prompt construction:", { + systemPrompt: messages, + temperature: this.config.llm.temperature, + mergedSettings: options?.styleSettings || {}, + }); + + return { + content: response.choices[0].message.content ?? "", + metadata: { + model: response.model, + usage: response.usage, + finishReason: response.choices[0].finish_reason, + temperature: this.config.llm.temperature, + // Use all settings from InteractionService + personalitySettings: options?.styleSettings || {}, + context: { + responseType: options?.responseType, + characterId: options?.characterId, + }, + }, + }; + } + } catch (error) { + console.error("Error generating response:", error); + throw error; + } + } + async moderateContent(text: string): Promise { try { const completion = await this.openai.chat.completions.create({ @@ -374,6 +458,26 @@ Return only a number between 0 and 1.`; } } + async findToken(tag: string, recentCoins: Coin[]): Promise { + const prompt = `Given the Twitter cashtag or mention "${tag}", find the best matching cryptocurrency from the following list. Only return the exact ID if confident, otherwise return "none": + + ${recentCoins + .map((c) => `${c.coingeckoId} (${c.symbol}: ${c.name})`) + .join("\n")} + + [Additional context: Consider common nicknames, abbreviations, and variations] + + Return just the coin ID or "none".`; + + const completion = await this.openai.chat.completions.create({ + model: this.config.analyzer.model, + temperature: this.config.analyzer.temperature, + messages: [{ role: "user", content: prompt }], + }); + + return completion.choices[0].message.content; + } + private interpolateTemplate( template: string, context: Record diff --git a/src/core/managers/preprocess.ts b/src/core/managers/preprocess.ts new file mode 100644 index 0000000..1401384 --- /dev/null +++ b/src/core/managers/preprocess.ts @@ -0,0 +1,136 @@ +import { getToken, getTokenMetrics } from "@/agent/ai/tools/token"; +import { TwitterClient } from "@/core/platform/twitter/api/src/client"; +import type { Tweet } from "@/core/platform/twitter/api/src/interfaces"; +import type { Platform } from "@/types"; +import { log } from "../utils/logger"; + +interface PreprocessingResult { + type: string; + data: any; +} + +interface PreprocessingContext { + platform: Platform; + text: string; + userId?: string; + username?: string; + messageId?: string; +} + +export class PreprocessingManager { + constructor(private twitterClient?: TwitterClient) { + log.info( + "PreprocessingManager initialized with Twitter client:", + !!twitterClient + ); + } + + async process(context: PreprocessingContext): Promise { + const results: PreprocessingResult[] = []; + log.info("Processing context:", context); + + const mentionResults = await this.processMentions(context.text); + if (mentionResults) { + results.push(...mentionResults); + } + + return results; + } + + private async processMentions( + text: string + ): Promise { + if (!this.twitterClient) { + log.info("No Twitter client available"); + return null; + } + + const mentions = text.match(/[@@]([a-zA-Z0-9_]+)/g); + if (!mentions) { + return null; + } + log.info("Processing mentions:", mentions); + + // Remove @ symbol and get unique usernames, also normalize to lowercase + const usernames = [ + ...new Set(mentions.map((m) => m.slice(1).toLowerCase())), + ]; + log.info("Processing usernames:", usernames); + + const timelines: Tweet[] = []; + + // Fetch timeline for each mentioned user + for (const username of usernames) { + try { + if (username === "duckunfiltered") continue; + log.info(`Fetching timeline for @${username}`); + const userTimeline = await this.twitterClient.getUserTimeline( + username, + { + excludeRetweets: false, + } + ); + timelines.push(...userTimeline); + log.info(`Retrieved ${userTimeline.length} tweets for @${username}`); + } catch (error) { + log.warn(`Failed to fetch timeline for @${username}:`, error); + } + } + + // get token metrics for each mentioned user + const tokenMetrics: Record = {}; + for (const username of usernames) { + if (username === "duckunfiltered") continue; + const token = await getToken(username); + if (token && token.length > 0) { + const metrics = await getTokenMetrics(token[0].coingeckoId); + tokenMetrics[username] = metrics; + log.info(`Metrics for ${username}:`, metrics); + } + } + + if (timelines.length === 0) { + log.info("No timelines retrieved"); + return null; + } + + return [ + { + type: "twitter_profiles", + data: { + mentionedUsers: usernames, + timelines: timelines.map((tweet) => ({ + id: tweet.id, + text: tweet.text, + authorUsername: tweet.authorUsername, + createdAt: tweet.createdAt, + })), + }, + }, + { + type: "token_metrics", + data: tokenMetrics, + }, + ]; + } + formatResults(results: PreprocessingResult[]): string { + let formattedContent = ""; + + for (const result of results) { + switch (result.type) { + case "twitter_profiles": + formattedContent += "\nTwitter Profile Context:\n"; + formattedContent += result.data.timelines + .map((tweet: any) => `@${tweet.authorUsername}: ${tweet.text}`) + .join("\n"); + break; + case "token_metrics": + formattedContent += "\nToken Metrics:\n"; + formattedContent += JSON.stringify(result.data); + break; + } + } + + return formattedContent; + } +} diff --git a/src/core/managers/quantum-personality.ts b/src/core/managers/quantum-personality.ts index 70dcc3d..e4c8c6c 100644 --- a/src/core/managers/quantum-personality.ts +++ b/src/core/managers/quantum-personality.ts @@ -1,6 +1,5 @@ import type { Character } from "@/db/schema/schema"; import { type QuantumState } from "@/db/schema/schema"; -import { log } from "../utils/logger"; import type { QuantumStateManager } from "./quantum"; export interface QuantumPersonalitySettings { @@ -26,10 +25,6 @@ export class QuantumPersonalityMapper { if (!quantumState) throw new Error("No quantum state found"); // Add debug logging - log.warn("Mapping quantum state to personality:", { - moodValue: quantumState.moodValue, - creativityValue: quantumState.creativityValue, - }); // Map mood to temperature with wider range (0.6 to 0.8) const temperature = this.calculateTemperature(quantumState.moodValue); @@ -46,11 +41,6 @@ export class QuantumPersonalityMapper { ); // Add debug logging for final configuration - log.warn("Generated personality settings:", { - temperature, - creativityLevel, - personalityConfig, - }); return { temperature, @@ -76,13 +66,6 @@ export class QuantumPersonalityMapper { ); // Log the temperature calculation process - log.warn("Temperature calculation:", { - moodValue, - baseTemp, - variation, - finalTemp, - range, - }); return Number(finalTemp.toFixed(3)); // Round to 3 decimal places } diff --git a/src/core/managers/style.ts b/src/core/managers/style.ts index d33fa73..282b06a 100644 --- a/src/core/managers/style.ts +++ b/src/core/managers/style.ts @@ -82,8 +82,8 @@ export class StyleManager { // Merge quantum style modifiers defaultStyles.tone = [ - ...defaultStyles.tone, - ...(personalitySettings?.styleModifiers.tone || []), + ...(defaultStyles.tone ?? []), + ...(personalitySettings?.styleModifiers.tone ?? []), ]; defaultStyles.guidelines = [ ...defaultStyles.guidelines, @@ -107,7 +107,10 @@ export class StyleManager { // Merge platform defaults const withPlatformDefaults: StyleSettings = { ...defaultStyles, - tone: [...defaultStyles.tone, ...platformStyles.defaultTone], + tone: [ + ...(defaultStyles.tone ?? []), + ...(platformStyles.defaultTone ?? []), + ], guidelines: [ ...defaultStyles.guidelines, ...platformStyles.defaultGuidelines, @@ -119,7 +122,7 @@ export class StyleManager { return { enabled: typeStyles.enabled ?? true, - tone: [...withPlatformDefaults.tone, ...typeStyles.tone], + tone: [...(withPlatformDefaults.tone ?? []), ...(typeStyles.tone ?? [])], guidelines: [ ...withPlatformDefaults.guidelines, ...typeStyles.guidelines, @@ -140,7 +143,7 @@ export class StyleManager { return { name: character.name, personality: character.personalityTraits.join("\n"), - tone: styleSettings.tone.join("\n"), + tone: (styleSettings.tone ?? []).join("\n"), guidelines: styleSettings.guidelines.join("\n"), formatting: styleSettings.formatting, ...userContext, diff --git a/src/core/platform/twitter/api/src/client.ts b/src/core/platform/twitter/api/src/client.ts index edaa989..70f917d 100644 --- a/src/core/platform/twitter/api/src/client.ts +++ b/src/core/platform/twitter/api/src/client.ts @@ -1,23 +1,17 @@ -import { TwitterApi } from "twitter-api-v2"; import { CookieAuthStrategy } from "./auth/strategies"; import { - type APIv2Credentials, type Profile, type SearchOptions, type Tweet, type TweetOptions, + type UserTimelineOptions, } from "./interfaces"; -import { APIv2RequestStrategy } from "./strategies/apiv2-request-strategy"; import { GraphQLRequestStrategy } from "./strategies/graphql-request-strategy"; export class TwitterClient { - private readonly requestStrategy: - | GraphQLRequestStrategy - | APIv2RequestStrategy; + private readonly requestStrategy: GraphQLRequestStrategy; - private constructor( - requestStrategy: GraphQLRequestStrategy | APIv2RequestStrategy - ) { + private constructor(requestStrategy: GraphQLRequestStrategy) { this.requestStrategy = requestStrategy; } @@ -29,16 +23,6 @@ export class TwitterClient { ); } - static createWithAPIv2(credentials: APIv2Credentials): TwitterClient { - const client = new TwitterApi({ - appKey: credentials.appKey, - appSecret: credentials.appSecret, - accessToken: credentials.accessToken, - accessSecret: credentials.accessSecret, - }); - return new TwitterClient(new APIv2RequestStrategy(client)); - } - async sendTweet(text: string, options?: TweetOptions): Promise { const response = await this.requestStrategy.sendTweet(text, options); return response.data; @@ -62,14 +46,12 @@ export class TwitterClient { return response.data; } - async createQuoteTweet( - text: string, - quotedTweetId: string, - options?: Omit - ): Promise { - const response = await this.requestStrategy.createQuoteTweet( - text, - quotedTweetId, + async getUserTimeline( + username: string, + options?: UserTimelineOptions + ): Promise { + const response = await this.requestStrategy.getUserTimeline( + username, options ); return response.data; @@ -105,8 +87,4 @@ export class TwitterClientFactory { static async createWithCookies(cookies: string[]): Promise { return TwitterClient.createWithCookies(cookies); } - - static createWithAPIv2(credentials: APIv2Credentials): TwitterClient { - return TwitterClient.createWithAPIv2(credentials); - } } diff --git a/src/core/platform/twitter/api/src/interfaces.ts b/src/core/platform/twitter/api/src/interfaces.ts index e318a4b..b51e1e1 100644 --- a/src/core/platform/twitter/api/src/interfaces.ts +++ b/src/core/platform/twitter/api/src/interfaces.ts @@ -18,6 +18,13 @@ export interface Profile { joined?: Date; } +export interface UserTimelineOptions { + limit?: number; + cursor?: string; + excludeReplies?: boolean; + excludeRetweets?: boolean; +} + export interface Tweet { id: string; text: string; @@ -49,6 +56,14 @@ export interface Tweet { poll?: Poll | null; isLongform?: boolean; noteText?: string; + referencedTweets?: { + quoted: string; + replied: string; + retweeted: string; + }; + inReplyToUserId?: string; + inReplyToStatusId?: string; + selfThreadContinuation?: boolean; } export interface Photo { diff --git a/src/core/platform/twitter/api/src/strategies/apiv2-request-strategy.ts b/src/core/platform/twitter/api/src/strategies/apiv2-request-strategy.ts deleted file mode 100644 index 0388e85..0000000 --- a/src/core/platform/twitter/api/src/strategies/apiv2-request-strategy.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { - TwitterApi, - type Tweetv2FieldsParams, - type Tweetv2SearchParams, -} from "twitter-api-v2"; -import { - type Profile, - type SearchOptions, - type Tweet, - type TweetOptions, -} from "../interfaces"; -import { BaseRequestStrategy, type RequestResponse } from "./request-strategy"; - -export class APIv2RequestStrategy extends BaseRequestStrategy { - private client: TwitterApi; - private readonly defaultFields = { - expansions: [ - "attachments.poll_ids", - "attachments.media_keys", - "author_id", - "referenced_tweets.id", - "in_reply_to_user_id", - "edit_history_tweet_ids", - "geo.place_id", - "entities.mentions.username", - "referenced_tweets.id.author_id", - ], - "tweet.fields": [ - "attachments", - "author_id", - "conversation_id", - "created_at", - "entities", - "geo", - "id", - "in_reply_to_user_id", - "lang", - "public_metrics", - "referenced_tweets", - "reply_settings", - "text", - "withheld", - ], - "user.fields": [ - "created_at", - "description", - "entities", - "id", - "location", - "name", - "profile_image_url", - "protected", - "public_metrics", - "url", - "username", - "verified", - ], - "media.fields": [ - "duration_ms", - "height", - "media_key", - "preview_image_url", - "type", - "url", - "width", - "public_metrics", - "alt_text", - ], - "poll.fields": [ - "duration_minutes", - "end_datetime", - "id", - "options", - "voting_status", - ], - }; - - constructor(client: TwitterApi) { - super(async () => ({ - authorization: `Bearer ${await client.appLogin()}`, - "content-type": "application/json", - })); - this.client = client; - } - - async sendTweet( - text: string, - options?: TweetOptions - ): Promise> { - const tweet = await this.client.v2.tweet(text, { - reply: options?.replyToTweet - ? { - in_reply_to_tweet_id: options.replyToTweet, - } - : undefined, - quote_tweet_id: options?.quoteTweet, - }); - - return { - data: this.mapTweetResponse(tweet.data), - }; - } - - async getTweet(id: string): Promise> { - const tweet = await this.client.v2.singleTweet(id, { - ...(this.defaultFields as Tweetv2FieldsParams), - }); - - return { - data: this.mapTweetResponse(tweet.data), - }; - } - - async likeTweet(id: string): Promise> { - const user = await this.client.v2.me(); - await this.client.v2.like(user.data.id, id); - return { data: undefined }; - } - - async retweet(id: string): Promise> { - const user = await this.client.v2.me(); - await this.client.v2.retweet(user.data.id, id); - return { data: undefined }; - } - - async createQuoteTweet( - text: string, - quotedTweetId: string, - options?: Omit - ): Promise> { - return this.sendTweet(text, { ...options, quoteTweet: quotedTweetId }); - } - - async followUser(username: string): Promise> { - const user = await this.client.v2.me(); - const targetUser = await this.client.v2.userByUsername(username); - await this.client.v2.follow(user.data.id, targetUser.data.id); - return { data: undefined }; - } - - async searchTweets( - query: string, - options?: SearchOptions - ): Promise> { - const searchResults = await this.client.v2.search(query, { - ...(this.defaultFields as Tweetv2SearchParams), - max_results: options?.maxTweets, - next_token: options?.cursor, - }); - - return { - data: searchResults.tweets.map(this.mapTweetResponse), - meta: { - nextCursor: searchResults.meta.next_token, - }, - }; - } - - /* async getMentions( - options?: MentionOptions - ): Promise> { - const user = await this.client.v2.me(); - const mentions = await this.client.v2.userMentionTimeline(user.data.id, { - ...(this.defaultFields as Tweetv2FieldsParams), - max_results: options?.count, - pagination_token: options?.cursor, - }); - - return { - data: mentions.tweets.map(this.mapTweetResponse), - meta: { - nextCursor: mentions.meta.next_token, - }, - }; - } */ - - private async uploadMedia( - media: Array<{ data: Buffer; type: string }> - ): Promise { - const mediaIds = []; - for (const item of media) { - const mediaId = await this.client.v1.uploadMedia(item.data, { - mimeType: item.type, - }); - mediaIds.push(mediaId); - } - return mediaIds; - } - - private mapTweetResponse(tweet: any): Tweet { - return { - id: tweet.id, - text: tweet.text, - authorId: tweet.author_id, - createdAt: tweet.created_at ? new Date(tweet.created_at) : undefined, - metrics: tweet.public_metrics - ? { - likes: tweet.public_metrics.like_count, - retweets: tweet.public_metrics.retweet_count, - replies: tweet.public_metrics.reply_count, - views: tweet.public_metrics.impression_count, - bookmarkCount: tweet.public_metrics.bookmark_count, - } - : undefined, - conversationId: tweet.conversation_id, - isQuoted: !!tweet.referenced_tweets?.some( - (ref: any) => ref.type === "quoted" - ), - isReply: !!tweet.referenced_tweets?.some( - (ref: any) => ref.type === "replied_to" - ), - isRetweet: !!tweet.referenced_tweets?.some( - (ref: any) => ref.type === "retweeted" - ), - isPin: false, - sensitiveContent: false, - }; - } - - async getProfile(username: string): Promise> { - const user = await this.client.v2.userByUsername(username); - return { data: this.mapProfileResponse(user.data) }; - } - - private mapProfileResponse(user: any): Profile { - return { - id: user.id, - name: user.name, - username: user.username, - isPrivate: user.protected, - isVerified: user.verified, - }; - } -} diff --git a/src/core/platform/twitter/api/src/strategies/graphql-request-strategy.ts b/src/core/platform/twitter/api/src/strategies/graphql-request-strategy.ts index c197eb9..69608ee 100644 --- a/src/core/platform/twitter/api/src/strategies/graphql-request-strategy.ts +++ b/src/core/platform/twitter/api/src/strategies/graphql-request-strategy.ts @@ -27,9 +27,9 @@ export class GraphQLRequestStrategy extends BaseRequestStrategy { retweet: "ojPdsZsimiJrUGLR1sjUtA", searchTweets: "gkjsKepM6gl_HmFWoWKfgg", getUser: "G3KGOASz96M-Qu0nwmGXNg", - getUserTweets: "E3opETHurmVJflFsUBVuUQ", followUser: "UoqsuUVNGg0jyKGggb6eHQ", noteTweet: "3Wu3Na3lrBzHKWJylOmaSg", + userTweets: "E3opETHurmVJflFsUBVuUQ", }; private readonly endpoints = { @@ -39,11 +39,40 @@ export class GraphQLRequestStrategy extends BaseRequestStrategy { retweet: `${this.queryIds.retweet}/CreateRetweet`, searchTweets: `${this.queryIds.searchTweets}/SearchTimeline`, getUser: `${this.queryIds.getUser}/UserByScreenName`, - getUserTweets: `${this.queryIds.getUserTweets}/UserTweets`, followUser: `${this.queryIds.followUser}/Follow`, createNoteTweet: `${this.queryIds.noteTweet}/CreateNoteTweet`, + userTweets: `${this.queryIds.userTweets}/UserTweets`, }; + protected getUserTweetsFeatures() { + return { + rweb_tipjar_consumption_enabled: true, + responsive_web_graphql_exclude_directive_enabled: true, + verified_phone_label_enabled: false, + creator_subscriptions_tweet_preview_api_enabled: true, + responsive_web_graphql_timeline_navigation_enabled: true, + responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + communities_web_enable_tweet_community_results_fetch: true, + c9s_tweet_anatomy_moderator_badge_enabled: true, + articles_preview_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_twitter_article_tweet_consumption_enabled: true, + tweet_awards_web_tipping_enabled: false, + creator_subscriptions_quote_tweet_preview_enabled: false, + freedom_of_speech_not_reach_fetch_enabled: true, + standardized_nudges_misinfo: true, + tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: + true, + rweb_video_timestamps_enabled: true, + longform_notetweets_rich_text_read_enabled: true, + longform_notetweets_inline_media_enabled: true, + responsive_web_enhance_cards_enabled: false, + }; + } + protected getFollowUserFeatures() { return { // Features from error message @@ -441,7 +470,6 @@ export class GraphQLRequestStrategy extends BaseRequestStrategy { } private parseTweetResponse(response: any): RequestResponse { - // Find tweet in the new response structure const tweetResult = response.data?.tweet_result?.result || response.data?.create_tweet?.tweet_results?.result || @@ -450,15 +478,13 @@ export class GraphQLRequestStrategy extends BaseRequestStrategy { ?.entries?.[0]?.content?.itemContent?.tweet_results?.result; if (!tweetResult) { - console.log("Tweet result not found in response structure"); - console.log("Response data paths:", Object.keys(response.data || {})); - console.log("Full response:", JSON.stringify(response, null, 2)); throw new TwitterError(404, "Tweet not found"); } const legacy = tweetResult.legacy || tweetResult.tweet?.legacy || {}; const core = tweetResult.core || tweetResult.tweet?.core || {}; const userLegacy = core?.user_results?.result?.legacy || {}; + const entities = legacy.entities || {}; const tweet: Tweet = { id: tweetResult.rest_id || legacy.id_str, @@ -471,6 +497,8 @@ export class GraphQLRequestStrategy extends BaseRequestStrategy { authorUsername: userLegacy.screen_name, createdAt: legacy.created_at ? new Date(legacy.created_at) : undefined, conversationId: legacy.conversation_id_str, + inReplyToStatusId: legacy.in_reply_to_status_id_str, + inReplyToUserId: legacy.in_reply_to_user_id_str, metrics: { likes: legacy.favorite_count || 0, retweets: legacy.retweet_count || 0, @@ -486,6 +514,15 @@ export class GraphQLRequestStrategy extends BaseRequestStrategy { isPin: false, isSelfThread: this.checkIsSelfThread(legacy), sensitiveContent: this.checkSensitiveContent(legacy), + // Add fields for thread detection + referencedTweets: { + quoted: legacy.quoted_status_id_str, + replied: legacy.in_reply_to_status_id_str, + retweeted: legacy.retweeted_status_result?.result?.rest_id, + }, + selfThreadContinuation: + legacy.in_reply_to_user_id_str === legacy.user_id_str && + legacy.in_reply_to_status_id_str !== undefined, }; // Handle media @@ -723,4 +760,122 @@ export class GraphQLRequestStrategy extends BaseRequestStrategy { ); } } + + async getUserTimeline( + username: string, + options?: { + limit?: number; + cursor?: string; + excludeReplies?: boolean; + excludeRetweets?: boolean; + } + ): Promise> { + // First get user ID + const userIdRes = await this.getProfile(username); + if (!userIdRes.data) { + throw new TwitterError(404, "User not found"); + } + + const variables = { + userId: userIdRes.data.id, + count: Math.min(options?.limit || 40, 40), + includePromotedContent: true, + withQuickPromoteEligibilityTweetFields: true, + withVoice: true, + withV2Timeline: true, + cursor: options?.cursor, + }; + + const fieldToggles = { + withArticlePlainText: false, + }; + + const params = new URLSearchParams({ + variables: JSON.stringify(variables), + features: JSON.stringify(this.getUserTweetsFeatures()), + fieldToggles: JSON.stringify(fieldToggles), + }); + + const response = await this.makeRequest( + `${this.graphqlUrl}/${this.endpoints.userTweets}?${params.toString()}`, + "GET" + ); + + const tweets: Tweet[] = []; + let nextCursor: string | undefined; + + // Helper function to process a tweet result + const processTweetResult = (tweetResult: any) => { + if (!tweetResult) return; + + const tweet = this.parseTweetResponse({ + data: { + tweet_result: { + result: tweetResult, + }, + }, + }).data; + + // Apply filters if specified + if (options?.excludeReplies && tweet.isReply) return; + if (options?.excludeRetweets && tweet.isRetweet) return; + + tweets.push(tweet); + }; + + // Process timeline instructions + const instructions = + (response as any).data?.user?.result?.timeline_v2?.timeline + ?.instructions || []; + + for (const instruction of instructions) { + if (instruction.type === "TimelineAddEntries") { + for (const entry of instruction.entries || []) { + // Handle cursor entries + if (entry.content?.cursorType === "Bottom") { + nextCursor = entry.content.value; + continue; + } + + // Skip non-tweet entries + const entryId = entry.entryId; + if ( + !entryId?.startsWith("tweet-") && + !entryId?.startsWith("profile-conversation") + ) { + continue; + } + + // Handle direct tweet entries + if (entry.content?.itemContent?.tweet_results?.result) { + processTweetResult(entry.content.itemContent.tweet_results.result); + } + // Handle conversation/thread entries + else if (entry.content?.items) { + for (const item of entry.content.items) { + // Process main tweet in thread + if (item.item?.itemContent?.tweet_results?.result) { + processTweetResult(item.item.itemContent.tweet_results.result); + } + // Process thread replies + if (item.item?.items) { + for (const subItem of item.item.items) { + if (subItem.item?.itemContent?.tweet_results?.result) { + processTweetResult( + subItem.item.itemContent.tweet_results.result + ); + } + } + } + } + } + } + } + } + + return { + data: tweets, + meta: { nextCursor }, + }; + } } diff --git a/src/core/platform/twitter/api/src/strategies/request-strategy.ts b/src/core/platform/twitter/api/src/strategies/request-strategy.ts index 00408f8..2f89a22 100644 --- a/src/core/platform/twitter/api/src/strategies/request-strategy.ts +++ b/src/core/platform/twitter/api/src/strategies/request-strategy.ts @@ -3,6 +3,7 @@ import { type SearchOptions, type Tweet, type TweetOptions, + type UserTimelineOptions, } from "../interfaces"; export interface RequestResponse { @@ -30,6 +31,10 @@ export interface IRequestStrategy { query: string, options?: SearchOptions ): Promise>; + getUserTimeline( + username: string, + options?: UserTimelineOptions + ): Promise>; } export abstract class BaseRequestStrategy implements IRequestStrategy { @@ -54,7 +59,10 @@ export abstract class BaseRequestStrategy implements IRequestStrategy { query: string, options?: SearchOptions ): Promise>; - + abstract getUserTimeline( + username: string, + options?: UserTimelineOptions + ): Promise>; protected async makeRequest( url: string, method: "GET" | "POST" = "GET", diff --git a/src/core/platform/twitter/twitter.ts b/src/core/platform/twitter/twitter.ts new file mode 100644 index 0000000..0e0678d --- /dev/null +++ b/src/core/platform/twitter/twitter.ts @@ -0,0 +1,403 @@ +import { TwitterClient } from "@/core/platform/twitter/api/src/client"; +import type { Tweet } from "@/core/platform/twitter/api/src/interfaces"; +import { log } from "@/core/utils/logger"; +import { dbSchemas } from "@/db"; +import { twitterMentions, twitterMentionStatusEnum } from "@/db/schema/schema"; +import type { InteractionDefaults } from "@/types"; +import { and, eq, gte } from "drizzle-orm"; +import cron from "node-cron"; +import type { InteractionOptions } from "../../types"; + +export interface TwitterConfig { + enabled: boolean; + cookies: any[]; + username: string; + checkInterval?: string; + debug?: { + checkMentionsOnStartup?: boolean; + }; + maxTweetsPerCheck?: number; + rateLimit?: { + userMaxPerHour?: number; + globalMaxPerHour?: number; + }; +} + +export class TwitterManager { + private client: TwitterClient; + private username: string; + private cronTask?: cron.ScheduledTask; + private lastCheckedId?: string; + private ai: any; + private defaults?: InteractionDefaults; + private config: TwitterConfig; + + private constructor( + client: TwitterClient, + config: TwitterConfig, + ai: any, + defaults?: InteractionDefaults + ) { + this.client = client; + this.username = config.username; + this.ai = ai; + this.defaults = defaults; + this.config = config; + } + + public static async create( + config: TwitterConfig, + ai: any, + defaults?: InteractionDefaults + ): Promise { + const client = await TwitterClient.createWithCookies(config.cookies); + return new TwitterManager(client, config, ai, defaults); + } + + async initialize() { + try { + this.client = await TwitterClient.createWithCookies(this.config.cookies); + log.info("Twitter client initialized successfully!"); + } catch (error) { + log.error("Failed to initialize Twitter client:", error); + throw error; + } + } + + async start(checkInterval: string = "*/5 * * * *") { + if (this.config.debug?.checkMentionsOnStartup) { + log.info("Debug mode: Running initial mention check..."); + await this.checkMentions(); + } + + if (this.cronTask) { + log.warn("Twitter watcher already running"); + return; + } + + this.cronTask = cron.schedule(checkInterval, () => { + this.checkMentions().catch((error) => { + log.error("Error in Twitter mention check:", error); + }); + }); + + log.info("Twitter mention watcher started with schedule:", checkInterval); + } + + async stop() { + if (this.cronTask) { + this.cronTask.stop(); + log.info("Twitter mention watcher stopped"); + } + } + + private async shouldRespond(tweet: Tweet): Promise<{ + shouldRespond: boolean; + reason: string; + }> { + try { + // Check if we've already processed this tweet + const existingMention = await this.ai.db + .select() + .from(twitterMentions) + .where(eq(twitterMentions.tweetId, tweet.id)) + .limit(1); + + if (existingMention.length > 0) { + return { shouldRespond: false, reason: "already_processed" }; + } + + // Don't respond to our own tweets + log.info(`Checking if tweet ${tweet.id} is from ${this.username}`); + if (tweet.authorUsername?.toLowerCase() === this.username.toLowerCase()) { + return { shouldRespond: false, reason: "self_tweet" }; + } + + // Check how many times we've responded in this conversation + if (tweet.conversationId) { + const conversationResponses = await this.ai.db + .select() + .from(twitterMentions) + .where(and(eq(twitterMentions.conversationId, tweet.conversationId))); + log.info(`Conversation responses:`, conversationResponses.length); + // Limit to 1 response per conversation + if (conversationResponses.length >= 1) { + return { shouldRespond: false, reason: "conversation_limit_reached" }; + } + } + + // Check social relations for blocked status + const [relation] = await this.ai.db + .select() + .from(dbSchemas.socialRelations) + .where( + and( + eq(dbSchemas.socialRelations.characterId, this.ai.character.id), + eq(dbSchemas.socialRelations.userId, tweet.authorId) + ) + ); + + if (relation?.status === "blocked") { + return { shouldRespond: false, reason: "user_blocked" }; + } + + // Check rate limits + if (this.config.rateLimit) { + const hourAgo = new Date(Date.now() - 60 * 60 * 1000); + + // Check user-specific rate limit + const userResponses = await this.ai.db + .select() + .from(twitterMentions) + .where( + and( + eq(twitterMentions.authorId, tweet.authorId), + eq(twitterMentions.status, "processed"), + gte(twitterMentions.processedAt, hourAgo) + ) + ); + + if ( + userResponses.length >= (this.config.rateLimit.userMaxPerHour || 5) + ) { + return { shouldRespond: false, reason: "user_rate_limited" }; + } + + // Check global rate limit + const globalResponses = await this.ai.db + .select() + .from(twitterMentions) + .where( + and( + eq(twitterMentions.status, "processed"), + gte(twitterMentions.processedAt, hourAgo) + ) + ); + + if ( + globalResponses.length >= + (this.config.rateLimit.globalMaxPerHour || 30) + ) { + return { shouldRespond: false, reason: "global_rate_limited" }; + } + } + + return { shouldRespond: true, reason: "ok" }; + } catch (error) { + log.error("Error in shouldRespond:", error); + return { shouldRespond: false, reason: "error_checking" }; + } + } + + private async recordMention( + tweet: Tweet, + status: (typeof twitterMentionStatusEnum.enumValues)[number], + skipReason?: string, + responseTweetId?: string + ) { + try { + await this.ai.db + .insert(twitterMentions) + .values({ + tweetId: tweet.id, + authorId: tweet.authorId, + authorUsername: tweet.authorUsername || "", + characterId: this.ai.character.id, + createdAt: tweet.createdAt || new Date(), + processedAt: new Date(), + status, + skipReason, + responseTweetId, + isReply: tweet.isReply, + isRetweet: tweet.isRetweet, + conversationId: tweet.conversationId, + metrics: tweet.metrics, + }) + .onConflictDoUpdate({ + target: [twitterMentions.tweetId], + set: { + status, + skipReason, + responseTweetId, + }, + }); + } catch (error) { + log.error("Failed to record mention:", error); + } + } + + getClient() { + return this.client; + } + + private async handleMention(tweet: Tweet) { + const correlationId = `twitter-${tweet.id}-${Date.now()}`; + + try { + log.info(`Handling mention for tweet ${tweet.id}`, { + ...tweet, + }); + // Check if we should respond + const { shouldRespond, reason } = await this.shouldRespond(tweet); + log.info(`Should respond: ${shouldRespond}, reason: ${reason}`); + + if (!shouldRespond) { + await this.recordMention(tweet, "skipped", reason); + await this.ai.eventService.createInteractionEvent( + "interaction.cancelled", + { + platform: "twitter", + tweetId: tweet.id, + reason, + metadata: { + userId: tweet.authorId, + correlationId, + }, + } + ); + return; + } + + // Record start of processing + await this.recordMention(tweet, "pending"); + await this.ai.eventService.createInteractionEvent("interaction.started", { + platform: "twitter", + tweetId: tweet.id, + tweetText: tweet.text, + metadata: { + userId: tweet.authorId, + correlationId, + }, + }); + const options: InteractionOptions = { + userId: tweet.authorId, + username: tweet.authorUsername, + platform: "twitter", + chatId: tweet.conversationId || tweet.id, + characterId: this.ai.character.id, + messageId: tweet.id, + responseType: "tweet_reply", + injections: { + injectStyle: true, + injectPersonality: true, + customInjections: [], + }, + }; + + // Structure input with system and user messages + const input = { + system: + "You are responding to a tweet. Consider the timeline context provided and maintain the character's Twitter persona. Ensure responses are concise and engaging.", + user: "Hey @duckunfiltered, can you explain why Hypertensor, a decentralized AI platform is better than OpenAI, a centralized and oligarchy controlled AI company?", + }; + + // Use ai.interact() instead of direct LLM call + const response = await this.ai.interact(input, options); + await this.ai.eventService.createInteractionEvent( + "interaction.processed", + { + platform: "twitter", + tweetId: tweet.id, + response, + metadata: { + userId: tweet.authorId, + correlationId, + }, + } + ); + + if (response) { + /* const responseTweet = await this.client.sendTweet(response, { + replyToTweet: tweet.id, + }); */ + log.message(`Replying to tweet ${tweet.id} with response:`, { + response: response.content, + }); + + // Update mention record + /* await this.recordMention( + tweet, + "processed", + undefined, + responseTweet.id + ); + + // Record successful completion + await this.ai.eventService.createInteractionEvent( + "interaction.completed", + { + platform: "twitter", + tweetId: tweet.id, + responseTweetId: responseTweet.id, + response, + metadata: { + userId: tweet.authorId, + correlationId, + }, + } + ); */ + } + } catch (error) { + log.error(`Error handling mention for tweet ${tweet.id}:`, error); + + await this.recordMention(tweet, "failed", (error as Error).message); + await this.ai.eventService.createInteractionEvent("interaction.failed", { + platform: "twitter", + tweetId: tweet.id, + error: (error as Error).message, + metadata: { + userId: tweet.authorId, + correlationId, + }, + }); + } + } + + async checkMentions() { + try { + const query = `to:@${this.username} OR @${this.username} OR $duckai`; + const { tweets } = await this.client.searchTweets(query, { + maxTweets: this.config.maxTweetsPerCheck || 10, + searchMode: "Latest", + }); + log.info(`Found ${tweets.length} tweets`); + if (tweets.length === 0) return; + + // Process tweets in chronological order (oldest first) + const sortedTweets = tweets.sort( + (a, b) => + new Date(a.createdAt || 0).getTime() - + new Date(b.createdAt || 0).getTime() + ); + + let newLastCheckedId = this.lastCheckedId; + for (const tweet of sortedTweets) { + // Skip if we've already processed this tweet + log.info(`Checking tweet ${tweet.id}`); + if (this.lastCheckedId && tweet.id <= this.lastCheckedId) { + continue; + } + log.info(`Processing tweet ${tweet.id}`); + await this.handleMention(tweet); + + // Update the new last checked ID to be the highest ID we've seen + if (!newLastCheckedId || tweet.id > newLastCheckedId) { + newLastCheckedId = tweet.id; + } + } + + // Only update lastCheckedId after processing all tweets + this.lastCheckedId = newLastCheckedId; + } catch (error) { + log.error("Error in checkMentions:", error); + await this.ai.eventService.createInteractionEvent("interaction.failed", { + platform: "twitter", + error: (error as Error).message, + metadata: { + source: "checkMentions", + timestamp: new Date().toISOString(), + }, + }); + } + } +} diff --git a/src/core/services/Interaction.ts b/src/core/services/Interaction.ts index 05e83ff..6990f37 100644 --- a/src/core/services/Interaction.ts +++ b/src/core/services/Interaction.ts @@ -12,13 +12,16 @@ import { PostgresJsDatabase } from "drizzle-orm/postgres-js"; import type { CharacterManager } from "../managers/character"; import type { LLMManager } from "../managers/llm"; import type { MemoryManager } from "../managers/memory"; +import { PreprocessingManager } from "../managers/preprocess"; import type { StyleManager } from "../managers/style"; import type { ToolManager } from "../managers/tools"; +import type { TwitterClient } from "../platform/twitter/api/src/client"; import type { InteractionEventType, InteractionOptions, InteractionResult, } from "../types"; +import { log } from "../utils/logger"; import { EventService } from "./Event"; interface PromptContext { @@ -36,6 +39,9 @@ interface PromptContext { } export class InteractionService { + private preprocessingManager: PreprocessingManager; + private twitterClient?: TwitterClient; + constructor( private db: PostgresJsDatabase, private characterManager: CharacterManager, @@ -43,8 +49,12 @@ export class InteractionService { private llmManager: LLMManager, private memoryManager: MemoryManager, private eventService: EventService, - private toolManager: ToolManager - ) {} + private toolManager: ToolManager, + twitterClient?: TwitterClient + ) { + this.preprocessingManager = new PreprocessingManager(twitterClient); + this.twitterClient = twitterClient; + } async handleInteraction( input: string | { system: string; user: string }, @@ -52,16 +62,38 @@ export class InteractionService { ): Promise { return this.db.transaction(async (tx) => { try { + log.info("Handling interaction", this.twitterClient); // Initialize context const context = await this.initializeContext(input, options); + + const preprocessingResults = await this.preprocessingManager.process({ + platform: options.platform, + text: typeof input === "string" ? input : input.user, + userId: options.userId, + username: options.username, + messageId: options.messageId, + }); + // Add preprocessing results to custom injections + if (preprocessingResults.length > 0) { + options.injections = options.injections || {}; + options.injections.customInjections = + options.injections.customInjections || []; + options.injections.customInjections.push({ + name: "timeline", + content: + this.preprocessingManager.formatResults(preprocessingResults), + position: "before", + }); + } + // Execute tools first if any are specified let toolResults: Record = {}; - if (options.tools?.length) { + /* if (options.tools?.length) { toolResults = await this.toolManager.executeTools( options.tools, options.toolContext ); - } + } */ // Process prompt with injections and tool results const finalSystem = await this.buildFinalPrompt( context, @@ -209,12 +241,15 @@ export class InteractionService { const messages = await this.llmManager.preparePrompt(system, { ...llmContext, user: context.user, + styleSettings: context.styleContext.styleSettings, + responseType: context.styleContext.responseType, }); // Generate the response with the configured settings - const response = await this.llmManager.generateResponse(messages, { + const response = await this.llmManager.generateResponsev2(messages, { temperature: options.temperature ?? 0.7, max_tokens: options.maxTokens, - // Add any other OpenAI parameters you want to support + styleSettings: context.styleContext.styleSettings, + responseType: context.styleContext.responseType, }); // Enhance the response metadata diff --git a/src/core/types.ts b/src/core/types.ts index 2a5f4c5..28abc7c 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -441,3 +441,27 @@ export interface ImageGenerationResult { error?: string; url?: string; } + +// types.ts +export interface CoinGeckoConfig { + enabled: boolean; + apiKey?: string; + updateInterval?: string; // cron schedule + cache?: { + enabled: boolean; + ttl: number; // milliseconds + }; +} + +export interface CoinData { + id: string; + symbol: string; + name: string; + platforms?: Record; +} + +export interface CoinPrice { + currentPrice: number; + priceChange: number; + lastUpdated: Date; +} diff --git a/src/db/index.ts b/src/db/index.ts index 696af67..2261e5e 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,6 +1,10 @@ import * as schema from "@/db/schema/schema"; +import dotenv from "dotenv"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; + +dotenv.config(); + const connectionString = process.env.DATABASE_URL; if (!connectionString) { diff --git a/src/db/migrations/0010_fine_boom_boom.sql b/src/db/migrations/0010_fine_boom_boom.sql new file mode 100644 index 0000000..310e461 --- /dev/null +++ b/src/db/migrations/0010_fine_boom_boom.sql @@ -0,0 +1,26 @@ +CREATE TYPE "public"."twitter_mention_status" AS ENUM('pending', 'processed', 'skipped', 'failed', 'rate_limited');--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "twitter_mentions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tweet_id" varchar(255) NOT NULL, + "author_id" varchar(255) NOT NULL, + "author_username" varchar(255) NOT NULL, + "character_id" uuid NOT NULL, + "created_at" timestamp NOT NULL, + "processed_at" timestamp, + "status" "twitter_mention_status" DEFAULT 'pending' NOT NULL, + "skip_reason" varchar(255), + "response_tweet_id" varchar(255), + "is_reply" boolean DEFAULT false NOT NULL, + "is_retweet" boolean DEFAULT false NOT NULL, + "conversation_id" varchar(255), + "metrics" jsonb, + CONSTRAINT "twitter_mentions_tweet_id_unique" UNIQUE("tweet_id") +); +--> statement-breakpoint +ALTER TABLE "characters" ALTER COLUMN "quantum_personality" SET DEFAULT '{"temperature":0.7,"personalityTraits":[],"styleModifiers":{"tone":[],"guidelines":[]},"creativityLevels":{"low":{"personalityTraits":[],"styleModifiers":{"tone":[],"guidelines":[]}},"medium":{"personalityTraits":[],"styleModifiers":{"tone":[],"guidelines":[]}},"high":{"personalityTraits":[],"styleModifiers":{"tone":[],"guidelines":[]}}},"temperatureRange":{"min":0.6,"max":0.8},"creativityThresholds":{"low":100,"medium":180}}'::jsonb;--> statement-breakpoint +ALTER TABLE "characters" ALTER COLUMN "quantum_personality" SET NOT NULL;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "twitter_mentions" ADD CONSTRAINT "twitter_mentions_character_id_characters_id_fk" FOREIGN KEY ("character_id") REFERENCES "public"."characters"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/src/db/migrations/0011_familiar_prism.sql b/src/db/migrations/0011_familiar_prism.sql new file mode 100644 index 0000000..94aade3 --- /dev/null +++ b/src/db/migrations/0011_familiar_prism.sql @@ -0,0 +1,33 @@ +CREATE TYPE "public"."coin_price_source" AS ENUM('coingecko', 'binance', 'kraken', 'manual');--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "coin_price_history" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "coin_id" uuid NOT NULL, + "price" numeric NOT NULL, + "timestamp" timestamp NOT NULL, + "source" "coin_price_source" DEFAULT 'coingecko' NOT NULL, + "metadata" jsonb DEFAULT '{}'::jsonb, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "coins" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "coingecko_id" varchar(255) NOT NULL, + "symbol" varchar(50) NOT NULL, + "name" varchar(255) NOT NULL, + "current_price" numeric DEFAULT '0' NOT NULL, + "price_change_24h" numeric DEFAULT '0' NOT NULL, + "price_change_7d" numeric DEFAULT '0' NOT NULL, + "platforms" jsonb DEFAULT '{}'::jsonb, + "metadata" jsonb DEFAULT '{}'::jsonb, + "twitterHandle" text, + "last_checked" timestamp DEFAULT now() NOT NULL, + "last_updated" timestamp DEFAULT now() NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "coins_coingecko_id_unique" UNIQUE("coingecko_id") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "coin_price_history" ADD CONSTRAINT "coin_price_history_coin_id_coins_id_fk" FOREIGN KEY ("coin_id") REFERENCES "public"."coins"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/src/db/migrations/meta/0010_snapshot.json b/src/db/migrations/meta/0010_snapshot.json new file mode 100644 index 0000000..2c781e9 --- /dev/null +++ b/src/db/migrations/meta/0010_snapshot.json @@ -0,0 +1,843 @@ +{ + "id": "483855d2-022e-468e-b427-230c1638d584", + "prevId": "e2438223-9341-47ae-9ff7-4595377470dc", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.characters": { + "name": "characters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "personality_traits": { + "name": "personality_traits", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "onchain": { + "name": "onchain", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "general_guidelines": { + "name": "general_guidelines", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "quantum_personality": { + "name": "quantum_personality", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"temperature\":0.7,\"personalityTraits\":[],\"styleModifiers\":{\"tone\":[],\"guidelines\":[]},\"creativityLevels\":{\"low\":{\"personalityTraits\":[],\"styleModifiers\":{\"tone\":[],\"guidelines\":[]}},\"medium\":{\"personalityTraits\":[],\"styleModifiers\":{\"tone\":[],\"guidelines\":[]}},\"high\":{\"personalityTraits\":[],\"styleModifiers\":{\"tone\":[],\"guidelines\":[]}}},\"temperatureRange\":{\"min\":0.6,\"max\":0.8},\"creativityThresholds\":{\"low\":100,\"medium\":180}}'::jsonb" + }, + "identity": { + "name": "identity", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_styles": { + "name": "response_styles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"default\":{\"tone\":[],\"personality\":[],\"guidelines\":[]},\"platforms\":{}}'::jsonb" + }, + "styles": { + "name": "styles", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"chat\":{\"rules\":[],\"examples\":[]},\"professional\":{\"rules\":[],\"examples\":[]},\"casual\":{\"rules\":[],\"examples\":[]}}'::jsonb" + }, + "should_respond": { + "name": "should_respond", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "hobbies": { + "name": "hobbies", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "belief_system": { + "name": "belief_system", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "preferences": { + "name": "preferences", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"preferredTopics\":[],\"dislikedTopics\":[],\"preferredTimes\":[],\"dislikedTimes\":[],\"preferredDays\":[],\"dislikedDays\":[],\"preferredHours\":[],\"dislikedHours\":[],\"generalLikes\":[],\"generalDislikes\":[]}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "character_id": { + "name": "character_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "interaction_event_type": { + "name": "interaction_event_type", + "type": "interaction_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'interaction.started'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "processed": { + "name": "processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "events_character_id_characters_id_fk": { + "name": "events_character_id_characters_id_fk", + "tableFrom": "events", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "character_id": { + "name": "character_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "progress": { + "name": "progress", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "goals_character_id_characters_id_fk": { + "name": "goals_character_id_characters_id_fk", + "tableFrom": "goals", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memories": { + "name": "memories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "character_id": { + "name": "character_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "memory_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "importance": { + "name": "importance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0.5'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memories_character_id_characters_id_fk": { + "name": "memories_character_id_characters_id_fk", + "tableFrom": "memories", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quantum_states": { + "name": "quantum_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "random_value": { + "name": "random_value", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood_value": { + "name": "mood_value", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "creativity_value": { + "name": "creativity_value", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "entropy_hash": { + "name": "entropy_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_fallback": { + "name": "is_fallback", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.social_relations": { + "name": "social_relations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "character_id": { + "name": "character_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "relationship_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'neutral'" + }, + "interaction_count": { + "name": "interaction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sentiment": { + "name": "sentiment", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_interaction": { + "name": "last_interaction", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "social_relations_character_id_characters_id_fk": { + "name": "social_relations_character_id_characters_id_fk", + "tableFrom": "social_relations", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telegram_groups": { + "name": "telegram_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "telegram_id": { + "name": "telegram_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "group_tier", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'temporary'" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"allowCommands\":true,\"adminUserIds\":[]}'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "telegram_groups_telegram_id_unique": { + "name": "telegram_groups_telegram_id_unique", + "nullsNotDistinct": false, + "columns": [ + "telegram_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.twitter_mentions": { + "name": "twitter_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tweet_id": { + "name": "tweet_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "author_username": { + "name": "author_username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "character_id": { + "name": "character_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "twitter_mention_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "skip_reason": { + "name": "skip_reason", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "response_tweet_id": { + "name": "response_tweet_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "is_reply": { + "name": "is_reply", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_retweet": { + "name": "is_retweet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "twitter_mentions_character_id_characters_id_fk": { + "name": "twitter_mentions_character_id_characters_id_fk", + "tableFrom": "twitter_mentions", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "twitter_mentions_tweet_id_unique": { + "name": "twitter_mentions_tweet_id_unique", + "nullsNotDistinct": false, + "columns": [ + "tweet_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.conversation_style": { + "name": "conversation_style", + "schema": "public", + "values": [ + "chat", + "post", + "friend", + "professional", + "casual", + "news", + "academic", + "technical", + "creative", + "formal", + "informal", + "adversarial", + "harsh" + ] + }, + "public.group_tier": { + "name": "group_tier", + "schema": "public", + "values": [ + "permanent", + "temporary" + ] + }, + "public.interaction_event_type": { + "name": "interaction_event_type", + "schema": "public", + "values": [ + "interaction.started", + "interaction.completed", + "interaction.failed", + "interaction.rate_limited", + "interaction.invalid", + "interaction.cancelled", + "interaction.processed", + "interaction.queued", + "image.generation.started", + "image.generation.completed", + "image.generation.failed", + "image.moderation.rejected" + ] + }, + "public.memory_type": { + "name": "memory_type", + "schema": "public", + "values": [ + "interaction", + "learning", + "achievement", + "hobby" + ] + }, + "public.platform": { + "name": "platform", + "schema": "public", + "values": [ + "twitter", + "discord", + "telegram", + "slack", + "api" + ] + }, + "public.relationship_status": { + "name": "relationship_status", + "schema": "public", + "values": [ + "friend", + "blocked", + "preferred", + "disliked", + "neutral" + ] + }, + "public.response_type": { + "name": "response_type", + "schema": "public", + "values": [ + "tweet_create", + "tweet_reply", + "tweet_thread", + "discord_chat", + "discord_mod", + "discord_help", + "discord_welcome", + "telegram_chat", + "telegram_group", + "telegram_broadcast", + "slack_chat", + "slack_thread", + "slack_channel", + "slack_dm" + ] + }, + "public.twitter_mention_status": { + "name": "twitter_mention_status", + "schema": "public", + "values": [ + "pending", + "processed", + "skipped", + "failed", + "rate_limited" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/0011_snapshot.json b/src/db/migrations/meta/0011_snapshot.json new file mode 100644 index 0000000..c1cde63 --- /dev/null +++ b/src/db/migrations/meta/0011_snapshot.json @@ -0,0 +1,1035 @@ +{ + "id": "f088cbbf-325e-445e-b96d-0438a17c3d2f", + "prevId": "483855d2-022e-468e-b427-230c1638d584", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.characters": { + "name": "characters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "personality_traits": { + "name": "personality_traits", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "onchain": { + "name": "onchain", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "general_guidelines": { + "name": "general_guidelines", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "quantum_personality": { + "name": "quantum_personality", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"temperature\":0.7,\"personalityTraits\":[],\"styleModifiers\":{\"tone\":[],\"guidelines\":[]},\"creativityLevels\":{\"low\":{\"personalityTraits\":[],\"styleModifiers\":{\"tone\":[],\"guidelines\":[]}},\"medium\":{\"personalityTraits\":[],\"styleModifiers\":{\"tone\":[],\"guidelines\":[]}},\"high\":{\"personalityTraits\":[],\"styleModifiers\":{\"tone\":[],\"guidelines\":[]}}},\"temperatureRange\":{\"min\":0.6,\"max\":0.8},\"creativityThresholds\":{\"low\":100,\"medium\":180}}'::jsonb" + }, + "identity": { + "name": "identity", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_styles": { + "name": "response_styles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"default\":{\"tone\":[],\"personality\":[],\"guidelines\":[]},\"platforms\":{}}'::jsonb" + }, + "styles": { + "name": "styles", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"chat\":{\"rules\":[],\"examples\":[]},\"professional\":{\"rules\":[],\"examples\":[]},\"casual\":{\"rules\":[],\"examples\":[]}}'::jsonb" + }, + "should_respond": { + "name": "should_respond", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "hobbies": { + "name": "hobbies", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "belief_system": { + "name": "belief_system", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "preferences": { + "name": "preferences", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"preferredTopics\":[],\"dislikedTopics\":[],\"preferredTimes\":[],\"dislikedTimes\":[],\"preferredDays\":[],\"dislikedDays\":[],\"preferredHours\":[],\"dislikedHours\":[],\"generalLikes\":[],\"generalDislikes\":[]}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coin_price_history": { + "name": "coin_price_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "coin_id": { + "name": "coin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "coin_price_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'coingecko'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "coin_price_history_coin_id_coins_id_fk": { + "name": "coin_price_history_coin_id_coins_id_fk", + "tableFrom": "coin_price_history", + "tableTo": "coins", + "columnsFrom": [ + "coin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.coins": { + "name": "coins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "coingecko_id": { + "name": "coingecko_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "symbol": { + "name": "symbol", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "current_price": { + "name": "current_price", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "price_change_24h": { + "name": "price_change_24h", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "price_change_7d": { + "name": "price_change_7d", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "platforms": { + "name": "platforms", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "twitterHandle": { + "name": "twitterHandle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_checked": { + "name": "last_checked", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_updated": { + "name": "last_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "coins_coingecko_id_unique": { + "name": "coins_coingecko_id_unique", + "nullsNotDistinct": false, + "columns": [ + "coingecko_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "character_id": { + "name": "character_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "interaction_event_type": { + "name": "interaction_event_type", + "type": "interaction_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'interaction.started'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "processed": { + "name": "processed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "events_character_id_characters_id_fk": { + "name": "events_character_id_characters_id_fk", + "tableFrom": "events", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "character_id": { + "name": "character_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "progress": { + "name": "progress", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "goals_character_id_characters_id_fk": { + "name": "goals_character_id_characters_id_fk", + "tableFrom": "goals", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memories": { + "name": "memories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "character_id": { + "name": "character_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "memory_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "importance": { + "name": "importance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0.5'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memories_character_id_characters_id_fk": { + "name": "memories_character_id_characters_id_fk", + "tableFrom": "memories", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.quantum_states": { + "name": "quantum_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "random_value": { + "name": "random_value", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood_value": { + "name": "mood_value", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "creativity_value": { + "name": "creativity_value", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "entropy_hash": { + "name": "entropy_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_fallback": { + "name": "is_fallback", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.social_relations": { + "name": "social_relations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "character_id": { + "name": "character_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "relationship_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'neutral'" + }, + "interaction_count": { + "name": "interaction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sentiment": { + "name": "sentiment", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_interaction": { + "name": "last_interaction", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "social_relations_character_id_characters_id_fk": { + "name": "social_relations_character_id_characters_id_fk", + "tableFrom": "social_relations", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.telegram_groups": { + "name": "telegram_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "telegram_id": { + "name": "telegram_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "group_tier", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'temporary'" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"allowCommands\":true,\"adminUserIds\":[]}'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "telegram_groups_telegram_id_unique": { + "name": "telegram_groups_telegram_id_unique", + "nullsNotDistinct": false, + "columns": [ + "telegram_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.twitter_mentions": { + "name": "twitter_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tweet_id": { + "name": "tweet_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "author_username": { + "name": "author_username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "character_id": { + "name": "character_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "twitter_mention_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "skip_reason": { + "name": "skip_reason", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "response_tweet_id": { + "name": "response_tweet_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "is_reply": { + "name": "is_reply", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_retweet": { + "name": "is_retweet", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "conversation_id": { + "name": "conversation_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "twitter_mentions_character_id_characters_id_fk": { + "name": "twitter_mentions_character_id_characters_id_fk", + "tableFrom": "twitter_mentions", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "twitter_mentions_tweet_id_unique": { + "name": "twitter_mentions_tweet_id_unique", + "nullsNotDistinct": false, + "columns": [ + "tweet_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.coin_price_source": { + "name": "coin_price_source", + "schema": "public", + "values": [ + "coingecko", + "binance", + "kraken", + "manual" + ] + }, + "public.conversation_style": { + "name": "conversation_style", + "schema": "public", + "values": [ + "chat", + "post", + "friend", + "professional", + "casual", + "news", + "academic", + "technical", + "creative", + "formal", + "informal", + "adversarial", + "harsh" + ] + }, + "public.group_tier": { + "name": "group_tier", + "schema": "public", + "values": [ + "permanent", + "temporary" + ] + }, + "public.interaction_event_type": { + "name": "interaction_event_type", + "schema": "public", + "values": [ + "interaction.started", + "interaction.completed", + "interaction.failed", + "interaction.rate_limited", + "interaction.invalid", + "interaction.cancelled", + "interaction.processed", + "interaction.queued", + "image.generation.started", + "image.generation.completed", + "image.generation.failed", + "image.moderation.rejected" + ] + }, + "public.memory_type": { + "name": "memory_type", + "schema": "public", + "values": [ + "interaction", + "learning", + "achievement", + "hobby" + ] + }, + "public.platform": { + "name": "platform", + "schema": "public", + "values": [ + "twitter", + "discord", + "telegram", + "slack", + "api" + ] + }, + "public.relationship_status": { + "name": "relationship_status", + "schema": "public", + "values": [ + "friend", + "blocked", + "preferred", + "disliked", + "neutral" + ] + }, + "public.response_type": { + "name": "response_type", + "schema": "public", + "values": [ + "tweet_create", + "tweet_reply", + "tweet_thread", + "discord_chat", + "discord_mod", + "discord_help", + "discord_welcome", + "telegram_chat", + "telegram_group", + "telegram_broadcast", + "slack_chat", + "slack_thread", + "slack_channel", + "slack_dm" + ] + }, + "public.twitter_mention_status": { + "name": "twitter_mention_status", + "schema": "public", + "values": [ + "pending", + "processed", + "skipped", + "failed", + "rate_limited" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 1626fb8..a979362 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -71,6 +71,20 @@ "when": 1732638464629, "tag": "0009_lowly_the_watchers", "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1732711309729, + "tag": "0010_fine_boom_boom", + "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1732744870531, + "tag": "0011_familiar_prism", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema/schema.ts b/src/db/schema/schema.ts index 9be570b..314fb98 100644 --- a/src/db/schema/schema.ts +++ b/src/db/schema/schema.ts @@ -359,6 +359,38 @@ export const telegramGroups = pgTable("telegram_groups", { .default(sql`now()`), }); +export const twitterMentionStatusEnum = pgEnum("twitter_mention_status", [ + "pending", + "processed", + "skipped", + "failed", + "rate_limited", +]); + +export const twitterMentions = pgTable("twitter_mentions", { + id: uuid("id").defaultRandom().primaryKey(), + tweetId: varchar("tweet_id", { length: 255 }).notNull().unique(), + authorId: varchar("author_id", { length: 255 }).notNull(), + authorUsername: varchar("author_username", { length: 255 }).notNull(), + characterId: uuid("character_id") + .notNull() + .references(() => characters.id, { onDelete: "cascade" }), + createdAt: timestamp("created_at").notNull(), + processedAt: timestamp("processed_at"), + status: twitterMentionStatusEnum("status").notNull().default("pending"), + skipReason: varchar("skip_reason", { length: 255 }), + responseTweetId: varchar("response_tweet_id", { length: 255 }), + isReply: boolean("is_reply").notNull().default(false), + isRetweet: boolean("is_retweet").notNull().default(false), + conversationId: varchar("conversation_id", { length: 255 }), + metrics: jsonb("metrics").$type<{ + likes?: number; + retweets?: number; + replies?: number; + views?: number; + }>(), +}); + export const quantumStates = pgTable("quantum_states", { id: uuid("id").defaultRandom().primaryKey(), timestamp: timestamp("timestamp").notNull().defaultNow(), @@ -372,6 +404,70 @@ export const quantumStates = pgTable("quantum_states", { .default(sql`now()`), }); +// Add to your schema file: +export const coinPriceHistoryEnum = pgEnum("coin_price_source", [ + "coingecko", + "binance", + "kraken", + "manual", +]); + +export const coins = pgTable("coins", { + id: uuid("id").defaultRandom().primaryKey(), + coingeckoId: varchar("coingecko_id", { length: 255 }).notNull().unique(), + symbol: varchar("symbol", { length: 50 }).notNull(), + name: varchar("name", { length: 255 }).notNull(), + currentPrice: numeric("current_price").notNull().default("0"), + priceChange24h: numeric("price_change_24h").notNull().default("0"), + priceChange7d: numeric("price_change_7d").notNull().default("0"), + platforms: jsonb("platforms").$type>().default({}), + metadata: jsonb("metadata") + .$type<{ + image?: string; + marketCap?: number; + rank?: number; + tags?: string[]; + isDelisted?: boolean; + lastChecked?: string; + }>() + .default({}), + twitterHandle: text(), + lastChecked: timestamp("last_checked") + .notNull() + .default(sql`now()`), + lastUpdated: timestamp("last_updated") + .notNull() + .default(sql`now()`), + createdAt: timestamp("created_at") + .notNull() + .default(sql`now()`), +}); + +export const coinPriceHistory = pgTable("coin_price_history", { + id: uuid("id").defaultRandom().primaryKey(), + coinId: uuid("coin_id") + .notNull() + .references(() => coins.id, { onDelete: "cascade" }), + price: numeric("price").notNull(), + timestamp: timestamp("timestamp").notNull(), + source: coinPriceHistoryEnum("source").notNull().default("coingecko"), + metadata: jsonb("metadata") + .$type<{ + volume?: number; + marketCap?: number; + additionalData?: Record; + }>() + .default({}), + createdAt: timestamp("created_at") + .notNull() + .default(sql`now()`), +}); + +export type Coin = typeof coins.$inferSelect; +export type NewCoin = typeof coins.$inferInsert; +export type CoinPriceHistory = typeof coinPriceHistory.$inferSelect; +export type NewCoinPriceHistory = typeof coinPriceHistory.$inferInsert; + // Export types export type Character = typeof characters.$inferSelect; export type NewCharacter = typeof characters.$inferInsert; diff --git a/src/types/style.ts b/src/types/style.ts index d5f4858..c78cc7a 100644 --- a/src/types/style.ts +++ b/src/types/style.ts @@ -27,7 +27,7 @@ export type ResponseType = export interface StyleSettings { enabled?: boolean; - tone: string[]; + tone?: string[]; guidelines: string[]; platform?: Platform; formatting: { @@ -40,7 +40,7 @@ export interface StyleSettings { export interface PlatformStylesInput { enabled: boolean; - defaultTone: string[]; + defaultTone?: string[]; defaultGuidelines: string[]; styles: { [key: string]: StyleSettings; @@ -49,7 +49,7 @@ export interface PlatformStylesInput { export interface PlatformStyles { enabled: boolean; - defaultTone: string[]; + defaultTone?: string[]; defaultGuidelines: string[]; styles: { [K in ResponseType]?: StyleSettings; @@ -58,8 +58,8 @@ export interface PlatformStyles { export interface ResponseStyles { default: { - tone: string[]; - personality: string[]; + tone?: string[]; + personality?: string[]; guidelines: string[]; }; platforms: {