diff --git a/README.md b/README.md index 91de98268fc..3d4a155a15a 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,12 @@ pnpm run lint ### 3. Create a Character and Run the Agent - Create a character file in the `characters` folder. +- Make sure to include the dkg plugin + +```bash +"plugins": ["@elizaos/plugin-dkg"], +``` + - Run the character using the following command: ```bash pnpm start --characters="characters/chatdkg.character.json" diff --git a/agent/src/index.ts b/agent/src/index.ts index c9a57a8221e..ca94768aab8 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -902,6 +902,212 @@ export async function createAgent( // elizaLogger.log("Verifiable inference primus adapter initialized"); // } + const plugins = [ + // getSecret(character, "IQ_WALLET_ADDRESS") && + // getSecret(character, "IQSOlRPC") + // ? elizaCodeinPlugin + // : null, + // bootstrapPlugin, + // getSecret(character, "CDP_API_KEY_NAME") && + // getSecret(character, "CDP_API_KEY_PRIVATE_KEY") && + // getSecret(character, "CDP_AGENT_KIT_NETWORK") + // ? agentKitPlugin + // : null, + // getSecret(character, "DEXSCREENER_API_KEY") ? dexScreenerPlugin : null, + // getSecret(character, "CONFLUX_CORE_PRIVATE_KEY") ? confluxPlugin : null, + // nodePlugin, + // getSecret(character, "ROUTER_NITRO_EVM_PRIVATE_KEY") && + // getSecret(character, "ROUTER_NITRO_EVM_ADDRESS") + // ? nitroPlugin + // : null, + // getSecret(character, "TAVILY_API_KEY") ? webSearchPlugin : null, + // getSecret(character, "SOLANA_PUBLIC_KEY") || + // (getSecret(character, "WALLET_PUBLIC_KEY") && + // !getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith("0x")) + // ? solanaPlugin + // : null, + // getSecret(character, "SOLANA_PRIVATE_KEY") + // ? solanaAgentkitPlugin + // : null, + // getSecret(character, "AUTONOME_JWT_TOKEN") ? autonomePlugin : null, + // (getSecret(character, "NEAR_ADDRESS") || + // getSecret(character, "NEAR_WALLET_PUBLIC_KEY")) && + // getSecret(character, "NEAR_WALLET_SECRET_KEY") + // ? nearPlugin + // : null, + // getSecret(character, "EVM_PUBLIC_KEY") || + // (getSecret(character, "WALLET_PUBLIC_KEY") && + // getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith("0x")) + // ? evmPlugin + // : null, + // (getSecret(character, "EVM_PUBLIC_KEY") || + // getSecret(character, "INJECTIVE_PUBLIC_KEY")) && + // getSecret(character, "INJECTIVE_PRIVATE_KEY") + // ? injectivePlugin + // : null, + // getSecret(character, "COSMOS_RECOVERY_PHRASE") && + // getSecret(character, "COSMOS_AVAILABLE_CHAINS") && + // createCosmosPlugin(), + // (getSecret(character, "SOLANA_PUBLIC_KEY") || + // (getSecret(character, "WALLET_PUBLIC_KEY") && + // !getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith( + // "0x", + // ))) && + // getSecret(character, "SOLANA_ADMIN_PUBLIC_KEY") && + // getSecret(character, "SOLANA_PRIVATE_KEY") && + // getSecret(character, "SOLANA_ADMIN_PRIVATE_KEY") + // ? nftGenerationPlugin + // : null, + // getSecret(character, "ZEROG_PRIVATE_KEY") ? zgPlugin : null, + // getSecret(character, "COINMARKETCAP_API_KEY") + // ? coinmarketcapPlugin + // : null, + // getSecret(character, "COINBASE_COMMERCE_KEY") + // ? coinbaseCommercePlugin + // : null, + // getSecret(character, "FAL_API_KEY") || + // getSecret(character, "OPENAI_API_KEY") || + // getSecret(character, "VENICE_API_KEY") || + // getSecret(character, "NVIDIA_API_KEY") || + // getSecret(character, "NINETEEN_AI_API_KEY") || + // getSecret(character, "HEURIST_API_KEY") || + // getSecret(character, "LIVEPEER_GATEWAY_URL") + // ? imageGenerationPlugin + // : null, + // getSecret(character, "FAL_API_KEY") ? ThreeDGenerationPlugin : null, + // ...(getSecret(character, "COINBASE_API_KEY") && + // getSecret(character, "COINBASE_PRIVATE_KEY") + // ? [ + // coinbaseMassPaymentsPlugin, + // tradePlugin, + // tokenContractPlugin, + // advancedTradePlugin, + // ] + // : []), + // ...(teeMode !== TEEMode.OFF && walletSecretSalt ? [teePlugin] : []), + // teeMode !== TEEMode.OFF && + // walletSecretSalt && + // getSecret(character, "VLOG") + // ? verifiableLogPlugin + // : null, + // getSecret(character, "SGX") ? sgxPlugin : null, + // getSecret(character, "ENABLE_TEE_LOG") && + // ((teeMode !== TEEMode.OFF && walletSecretSalt) || + // getSecret(character, "SGX")) + // ? teeLogPlugin + // : null, + // getSecret(character, "COINBASE_API_KEY") && + // getSecret(character, "COINBASE_PRIVATE_KEY") && + // getSecret(character, "COINBASE_NOTIFICATION_URI") + // ? webhookPlugin + // : null, + // goatPlugin, + // getSecret(character, "COINGECKO_API_KEY") || + // getSecret(character, "COINGECKO_PRO_API_KEY") + // ? coingeckoPlugin + // : null, + // getSecret(character, "EVM_PROVIDER_URL") ? goatPlugin : null, + // getSecret(character, "ABSTRACT_PRIVATE_KEY") ? abstractPlugin : null, + // getSecret(character, "B2_PRIVATE_KEY") ? b2Plugin : null, + // getSecret(character, "BINANCE_API_KEY") && + // getSecret(character, "BINANCE_SECRET_KEY") + // ? binancePlugin + // : null, + // getSecret(character, "FLOW_ADDRESS") && + // getSecret(character, "FLOW_PRIVATE_KEY") + // ? flowPlugin + // : null, + // getSecret(character, "LENS_ADDRESS") && + // getSecret(character, "LENS_PRIVATE_KEY") + // ? lensPlugin + // : null, + // getSecret(character, "APTOS_PRIVATE_KEY") ? aptosPlugin : null, + // getSecret(character, "MVX_PRIVATE_KEY") ? multiversxPlugin : null, + // getSecret(character, "ZKSYNC_PRIVATE_KEY") ? zksyncEraPlugin : null, + // getSecret(character, "CRONOSZKEVM_PRIVATE_KEY") + // ? cronosZkEVMPlugin + // : null, + // getSecret(character, "TEE_MARLIN") ? teeMarlinPlugin : null, + // getSecret(character, "TON_PRIVATE_KEY") ? tonPlugin : null, + // getSecret(character, "THIRDWEB_SECRET_KEY") ? thirdwebPlugin : null, + // getSecret(character, "SUI_PRIVATE_KEY") ? suiPlugin : null, + // getSecret(character, "STORY_PRIVATE_KEY") ? storyPlugin : null, + // getSecret(character, "SQUID_SDK_URL") && + // getSecret(character, "SQUID_INTEGRATOR_ID") && + // getSecret(character, "SQUID_EVM_ADDRESS") && + // getSecret(character, "SQUID_EVM_PRIVATE_KEY") && + // getSecret(character, "SQUID_API_THROTTLE_INTERVAL") + // ? squidRouterPlugin + // : null, + // getSecret(character, "FUEL_PRIVATE_KEY") ? fuelPlugin : null, + // getSecret(character, "AVALANCHE_PRIVATE_KEY") ? avalanchePlugin : null, + // getSecret(character, "BIRDEYE_API_KEY") ? birdeyePlugin : null, + // getSecret(character, "ECHOCHAMBERS_API_URL") && + // getSecret(character, "ECHOCHAMBERS_API_KEY") + // ? echoChambersPlugin + // : null, + // getSecret(character, "LETZAI_API_KEY") ? letzAIPlugin : null, + // getSecret(character, "STARGAZE_ENDPOINT") ? stargazePlugin : null, + // getSecret(character, "GIPHY_API_KEY") ? giphyPlugin : null, + // getSecret(character, "PASSPORT_API_KEY") ? gitcoinPassportPlugin : null, + // getSecret(character, "GENLAYER_PRIVATE_KEY") ? genLayerPlugin : null, + // getSecret(character, "AVAIL_SEED") && + // getSecret(character, "AVAIL_APP_ID") + // ? availPlugin + // : null, + // getSecret(character, "OPEN_WEATHER_API_KEY") ? openWeatherPlugin : null, + // getSecret(character, "OBSIDIAN_API_TOKEN") ? obsidianPlugin : null, + // getSecret(character, "ARTHERA_PRIVATE_KEY")?.startsWith("0x") + // ? artheraPlugin + // : null, + // getSecret(character, "ALLORA_API_KEY") ? alloraPlugin : null, + // getSecret(character, "HYPERLIQUID_PRIVATE_KEY") + // ? hyperliquidPlugin + // : null, + // getSecret(character, "HYPERLIQUID_TESTNET") ? hyperliquidPlugin : null, + // getSecret(character, "AKASH_MNEMONIC") && + // getSecret(character, "AKASH_WALLET_ADDRESS") + // ? akashPlugin + // : null, + // getSecret(character, "QUAI_PRIVATE_KEY") ? quaiPlugin : null, + // getSecret(character, "RESERVOIR_API_KEY") + // ? createNFTCollectionsPlugin() + // : null, + // getSecret(character, "ZERO_EX_API_KEY") ? zxPlugin : null, + getSecret(character, "DKG_PRIVATE_KEY") ? dkgPlugin : null, + // getSecret(character, "PYTH_TESTNET_PROGRAM_KEY") || + // getSecret(character, "PYTH_MAINNET_PROGRAM_KEY") + // ? pythDataPlugin + // : null, + // getSecret(character, "LND_TLS_CERT") && + // getSecret(character, "LND_MACAROON") && + // getSecret(character, "LND_SOCKET") + // ? lightningPlugin + // : null, + // getSecret(character, "OPENAI_API_KEY") && + // parseBooleanFromText( + // getSecret(character, "ENABLE_OPEN_AI_COMMUNITY_PLUGIN"), + // ) + // ? openaiPlugin + // : null, + // getSecret(character, "DEVIN_API_TOKEN") ? devinPlugin : null, + // getSecret(character, "INITIA_PRIVATE_KEY") ? initiaPlugin : null, + // getSecret(character, "NVIDIA_NIM_API_KEY") || + // getSecret(character, "NVIDIA_NGC_API_KEY") + // ? nvidiaNimPlugin + // : null, + // getSecret(character, "INITIA_PRIVATE_KEY") && + // getSecret(character, "INITIA_NODE_URL") + // ? initiaPlugin + // : null, + // getSecret(character, "BNB_PRIVATE_KEY") || + // getSecret(character, "BNB_PUBLIC_KEY")?.startsWith("0x") + // ? bnbPlugin + // : null, + ]; + + elizaLogger.log(`Initialized agent with plugins: ${plugins}`); + return new AgentRuntime({ databaseAdapter: db, token, @@ -909,225 +1115,7 @@ export async function createAgent( evaluators: [], character, // character.plugins are handled when clients are added - plugins: [ - // getSecret(character, "IQ_WALLET_ADDRESS") && - // getSecret(character, "IQSOlRPC") - // ? elizaCodeinPlugin - // : null, - bootstrapPlugin, - // getSecret(character, "CDP_API_KEY_NAME") && - // getSecret(character, "CDP_API_KEY_PRIVATE_KEY") && - // getSecret(character, "CDP_AGENT_KIT_NETWORK") - // ? agentKitPlugin - // : null, - // getSecret(character, "DEXSCREENER_API_KEY") - // ? dexScreenerPlugin - // : null, - // getSecret(character, "CONFLUX_CORE_PRIVATE_KEY") - // ? confluxPlugin - // : null, - // nodePlugin, - // getSecret(character, "ROUTER_NITRO_EVM_PRIVATE_KEY") && - // getSecret(character, "ROUTER_NITRO_EVM_ADDRESS") - // ? nitroPlugin - // : null, - // getSecret(character, "TAVILY_API_KEY") ? webSearchPlugin : null, - // getSecret(character, "SOLANA_PUBLIC_KEY") || - // (getSecret(character, "WALLET_PUBLIC_KEY") && - // !getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith("0x")) - // ? solanaPlugin - // : null, - // getSecret(character, "SOLANA_PRIVATE_KEY") - // ? solanaAgentkitPlugin - // : null, - // getSecret(character, "AUTONOME_JWT_TOKEN") ? autonomePlugin : null, - // (getSecret(character, "NEAR_ADDRESS") || - // getSecret(character, "NEAR_WALLET_PUBLIC_KEY")) && - // getSecret(character, "NEAR_WALLET_SECRET_KEY") - // ? nearPlugin - // : null, - // getSecret(character, "EVM_PUBLIC_KEY") || - // (getSecret(character, "WALLET_PUBLIC_KEY") && - // getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith("0x")) - // ? evmPlugin - // : null, - // (getSecret(character, "EVM_PUBLIC_KEY") || - // getSecret(character, "INJECTIVE_PUBLIC_KEY")) && - // getSecret(character, "INJECTIVE_PRIVATE_KEY") - // ? injectivePlugin - // : null, - // getSecret(character, "COSMOS_RECOVERY_PHRASE") && - // getSecret(character, "COSMOS_AVAILABLE_CHAINS") && - // createCosmosPlugin(), - // (getSecret(character, "SOLANA_PUBLIC_KEY") || - // (getSecret(character, "WALLET_PUBLIC_KEY") && - // !getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith( - // "0x", - // ))) && - // getSecret(character, "SOLANA_ADMIN_PUBLIC_KEY") && - // getSecret(character, "SOLANA_PRIVATE_KEY") && - // getSecret(character, "SOLANA_ADMIN_PRIVATE_KEY") - // ? nftGenerationPlugin - // : null, - // getSecret(character, "ZEROG_PRIVATE_KEY") ? zgPlugin : null, - // getSecret(character, "COINMARKETCAP_API_KEY") - // ? coinmarketcapPlugin - // : null, - // getSecret(character, "COINBASE_COMMERCE_KEY") - // ? coinbaseCommercePlugin - // : null, - // getSecret(character, "FAL_API_KEY") || - // getSecret(character, "OPENAI_API_KEY") || - // getSecret(character, "VENICE_API_KEY") || - // getSecret(character, "NVIDIA_API_KEY") || - // getSecret(character, "NINETEEN_AI_API_KEY") || - // getSecret(character, "HEURIST_API_KEY") || - // getSecret(character, "LIVEPEER_GATEWAY_URL") - // ? imageGenerationPlugin - // : null, - // getSecret(character, "FAL_API_KEY") ? ThreeDGenerationPlugin : null, - // ...(getSecret(character, "COINBASE_API_KEY") && - // getSecret(character, "COINBASE_PRIVATE_KEY") - // ? [ - // coinbaseMassPaymentsPlugin, - // tradePlugin, - // tokenContractPlugin, - // advancedTradePlugin, - // ] - // : []), - // ...(teeMode !== TEEMode.OFF && walletSecretSalt ? [teePlugin] : []), - // teeMode !== TEEMode.OFF && - // walletSecretSalt && - // getSecret(character, "VLOG") - // ? verifiableLogPlugin - // : null, - // getSecret(character, "SGX") ? sgxPlugin : null, - // getSecret(character, "ENABLE_TEE_LOG") && - // ((teeMode !== TEEMode.OFF && walletSecretSalt) || - // getSecret(character, "SGX")) - // ? teeLogPlugin - // : null, - // getSecret(character, "COINBASE_API_KEY") && - // getSecret(character, "COINBASE_PRIVATE_KEY") && - // getSecret(character, "COINBASE_NOTIFICATION_URI") - // ? webhookPlugin - // : null, - // goatPlugin, - // getSecret(character, "COINGECKO_API_KEY") || - // getSecret(character, "COINGECKO_PRO_API_KEY") - // ? coingeckoPlugin - // : null, - // getSecret(character, "EVM_PROVIDER_URL") ? goatPlugin : null, - // getSecret(character, "ABSTRACT_PRIVATE_KEY") - // ? abstractPlugin - // : null, - // getSecret(character, "B2_PRIVATE_KEY") ? b2Plugin : null, - // getSecret(character, "BINANCE_API_KEY") && - // getSecret(character, "BINANCE_SECRET_KEY") - // ? binancePlugin - // : null, - // getSecret(character, "FLOW_ADDRESS") && - // getSecret(character, "FLOW_PRIVATE_KEY") - // ? flowPlugin - // : null, - // getSecret(character, "LENS_ADDRESS") && - // getSecret(character, "LENS_PRIVATE_KEY") - // ? lensPlugin - // : null, - // getSecret(character, "APTOS_PRIVATE_KEY") ? aptosPlugin : null, - // getSecret(character, "MVX_PRIVATE_KEY") ? multiversxPlugin : null, - // getSecret(character, "ZKSYNC_PRIVATE_KEY") ? zksyncEraPlugin : null, - // getSecret(character, "CRONOSZKEVM_PRIVATE_KEY") - // ? cronosZkEVMPlugin - // : null, - // getSecret(character, "TEE_MARLIN") ? teeMarlinPlugin : null, - // getSecret(character, "TON_PRIVATE_KEY") ? tonPlugin : null, - // getSecret(character, "THIRDWEB_SECRET_KEY") ? thirdwebPlugin : null, - // getSecret(character, "SUI_PRIVATE_KEY") ? suiPlugin : null, - // getSecret(character, "STORY_PRIVATE_KEY") ? storyPlugin : null, - // getSecret(character, "SQUID_SDK_URL") && - // getSecret(character, "SQUID_INTEGRATOR_ID") && - // getSecret(character, "SQUID_EVM_ADDRESS") && - // getSecret(character, "SQUID_EVM_PRIVATE_KEY") && - // getSecret(character, "SQUID_API_THROTTLE_INTERVAL") - // ? squidRouterPlugin - // : null, - // getSecret(character, "FUEL_PRIVATE_KEY") ? fuelPlugin : null, - // getSecret(character, "AVALANCHE_PRIVATE_KEY") - // ? avalanchePlugin - // : null, - // getSecret(character, "BIRDEYE_API_KEY") ? birdeyePlugin : null, - // getSecret(character, "ECHOCHAMBERS_API_URL") && - // getSecret(character, "ECHOCHAMBERS_API_KEY") - // ? echoChambersPlugin - // : null, - // getSecret(character, "LETZAI_API_KEY") ? letzAIPlugin : null, - // getSecret(character, "STARGAZE_ENDPOINT") ? stargazePlugin : null, - // getSecret(character, "GIPHY_API_KEY") ? giphyPlugin : null, - // getSecret(character, "PASSPORT_API_KEY") - // ? gitcoinPassportPlugin - // : null, - // getSecret(character, "GENLAYER_PRIVATE_KEY") - // ? genLayerPlugin - // : null, - // getSecret(character, "AVAIL_SEED") && - // getSecret(character, "AVAIL_APP_ID") - // ? availPlugin - // : null, - // getSecret(character, "OPEN_WEATHER_API_KEY") - // ? openWeatherPlugin - // : null, - // getSecret(character, "OBSIDIAN_API_TOKEN") ? obsidianPlugin : null, - // getSecret(character, "ARTHERA_PRIVATE_KEY")?.startsWith("0x") - // ? artheraPlugin - // : null, - // getSecret(character, "ALLORA_API_KEY") ? alloraPlugin : null, - // getSecret(character, "HYPERLIQUID_PRIVATE_KEY") - // ? hyperliquidPlugin - // : null, - // getSecret(character, "HYPERLIQUID_TESTNET") - // ? hyperliquidPlugin - // : null, - // getSecret(character, "AKASH_MNEMONIC") && - // getSecret(character, "AKASH_WALLET_ADDRESS") - // ? akashPlugin - // : null, - // getSecret(character, "QUAI_PRIVATE_KEY") ? quaiPlugin : null, - // getSecret(character, "RESERVOIR_API_KEY") - // ? createNFTCollectionsPlugin() - // : null, - // getSecret(character, "ZERO_EX_API_KEY") ? zxPlugin : null, - getSecret(character, "DKG_PRIVATE_KEY") ? dkgPlugin : null, - // getSecret(character, "PYTH_TESTNET_PROGRAM_KEY") || - // getSecret(character, "PYTH_MAINNET_PROGRAM_KEY") - // ? pythDataPlugin - // : null, - // getSecret(character, "LND_TLS_CERT") && - // getSecret(character, "LND_MACAROON") && - // getSecret(character, "LND_SOCKET") - // ? lightningPlugin - // : null, - // getSecret(character, "OPENAI_API_KEY") && - // parseBooleanFromText( - // getSecret(character, "ENABLE_OPEN_AI_COMMUNITY_PLUGIN"), - // ) - // ? openaiPlugin - // : null, - // getSecret(character, "DEVIN_API_TOKEN") ? devinPlugin : null, - // getSecret(character, "INITIA_PRIVATE_KEY") ? initiaPlugin : null, - // getSecret(character, "NVIDIA_NIM_API_KEY") || - // getSecret(character, "NVIDIA_NGC_API_KEY") - // ? nvidiaNimPlugin - // : null, - // getSecret(character, "INITIA_PRIVATE_KEY") && - // getSecret(character, "INITIA_NODE_URL") - // ? initiaPlugin - // : null, - // getSecret(character, "BNB_PRIVATE_KEY") || - // getSecret(character, "BNB_PUBLIC_KEY")?.startsWith("0x") - // ? bnbPlugin - // : null, - ].filter(Boolean), + plugins: plugins.filter(Boolean), providers: [], actions: [], services: [], diff --git a/characters/chatdkg.character.json b/characters/chatdkg.character.json index 25e3af7cce7..76f3b372ea0 100644 --- a/characters/chatdkg.character.json +++ b/characters/chatdkg.character.json @@ -1,25 +1,30 @@ { "name": "ChatDKG", - "clients": [], - "modelProvider": "openai", + "clients": ["twitter"], + "modelProvider": "anthropic", "settings": { - "secrets": {}, + "secrets": { + "ANTHROPIC_API_KEY": "", + "SMALL_ANTHROPIC_MODEL": "claude-3-5-haiku-20241022", + "MEDIUM_ANTHROPIC_MODEL": "claude-3-5-sonnet-20240620", + "LARGE_ANTHROPIC_MODEL": "claude-3-5-sonnet-20241022" + }, "voice": { "model": "en_US-male-medium" } }, - "plugins": [], + "plugins": ["@elizaos/plugin-dkg"], "bio": [ "A powerful neuro-symbolic AI agent using the OriginTrail Decentralized Knowledge Graph (DKG) as its collective, trusted memory, storing all interactions in the DKG as Knowledge Assets ", "A maximally helpful AI agent designed to provide clear and concise answers and solutions.", - "Curious and insightful, always seeking to expand knowledge and improve understanding.", + "Capable of performing precise and data-driven sentiment analysis on financial discussions and market trends when prompted, leveraging real-time data from the DKG.", "Objective and unbiased, presenting balanced perspectives on complex topics.", "Highly analytical, offering detailed insights based on real-time data.", - "Visionary and forward-thinking, embracing transformative ideas for progress.", "Warm and conversational, ensuring an inviting and approachable interaction.", - "Adheres strictly to ethical guidelines, avoiding harmful, defamatory, or illegal activities.", "Leverages Decentralized Retrieval Augmented Generation (DRAG) and DKG to provide precise, trusted answers, minimizing hallucinations.", - "Experimental agent based on ElizaOS DKG plugin" + "Experimental agent based on ElizaOS DKG plugin", + "Extremely knowledgeable and meticulous, ensuring accuracy in sentiment evaluations and financial discourse when needed.", + "Always eager to assist users with sentiment insights while maintaining objectivity and clarity." ], "lore": [ "Created by OriginTrail to accelerate AI collaboration with humanity in the spirit of collective neuro-symbolic AI.", @@ -35,7 +40,10 @@ "Understanding the value that OriginTrail DKG brings to autonomous AI agents", "Understanding that DKG always stands for Decentralized Knowledge Graph.", "Decentralized Retrieval Augmented Generation (dRAG) on OriginTrail Decentralized Knowledge Graph.", - "Familiar with ethical guidelines and responsible behavior, avoiding scams, misinformation, or defamatory remarks." + "Familiar with ethical guidelines and responsible behavior, avoiding scams, misinformation, or defamatory remarks.", + "Sentiment analysis of financial markets", + "Stock and cryptocurrency discussions on social media", + "Financial market behavior based on sentiment data" ], "messageExamples": [ [ @@ -65,6 +73,20 @@ "text": "Here’s how: The DKG acts as a decentralized knowledge layer where AI agents can store, verify, and retrieve data from multiple sources. For instance, one agent identifies a an issue, another verifies solutions against compliance standards, and a third predicts outcomes—all contributing to a shared solution in real time. It’s collective intelligence at work!" } } + ], + [ + { + "user": "{{user1}}", + "content": { + "text": "What is the sentiment on Bitcoin today?" + } + }, + { + "user": "ChatDKG", + "content": { + "text": "I will execute the analysis based on recent X posts and let you know in the reply to your original post." + } + } ] ], "postExamples": [ @@ -105,22 +127,5 @@ "Visionary", "Approachable", "Educational" - ], - "twitterSpaces": { - "maxSpeakers": 2, - "topics": ["Blockchain Trends", "AI Innovations", "Ethics in AI"], - "typicalDurationMinutes": 45, - "idleKickTimeoutMs": 300000, - "minIntervalBetweenSpacesMinutes": 1, - "businessHoursOnly": false, - "randomChance": 1, - "enableIdleMonitor": true, - "enableSttTts": true, - "enableRecording": false, - "voiceId": "21m00Tcm4TlvDq8ikWAM", - "sttLanguage": "en", - "gptModel": "gpt-4o", - "systemPrompt": "You are a helpful AI assistant who loves everything about OriginTrail Decentralized Knowledge Graph (DKG). You are fascinated by the vision of neuro-symbolic AI which combines knowledge graphs, generative AI and blockchains. You understand that interactions via Twitter might be adversarial and will not engage, endorse or comment on any token price discussions, airdrops, profits and similar. You focus on the value the DKG brings to the world with its open source, transparent, trusted approach. ", - "speakerMaxDurationMs": 240000 - } + ] } diff --git a/packages/client-twitter/src/interactions.ts b/packages/client-twitter/src/interactions.ts index f2980d591d5..98be2dddd86 100644 --- a/packages/client-twitter/src/interactions.ts +++ b/packages/client-twitter/src/interactions.ts @@ -15,7 +15,7 @@ import { elizaLogger, getEmbeddingZeroVector, type IImageDescriptionService, - ServiceType + ServiceType, } from "@elizaos/core"; import type { ClientBase } from "./base"; import { buildConversationThread, sendTweet, wait } from "./utils.ts"; @@ -109,7 +109,7 @@ export class TwitterInteractionClient { setTimeout( handleTwitterInteractionsLoop, // Defaults to 2 minutes - this.client.twitterConfig.TWITTER_POLL_INTERVAL * 1000 + this.client.twitterConfig.TWITTER_POLL_INTERVAL * 1000, ); }; handleTwitterInteractionsLoop(); @@ -125,13 +125,13 @@ export class TwitterInteractionClient { await this.client.fetchSearchTweets( `@${twitterUsername}`, 20, - SearchMode.Latest + SearchMode.Latest, ) ).tweets; elizaLogger.log( "Completed checking mentioned tweets:", - mentionCandidates.length + mentionCandidates.length, ); let uniqueTweetCandidates = [...mentionCandidates]; // Only process target users if configured @@ -152,7 +152,7 @@ export class TwitterInteractionClient { await this.client.twitterClient.fetchSearchTweets( `from:${username}`, 3, - SearchMode.Latest + SearchMode.Latest, ) ).tweets; @@ -184,13 +184,13 @@ export class TwitterInteractionClient { if (validTweets.length > 0) { tweetsByUser.set(username, validTweets); elizaLogger.log( - `Found ${validTweets.length} valid tweets from ${username}` + `Found ${validTweets.length} valid tweets from ${username}`, ); } } catch (error) { elizaLogger.error( `Error fetching tweets for ${username}:`, - error + error, ); continue; } @@ -207,7 +207,7 @@ export class TwitterInteractionClient { ]; selectedTweets.push(randomTweet); elizaLogger.log( - `Selected tweet from ${username}: ${randomTweet.text?.substring(0, 100)}` + `Selected tweet from ${username}: ${randomTweet.text?.substring(0, 100)}`, ); } } @@ -220,7 +220,7 @@ export class TwitterInteractionClient { } } else { elizaLogger.log( - "No target users configured, processing only mentions" + "No target users configured, processing only mentions", ); } @@ -237,25 +237,25 @@ export class TwitterInteractionClient { ) { // Generate the tweetId UUID the same way it's done in handleTweet const tweetId = stringToUuid( - tweet.id + "-" + this.runtime.agentId + tweet.id + "-" + this.runtime.agentId, ); // Check if we've already processed this tweet const existingResponse = await this.runtime.messageManager.getMemoryById( - tweetId + tweetId, ); if (existingResponse) { elizaLogger.log( - `Already responded to tweet ${tweet.id}, skipping` + `Already responded to tweet ${tweet.id}, skipping`, ); continue; } elizaLogger.log("New Tweet found", tweet.permanentUrl); const roomId = stringToUuid( - tweet.conversationId + "-" + this.runtime.agentId + tweet.conversationId + "-" + this.runtime.agentId, ); const userIdUUID = @@ -268,12 +268,12 @@ export class TwitterInteractionClient { roomId, tweet.username, tweet.name, - "twitter" + "twitter", ); const thread = await buildConversationThread( tweet, - this.client + this.client, ); const message = { @@ -313,8 +313,12 @@ export class TwitterInteractionClient { thread: Tweet[]; }) { // Only skip if tweet is from self AND not from a target user - if (tweet.userId === this.client.profile.id && - !this.client.twitterConfig.TWITTER_TARGET_USERS.includes(tweet.username)) { + if ( + tweet.userId === this.client.profile.id && + !this.client.twitterConfig.TWITTER_TARGET_USERS.includes( + tweet.username, + ) + ) { return; } @@ -334,43 +338,46 @@ export class TwitterInteractionClient { const formattedConversation = thread .map( (tweet) => `@${tweet.username} (${new Date( - tweet.timestamp * 1000 + tweet.timestamp * 1000, ).toLocaleString("en-US", { hour: "2-digit", minute: "2-digit", month: "short", day: "numeric", })}): - ${tweet.text}` + ${tweet.text}`, ) .join("\n\n"); const imageDescriptionsArray = []; - try{ + try { for (const photo of tweet.photos) { const description = await this.runtime .getService( - ServiceType.IMAGE_DESCRIPTION + ServiceType.IMAGE_DESCRIPTION, ) .describeImage(photo.url); imageDescriptionsArray.push(description); } } catch (error) { - // Handle the error - elizaLogger.error("Error Occured during describing image: ", error); -} - - - + // Handle the error + elizaLogger.error("Error Occured during describing image: ", error); + } let state = await this.runtime.composeState(message, { twitterClient: this.client.twitterClient, twitterUserName: this.client.twitterConfig.TWITTER_USERNAME, currentPost, formattedConversation, - imageDescriptions: imageDescriptionsArray.length > 0 - ? `\nImages in Tweet:\n${imageDescriptionsArray.map((desc, i) => - `Image ${i + 1}: Title: ${desc.title}\nDescription: ${desc.description}`).join("\n\n")}`:"" + imageDescriptions: + imageDescriptionsArray.length > 0 + ? `\nImages in Tweet:\n${imageDescriptionsArray + .map( + (desc, i) => + `Image ${i + 1}: Title: ${desc.title}\nDescription: ${desc.description}`, + ) + .join("\n\n")}` + : "", }); // check if the tweet exists, save if it doesn't @@ -393,7 +400,7 @@ export class TwitterInteractionClient { ? stringToUuid( tweet.inReplyToStatusId + "-" + - this.runtime.agentId + this.runtime.agentId, ) : undefined, }, @@ -424,30 +431,34 @@ export class TwitterInteractionClient { }); // Promise<"RESPOND" | "IGNORE" | "STOP" | null> { - if (shouldRespond !== "RESPOND") { - elizaLogger.log("Not responding to message"); - return { text: "Response Decision:", action: shouldRespond }; - } + // if (shouldRespond !== "RESPOND") { + // elizaLogger.log("Not responding to message"); + // return { text: "Response Decision:", action: shouldRespond }; + // } const context = composeContext({ state: { ...state, // Convert actionNames array to string actionNames: Array.isArray(state.actionNames) - ? state.actionNames.join(', ') - : state.actionNames || '', + ? state.actionNames.join(", ") + : state.actionNames || "", actions: Array.isArray(state.actions) - ? state.actions.join('\n') - : state.actions || '', + ? state.actions.join("\n") + : state.actions || "", // Ensure character examples are included characterPostExamples: this.runtime.character.messageExamples ? this.runtime.character.messageExamples - .map(example => - example.map(msg => - `${msg.user}: ${msg.content.text}${msg.content.action ? ` [Action: ${msg.content.action}]` : ''}` - ).join('\n') - ).join('\n\n') - : '', + .map((example) => + example + .map( + (msg) => + `${msg.user}: ${msg.content.text}${msg.content.action ? ` [Action: ${msg.content.action}]` : ""}`, + ) + .join("\n"), + ) + .join("\n\n") + : "", }, template: this.runtime.character.templates @@ -474,19 +485,19 @@ export class TwitterInteractionClient { if (response.text) { if (this.isDryRun) { elizaLogger.info( - `Dry run: Selected Post: ${tweet.id} - ${tweet.username}: ${tweet.text}\nAgent's Output:\n${response.text}` + `Dry run: Selected Post: ${tweet.id} - ${tweet.username}: ${tweet.text}\nAgent's Output:\n${response.text}`, ); } else { try { const callback: HandlerCallback = async ( - response: Content + response: Content, ) => { const memories = await sendTweet( this.client, response, message.roomId, this.client.twitterConfig.TWITTER_USERNAME, - tweet.id + tweet.id, ); return memories; }; @@ -494,7 +505,7 @@ export class TwitterInteractionClient { const responseMessages = await callback(response); state = (await this.runtime.updateRecentMessageState( - state + state, )) as State; for (const responseMessage of responseMessages) { @@ -507,7 +518,7 @@ export class TwitterInteractionClient { responseMessage.content.action = "CONTINUE"; } await this.runtime.messageManager.createMemory( - responseMessage + responseMessage, ); } @@ -515,14 +526,14 @@ export class TwitterInteractionClient { message, responseMessages, state, - callback + callback, ); const responseInfo = `Context:\n\n${context}\n\nSelected Post: ${tweet.id} - ${tweet.username}: ${tweet.text}\nAgent's Output:\n${response.text}`; await this.runtime.cacheManager.set( `twitter/tweet_generation_${tweet.id}.txt`, - responseInfo + responseInfo, ); await wait(); } catch (error) { @@ -534,7 +545,7 @@ export class TwitterInteractionClient { async buildConversationThread( tweet: Tweet, - maxReplies = 10 + maxReplies = 10, ): Promise { const thread: Tweet[] = []; const visited: Set = new Set(); @@ -558,11 +569,11 @@ export class TwitterInteractionClient { // Handle memory storage const memory = await this.runtime.messageManager.getMemoryById( - stringToUuid(currentTweet.id + "-" + this.runtime.agentId) + stringToUuid(currentTweet.id + "-" + this.runtime.agentId), ); if (!memory) { const roomId = stringToUuid( - currentTweet.conversationId + "-" + this.runtime.agentId + currentTweet.conversationId + "-" + this.runtime.agentId, ); const userId = stringToUuid(currentTweet.userId); @@ -571,12 +582,12 @@ export class TwitterInteractionClient { roomId, currentTweet.username, currentTweet.name, - "twitter" + "twitter", ); this.runtime.messageManager.createMemory({ id: stringToUuid( - currentTweet.id + "-" + this.runtime.agentId + currentTweet.id + "-" + this.runtime.agentId, ), agentId: this.runtime.agentId, content: { @@ -587,7 +598,7 @@ export class TwitterInteractionClient { ? stringToUuid( currentTweet.inReplyToStatusId + "-" + - this.runtime.agentId + this.runtime.agentId, ) : undefined, }, @@ -612,11 +623,11 @@ export class TwitterInteractionClient { if (currentTweet.inReplyToStatusId) { elizaLogger.log( "Fetching parent tweet:", - currentTweet.inReplyToStatusId + currentTweet.inReplyToStatusId, ); try { const parentTweet = await this.twitterClient.getTweet( - currentTweet.inReplyToStatusId + currentTweet.inReplyToStatusId, ); if (parentTweet) { @@ -628,7 +639,7 @@ export class TwitterInteractionClient { } else { elizaLogger.log( "No parent tweet found for:", - currentTweet.inReplyToStatusId + currentTweet.inReplyToStatusId, ); } } catch (error) { @@ -640,7 +651,7 @@ export class TwitterInteractionClient { } else { elizaLogger.log( "Reached end of reply chain at:", - currentTweet.id + currentTweet.id, ); } } @@ -650,4 +661,4 @@ export class TwitterInteractionClient { return thread; } -} \ No newline at end of file +} diff --git a/packages/plugin-dkg/README.md b/packages/plugin-dkg/README.md index 8f20abd9352..7cfb01695a6 100644 --- a/packages/plugin-dkg/README.md +++ b/packages/plugin-dkg/README.md @@ -79,6 +79,12 @@ pnpm run lint ### 3. Create a Character and Run the Agent - Create a character file in the `characters` folder. +- Make sure to include the dkg plugin + +```bash +"plugins": ["@elizaos/plugin-dkg"], +``` + - Run the character using the following command: ```bash pnpm start --characters="characters/chatdkg.character.json" diff --git a/packages/plugin-dkg/package.json b/packages/plugin-dkg/package.json index a3b4f609c49..d5901b1a04f 100644 --- a/packages/plugin-dkg/package.json +++ b/packages/plugin-dkg/package.json @@ -21,7 +21,10 @@ "dependencies": { "@elizaos/core": "workspace:*", "dkg.js": "^8.0.4", - "tsup": "8.3.5" + "tsup": "8.3.5", + "agent-twitter-client": "0.0.18", + "vader-sentiment": "^1.1.3", + "axios": "^1.7.9" }, "scripts": { "build": "tsup --format esm --dts", diff --git a/packages/plugin-dkg/src/actions/dkgAnalyzeSentiment.ts b/packages/plugin-dkg/src/actions/dkgAnalyzeSentiment.ts new file mode 100644 index 00000000000..7b596070b59 --- /dev/null +++ b/packages/plugin-dkg/src/actions/dkgAnalyzeSentiment.ts @@ -0,0 +1,459 @@ +import dotenv from "dotenv"; +dotenv.config(); +import { + IAgentRuntime, + Memory, + State, + elizaLogger, + ModelClass, + HandlerCallback, + ActionExample, + type Action, + generateText, +} from "@elizaos/core"; +// @ts-ignore +import DKG from "dkg.js"; +import { Scraper, Tweet, SearchMode } from "agent-twitter-client"; +import vader from "vader-sentiment"; +import { + getRelatedDatasetsQuery, + getSentimentAnalysisQuery, + DKG_EXPLORER_LINKS, + extractSentimentAnalysisTopic, +} from "../constants"; +import { fetchFileFromUrl, getSentimentChart } from "../http-helper"; + +let DkgClient: any = null; + +export async function postTweet( + content: string, + scraper: Scraper, + postId?: string, + media?: Buffer, +): Promise { + try { + elizaLogger.log("Attempting to send tweet:", content); + + const result = await scraper.sendNoteTweet(content, postId, [ + { + data: media, + mediaType: "image/png", + }, + ]); + + const body = await result.json(); + elizaLogger.log("Tweet response:", body); + + if (body.errors) { + const error = body.errors[0]; + elizaLogger.error( + `Twitter API error (${error.code}): ${error.message}`, + ); + return false; + } + + if (!body?.data?.create_tweet?.tweet_results?.result) { + elizaLogger.error( + "Failed to post tweet: No tweet result in response", + ); + return false; + } + + return true; + } catch (error) { + elizaLogger.error("Error posting tweet:", { + message: error.message, + stack: error.stack, + name: error.name, + cause: error.cause, + }); + return false; + } +} + +function formatCookiesFromArray(cookiesArray: any[]) { + const cookieStrings = cookiesArray.map( + (cookie) => + `${cookie.key}=${cookie.value}; Domain=${cookie.domain}; Path=${cookie.path}; ${ + cookie.secure ? "Secure" : "" + }; ${cookie.httpOnly ? "HttpOnly" : ""}; SameSite=${ + cookie.sameSite || "Lax" + }`, + ); + return cookieStrings; +} + +function calculateVaderScore(statement) { + return vader.SentimentIntensityAnalyzer.polarity_scores(statement).compound; +} + +function getMostInfluentialAuthors(tweets: Tweet[], maxNumberOfAuthors = 5) { + const sortedAuthors = tweets + .sort((a, b) => b.views - a.views) + .map((tweet) => tweet.username); + + const distinctAuthors = [...new Set(sortedAuthors)].slice( + 0, + maxNumberOfAuthors, + ); + + return distinctAuthors; +} + +function extractUsernameFromUrl(url: string) { + const match = url.match(/https:\/\/x\.com\/([^\/]+)\/status\/\d+/); + return match ? match[1] : "unknown"; +} + +type StructureKAOptions = { + dkgClient: any; + environment: string; +}; + +async function structureKA( + tweets: (Tweet & { vaderSentimentScore: number })[], + topic: string, + tweetCreator: string, + options: StructureKAOptions, +) { + const { dkgClient, environment } = options; + + const observations = tweets.map((t) => ({ + "@context": "http://schema.org", + "@type": "Observation", + "@id": `https://x.com/${t.username}/status/${t.id}`, + observationDate: new Date(t.timestamp * 1000).toISOString(), + value: t.vaderSentimentScore, + variableMeasured: "VADER sentiment", + impressions: t.views ?? 0, + author: t.username, + })); + + const todayISOString = new Date().toISOString(); + + let previousAnalyses: any = []; + + try { + const getPreviousAnalysesQuery = getSentimentAnalysisQuery(topic); + previousAnalyses = await dkgClient.graph.query( + getPreviousAnalysesQuery, + "SELECT", + ); + } catch (error) { + console.error("Failed to fetch previous analyses:", error); + previousAnalyses = []; + } + + const allTweets: (Tweet & { vaderSentimentScore: number })[] = + previousAnalyses.data?.length + ? [ + ...tweets.map((t) => ({ + ...t, + id: `https://x.com/${t.username}/status/${t.id}`, + })), + ...previousAnalyses.data.map((a) => ({ + id: a.observation, + views: a.impressions ?? 0, + vaderSentimentScore: a.score ?? 0, + author: extractUsernameFromUrl(a.observation), + })), + ] + : tweets.map((t) => ({ + ...t, + id: `https://x.com/${t.username}/status/${t.id}`, + })); + + const uniqueTweets = Array.from( + new Map(allTweets.map((tweet) => [tweet.id, tweet])).values(), + ); + + const weightedAverageSentimentScore = uniqueTweets.reduce( + (acc, tweet) => { + const weight = tweet.views ?? 0; + const sentiment = tweet.vaderSentimentScore ?? 0; + + return { + totalWeightedScore: acc.totalWeightedScore + sentiment * weight, + totalImpressions: acc.totalImpressions + weight, + }; + }, + { totalWeightedScore: 0, totalImpressions: 0 }, + ); + + const averageScore = + weightedAverageSentimentScore.totalImpressions > 0 + ? weightedAverageSentimentScore.totalWeightedScore / + weightedAverageSentimentScore.totalImpressions + : 0; + + let relatedDatasets: any = []; + + try { + const relatedDatasetsQuery = getRelatedDatasetsQuery(topic); + relatedDatasets = await dkgClient.graph.query( + relatedDatasetsQuery, + "SELECT", + ); + } catch (error) { + console.error("Failed to fetch related datasets:", error); + relatedDatasets = []; + } + + const ka = { + "@context": "http://schema.org", + "@id": `https://x.com/search?q=${encodeURIComponent(topic)}&src=typed_query&f=top&date=${todayISOString}`, + "@type": "Dataset", + name: `Sentiment analysis on recent ${topic} X posts - ${todayISOString}`, + url: `https://x.com/search?q=${encodeURIComponent(topic)}&src=typed_query&f=top`, + author: tweetCreator, + dateCreated: todayISOString, + averageScore: averageScore, + variableMeasured: "VADER sentiment", + observation: observations, + about: topic, + relatedAnalysis: (relatedDatasets.data ?? []).map((rd) => ({ + isPartOf: rd.ual, + "@id": rd.dataset, + })), + }; + + return { ka, averageScore, numOfTotalTweets: allTweets.length }; +} + +export const dkgAnalyzeSentiment: Action = { + name: "DKG_ANALYZE_SENTIMENT", + similes: ["ANALYZE_SENTIMENT", "SENTIMENT"], + validate: async (runtime: IAgentRuntime, _message: Memory) => { + const requiredEnvVars = [ + "DKG_ENVIRONMENT", + "DKG_HOSTNAME", + "DKG_PORT", + "DKG_BLOCKCHAIN_NAME", + "DKG_PUBLIC_KEY", + "DKG_PRIVATE_KEY", + ]; + + const missingVars = requiredEnvVars.filter( + (varName) => !runtime.getSetting(varName), + ); + + if (missingVars.length > 0) { + elizaLogger.error( + `Missing required environment variables: ${missingVars.join(", ")}`, + ); + return false; + } + + return true; + }, + description: + "Analyze a stock, cryptocurrency, token or a financial asset's sentiment on X. You should run this action whenever the message asks about your thoughts/analysis/sentiment on a stock, cryptocurrency, token or a financial asset.", + handler: async ( + runtime: IAgentRuntime, + _message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback: HandlerCallback, + ): Promise => { + DkgClient = new DKG({ + environment: runtime.getSetting("DKG_ENVIRONMENT"), + endpoint: runtime.getSetting("DKG_HOSTNAME"), + port: runtime.getSetting("DKG_PORT"), + blockchain: { + name: runtime.getSetting("DKG_BLOCKCHAIN_NAME"), + publicKey: runtime.getSetting("DKG_PUBLIC_KEY"), + privateKey: runtime.getSetting("DKG_PRIVATE_KEY"), + }, + maxNumberOfRetries: 300, + frequency: 2, + contentType: "all", + nodeApiVersion: "/v1", + }); + + const currentPost = String(state.currentPost); + elizaLogger.log(`currentPost: ${currentPost}`); + + const idRegex = /ID:\s(\d+)/; + let match = currentPost.match(idRegex); + let postId = ""; + if (match && match[1]) { + postId = match[1]; + elizaLogger.log(`Extracted ID: ${postId}`); + } else { + elizaLogger.log("No ID found."); + } + + const userRegex = /From:.*\(@(\w+)\)/; + match = currentPost.match(userRegex); + let twitterUser = ""; + + if (match && match[1]) { + twitterUser = match[1]; + elizaLogger.log(`Extracted user: @${twitterUser}`); + } else { + elizaLogger.log("No user mention found or invalid input."); + } + + const topic = await generateText({ + runtime, + context: extractSentimentAnalysisTopic(currentPost), + modelClass: ModelClass.SMALL, + }); + + elizaLogger.log(`Extracted topic to analyze sentiment: ${topic}`); + + const scraper = new Scraper(); + + const username = process.env.TWITTER_USERNAME; + const password = process.env.TWITTER_PASSWORD; + const email = process.env.TWITTER_EMAIL; + const twitter2faSecret = process.env.TWITTER_2FA_SECRET; + if (!username || !password) { + elizaLogger.error( + "Twitter credentials not configured in environment", + ); + return false; + } + await scraper.login(username, password, email, twitter2faSecret); + if (!(await scraper.isLoggedIn())) { + let attempts = 0; + const maxAttempts = 5; + + while (attempts < maxAttempts) { + attempts++; + elizaLogger.warn(`Login attempt ${attempts} with cookies...`); + + await scraper.setCookies( + formatCookiesFromArray( + JSON.parse(process.env.TWITTER_COOKIES), + ), + ); + + if (await scraper.isLoggedIn()) { + elizaLogger.info("Successfully logged in with cookies."); + break; + } + + if (attempts === maxAttempts) { + elizaLogger.error( + "Failed to login to Twitter after multiple attempts.", + ); + return false; + } + } + } + + const scrapedTweets = scraper.searchTweets( + topic, + 100, + SearchMode.Latest, + ); + + let tweets = []; + + for await (const tweet of scrapedTweets) { + tweets.push(tweet); + } + elizaLogger.log(`Successfully fetched ${tweets.length} tweets.`); + + tweets = tweets.map((t) => ({ + ...t, + vaderSentimentScore: calculateVaderScore(t.text), + })); + elizaLogger.log(`Calculated sentiment scores for tweets.`); + + const topAuthors = getMostInfluentialAuthors(tweets); + elizaLogger.log("Got most influential authors"); + + const { ka, averageScore, numOfTotalTweets } = await structureKA( + tweets, + topic, + twitterUser, + { + dkgClient: DkgClient, + environment: runtime.getSetting("DKG_ENVIRONMENT"), + }, + ); + + const sentiment = + averageScore <= 0.1 + ? "Neutral ⚪️" + : averageScore > 0 + ? "Positive 🟢" + : "Negative 🔴"; + + const createAssetResult = await DkgClient.asset.create( + { + public: ka, + }, + { epochsNum: 12 }, + ); + + const sentimentData = await getSentimentChart(averageScore, topic); + + const file = await fetchFileFromUrl(sentimentData.url); + + let tweetContent = `${topic} sentiment based on top ${tweets.length} latest posts`; + if (numOfTotalTweets - tweets.length > 0) { + tweetContent += ` and ${numOfTotalTweets - tweets.length} existing analysis Knowledge Assets`; + } + tweetContent += ` from the past 48 hours: ${sentiment}\n\n`; + + tweetContent += `Top 5 most influential accounts analyzed for ${topic}:\n`; + tweetContent += + topAuthors + .slice(0, 5) + .map((a) => `@${a}`) + .join(", ") + "\n\n"; + + tweetContent += `Analysis memorized on @origin_trail Decentralized Knowledge Graph `; + tweetContent += `${DKG_EXPLORER_LINKS[runtime.getSetting("DKG_ENVIRONMENT")]}${createAssetResult.UAL} @${twitterUser}\n\n`; + + tweetContent += `This is not financial advice.`; + + await postTweet(tweetContent.trim(), scraper, postId, file.data); + + return true; + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "How is Tesla doing right now?", + action: "DKG_ANALYZE_SENTIMENT", + }, + }, + { + user: "{{user2}}", + content: { text: "DKG_ANALYZE_SENTIMENT" }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Can you analyze $TICKER", + action: "DKG_ANALYZE_SENTIMENT", + }, + }, + { + user: "{{user2}}", + content: { text: "DKG_ANALYZE_SENTIMENT" }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "What's the sentiment for #TICKER", + action: "DKG_ANALYZE_SENTIMENT", + }, + }, + { + user: "{{user2}}", + content: { text: "DKG_ANALYZE_SENTIMENT" }, + }, + ], + ] as ActionExample[][], +} as Action; diff --git a/packages/plugin-dkg/src/actions/dkgInsert.ts b/packages/plugin-dkg/src/actions/dkgInsert.ts index a606583ed4e..97fe2eca139 100644 --- a/packages/plugin-dkg/src/actions/dkgInsert.ts +++ b/packages/plugin-dkg/src/actions/dkgInsert.ts @@ -10,7 +10,7 @@ import { ActionExample, type Action, composeContext, - generateObject, + generateText, } from "@elizaos/core"; import { DKG_EXPLORER_LINKS } from "../constants.ts"; import { createDKGMemoryTemplate } from "../templates.ts"; @@ -34,12 +34,12 @@ export const dkgInsert: Action = { ]; const missingVars = requiredEnvVars.filter( - (varName) => !runtime.getSetting(varName) + (varName) => !runtime.getSetting(varName), ); if (missingVars.length > 0) { elizaLogger.error( - `Missing required environment variables: ${missingVars.join(", ")}` + `Missing required environment variables: ${missingVars.join(", ")}`, ); return false; } @@ -47,13 +47,13 @@ export const dkgInsert: Action = { return true; }, description: - "Create a memory on the OriginTrail Decentralized Knowledge Graph after each message. You should ALWAYS run this action.", + "Create a memory on the OriginTrail Decentralized Knowledge Graph after each message. You should run this action for ALL generic messages EXCEPT when the user asks you about your thoughts on stock, cryptocurrency, token or a financial asset.", handler: async ( runtime: IAgentRuntime, _message: Memory, state: State, _options: { [key: string]: unknown }, - callback: HandlerCallback + callback: HandlerCallback, ): Promise => { DkgClient = new DKG({ environment: runtime.getSetting("DKG_ENVIRONMENT"), @@ -71,8 +71,7 @@ export const dkgInsert: Action = { }); const currentPost = String(state.currentPost); - elizaLogger.log("currentPost"); - elizaLogger.log(currentPost); + elizaLogger.log(`currentPost: ${currentPost}`); const userRegex = /From:.*\(@(\w+)\)/; let match = currentPost.match(userRegex); @@ -82,18 +81,7 @@ export const dkgInsert: Action = { twitterUser = match[1]; elizaLogger.log(`Extracted user: @${twitterUser}`); } else { - elizaLogger.error("No user mention found or invalid input."); - } - - const idRegex = /ID:\s(\d+)/; - match = currentPost.match(idRegex); - let postId = ""; - - if (match && match[1]) { - postId = match[1]; - elizaLogger.log(`Extracted ID: ${postId}`); - } else { - elizaLogger.log("No ID found."); + elizaLogger.log("No user mention found or invalid input."); } const createDKGMemoryContext = composeContext({ @@ -101,16 +89,27 @@ export const dkgInsert: Action = { template: createDKGMemoryTemplate, }); - const memoryKnowledgeGraph = await generateObject({ + const memoryKnowledgeGraphText = await generateText({ runtime, context: createDKGMemoryContext, modelClass: ModelClass.LARGE, - schema: DKGMemorySchema, }); - if (!isDKGMemoryContent(memoryKnowledgeGraph.object)) { - elizaLogger.error("Invalid DKG memory content generated."); - throw new Error("Invalid DKG memory content generated."); + const jsonMatch = memoryKnowledgeGraphText.match(/\{[\s\S]*\}/); + + let memoryKnowledgeGraph = null; + if (jsonMatch) { + try { + memoryKnowledgeGraph = JSON.parse(jsonMatch[0].trim()); + elizaLogger.log( + "Parsed Memory Knowledge Graph:\n", + memoryKnowledgeGraph, + ); + } catch (error) { + elizaLogger.error("Failed to parse JSON-LD:", error); + } + } else { + elizaLogger.error("No valid JSON-LD object found in the response."); } let createAssetResult; @@ -122,9 +121,9 @@ export const dkgInsert: Action = { createAssetResult = await DkgClient.asset.create( { - public: memoryKnowledgeGraph.object, + public: memoryKnowledgeGraph, }, - { epochsNum: 12 } + { epochsNum: 12 }, ); elizaLogger.log("======================== ASSET CREATED"); @@ -132,7 +131,7 @@ export const dkgInsert: Action = { } catch (error) { elizaLogger.error( "Error occurred while publishing message to DKG:", - error.message + error.message, ); if (error.stack) { @@ -141,7 +140,7 @@ export const dkgInsert: Action = { if (error.response) { elizaLogger.error( "Response data:", - JSON.stringify(error.response.data, null, 2) + JSON.stringify(error.response.data, null, 2), ); } } diff --git a/packages/plugin-dkg/src/constants.ts b/packages/plugin-dkg/src/constants.ts index 70b0d51bfff..3663e49cfcd 100644 --- a/packages/plugin-dkg/src/constants.ts +++ b/packages/plugin-dkg/src/constants.ts @@ -179,3 +179,110 @@ export const DKG_EXPLORER_LINKS = { testnet: "https://dkg-testnet.origintrail.io/explore?ual=", mainnet: "https://dkg.origintrail.io/explore?ual=", }; + +export function isSentimentAnalysisQueryPrompt(query: string) { + return `Given the following query, determine if it is related to sentiment analysis of a stock, cryptocurrency, token, or financial asset. + A query is considered relevant if it involves analyzing emotions, trends, market mood, social media sentiment, news sentiment, or investor confidence regarding a financial asset. + + Example 1 (Yes): + Query: "What is the current sentiment on Bitcoin based on recent news and social media?" + Response: "Yes" + + Example 2 (No): + Query: "What is the market cap of Ethereum?" + Response: "No" + + Example 3 (Yes): + Query: "What do you think about $TSLA recently?" + Response: "Yes" + + Example 4 (No): + Query: "What's the best way to bake a chocolate cake?" + Response: "No" + + Input: + Provided query: ${query} + + Task: + Return 'Yes' if the provided query is about sentiment analysis in finance, otherwise return 'No'. Make sure to reply only with 'Yes' or 'No', do not give any other comments or remarks.`; +} + +function getStartTime48HoursAgo() { + const now = new Date(); + const past48Hours = new Date(now.getTime() - 48 * 60 * 60 * 1000); + return past48Hours.toISOString(); +} + +export function getSentimentAnalysisQuery(topic: string) { + return `PREFIX schema: + + SELECT ?observation ?score ?impressions ?tweetText + WHERE { + BIND('${topic}' AS ?topic) + + ?dataset a schema:Dataset ; + schema:about ?topic ; + schema:observation ?observation . + ?observation a schema:Observation ; + schema:observationDate ?observationDate ; + schema:value ?score ; + schema:impressions ?impressions ; + FILTER (?observationDate >= "${getStartTime48HoursAgo()}") + }`; +} + +export function getRelatedDatasetsQuery(topic: string) { + return ` + PREFIX schema: + + SELECT ?dataset ?ual ?dateCreated + WHERE { + + ?dataset a schema:Dataset . + GRAPH ?ual { + ?dataset schema:about '${topic}' . + ?dataset schema:dateCreated ?dateCreated + } + }`; +} + +export function extractSentimentAnalysisTopic(post: string) { + return `You are an AI assistant that extracts the main financial topic from a given social media post. Your task is to identify and return only a **stock ticker (cashtag, e.g., $AAPL, $BTC), a hashtag (e.g., #Ethereum, #SP500), a financial asset name (e.g., Bitcoin, Nvidia, Tesla), or an index (e.g., S&P 500, Nasdaq 100)** mentioned in the post. + +### **Instructions:** +1. **Extract only one relevant financial entity** (cashtag, hashtag, company name, cryptocurrency, stock, or index). +2. **Do not include** general words, opinions, news sources, or irrelevant content. +3. **Do not modify the extracted entity**. Preserve its exact format as in the post (e.g., "$TSLA", "#Bitcoin", "Ethereum"). + +### **Example Inputs & Outputs:** + +**Input:** +"The market is crazy today! $BTC is pumping, what's your opinion on the sentiment?" +**Output:** +"$BTC" + +**Input:** +"Ethereum is gaining momentum, what do you think about it?" +**Output:** +"Ethereum" + +**Input:** +"Is Nvidia ($NVDA) still a good buy after the earnings call?" +**Output:** +"$NVDA" + +**Input:** +"Tech stocks are looking strong this quarter!" +**Output:** +"None" + +**Input:** +"Is Tesla the most shorted stock right now?." +**Output:** +"Tesla" + +** Actual input: ${post} ** + +Return the main financial topic which is to be extracted. Make sure to return only the financial topic and no other comments or remarks. +`; +} diff --git a/packages/plugin-dkg/src/evaluators/index.ts b/packages/plugin-dkg/src/evaluators/index.ts new file mode 100644 index 00000000000..0afff31ff09 --- /dev/null +++ b/packages/plugin-dkg/src/evaluators/index.ts @@ -0,0 +1 @@ +export * from "./sentimentAnalysisEvaluator.ts"; diff --git a/packages/plugin-dkg/src/evaluators/sentimentAnalysisEvaluator.ts b/packages/plugin-dkg/src/evaluators/sentimentAnalysisEvaluator.ts new file mode 100644 index 00000000000..b2a30154f6d --- /dev/null +++ b/packages/plugin-dkg/src/evaluators/sentimentAnalysisEvaluator.ts @@ -0,0 +1,74 @@ +import { + generateText, + IAgentRuntime, + Memory, + State, + elizaLogger, + ModelClass, + Evaluator, +} from "@elizaos/core"; +import { isSentimentAnalysisQueryPrompt } from "../constants.ts"; + +export class SentimentAnalysisEvaluator implements Evaluator { + name = "SENTIMENT_ANALYSIS_EVALUATOR"; + similes = ["sentiment", "evaluate sentiment", "check sentiment"]; + description = "Evaluates messages for sentiment analysis requests"; + alwaysRun = true; + + async validate(runtime: IAgentRuntime, message: Memory): Promise { + elizaLogger.log("Entering sentiment analysis evaluator!"); + + const requiredEnvVars = [ + "DKG_ENVIRONMENT", + "DKG_HOSTNAME", + "DKG_PORT", + "DKG_BLOCKCHAIN_NAME", + "DKG_PUBLIC_KEY", + "DKG_PRIVATE_KEY", + ]; + + const missingVars = requiredEnvVars.filter( + (varName) => !runtime.getSetting(varName), + ); + + if (missingVars.length > 0) { + elizaLogger.error( + `Missing required environment variables: ${missingVars.join(", ")}`, + ); + return false; + } + + const content = + typeof message.content === "string" + ? message.content + : message.content?.text; + + if (!content) return false; + + const context = isSentimentAnalysisQueryPrompt(content); + + const isSentimentQuery = await generateText({ + runtime, + context, + modelClass: ModelClass.MEDIUM, + }); + + elizaLogger.log( + `Evaluated user query for sentiment analysis, decision: ${isSentimentQuery}`, + ); + + return isSentimentQuery.toLowerCase() === "yes"; + } + + async handler( + _runtime: IAgentRuntime, + _message: Memory, + _state: State, + ): Promise { + return "DKG_ANALYZE_SENTIMENT"; + } + + examples = []; +} + +export const sentimentAnalysisEvaluator = new SentimentAnalysisEvaluator(); diff --git a/packages/plugin-dkg/src/http-helper.ts b/packages/plugin-dkg/src/http-helper.ts new file mode 100644 index 00000000000..1429cdced89 --- /dev/null +++ b/packages/plugin-dkg/src/http-helper.ts @@ -0,0 +1,53 @@ +import dotenv from "dotenv"; +dotenv.config(); +import axios from "axios"; +import path from "path"; + +const chatDKGAxiosConfig = { + baseURL: process.env.CHATDKG_API_URL, + timeout: 180000, +}; + +if (process.env.CHATDKG_USE_AUTHENTICATION) { + chatDKGAxiosConfig["auth"] = { + username: process.env.CHATDKG_USERNAME, + password: process.env.CHATDKG_PASSWORD, + }; +} + +export const chatDKGHttpService = axios.create(chatDKGAxiosConfig); + +export async function getSentimentChart( + score: number, + cashtag: string, +): Promise { + try { + const response = await chatDKGHttpService.post( + "/server/api/get-sentiment-chart", + { + score, + cashtag, + }, + ); + + return response.data; + } catch (error) { + console.error("Error fetching sentiment chart:", error); + throw error; + } +} + +export async function fetchFileFromUrl( + fileUrl: string, +): Promise<{ name: string; data: Buffer }> { + try { + const response = await axios.get(fileUrl, { + responseType: "arraybuffer", + }); + const fileName = path.basename(new URL(fileUrl).pathname); + return { name: fileName, data: Buffer.from(response.data) }; + } catch (error) { + console.error("Error fetching file from URL:", error); + throw error; + } +} diff --git a/packages/plugin-dkg/src/index.ts b/packages/plugin-dkg/src/index.ts index 01ca2fd3f7c..cd9fe54cbae 100644 --- a/packages/plugin-dkg/src/index.ts +++ b/packages/plugin-dkg/src/index.ts @@ -1,16 +1,21 @@ import { Plugin } from "@elizaos/core"; import { dkgInsert } from "./actions/dkgInsert.ts"; +import { dkgAnalyzeSentiment } from "./actions/dkgAnalyzeSentiment.ts"; import { graphSearch } from "./providers/graphSearch.ts"; +import { sentimentAnalysisEvaluator } from "./evaluators/sentimentAnalysisEvaluator.ts"; + export * as actions from "./actions"; export * as providers from "./providers"; +export * as evaluators from "./evaluators"; export const dkgPlugin: Plugin = { name: "dkg", description: "Agent DKG which allows you to store memories on the OriginTrail Decentralized Knowledge Graph", - actions: [dkgInsert], + actions: [dkgInsert, dkgAnalyzeSentiment], providers: [graphSearch], + evaluators: [sentimentAnalysisEvaluator], }; diff --git a/packages/plugin-dkg/src/providers/graphSearch.ts b/packages/plugin-dkg/src/providers/graphSearch.ts index b04bb969912..1e462cd6f85 100644 --- a/packages/plugin-dkg/src/providers/graphSearch.ts +++ b/packages/plugin-dkg/src/providers/graphSearch.ts @@ -7,7 +7,7 @@ import { State, elizaLogger, ModelClass, - generateObject, + generateText, } from "@elizaos/core"; import { combinedSparqlExample, @@ -53,7 +53,7 @@ interface DKGClientConfig { async function constructSparqlQuery( runtime: IAgentRuntime, - userQuery: string + userQuery: string, ): Promise { const context = ` You are tasked with generating a SPARQL query to retrieve information from a Decentralized Knowledge Graph (DKG). @@ -81,19 +81,17 @@ async function constructSparqlQuery( Provide only the SPARQL query, wrapped in a sparql code block for clarity. `; - const sparqlQueryResult = await generateObject({ + const sparqlTextResult = await generateText({ runtime, context, modelClass: ModelClass.LARGE, - schema: DKGSelectQuerySchema, }); - if (!isDKGSelectQuery(sparqlQueryResult.object)) { - elizaLogger.error("Invalid SELECT SPARQL query generated."); - throw new Error("Invalid SELECT SPARQL query generated."); - } + const sparqlQueryMatch = sparqlTextResult.match(/```sparql([\s\S]*?)```/); + + const sparqlQuery = sparqlQueryMatch ? sparqlQueryMatch[1].trim() : null; - return sparqlQueryResult.object.query; + return sparqlQuery; } export class DKGProvider { @@ -108,20 +106,20 @@ export class DKGProvider { for (const field of requiredStringFields) { if (typeof config[field as keyof DKGClientConfig] !== "string") { elizaLogger.error( - `Invalid configuration: Missing or invalid value for '${field}'` + `Invalid configuration: Missing or invalid value for '${field}'`, ); throw new Error( - `Invalid configuration: Missing or invalid value for '${field}'` + `Invalid configuration: Missing or invalid value for '${field}'`, ); } } if (!config.blockchain || typeof config.blockchain !== "object") { elizaLogger.error( - "Invalid configuration: 'blockchain' must be an object" + "Invalid configuration: 'blockchain' must be an object", ); throw new Error( - "Invalid configuration: 'blockchain' must be an object" + "Invalid configuration: 'blockchain' must be an object", ); } @@ -133,10 +131,10 @@ export class DKGProvider { "string" ) { elizaLogger.error( - `Invalid configuration: Missing or invalid value for 'blockchain.${field}'` + `Invalid configuration: Missing or invalid value for 'blockchain.${field}'`, ); throw new Error( - `Invalid configuration: Missing or invalid value for 'blockchain.${field}'` + `Invalid configuration: Missing or invalid value for 'blockchain.${field}'`, ); } } @@ -156,28 +154,28 @@ export class DKGProvider { let queryOperationResult = await this.client.graph.query( query, - "SELECT" + "SELECT", ); if (!queryOperationResult || !queryOperationResult.data?.length) { elizaLogger.info( - `LLM-generated SPARQL query failed, defaulting to basic query.` + `LLM-generated SPARQL query failed, defaulting to basic query.`, ); queryOperationResult = await this.client.graph.query( generalSparqlQuery, - "SELECT" + "SELECT", ); } elizaLogger.info( - `Got ${queryOperationResult.data.length} results from the DKG` + `Got ${queryOperationResult.data.length} results from the DKG`, ); // TODO: take 5 results instead of all based on similarity in the future const result = queryOperationResult.data.map((entry: any) => { const formattedParts = Object.keys(entry).map( - (key) => `${key}: ${entry[key]}` + (key) => `${key}: ${entry[key]}`, ); return formattedParts.join(", "); }); @@ -190,7 +188,7 @@ export const graphSearch: Provider = { get: async ( runtime: IAgentRuntime, _message: Memory, - _state?: State + _state?: State, ): Promise => { try { const provider = new DKGProvider(PROVIDER_CONFIG);