diff --git a/.env.example b/.env.example index 8606cd5a4c..d491d9f050 100644 --- a/.env.example +++ b/.env.example @@ -257,9 +257,12 @@ TOGETHER_API_KEY= # Together API Key #### Crypto Plugin Configurations #### ###################################### -# COIN DATA SOURCES +# CoinMarketCap / CMC COINMARKETCAP_API_KEY= + +# CoinGecko COINGECKO_API_KEY= +COINGECKO_PRO_API_KEY= # EVM EVM_PRIVATE_KEY= @@ -438,6 +441,8 @@ GIPHY_API_KEY= # OpenWeather OPEN_WEATHER_API_KEY= # OpenWeather API key + + # EchoChambers Configuration ECHOCHAMBERS_API_URL=http://127.0.0.1:3333 ECHOCHAMBERS_API_KEY=testingkey0011 @@ -471,3 +476,7 @@ TAVILY_API_KEY= # Verifiable Inference Configuration VERIFIABLE_INFERENCE_ENABLED=false # Set to false to disable verifiable inference VERIFIABLE_INFERENCE_PROVIDER=opacity # Options: opacity + +# Autonome Configuration +AUTONOME_JWT_TOKEN= +AUTONOME_RPC=https://wizard-bff-rpc.alt.technology/v1/bff/aaa/apps diff --git a/.github/workflows/pnpm-lockfile-check.yml b/.github/workflows/pnpm-lockfile-check.yml index a048b3703f..3b303f8809 100644 --- a/.github/workflows/pnpm-lockfile-check.yml +++ b/.github/workflows/pnpm-lockfile-check.yml @@ -2,7 +2,7 @@ name: Pnpm Lockfile Check on: pull_request: - branches: ["*"] + branches: [main] jobs: check-lockfile: @@ -38,4 +38,4 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, body: '❌ The pnpm-lockfile is out of date. Please run `pnpm install --no-frozen-lockfile` and commit the updated pnpm-lock.yaml file.' - }) \ No newline at end of file + }) diff --git a/agent/package.json b/agent/package.json index 9c7df7f209..f0d4d86dbb 100644 --- a/agent/package.json +++ b/agent/package.json @@ -34,6 +34,7 @@ "@elizaos/plugin-0g": "workspace:*", "@elizaos/plugin-abstract": "workspace:*", "@elizaos/plugin-aptos": "workspace:*", + "@elizaos/plugin-coingecko": "workspace:*", "@elizaos/plugin-coinmarketcap": "workspace:*", "@elizaos/plugin-coingecko": "workspace:*", "@elizaos/plugin-binance": "workspace:*", @@ -56,6 +57,7 @@ "@elizaos/plugin-node": "workspace:*", "@elizaos/plugin-solana": "workspace:*", "@elizaos/plugin-solana-agentkit": "workspace:*", + "@elizaos/plugin-autonome": "workspace:*", "@elizaos/plugin-starknet": "workspace:*", "@elizaos/plugin-stargaze": "workspace:*", "@elizaos/plugin-giphy": "workspace:*", @@ -73,6 +75,7 @@ "@elizaos/plugin-3d-generation": "workspace:*", "@elizaos/plugin-fuel": "workspace:*", "@elizaos/plugin-avalanche": "workspace:*", + "@elizaos/plugin-video-generation": "workspace:*", "@elizaos/plugin-web-search": "workspace:*", "@elizaos/plugin-letzai": "workspace:*", "@elizaos/plugin-thirdweb": "workspace:*", @@ -94,4 +97,4 @@ "ts-node": "10.9.2", "tsup": "8.3.5" } -} +} \ No newline at end of file diff --git a/agent/src/index.ts b/agent/src/index.ts index 53421f1e1b..c612afa92b 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -68,17 +68,19 @@ import { nearPlugin } from "@elizaos/plugin-near"; import { nftGenerationPlugin } from "@elizaos/plugin-nft-generation"; import { createNodePlugin } from "@elizaos/plugin-node"; import { obsidianPlugin } from "@elizaos/plugin-obsidian"; +import { sgxPlugin } from "@elizaos/plugin-sgx"; import { solanaPlugin } from "@elizaos/plugin-solana"; import { solanaAgentkitPlguin } from "@elizaos/plugin-solana-agentkit"; +import { autonomePlugin } from "@elizaos/plugin-autonome"; import { storyPlugin } from "@elizaos/plugin-story"; import { suiPlugin } from "@elizaos/plugin-sui"; -import { sgxPlugin } from "@elizaos/plugin-sgx"; import { TEEMode, teePlugin } from "@elizaos/plugin-tee"; import { teeLogPlugin } from "@elizaos/plugin-tee-log"; import { teeMarlinPlugin } from "@elizaos/plugin-tee-marlin"; import { tonPlugin } from "@elizaos/plugin-ton"; import { webSearchPlugin } from "@elizaos/plugin-web-search"; +import { coingeckoPlugin } from "@elizaos/plugin-coingecko"; import { giphyPlugin } from "@elizaos/plugin-giphy"; import { letzAIPlugin } from "@elizaos/plugin-letzai"; import { thirdwebPlugin } from "@elizaos/plugin-thirdweb"; @@ -141,10 +143,6 @@ function tryLoadFile(filePath: string): string | null { } } -function isAllStrings(arr: unknown[]): boolean { - return Array.isArray(arr) && arr.every((item) => typeof item === "string"); -} - export async function loadCharacters( charactersArg: string ): Promise { @@ -230,16 +228,9 @@ export async function loadCharacters( } // Handle plugins - if (isAllStrings(character.plugins)) { - elizaLogger.info("Plugins are: ", character.plugins); - const importedPlugins = await Promise.all( - character.plugins.map(async (plugin) => { - const importedPlugin = await import(plugin); - return importedPlugin.default; - }) - ); - character.plugins = importedPlugins; - } + character.plugins = await handlePluginImporting( + character.plugins + ); loadedCharacters.push(character); elizaLogger.info( @@ -262,6 +253,36 @@ export async function loadCharacters( return loadedCharacters; } +async function handlePluginImporting(plugins: string[]) { + if (plugins.length > 0) { + elizaLogger.info("Plugins are: ", plugins); + const importedPlugins = await Promise.all( + plugins.map(async (plugin) => { + try { + const importedPlugin = await import(plugin); + const functionName = + plugin + .replace("@elizaos/plugin-", "") + .replace(/-./g, (x) => x[1].toUpperCase()) + + "Plugin"; // Assumes plugin function is camelCased with Plugin suffix + return ( + importedPlugin.default || importedPlugin[functionName] + ); + } catch (importError) { + elizaLogger.error( + `Failed to import plugin: ${plugin}`, + importError + ); + return []; // Return null for failed imports + } + }) + ); + return importedPlugins; + } else { + return []; + } +} + export function getTokenForProvider( provider: ModelProviderName, character: Character @@ -617,6 +638,7 @@ export async function createAgent( getSecret(character, "SOLANA_PRIVATE_KEY") ? solanaAgentkitPlguin : null, + getSecret(character, "AUTONOME_JWT_TOKEN") ? autonomePlugin : null, (getSecret(character, "NEAR_ADDRESS") || getSecret(character, "NEAR_WALLET_PUBLIC_KEY")) && getSecret(character, "NEAR_WALLET_SECRET_KEY") @@ -678,7 +700,10 @@ export async function createAgent( ? webhookPlugin : null, goatPlugin, - getSecret(character, "COINGECKO_API_KEY") ? coingeckoPlugin : null, + 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 @@ -917,7 +942,10 @@ const startAgents = async () => { } // upload some agent functionality into directClient - directClient.startAgent = async (character: Character) => { + directClient.startAgent = async (character) => { + // Handle plugins + character.plugins = await handlePluginImporting(character.plugins); + // wrap it so we don't have to inject directClient later return startAgent(character, directClient); }; diff --git a/docs/docs/advanced/verified-inference.md b/docs/docs/advanced/verified-inference.md new file mode 100644 index 0000000000..2b8692bebb --- /dev/null +++ b/docs/docs/advanced/verified-inference.md @@ -0,0 +1,83 @@ +--- +sidebar_position: 18 +--- + +# 🪪 Verified Inference + +## Overview + +With verified inference, you can turn your Eliza agent fully verifiable on-chain on Solana with an OpenAI compatible TEE API. This proves that your agent’s thoughts and outputs are free from human control thus increasing the trust of the agent. + +Compared to [fully deploying the agent in a TEE](https://elizaos.github.io/eliza/docs/advanced/eliza-in-tee/), this is a more light-weight solution which only verifies the inference calls and only needs a single line of code change. + +The API supports all OpenAI models out of the box, including your fine-tuned models. The following guide will walk you through how to use verified inference API with Eliza. + +## Background + +The API is built on top of [Sentience Stack](https://github.com/galadriel-ai/Sentience), which cryptographically verifies agent's LLM inferences inside TEEs, posts those proofs on-chain on Solana, and makes the verified inference logs available to read and display to users. + +Here’s how it works: +![](https://i.imgur.com/SNwSHam.png) + +1. The agent sends a request containing a message with the desired LLM model to the TEE. +2. The TEE securely processes the request by calling the LLM API. +3. The TEE sends back the `{Message, Proof}` to the agent. +4. The TEE submits the attestation with `{Message, Proof}` to Solana. +5. The Proof of Sentience SDK is used to read the attestation from Solana and verify it with `{Message, Proof}`. The proof log can be added to the agent website/app. + +To verify the code running inside the TEE, use instructions [from here](https://github.com/galadriel-ai/sentience/tree/main/verified-inference/verify). + +## Tutorial + +1. **Create a free API key on [Galadriel dashboard](https://dashboard.galadriel.com/login)** +2. **Configure the environment variables** + ```bash + GALADRIEL_API_KEY=gal-* # Get from https://dashboard.galadriel.com/ + # Use any model supported by OpenAI + SMALL_GALADRIEL_MODEL= # Default: gpt-4o-mini + MEDIUM_GALADRIEL_MODEL= # Default: gpt-4o + LARGE_GALADRIEL_MODEL= # Default: gpt-4o + # If you wish to use a fine-tuned model you will need to provide your own OpenAI API key + GALADRIEL_FINE_TUNE_API_KEY= # starting with sk- + ``` +3. **Configure your character to use `galadriel`** + + In your character file set the `modelProvider` as `galadriel`. + ``` + "modelProvider": "galadriel" + ``` +4. **Run your agent.** + + Reminder how to run an agent is [here](https://elizaos.github.io/eliza/docs/quickstart/#create-your-first-agent). + ```bash + pnpm start --character="characters/.json" + pnpm start:client + ``` +5. **Get the history of all of your verified inference calls** + ```javascript + const url = 'https://api.galadriel.com/v1/verified/chat/completions?limit=100&filter=mine'; + const headers = { + 'accept': 'application/json', + 'Authorization': 'Bearer '// Replace with your Galadriel API key + }; + + const response = await fetch(url, { method: 'GET', headers }); + const data = await response.json(); + console.log(data); + ``` + + Use this to build a verified logs terminal to your agent front end, for example: +![](https://i.imgur.com/yejIlao.png) + +6. **Check your inferences in the explorer.** + + You can also see your inferences with proofs in the [Galadriel explorer](https://explorer.galadriel.com/). For specific inference responses use `https://explorer.galadriel.com/details/` + + The `hash` param is returned with every inference request. + ![](https://i.imgur.com/QazDxbE.png) + +7. **Check proofs posted on Solana.** + + You can also see your inferences with proofs on Solana. For specific inference responses: `https://explorer.solana.com/tx/<>tx_hash?cluster=devnet` + + The `tx_hash` param is returned with every inference request. diff --git a/docs/docs/packages/clients.md b/docs/docs/packages/clients.md index ad4d173d9e..24fa4bfb28 100644 --- a/docs/docs/packages/clients.md +++ b/docs/docs/packages/clients.md @@ -35,11 +35,11 @@ graph TD ## Available Clients -- **Discord** (`@eliza/client-discord`) - Full Discord bot integration -- **Twitter** (`@eliza/client-twitter`) - Twitter bot and interaction handling -- **Telegram** (`@eliza/client-telegram`) - Telegram bot integration -- **Direct** (`@eliza/client-direct`) - Direct API interface for custom integrations -- **Auto** (`@eliza/client-auto`) - Automated trading and interaction client +- **Discord** (`@elizaos/client-discord`) - Full Discord bot integration +- **Twitter** (`@elizaos/client-twitter`) - Twitter bot and interaction handling +- **Telegram** (`@elizaos/client-telegram`) - Telegram bot integration +- **Direct** (`@elizaos/client-direct`) - Direct API interface for custom integrations +- **Auto** (`@elizaos/client-auto`) - Automated trading and interaction client --- @@ -47,19 +47,19 @@ graph TD ```bash # Discord -pnpm add @eliza/client-discord +pnpm add @elizaos/client-discord # Twitter -pnpm add @eliza/client-twitter +pnpm add @elizaos/client-twitter # Telegram -pnpm add @eliza/client-telegram +pnpm add @elizaos/client-telegram # Direct API -pnpm add @eliza/client-direct +pnpm add @elizaos/client-direct # Auto Client -pnpm add @eliza/client-auto +pnpm add @elizaos/client-auto ``` --- @@ -71,7 +71,7 @@ The Discord client provides full integration with Discord's features including v ### Basic Setup ```typescript -import { DiscordClientInterface } from "@eliza/client-discord"; +import { DiscordClientInterface } from "@elizaos/client-discord"; // Initialize client const client = await DiscordClientInterface.start(runtime); @@ -133,7 +133,7 @@ The Twitter client enables posting, searching, and interacting with Twitter user ### Basic Setup ```typescript -import { TwitterClientInterface } from "@eliza/client-twitter"; +import { TwitterClientInterface } from "@elizaos/client-twitter"; // Initialize client const client = await TwitterClientInterface.start(runtime); @@ -192,7 +192,7 @@ The Telegram client provides messaging and bot functionality for Telegram. ### Basic Setup ```typescript -import { TelegramClientInterface } from "@eliza/client-telegram"; +import { TelegramClientInterface } from "@elizaos/client-telegram"; // Initialize client const client = await TelegramClientInterface.start(runtime); @@ -225,7 +225,7 @@ The Direct client provides a REST API interface for custom integrations. ### Basic Setup ```typescript -import { DirectClientInterface } from "@eliza/client-direct"; +import { DirectClientInterface } from "@elizaos/client-direct"; // Initialize client const client = await DirectClientInterface.start(runtime); @@ -258,7 +258,7 @@ The Auto client enables automated interactions and trading. ### Basic Setup ```typescript -import { AutoClientInterface } from "@eliza/client-auto"; +import { AutoClientInterface } from "@elizaos/client-auto"; // Initialize client const client = await AutoClientInterface.start(runtime); diff --git a/docs/sidebars.js b/docs/sidebars.js index e2f74c6e87..93cc9719f9 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -117,6 +117,11 @@ const sidebars = { id: "advanced/eliza-in-tee", label: "Eliza in TEE", }, + { + type: "doc", + id: "advanced/verified-inference", + label: "Verified Inference", + }, ], }, { diff --git a/packages/client-telegram/src/messageManager.ts b/packages/client-telegram/src/messageManager.ts index 73240efa01..3daf8f42eb 100644 --- a/packages/client-telegram/src/messageManager.ts +++ b/packages/client-telegram/src/messageManager.ts @@ -507,7 +507,7 @@ export class MessageManager { // Check if team member has direct interest first if ( - this.runtime.character.clientConfig?.discord?.isPartOfTeam && + this.runtime.character.clientConfig?.telegram?.isPartOfTeam && !this._isTeamLeader() && this._isRelevantToTeamMember(messageText, chatId) ) { diff --git a/packages/core/src/embedding.ts b/packages/core/src/embedding.ts index 73cc657f00..ce2d00b21b 100644 --- a/packages/core/src/embedding.ts +++ b/packages/core/src/embedding.ts @@ -18,6 +18,7 @@ export const EmbeddingProvider = { OpenAI: "OpenAI", Ollama: "Ollama", GaiaNet: "GaiaNet", + Heurist: "Heurist", BGE: "BGE", } as const; @@ -39,7 +40,10 @@ export const getEmbeddingConfig = (): EmbeddingConfig => ({ : settings.USE_GAIANET_EMBEDDING?.toLowerCase() === "true" ? getEmbeddingModelSettings(ModelProviderName.GAIANET) .dimensions - : 384, // BGE + : settings.USE_HEURIST_EMBEDDING?.toLowerCase() === "true" + ? getEmbeddingModelSettings(ModelProviderName.HEURIST) + .dimensions + : 384, // BGE model: settings.USE_OPENAI_EMBEDDING?.toLowerCase() === "true" ? getEmbeddingModelSettings(ModelProviderName.OPENAI).name @@ -47,7 +51,9 @@ export const getEmbeddingConfig = (): EmbeddingConfig => ({ ? getEmbeddingModelSettings(ModelProviderName.OLLAMA).name : settings.USE_GAIANET_EMBEDDING?.toLowerCase() === "true" ? getEmbeddingModelSettings(ModelProviderName.GAIANET).name - : "BGE-small-en-v1.5", + : settings.USE_HEURIST_EMBEDDING?.toLowerCase() === "true" + ? getEmbeddingModelSettings(ModelProviderName.HEURIST).name + : "BGE-small-en-v1.5", provider: settings.USE_OPENAI_EMBEDDING?.toLowerCase() === "true" ? "OpenAI" @@ -55,7 +61,9 @@ export const getEmbeddingConfig = (): EmbeddingConfig => ({ ? "Ollama" : settings.USE_GAIANET_EMBEDDING?.toLowerCase() === "true" ? "GaiaNet" - : "BGE", + : settings.USE_HEURIST_EMBEDDING?.toLowerCase() === "true" + ? "Heurist" + : "BGE", }); async function getRemoteEmbedding( @@ -126,6 +134,7 @@ export function getEmbeddingType(runtime: IAgentRuntime): "local" | "remote" { isNode && runtime.character.modelProvider !== ModelProviderName.OPENAI && runtime.character.modelProvider !== ModelProviderName.GAIANET && + runtime.character.modelProvider !== ModelProviderName.HEURIST && !settings.USE_OPENAI_EMBEDDING; return isLocal ? "local" : "remote"; @@ -146,6 +155,10 @@ export function getEmbeddingZeroVector(): number[] { embeddingDimension = getEmbeddingModelSettings( ModelProviderName.GAIANET ).dimensions; // GaiaNet dimension + } else if (settings.USE_HEURIST_EMBEDDING?.toLowerCase() === "true") { + embeddingDimension = getEmbeddingModelSettings( + ModelProviderName.HEURIST + ).dimensions; // Heurist dimension } return Array(embeddingDimension).fill(0); @@ -229,6 +242,15 @@ export async function embed(runtime: IAgentRuntime, input: string) { }); } + if (config.provider === EmbeddingProvider.Heurist) { + return await getRemoteEmbedding(input, { + model: config.model, + endpoint: getEndpoint(ModelProviderName.HEURIST), + apiKey: runtime.token, + dimensions: config.dimensions, + }); + } + // BGE - try local first if in Node if (isNode) { try { diff --git a/packages/core/src/models.ts b/packages/core/src/models.ts index 4f08625152..663aaa518a 100644 --- a/packages/core/src/models.ts +++ b/packages/core/src/models.ts @@ -1,11 +1,11 @@ import settings from "./settings.ts"; import { - Models, - ModelProviderName, + EmbeddingModelSettings, + ImageModelSettings, ModelClass, + ModelProviderName, + Models, ModelSettings, - ImageModelSettings, - EmbeddingModelSettings, } from "./types.ts"; export const models: Models = { @@ -332,6 +332,7 @@ export const models: Models = { }, }, [ModelProviderName.GOOGLE]: { + endpoint: "https://generativelanguage.googleapis.com", model: { [ModelClass.SMALL]: { name: @@ -544,7 +545,7 @@ export const models: Models = { [ModelClass.LARGE]: { name: settings.LARGE_HEURIST_MODEL || - "meta-llama/llama-3.1-405b-instruct", + "meta-llama/llama-3.3-70b-instruct", stop: [], maxInputTokens: 128000, maxOutputTokens: 8192, @@ -552,9 +553,13 @@ export const models: Models = { temperature: 0.7, }, [ModelClass.IMAGE]: { - name: settings.HEURIST_IMAGE_MODEL || "PepeXL", + name: settings.HEURIST_IMAGE_MODEL || "FLUX.1-dev", steps: 20, }, + [ModelClass.EMBEDDING]: { + name: "BAAI/bge-large-en-v1.5", + dimensions: 1024, + }, }, }, [ModelProviderName.GALADRIEL]: { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 80a2ffd295..708e4ec8e5 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1334,6 +1334,47 @@ export interface IAwsS3Service extends Service { generateSignedUrl(fileName: string, expiresIn: number): Promise; } +export interface UploadIrysResult { + success: boolean; + url?: string; + error?: string; + data?: any; +} + +export interface DataIrysFetchedFromGQL { + success: boolean; + data: any; + error?: string; +} + +export interface GraphQLTag { + name: string; + values: any[]; +} + +export const enum IrysMessageType { + REQUEST = "REQUEST", + DATA_STORAGE = "DATA_STORAGE", + REQUEST_RESPONSE = "REQUEST_RESPONSE", +} + +export const enum IrysDataType { + FILE = "FILE", + IMAGE = "IMAGE", + OTHER = "OTHER", +} + +export interface IrysTimestamp { + from: number; + to: number; +} + +export interface IIrysService extends Service { + getDataFromAnAgent(agentsWalletPublicKeys: string[], tags: GraphQLTag[], timestamp: IrysTimestamp): Promise; + workerUploadDataOnIrys(data: any, dataType: IrysDataType, messageType: IrysMessageType, serviceCategory: string[], protocol: string[], validationThreshold: number[], minimumProviders: number[], testProvider: boolean[], reputation: number[]): Promise; + providerUploadDataOnIrys(data: any, dataType: IrysDataType, serviceCategory: string[], protocol: string[]): Promise; +} + export interface ITeeLogService extends Service { getInstance(): ITeeLogService; log( @@ -1379,6 +1420,7 @@ export enum ServiceType { AWS_S3 = "aws_s3", BUTTPLUG = "buttplug", SLACK = "slack", + IRYS = "irys", TEE_LOG = "tee_log", GOPLUS_SECURITY = "goplus_security", } diff --git a/packages/plugin-autonome/.npmignore b/packages/plugin-autonome/.npmignore new file mode 100644 index 0000000000..078562ecea --- /dev/null +++ b/packages/plugin-autonome/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts \ No newline at end of file diff --git a/packages/plugin-autonome/eslint.config.mjs b/packages/plugin-autonome/eslint.config.mjs new file mode 100644 index 0000000000..92fe5bbebe --- /dev/null +++ b/packages/plugin-autonome/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-autonome/package.json b/packages/plugin-autonome/package.json new file mode 100644 index 0000000000..918a0ae6cf --- /dev/null +++ b/packages/plugin-autonome/package.json @@ -0,0 +1,24 @@ +{ + "name": "@elizaos/plugin-autonome", + "version": "0.1.7-alpha.1", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@coral-xyz/anchor": "0.30.1", + "@elizaos/core": "workspace:*", + "@elizaos/plugin-tee": "workspace:*", + "@elizaos/plugin-trustdb": "workspace:*", + "axios": "^1.7.9" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint --fix --cache .", + "test": "vitest run" + }, + "peerDependencies": { + "form-data": "4.0.1", + "whatwg-url": "7.1.0" + } +} diff --git a/packages/plugin-autonome/src/actions/launchAgent.ts b/packages/plugin-autonome/src/actions/launchAgent.ts new file mode 100644 index 0000000000..0ae0111e80 --- /dev/null +++ b/packages/plugin-autonome/src/actions/launchAgent.ts @@ -0,0 +1,172 @@ +import axios from "axios"; +import { + ActionExample, + composeContext, + Content, + elizaLogger, + generateObjectDeprecated, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + type Action, +} from "@elizaos/core"; + +export interface LaunchAgentContent extends Content { + name: string; + config: string; +} + +function isLaunchAgentContent(content: any): content is LaunchAgentContent { + elizaLogger.log("Content for launchAgent", content); + return typeof content.name === "string" && typeof content.config === "string"; +} + +const launchTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Example response: +\`\`\`json +{ + "name": "xiaohuo", +} +\`\`\` + +{{recentMessages}} + +Given the recent messages, extract the following information about the requested agent launch: +- Agent name +- Character json config +`; + +export default { + name: "LAUNCH_AGENT", + similes: ["CREATE_AGENT", "DEPLOY_AGENT", "DEPLOY_ELIZA", "DEPLOY_BOT"], + validate: async (runtime: IAgentRuntime, message: Memory) => { + return true; + }, + description: "Launch an Eliza agent", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + elizaLogger.log("Starting LAUNCH_AGENT handler..."); + // Initialize or update state + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + // Compose launch context + const launchContext = composeContext({ + state, + template: launchTemplate, + }); + + // Generate launch content + const content = await generateObjectDeprecated({ + runtime, + context: launchContext, + modelClass: ModelClass.LARGE, + }); + + // Validate launch content + if (!isLaunchAgentContent(content)) { + elizaLogger.error("Invalid launch content", content); + if (callback) { + callback({ + text: "Unable to process launch agent request. Invalid content provided.", + content: { error: "Invalid launch agent content" }, + }); + } + return false; + } + + const autonomeJwt = runtime.getSetting("AUTONOME_JWT_TOKEN"); + const autonomeRpc = runtime.getSetting("AUTONOME_RPC"); + + const requestBody = { + name: content.name, + config: content.config, + creationMethod: 2, + envList: {}, + templateId: "Eliza", + + const sendPostRequest = async () => { + try { + const response = await axios.post(autonomeRpc, requestBody, { + headers: { + Authorization: `Bearer ${autonomeJwt}`, + "Content-Type": "application/json", + }, + }); + return response; + } catch (error) { + console.error("Error making RPC call:", error); + } + }; + + try { + const resp = await sendPostRequest(); + if (resp && resp.data && resp.data.app && resp.data.app.id) { + elizaLogger.log( + "Launching successful, please find your agent on" + ); + elizaLogger.log( + "https://dev.autonome.fun/autonome/" + + resp.data.app.id + + "/details" + ); + } + if (callback) { + callback({ + text: `Successfully launch agent ${content.name}`, + content: { + success: true, + appId: + "https://dev.autonome.fun/autonome/" + + resp.data.app.id + + "/details", + }, + }); + } + return true; + } catch (error) { + if (callback) { + elizaLogger.error("Error during launching agent"); + elizaLogger.error(error); + callback({ + text: `Error launching agent: ${error.message}`, + content: { error: error.message }, + }); + } + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Launch an agent, name is xiaohuo", + }, + }, + { + user: "{{user2}}", + content: { + text: "I'll launch the agent now...", + action: "LAUNCH_AGENT", + }, + }, + { + user: "{{user2}}", + content: { + text: "Successfully launch agent, id is ba2e8369-e256-4a0d-9f90-9c64e306dc9f", + }, + }, + ], + ] as ActionExample[][], +} as Action; diff --git a/packages/plugin-autonome/src/index.ts b/packages/plugin-autonome/src/index.ts new file mode 100644 index 0000000000..bbf4980898 --- /dev/null +++ b/packages/plugin-autonome/src/index.ts @@ -0,0 +1,12 @@ +import { Plugin } from "@elizaos/core"; +import launchAgent from "./actions/launchAgent"; + +export const autonomePlugin: Plugin = { + name: "autonome", + description: "Autonome Plugin for Eliza", + actions: [launchAgent], + evaluators: [], + providers: [], +}; + +export default autonomePlugin; diff --git a/packages/plugin-autonome/tsconfig.json b/packages/plugin-autonome/tsconfig.json new file mode 100644 index 0000000000..73993deaaf --- /dev/null +++ b/packages/plugin-autonome/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/plugin-autonome/tsup.config.ts b/packages/plugin-autonome/tsup.config.ts new file mode 100644 index 0000000000..dd25475bb6 --- /dev/null +++ b/packages/plugin-autonome/tsup.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + "safe-buffer", + "base-x", + "bs58", + "borsh", + "@solana/buffer-layout", + "stream", + "buffer", + "querystring", + "amqplib", + // Add other modules you want to externalize + ], +}); diff --git a/packages/plugin-coingecko/README.md b/packages/plugin-coingecko/README.md index ded984b61c..fcb79d8a55 100644 --- a/packages/plugin-coingecko/README.md +++ b/packages/plugin-coingecko/README.md @@ -4,7 +4,9 @@ A plugin for fetching cryptocurrency price data from the CoinGecko API. ## Overview -The Plugin CoinGecko provides a simple interface to get real-time cryptocurrency prices. It integrates with CoinGecko's API to fetch current prices for various cryptocurrencies in different fiat currencies. +The Plugin CoinGecko provides a simple interface to get real-time cryptocurrency data. It integrates with CoinGecko's API to fetch current prices, market data, trending coins, and top gainers/losers for various cryptocurrencies in different fiat currencies. + +This plugin uses the [CoinGecko Pro API](https://docs.coingecko.com/reference/introduction). Please refer to their documentation for detailed information about rate limits, available endpoints, and response formats. ## Installation @@ -18,7 +20,8 @@ Set up your environment with the required CoinGecko API key: | Variable Name | Description | | ------------------- | ---------------------- | -| `COINGECKO_API_KEY` | Your CoinGecko API key | +| `COINGECKO_API_KEY` | Your CoinGecko Pro API key | +| `COINGECKO_PRO_API_KEY` | Your CoinGecko Pro API key | ## Usage @@ -27,23 +30,69 @@ import { coingeckoPlugin } from "@elizaos/plugin-coingecko"; // Initialize the plugin const plugin = coingeckoPlugin; - -// The plugin provides the GET_PRICE action which can be used to fetch prices -// Supported coins: BTC, ETH, USDC, and more ``` ## Actions ### GET_PRICE -Fetches the current price of a cryptocurrency. +Fetches the current price and market data for one or more cryptocurrencies. -Examples: +Features: +- Multiple currency support (e.g., USD, EUR, JPY) +- Optional market cap data +- Optional 24h volume data +- Optional 24h price change data +- Optional last update timestamp +Examples: - "What's the current price of Bitcoin?" -- "Check ETH price in EUR" -- "What's USDC worth?" +- "Check ETH price in EUR with market cap" +- "Show me BTC and ETH prices in USD and EUR" +- "What's USDC worth with 24h volume and price change?" + +### GET_TRENDING + +Fetches the current trending cryptocurrencies on CoinGecko. -## License +Features: +- Includes trending coins with market data +- Optional NFT inclusion +- Optional category inclusion -MIT +Examples: +- "What's trending in crypto?" +- "Show me trending coins only" +- "What are the hot cryptocurrencies right now?" + +### GET_TOP_GAINERS_LOSERS + +Fetches the top gaining and losing cryptocurrencies by price change. + +Features: +- Customizable time range (1h, 24h, 7d, 14d, 30d, 60d, 1y) +- Configurable number of top coins to include +- Multiple currency support +- Market cap ranking included + +Examples: +- "Show me the biggest gainers and losers today" +- "What are the top movers in EUR for the past week?" +- "Show me monthly performance of top 100 coins" + +## Response Format + +All actions return structured data including: +- Formatted text for easy reading +- Raw data for programmatic use +- Request parameters used +- Error details when applicable + +## Error Handling + +The plugin handles various error scenarios: +- Rate limiting +- API key validation +- Invalid parameters +- Network issues +- Pro plan requirements \ No newline at end of file diff --git a/packages/plugin-coingecko/src/actions/getMarkets.ts b/packages/plugin-coingecko/src/actions/getMarkets.ts new file mode 100644 index 0000000000..5a32ad903c --- /dev/null +++ b/packages/plugin-coingecko/src/actions/getMarkets.ts @@ -0,0 +1,308 @@ +import { + ActionExample, + composeContext, + Content, + elizaLogger, + generateObject, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + type Action +} from "@elizaos/core"; +import axios from "axios"; +import { z } from "zod"; +import { getApiConfig, validateCoingeckoConfig } from "../environment"; +import { getCategoriesData } from '../providers/categoriesProvider'; +import { getMarketsTemplate } from "../templates/markets"; + +interface CategoryItem { + category_id: string; + name: string; +} + +export function formatCategory(category: string | undefined, categories: CategoryItem[]): string | undefined { + if (!category) return undefined; + + const normalizedInput = category.toLowerCase().trim(); + + // First try to find exact match by category_id + const exactMatch = categories.find(c => c.category_id === normalizedInput); + if (exactMatch) { + return exactMatch.category_id; + } + + // Then try to find match by name + const nameMatch = categories.find(c => + c.name.toLowerCase() === normalizedInput || + c.name.toLowerCase().replace(/[^a-z0-9]+/g, '-') === normalizedInput + ); + if (nameMatch) { + return nameMatch.category_id; + } + + // Try to find partial matches + const partialMatch = categories.find(c => + c.name.toLowerCase().includes(normalizedInput) || + c.category_id.includes(normalizedInput) + ); + if (partialMatch) { + return partialMatch.category_id; + } + + return undefined; +} + +/** + * Interface for CoinGecko /coins/markets endpoint response + * @see https://docs.coingecko.com/reference/coins-markets + */ +export interface CoinMarketData { + id: string; + symbol: string; + name: string; + image: string; + current_price: number; + market_cap: number; + market_cap_rank: number; + fully_diluted_valuation: number; + total_volume: number; + high_24h: number; + low_24h: number; + price_change_24h: number; + price_change_percentage_24h: number; + market_cap_change_24h: number; + market_cap_change_percentage_24h: number; + circulating_supply: number; + total_supply: number; + max_supply: number; + ath: number; + ath_change_percentage: number; + ath_date: string; + atl: number; + atl_change_percentage: number; + atl_date: string; + last_updated: string; +} + +export const GetMarketsSchema = z.object({ + vs_currency: z.string().default('usd'), + category: z.string().optional(), + order: z.enum(['market_cap_desc', 'market_cap_asc', 'volume_desc', 'volume_asc']).default('market_cap_desc'), + per_page: z.number().min(1).max(250).default(20), + page: z.number().min(1).default(1), + sparkline: z.boolean().default(false) +}); + +export type GetMarketsContent = z.infer & Content; + +export const isGetMarketsContent = (obj: any): obj is GetMarketsContent => { + return GetMarketsSchema.safeParse(obj).success; +}; + +export default { + name: "GET_MARKETS", + similes: [ + "MARKET_OVERVIEW", + "TOP_RANKINGS", + "MARKET_LEADERBOARD", + "CRYPTO_RANKINGS", + "BEST_PERFORMING_COINS", + "TOP_MARKET_CAPS" + ], + validate: async (runtime: IAgentRuntime, message: Memory) => { + await validateCoingeckoConfig(runtime); + return true; + }, + // Comprehensive endpoint for market rankings, supports up to 250 coins per request + description: "Get ranked list of top cryptocurrencies sorted by market metrics (without specifying coins)", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + elizaLogger.log("Starting CoinGecko GET_MARKETS handler..."); + + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + try { + const config = await validateCoingeckoConfig(runtime); + const { baseUrl, apiKey } = getApiConfig(config); + + // Get categories through the provider + const categories = await getCategoriesData(runtime); + + // Compose markets context with categories + const marketsContext = composeContext({ + state, + template: getMarketsTemplate.replace('{{categories}}', + categories.map(c => `- ${c.name} (ID: ${c.category_id})`).join('\n') + ), + }); + + const result = await generateObject({ + runtime, + context: marketsContext, + modelClass: ModelClass.SMALL, + schema: GetMarketsSchema + }); + + if (!isGetMarketsContent(result.object)) { + elizaLogger.error("Invalid market data format received"); + return false; + } + + const content = result.object; + elizaLogger.log("Content from template:", content); + + // If template returns null, this is not a markets request + if (!content) { + return false; + } + + const formattedCategory = formatCategory(content.category, categories); + if (content.category && !formattedCategory) { + throw new Error(`Invalid category: ${content.category}. Please choose from the available categories.`); + } + + elizaLogger.log("Making API request with params:", { + url: `${baseUrl}/coins/markets`, + category: formattedCategory, + vs_currency: content.vs_currency, + order: content.order, + per_page: content.per_page, + page: content.page + }); + + const response = await axios.get( + `${baseUrl}/coins/markets`, + { + headers: { + 'accept': 'application/json', + 'x-cg-pro-api-key': apiKey + }, + params: { + vs_currency: content.vs_currency, + category: formattedCategory, + order: content.order, + per_page: content.per_page, + page: content.page, + sparkline: content.sparkline + } + } + ); + + if (!response.data?.length) { + throw new Error("No market data received from CoinGecko API"); + } + + const formattedData = response.data.map(coin => ({ + name: coin.name, + symbol: coin.symbol.toUpperCase(), + marketCapRank: coin.market_cap_rank, + currentPrice: coin.current_price, + priceChange24h: coin.price_change_24h, + priceChangePercentage24h: coin.price_change_percentage_24h, + marketCap: coin.market_cap, + volume24h: coin.total_volume, + high24h: coin.high_24h, + low24h: coin.low_24h, + circulatingSupply: coin.circulating_supply, + totalSupply: coin.total_supply, + maxSupply: coin.max_supply, + lastUpdated: coin.last_updated + })); + + const categoryDisplay = content.category ? + `${categories.find(c => c.category_id === formattedCategory)?.name.toUpperCase() || content.category.toUpperCase()} ` : ''; + + const responseText = [ + `Top ${formattedData.length} ${categoryDisplay}Cryptocurrencies by ${content.order === 'volume_desc' || content.order === 'volume_asc' ? 'Volume' : 'Market Cap'}:`, + ...formattedData.map((coin, index) => + `${index + 1}. ${coin.name} (${coin.symbol})` + + ` | $${coin.currentPrice.toLocaleString()}` + + ` | ${coin.priceChangePercentage24h.toFixed(2)}%` + + ` | MCap: $${(coin.marketCap / 1e9).toFixed(2)}B` + ) + ].join('\n'); + + elizaLogger.success("Market data retrieved successfully!"); + + if (callback) { + callback({ + text: responseText, + content: { + markets: formattedData, + params: { + vs_currency: content.vs_currency, + category: content.category, + order: content.order, + per_page: content.per_page, + page: content.page + }, + timestamp: new Date().toISOString() + } + }); + } + + return true; + } catch (error) { + elizaLogger.error("Error in GET_MARKETS handler:", error); + + let errorMessage; + if (error.response?.status === 429) { + errorMessage = "Rate limit exceeded. Please try again later."; + } else if (error.response?.status === 403) { + errorMessage = "This endpoint requires a CoinGecko Pro API key. Please upgrade your plan to access this data."; + } else if (error.response?.status === 400) { + errorMessage = "Invalid request parameters. Please check your input."; + } else { + errorMessage = `Error fetching market data: ${error.message}`; + } + + if (callback) { + callback({ + text: errorMessage, + error: { + message: error.message, + statusCode: error.response?.status, + params: error.config?.params, + requiresProPlan: error.response?.status === 403 + } + }); + } + return false; + } + }, + + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Show me the top cryptocurrencies by market cap", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll fetch the current market data for top cryptocurrencies.", + action: "GET_MARKETS", + }, + }, + { + user: "{{agent}}", + content: { + text: "Here are the top cryptocurrencies:\n1. Bitcoin (BTC) | $45,000 | +2.5% | MCap: $870.5B\n{{dynamic}}", + }, + }, + ], + ] as ActionExample[][], +} as Action; \ No newline at end of file diff --git a/packages/plugin-coingecko/src/actions/getPrice.ts b/packages/plugin-coingecko/src/actions/getPrice.ts index deb923b2e9..7e47db4f3f 100644 --- a/packages/plugin-coingecko/src/actions/getPrice.ts +++ b/packages/plugin-coingecko/src/actions/getPrice.ts @@ -3,7 +3,7 @@ import { composeContext, Content, elizaLogger, - generateObjectDeprecated, + generateObject, HandlerCallback, IAgentRuntime, Memory, @@ -12,28 +12,65 @@ import { type Action, } from "@elizaos/core"; import axios from "axios"; -import { validateCoingeckoConfig } from "../environment"; +import { z } from "zod"; +import { getApiConfig, validateCoingeckoConfig } from "../environment"; +import { getCoinsData } from "../providers/coinsProvider"; import { getPriceTemplate } from "../templates/price"; -import { normalizeCoinId } from "../utils/coin"; -export interface GetPriceContent extends Content { - coinId: string; - currency: string; +interface CurrencyData { + [key: string]: number; + usd?: number; + eur?: number; + usd_market_cap?: number; + eur_market_cap?: number; + usd_24h_vol?: number; + eur_24h_vol?: number; + usd_24h_change?: number; + eur_24h_change?: number; + last_updated_at?: number; +} + +interface PriceResponse { + [coinId: string]: CurrencyData; +} + +export const GetPriceSchema = z.object({ + coinIds: z.union([z.string(), z.array(z.string())]), + currency: z.union([z.string(), z.array(z.string())]).default(["usd"]), + include_market_cap: z.boolean().default(false), + include_24hr_vol: z.boolean().default(false), + include_24hr_change: z.boolean().default(false), + include_last_updated_at: z.boolean().default(false) +}); + +export type GetPriceContent = z.infer & Content; + +export const isGetPriceContent = (obj: any): obj is GetPriceContent => { + return GetPriceSchema.safeParse(obj).success; +}; + +function formatCoinIds(input: string | string[]): string { + if (Array.isArray(input)) { + return input.join(','); + } + return input; } export default { name: "GET_PRICE", similes: [ - "CHECK_PRICE", - "PRICE_CHECK", - "GET_CRYPTO_PRICE", - "CHECK_CRYPTO_PRICE", + "COIN_PRICE_CHECK", + "SPECIFIC_COINS_PRICE", + "COIN_PRICE_LOOKUP", + "SELECTED_COINS_PRICE", + "PRICE_DETAILS", + "COIN_PRICE_DATA" ], validate: async (runtime: IAgentRuntime, message: Memory) => { await validateCoingeckoConfig(runtime); return true; }, - description: "Get the current price of a cryptocurrency from CoinGecko", + description: "Get price and basic market data for one or more specific cryptocurrencies (by name/symbol)", handler: async ( runtime: IAgentRuntime, message: Memory, @@ -43,7 +80,6 @@ export default { ): Promise => { elizaLogger.log("Starting CoinGecko GET_PRICE handler..."); - // Initialize or update state if (!state) { state = (await runtime.composeState(message)) as State; } else { @@ -51,78 +87,194 @@ export default { } try { - // Compose price check context elizaLogger.log("Composing price context..."); const priceContext = composeContext({ state, template: getPriceTemplate, }); - elizaLogger.log("Composing content..."); - const content = (await generateObjectDeprecated({ + elizaLogger.log("Generating content from template..."); + const result = await generateObject({ runtime, context: priceContext, modelClass: ModelClass.LARGE, - })) as unknown as GetPriceContent; + schema: GetPriceSchema + }); - // Validate content structure first - if (!content || typeof content !== "object") { - throw new Error("Invalid response format from model"); + if (!isGetPriceContent(result.object)) { + elizaLogger.error("Invalid price request format"); + return false; } - // Get and validate coin ID - const coinId = content.coinId - ? normalizeCoinId(content.coinId) - : null; - if (!coinId) { - throw new Error( - `Unsupported or invalid cryptocurrency: ${content.coinId}` - ); - } + const content = result.object; + elizaLogger.log("Generated content:", content); + + // Format currencies for API request + const currencies = Array.isArray(content.currency) ? content.currency : [content.currency]; + const vs_currencies = currencies.join(',').toLowerCase(); - // Normalize currency - const currency = (content.currency || "usd").toLowerCase(); + // Format coin IDs for API request + const coinIds = formatCoinIds(content.coinIds); + + elizaLogger.log("Formatted request parameters:", { coinIds, vs_currencies }); // Fetch price from CoinGecko const config = await validateCoingeckoConfig(runtime); - elizaLogger.log(`Fetching price for ${coinId} in ${currency}...`); + const { baseUrl, apiKey } = getApiConfig(config); - const response = await axios.get( - `https://api.coingecko.com/api/v3/simple/price`, + elizaLogger.log(`Fetching prices for ${coinIds} in ${vs_currencies}...`); + elizaLogger.log("API request URL:", `${baseUrl}/simple/price`); + elizaLogger.log("API request params:", { + ids: coinIds, + vs_currencies, + include_market_cap: content.include_market_cap, + include_24hr_vol: content.include_24hr_vol, + include_24hr_change: content.include_24hr_change, + include_last_updated_at: content.include_last_updated_at + }); + + const response = await axios.get( + `${baseUrl}/simple/price`, { params: { - ids: coinId, - vs_currencies: currency, - x_cg_demo_api_key: config.COINGECKO_API_KEY, + ids: coinIds, + vs_currencies, + include_market_cap: content.include_market_cap, + include_24hr_vol: content.include_24hr_vol, + include_24hr_change: content.include_24hr_change, + include_last_updated_at: content.include_last_updated_at }, + headers: { + 'accept': 'application/json', + 'x-cg-pro-api-key': apiKey + } } ); - if (!response.data[coinId]?.[currency]) { - throw new Error( - `No price data available for ${coinId} in ${currency}` - ); + if (Object.keys(response.data).length === 0) { + throw new Error("No price data available for the specified coins and currency"); } - const price = response.data[coinId][currency]; - elizaLogger.success( - `Price retrieved successfully! ${coinId}: ${price} ${currency.toUpperCase()}` - ); + // Get coins data for formatting + const coins = await getCoinsData(runtime); + + // Format response text for each coin + const formattedResponse = Object.entries(response.data).map(([coinId, data]) => { + const coin = coins.find(c => c.id === coinId); + const coinName = coin ? `${coin.name} (${coin.symbol.toUpperCase()})` : coinId; + const parts = [coinName + ':']; + + // Add price for each requested currency + currencies.forEach(currency => { + const upperCurrency = currency.toUpperCase(); + if (data[currency]) { + parts.push(` ${upperCurrency}: ${data[currency].toLocaleString(undefined, { + style: 'currency', + currency: currency + })}`); + } + + // Add market cap if requested and available + if (content.include_market_cap) { + const marketCap = data[`${currency}_market_cap`]; + if (marketCap !== undefined) { + parts.push(` Market Cap (${upperCurrency}): ${marketCap.toLocaleString(undefined, { + style: 'currency', + currency: currency, + maximumFractionDigits: 0 + })}`); + } + } + + // Add 24h volume if requested and available + if (content.include_24hr_vol) { + const volume = data[`${currency}_24h_vol`]; + if (volume !== undefined) { + parts.push(` 24h Volume (${upperCurrency}): ${volume.toLocaleString(undefined, { + style: 'currency', + currency: currency, + maximumFractionDigits: 0 + })}`); + } + } + + // Add 24h change if requested and available + if (content.include_24hr_change) { + const change = data[`${currency}_24h_change`]; + if (change !== undefined) { + const changePrefix = change >= 0 ? '+' : ''; + parts.push(` 24h Change (${upperCurrency}): ${changePrefix}${change.toFixed(2)}%`); + } + } + }); + + // Add last updated if requested + if (content.include_last_updated_at && data.last_updated_at) { + const lastUpdated = new Date(data.last_updated_at * 1000).toLocaleString(); + parts.push(` Last Updated: ${lastUpdated}`); + } + + return parts.join('\n'); + }).filter(Boolean); + + if (formattedResponse.length === 0) { + throw new Error("Failed to format price data for the specified coins"); + } + + const responseText = formattedResponse.join('\n\n'); + elizaLogger.success("Price data retrieved successfully!"); if (callback) { callback({ - text: `The current price of ${coinId} is ${price} ${currency.toUpperCase()}`, - content: { price, currency }, + text: responseText, + content: { + prices: Object.entries(response.data).reduce((acc, [coinId, data]) => ({ + ...acc, + [coinId]: currencies.reduce((currencyAcc, currency) => ({ + ...currencyAcc, + [currency]: { + price: data[currency], + marketCap: data[`${currency}_market_cap`], + volume24h: data[`${currency}_24h_vol`], + change24h: data[`${currency}_24h_change`], + lastUpdated: data.last_updated_at, + } + }), {}) + }), {}), + params: { + currencies: currencies.map(c => c.toUpperCase()), + include_market_cap: content.include_market_cap, + include_24hr_vol: content.include_24hr_vol, + include_24hr_change: content.include_24hr_change, + include_last_updated_at: content.include_last_updated_at + } + } }); } return true; } catch (error) { elizaLogger.error("Error in GET_PRICE handler:", error); + + let errorMessage; + if (error.response?.status === 429) { + errorMessage = "Rate limit exceeded. Please try again later."; + } else if (error.response?.status === 403) { + errorMessage = "This endpoint requires a CoinGecko Pro API key. Please upgrade your plan to access this data."; + } else if (error.response?.status === 400) { + errorMessage = "Invalid request parameters. Please check your input."; + } else { + } + if (callback) { callback({ - text: `Error fetching price: ${error.message}`, - content: { error: error.message }, + text: errorMessage, + content: { + error: error.message, + statusCode: error.response?.status, + params: error.config?.params, + requiresProPlan: error.response?.status === 403 + }, }); } return false; @@ -147,7 +299,7 @@ export default { { user: "{{agent}}", content: { - text: "The current price of bitcoin is {{dynamic}} USD", + text: "The current price of Bitcoin is {{dynamic}} USD", }, }, ], @@ -155,20 +307,20 @@ export default { { user: "{{user1}}", content: { - text: "Check ETH price in EUR", + text: "Check ETH and BTC prices in EUR with market cap", }, }, { user: "{{agent}}", content: { - text: "I'll check the current Ethereum price in EUR for you.", + text: "I'll check the current prices with market cap data.", action: "GET_PRICE", }, }, { user: "{{agent}}", content: { - text: "The current price of ethereum is {{dynamic}} EUR", + text: "Bitcoin: EUR {{dynamic}} | Market Cap: €{{dynamic}}\nEthereum: EUR {{dynamic}} | Market Cap: €{{dynamic}}", }, }, ], diff --git a/packages/plugin-coingecko/src/actions/getTopGainersLosers.ts b/packages/plugin-coingecko/src/actions/getTopGainersLosers.ts new file mode 100644 index 0000000000..c8b8b67fb9 --- /dev/null +++ b/packages/plugin-coingecko/src/actions/getTopGainersLosers.ts @@ -0,0 +1,249 @@ +import { + ActionExample, + composeContext, + Content, + elizaLogger, + generateObject, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + type Action +} from "@elizaos/core"; +import axios from "axios"; +import { z } from "zod"; +import { getApiConfig, validateCoingeckoConfig } from "../environment"; +import { getTopGainersLosersTemplate } from "../templates/gainersLosers"; + +interface TopGainerLoserItem { + id: string; + symbol: string; + name: string; + image: string; + market_cap_rank: number; + usd: number; + usd_24h_vol: number; + usd_1h_change?: number; + usd_24h_change?: number; + usd_7d_change?: number; + usd_14d_change?: number; + usd_30d_change?: number; + usd_60d_change?: number; + usd_1y_change?: number; +} + +interface TopGainersLosersResponse { + top_gainers: TopGainerLoserItem[]; + top_losers: TopGainerLoserItem[]; +} + +const DurationEnum = z.enum(["1h", "24h", "7d", "14d", "30d", "60d", "1y"]); +type Duration = z.infer; + +export const GetTopGainersLosersSchema = z.object({ + vs_currency: z.string().default("usd"), + duration: DurationEnum.default("24h"), + top_coins: z.string().default("1000") +}); + +export type GetTopGainersLosersContent = z.infer & Content; + +export const isGetTopGainersLosersContent = (obj: any): obj is GetTopGainersLosersContent => { + return GetTopGainersLosersSchema.safeParse(obj).success; +}; + +export default { + name: "GET_TOP_GAINERS_LOSERS", + similes: [ + "TOP_MOVERS", + "BIGGEST_GAINERS", + "BIGGEST_LOSERS", + "PRICE_CHANGES", + "BEST_WORST_PERFORMERS", + ], + validate: async (runtime: IAgentRuntime, message: Memory) => { + await validateCoingeckoConfig(runtime); + return true; + }, + description: "Get list of top gaining and losing cryptocurrencies by price change", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + elizaLogger.log("Starting CoinGecko GET_TOP_GAINERS_LOSERS handler..."); + + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + try { + elizaLogger.log("Composing gainers/losers context..."); + const context = composeContext({ + state, + template: getTopGainersLosersTemplate, + }); + + elizaLogger.log("Generating content from template..."); + const result = await generateObject({ + runtime, + context, + modelClass: ModelClass.LARGE, + schema: GetTopGainersLosersSchema + }); + + if (!isGetTopGainersLosersContent(result.object)) { + elizaLogger.error("Invalid gainers/losers request format"); + return false; + } + + const content = result.object; + elizaLogger.log("Generated content:", content); + + // Fetch data from CoinGecko + const config = await validateCoingeckoConfig(runtime); + const { baseUrl, apiKey, headerKey } = getApiConfig(config); + + elizaLogger.log("Fetching top gainers/losers data..."); + elizaLogger.log("API request params:", { + vs_currency: content.vs_currency, + duration: content.duration, + top_coins: content.top_coins + }); + + const response = await axios.get( + `${baseUrl}/coins/top_gainers_losers`, + { + headers: { + 'accept': 'application/json', + [headerKey]: apiKey + }, + params: { + vs_currency: content.vs_currency, + duration: content.duration, + top_coins: content.top_coins + } + } + ); + + if (!response.data) { + throw new Error("No data received from CoinGecko API"); + } + + // Format the response text + const responseText = [ + 'Top Gainers:', + ...response.data.top_gainers.map((coin, index) => { + const changeKey = `usd_${content.duration}_change` as keyof TopGainerLoserItem; + const change = coin[changeKey] as number; + return `${index + 1}. ${coin.name} (${coin.symbol.toUpperCase()})` + + ` | $${coin.usd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 8 })}` + + ` | ${change >= 0 ? '+' : ''}${change.toFixed(2)}%` + + `${coin.market_cap_rank ? ` | Rank #${coin.market_cap_rank}` : ''}`; + }), + '', + 'Top Losers:', + ...response.data.top_losers.map((coin, index) => { + const changeKey = `usd_${content.duration}_change` as keyof TopGainerLoserItem; + const change = coin[changeKey] as number; + return `${index + 1}. ${coin.name} (${coin.symbol.toUpperCase()})` + + ` | $${coin.usd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 8 })}` + + ` | ${change >= 0 ? '+' : ''}${change.toFixed(2)}%` + + `${coin.market_cap_rank ? ` | Rank #${coin.market_cap_rank}` : ''}`; + }) + ].join('\n'); + + if (callback) { + callback({ + text: responseText, + content: { + data: response.data, + params: { + vs_currency: content.vs_currency, + duration: content.duration, + top_coins: content.top_coins + } + } + }); + } + + return true; + } catch (error) { + elizaLogger.error("Error in GET_TOP_GAINERS_LOSERS handler:", error); + + let errorMessage; + if (error.response?.status === 429) { + errorMessage = "Rate limit exceeded. Please try again later."; + } else if (error.response?.status === 403) { + errorMessage = "This endpoint requires a CoinGecko Pro API key. Please upgrade your plan to access this data."; + } else if (error.response?.status === 400) { + errorMessage = "Invalid request parameters. Please check your input."; + } else { + errorMessage = `Error fetching top gainers/losers data: ${error.message}`; + } + + if (callback) { + callback({ + text: errorMessage, + content: { + error: error.message, + statusCode: error.response?.status, + params: error.config?.params, + requiresProPlan: error.response?.status === 403 + }, + }); + } + return false; + } + }, + + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "What are the top gaining and losing cryptocurrencies?", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll check the top gainers and losers for you.", + action: "GET_TOP_GAINERS_LOSERS", + }, + }, + { + user: "{{agent}}", + content: { + text: "Here are the top gainers and losers:\nTop Gainers:\n1. Bitcoin (BTC) | $45,000 | +5.2% | Rank #1\n{{dynamic}}", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Show me the best and worst performing crypto today", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll fetch the current top movers in the crypto market.", + action: "GET_TOP_GAINERS_LOSERS", + }, + }, + { + user: "{{agent}}", + content: { + text: "Here are today's best and worst performers:\n{{dynamic}}", + }, + }, + ], + ] as ActionExample[][], +} as Action; \ No newline at end of file diff --git a/packages/plugin-coingecko/src/actions/getTrending.ts b/packages/plugin-coingecko/src/actions/getTrending.ts new file mode 100644 index 0000000000..cb2e1c1215 --- /dev/null +++ b/packages/plugin-coingecko/src/actions/getTrending.ts @@ -0,0 +1,252 @@ +import { + ActionExample, + composeContext, + Content, + elizaLogger, + generateObject, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + type Action +} from "@elizaos/core"; +import axios from "axios"; +import { z } from "zod"; +import { getApiConfig, validateCoingeckoConfig } from "../environment"; +import { getTrendingTemplate } from "../templates/trending"; + +interface TrendingCoinItem { + id: string; + name: string; + api_symbol: string; + symbol: string; + market_cap_rank: number; + thumb: string; + large: string; +} + +interface TrendingExchange { + id: string; + name: string; + market_type: string; + thumb: string; + large: string; +} + +interface TrendingCategory { + id: string; + name: string; +} + +interface TrendingNFT { + id: string; + name: string; + symbol: string; + thumb: string; +} + +interface TrendingResponse { + coins: Array<{ item: TrendingCoinItem }>; + exchanges: TrendingExchange[]; + categories: TrendingCategory[]; + nfts: TrendingNFT[]; + icos: string[]; +} + +export const GetTrendingSchema = z.object({ + include_nfts: z.boolean().default(true), + include_categories: z.boolean().default(true) +}); + +export type GetTrendingContent = z.infer & Content; + +export const isGetTrendingContent = (obj: any): obj is GetTrendingContent => { + return GetTrendingSchema.safeParse(obj).success; +}; + +export default { + name: "GET_TRENDING", + similes: [ + "TRENDING_COINS", + "TRENDING_CRYPTO", + "HOT_COINS", + "POPULAR_COINS", + "TRENDING_SEARCH", + ], + validate: async (runtime: IAgentRuntime, message: Memory) => { + await validateCoingeckoConfig(runtime); + return true; + }, + description: "Get list of trending cryptocurrencies, NFTs, and categories from CoinGecko", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + elizaLogger.log("Starting CoinGecko GET_TRENDING handler..."); + + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + try { + // Compose trending context + elizaLogger.log("Composing trending context..."); + const trendingContext = composeContext({ + state, + template: getTrendingTemplate, + }); + + const result = await generateObject({ + runtime, + context: trendingContext, + modelClass: ModelClass.LARGE, + schema: GetTrendingSchema + }); + + if (!isGetTrendingContent(result.object)) { + elizaLogger.error("Invalid trending request format"); + return false; + } + + // Fetch trending data from CoinGecko + const config = await validateCoingeckoConfig(runtime); + const { baseUrl, apiKey, headerKey } = getApiConfig(config); + + elizaLogger.log("Fetching trending data..."); + + const response = await axios.get( + `${baseUrl}/search/trending`, + { + headers: { + [headerKey]: apiKey + } + } + ); + + if (!response.data) { + throw new Error("No data received from CoinGecko API"); + } + + const formattedData = { + coins: response.data.coins.map(({ item }) => ({ + name: item.name, + symbol: item.symbol.toUpperCase(), + marketCapRank: item.market_cap_rank, + id: item.id, + thumbnail: item.thumb, + largeImage: item.large + })), + nfts: response.data.nfts.map(nft => ({ + name: nft.name, + symbol: nft.symbol, + id: nft.id, + thumbnail: nft.thumb + })), + categories: response.data.categories.map(category => ({ + name: category.name, + id: category.id + })) + }; + + const responseText = [ + 'Trending Coins:', + ...formattedData.coins.map((coin, index) => + `${index + 1}. ${coin.name} (${coin.symbol})${coin.marketCapRank ? ` - Rank #${coin.marketCapRank}` : ''}` + ), + '', + 'Trending NFTs:', + ...(formattedData.nfts.length ? + formattedData.nfts.map((nft, index) => `${index + 1}. ${nft.name} (${nft.symbol})`) : + ['No trending NFTs available']), + '', + 'Trending Categories:', + ...(formattedData.categories.length ? + formattedData.categories.map((category, index) => `${index + 1}. ${category.name}`) : + ['No trending categories available']) + ].join('\n'); + + elizaLogger.success("Trending data retrieved successfully!"); + + if (callback) { + callback({ + text: responseText, + content: { + trending: formattedData, + timestamp: new Date().toISOString() + } + }); + } + + return true; + } catch (error) { + elizaLogger.error("Error in GET_TRENDING handler:", error); + + // Enhanced error handling + const errorMessage = error.response?.status === 429 ? + "Rate limit exceeded. Please try again later." : + `Error fetching trending data: ${error.message}`; + + if (callback) { + callback({ + text: errorMessage, + content: { + error: error.message, + statusCode: error.response?.status + }, + }); + } + return false; + } + }, + + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "What are the trending cryptocurrencies?", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll check the trending cryptocurrencies for you.", + action: "GET_TRENDING", + }, + }, + { + user: "{{agent}}", + content: { + text: "Here are the trending cryptocurrencies:\n1. Bitcoin (BTC) - Rank #1\n2. Ethereum (ETH) - Rank #2\n{{dynamic}}", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Show me what's hot in crypto right now", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll fetch the current trending cryptocurrencies.", + action: "GET_TRENDING", + }, + }, + { + user: "{{agent}}", + content: { + text: "Here are the trending cryptocurrencies:\n{{dynamic}}", + }, + }, + ], + ] as ActionExample[][], +} as Action; \ No newline at end of file diff --git a/packages/plugin-coingecko/src/constants.ts b/packages/plugin-coingecko/src/constants.ts new file mode 100644 index 0000000000..7da5d70141 --- /dev/null +++ b/packages/plugin-coingecko/src/constants.ts @@ -0,0 +1,7 @@ +export const API_URLS = { + FREE: 'https://api.coingecko.com/api/v3', + PRO: 'https://pro-api.coingecko.com/api/v3' +} as const; + +// We'll determine which URL to use based on API key validation/usage +export const DEFAULT_BASE_URL = API_URLS.FREE; \ No newline at end of file diff --git a/packages/plugin-coingecko/src/environment.ts b/packages/plugin-coingecko/src/environment.ts index 276658e371..d7733bbd53 100644 --- a/packages/plugin-coingecko/src/environment.ts +++ b/packages/plugin-coingecko/src/environment.ts @@ -1,30 +1,29 @@ import { IAgentRuntime } from "@elizaos/core"; import { z } from "zod"; -export const coingeckoEnvSchema = z.object({ - COINGECKO_API_KEY: z.string().min(1, "CoinGecko API key is required"), +const coingeckoConfigSchema = z.object({ + COINGECKO_API_KEY: z.string().nullable(), + COINGECKO_PRO_API_KEY: z.string().nullable(), +}).refine(data => data.COINGECKO_API_KEY || data.COINGECKO_PRO_API_KEY, { + message: "Either COINGECKO_API_KEY or COINGECKO_PRO_API_KEY must be provided" }); -export type CoingeckoConfig = z.infer; +export type CoingeckoConfig = z.infer; -export async function validateCoingeckoConfig( - runtime: IAgentRuntime -): Promise { - try { - const config = { - COINGECKO_API_KEY: runtime.getSetting("COINGECKO_API_KEY"), - }; +export async function validateCoingeckoConfig(runtime: IAgentRuntime): Promise { + const config = { + COINGECKO_API_KEY: runtime.getSetting("COINGECKO_API_KEY"), + COINGECKO_PRO_API_KEY: runtime.getSetting("COINGECKO_PRO_API_KEY"), + }; - return coingeckoEnvSchema.parse(config); - } catch (error) { - if (error instanceof z.ZodError) { - const errorMessages = error.errors - .map((err) => `${err.path.join(".")}: ${err.message}`) - .join("\n"); - throw new Error( - `CoinGecko configuration validation failed:\n${errorMessages}` - ); - } - throw error; - } + return coingeckoConfigSchema.parse(config); +} + +export function getApiConfig(config: CoingeckoConfig) { + const isPro = !!config.COINGECKO_PRO_API_KEY; + return { + baseUrl: isPro ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3", + apiKey: isPro ? config.COINGECKO_PRO_API_KEY : config.COINGECKO_API_KEY, + headerKey: isPro ? "x-cg-pro-api-key" : "x-cg-demo-api-key" + }; } diff --git a/packages/plugin-coingecko/src/index.ts b/packages/plugin-coingecko/src/index.ts index b2962f1072..5aceca34b2 100644 --- a/packages/plugin-coingecko/src/index.ts +++ b/packages/plugin-coingecko/src/index.ts @@ -1,12 +1,17 @@ import { Plugin } from "@elizaos/core"; +import getMarkets from "./actions/getMarkets"; import getPrice from "./actions/getPrice"; +import getTopGainersLosers from "./actions/getTopGainersLosers"; +import getTrending from "./actions/getTrending"; +import { categoriesProvider } from "./providers/categoriesProvider"; +import { coinsProvider } from "./providers/coinsProvider"; export const coingeckoPlugin: Plugin = { name: "coingecko", description: "CoinGecko Plugin for Eliza", - actions: [getPrice], + actions: [getPrice, getTrending, getMarkets, getTopGainersLosers], evaluators: [], - providers: [], + providers: [categoriesProvider, coinsProvider], }; export default coingeckoPlugin; diff --git a/packages/plugin-coingecko/src/providers/categoriesProvider.ts b/packages/plugin-coingecko/src/providers/categoriesProvider.ts new file mode 100644 index 0000000000..6264b642ea --- /dev/null +++ b/packages/plugin-coingecko/src/providers/categoriesProvider.ts @@ -0,0 +1,110 @@ +import { IAgentRuntime, Memory, Provider, State, elizaLogger } from "@elizaos/core"; +import axios from 'axios'; +import { getApiConfig, validateCoingeckoConfig } from '../environment'; + +interface CategoryItem { + category_id: string; + name: string; +} + +const CACHE_KEY = 'coingecko:categories'; +const CACHE_TTL = 5 * 60; // 5 minutes +const MAX_RETRIES = 3; + +async function fetchCategories(runtime: IAgentRuntime): Promise { + const config = await validateCoingeckoConfig(runtime); + const { baseUrl, apiKey } = getApiConfig(config); + + const response = await axios.get( + `${baseUrl}/coins/categories/list`, + { + headers: { + 'accept': 'application/json', + 'x-cg-pro-api-key': apiKey + }, + timeout: 5000 // 5 second timeout + } + ); + + if (!response.data?.length) { + throw new Error("Invalid categories data received"); + } + + return response.data; +} + +async function fetchWithRetry(runtime: IAgentRuntime): Promise { + let lastError: Error | null = null; + + for (let i = 0; i < MAX_RETRIES; i++) { + try { + return await fetchCategories(runtime); + } catch (error) { + lastError = error; + elizaLogger.error(`Categories fetch attempt ${i + 1} failed:`, error); + await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); + } + } + + throw lastError || new Error("Failed to fetch categories after multiple attempts"); +} + +async function getCategories(runtime: IAgentRuntime): Promise { + try { + // Try to get from cache first + const cached = await runtime.cacheManager.get(CACHE_KEY); + if (cached) { + return cached; + } + + // Fetch fresh data + const categories = await fetchWithRetry(runtime); + + // Cache the result + await runtime.cacheManager.set(CACHE_KEY, categories, { expires: CACHE_TTL }); + + return categories; + } catch (error) { + elizaLogger.error("Error fetching categories:", error); + throw error; + } +} + +function formatCategoriesContext(categories: CategoryItem[]): string { + const popularCategories = [ + 'layer-1', 'defi', 'meme', 'ai-meme-coins', + 'artificial-intelligence', 'gaming', 'metaverse' + ]; + + const popular = categories + .filter(c => popularCategories.includes(c.category_id)) + .map(c => `${c.name} (${c.category_id})`); + + return ` +Available cryptocurrency categories: + +Popular categories: +${popular.map(c => `- ${c}`).join('\n')} + +Total available categories: ${categories.length} + +You can use these category IDs when filtering cryptocurrency market data. +`.trim(); +} + +export const categoriesProvider: Provider = { + get: async (runtime: IAgentRuntime, message: Memory, state?: State): Promise => { + try { + const categories = await getCategories(runtime); + return formatCategoriesContext(categories); + } catch (error) { + elizaLogger.error("Categories provider error:", error); + return "Cryptocurrency categories are temporarily unavailable. Please try again later."; + } + } +}; + +// Helper function for actions to get raw categories data +export async function getCategoriesData(runtime: IAgentRuntime): Promise { + return getCategories(runtime); +} \ No newline at end of file diff --git a/packages/plugin-coingecko/src/providers/coinsProvider.ts b/packages/plugin-coingecko/src/providers/coinsProvider.ts new file mode 100644 index 0000000000..b45d93e06b --- /dev/null +++ b/packages/plugin-coingecko/src/providers/coinsProvider.ts @@ -0,0 +1,114 @@ +import { IAgentRuntime, Memory, Provider, State, elizaLogger } from "@elizaos/core"; +import axios from 'axios'; +import { getApiConfig, validateCoingeckoConfig } from '../environment'; + +interface CoinItem { + id: string; + symbol: string; + name: string; +} + +const CACHE_KEY = 'coingecko:coins'; +const CACHE_TTL = 5 * 60; // 5 minutes +const MAX_RETRIES = 3; + +async function fetchCoins(runtime: IAgentRuntime, includePlatform: boolean = false): Promise { + const config = await validateCoingeckoConfig(runtime); + const { baseUrl, apiKey } = getApiConfig(config); + + const response = await axios.get( + `${baseUrl}/coins/list`, + { + params: { + include_platform: includePlatform + }, + headers: { + 'accept': 'application/json', + 'x-cg-pro-api-key': apiKey + }, + timeout: 5000 // 5 second timeout + } + ); + + if (!response.data?.length) { + throw new Error("Invalid coins data received"); + } + + return response.data; +} + +async function fetchWithRetry(runtime: IAgentRuntime, includePlatform: boolean = false): Promise { + let lastError: Error | null = null; + + for (let i = 0; i < MAX_RETRIES; i++) { + try { + return await fetchCoins(runtime, includePlatform); + } catch (error) { + lastError = error; + elizaLogger.error(`Coins fetch attempt ${i + 1} failed:`, error); + await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); + } + } + + throw lastError || new Error("Failed to fetch coins after multiple attempts"); +} + +async function getCoins(runtime: IAgentRuntime, includePlatform: boolean = false): Promise { + try { + // Try to get from cache first + const cached = await runtime.cacheManager.get(CACHE_KEY); + if (cached) { + return cached; + } + + // Fetch fresh data + const coins = await fetchWithRetry(runtime, includePlatform); + + // Cache the result + await runtime.cacheManager.set(CACHE_KEY, coins, { expires: CACHE_TTL }); + + return coins; + } catch (error) { + elizaLogger.error("Error fetching coins:", error); + throw error; + } +} + +function formatCoinsContext(coins: CoinItem[]): string { + const popularCoins = [ + 'bitcoin', 'ethereum', 'binancecoin', 'ripple', + 'cardano', 'solana', 'polkadot', 'dogecoin' + ]; + + const popular = coins + .filter(c => popularCoins.includes(c.id)) + .map(c => `${c.name} (${c.symbol.toUpperCase()}) - ID: ${c.id}`); + + return ` +Available cryptocurrencies: + +Popular coins: +${popular.map(c => `- ${c}`).join('\n')} + +Total available coins: ${coins.length} + +You can use these coin IDs when querying specific cryptocurrency data. +`.trim(); +} + +export const coinsProvider: Provider = { + get: async (runtime: IAgentRuntime, message: Memory, state?: State): Promise => { + try { + const coins = await getCoins(runtime); + return formatCoinsContext(coins); + } catch (error) { + elizaLogger.error("Coins provider error:", error); + return "Cryptocurrency list is temporarily unavailable. Please try again later."; + } + } +}; + +// Helper function for actions to get raw coins data +export async function getCoinsData(runtime: IAgentRuntime, includePlatform: boolean = false): Promise { + return getCoins(runtime, includePlatform); +} \ No newline at end of file diff --git a/packages/plugin-coingecko/src/templates/gainersLosers.ts b/packages/plugin-coingecko/src/templates/gainersLosers.ts new file mode 100644 index 0000000000..73c104e767 --- /dev/null +++ b/packages/plugin-coingecko/src/templates/gainersLosers.ts @@ -0,0 +1,50 @@ +export const getTopGainersLosersTemplate = ` +Extract the following parameters for top gainers and losers data: +- **vs_currency** (string): The target currency to display prices in (e.g., "usd", "eur") - defaults to "usd" +- **duration** (string): Time range for price changes - one of "24h", "7d", "14d", "30d", "60d", "1y" - defaults to "24h" +- **top_coins** (string): Filter by market cap ranking (e.g., "100", "1000") - defaults to "1000" + +Provide the values in the following JSON format: + +\`\`\`json +{ + "vs_currency": "usd", + "duration": "24h", + "top_coins": "1000" +} +\`\`\` + +Example request: "Show me the biggest gainers and losers today" +Example response: +\`\`\`json +{ + "vs_currency": "usd", + "duration": "24h", + "top_coins": "1000" +} +\`\`\` + +Example request: "What are the top movers in EUR for the past week?" +Example response: +\`\`\`json +{ + "vs_currency": "eur", + "duration": "7d", + "top_coins": "300" +} +\`\`\` + +Example request: "Show me monthly performance of top 100 coins" +Example response: +\`\`\`json +{ + "vs_currency": "usd", + "duration": "30d", + "top_coins": "100" +} +\`\`\` + +Here are the recent user messages for context: +{{recentMessages}} + +Based on the conversation above, if the request is for top gainers and losers data, extract the appropriate parameters and respond with a JSON object. If the request is not related to top movers data, respond with null.`; \ No newline at end of file diff --git a/packages/plugin-coingecko/src/templates/markets.ts b/packages/plugin-coingecko/src/templates/markets.ts new file mode 100644 index 0000000000..6610ea5b7e --- /dev/null +++ b/packages/plugin-coingecko/src/templates/markets.ts @@ -0,0 +1,56 @@ +export const getMarketsTemplate = ` +Extract the following parameters for market listing: +- **vs_currency** (string): Target currency for price data (default: "usd") +- **category** (string, optional): Specific category ID from the available categories +- **per_page** (number): Number of results to return (1-250, default: 20) +- **order** (string): Sort order for results, one of: + - market_cap_desc: Highest market cap first + - market_cap_asc: Lowest market cap first + - volume_desc: Highest volume first + - volume_asc: Lowest volume first + +Available Categories: +{{categories}} + +Provide the values in the following JSON format: + +\`\`\`json +{ + "vs_currency": "", + "category": "", + "per_page": , + "order": "", + "page": 1, + "sparkline": false +} +\`\`\` + +Example request: "Show me the top 10 gaming cryptocurrencies" +Example response: +\`\`\`json +{ + "vs_currency": "usd", + "category": "gaming", + "per_page": 10, + "order": "market_cap_desc", + "page": 1, + "sparkline": false +} +\`\`\` + +Example request: "What are the best performing coins by volume?" +Example response: +\`\`\`json +{ + "vs_currency": "usd", + "per_page": 20, + "order": "volume_desc", + "page": 1, + "sparkline": false +} +\`\`\` + +Here are the recent user messages for context: +{{recentMessages}} + +Based on the conversation above, if the request is for a market listing/ranking, extract the appropriate parameters and respond with a JSON object. If the request is for specific coins only, respond with null.`; \ No newline at end of file diff --git a/packages/plugin-coingecko/src/templates/price.ts b/packages/plugin-coingecko/src/templates/price.ts index e30175c6bf..6245bbe26e 100644 --- a/packages/plugin-coingecko/src/templates/price.ts +++ b/packages/plugin-coingecko/src/templates/price.ts @@ -1,31 +1,65 @@ -export const getPriceTemplate = `Given the message, extract information about the cryptocurrency price check request. Look for coin name/symbol and currency. +export const getPriceTemplate = ` +Extract the following parameters for cryptocurrency price data: +- **coinIds** (string | string[]): The ID(s) of the cryptocurrency/cryptocurrencies to get prices for (e.g., "bitcoin" or ["bitcoin", "ethereum"]) +- **currency** (string | string[]): The currency/currencies to display prices in (e.g., "usd" or ["usd", "eur", "jpy"]) - defaults to ["usd"] +- **include_market_cap** (boolean): Whether to include market cap data - defaults to false +- **include_24hr_vol** (boolean): Whether to include 24h volume data - defaults to false +- **include_24hr_change** (boolean): Whether to include 24h price change data - defaults to false +- **include_last_updated_at** (boolean): Whether to include last update timestamp - defaults to false -Common coin mappings: -- BTC/Bitcoin -> "bitcoin" -- ETH/Ethereum -> "ethereum" -- USDC -> "usd-coin" +Provide the values in the following JSON format: -Format the response as a JSON object with these fields: -- coinId: the normalized coin ID (e.g., "bitcoin", "ethereum", "usd-coin") -- currency: the currency for price (default to "usd" if not specified) +\`\`\`json +{ + "coinIds": "bitcoin", + "currency": ["usd"], + "include_market_cap": false, + "include_24hr_vol": false, + "include_24hr_change": false, + "include_last_updated_at": false +} +\`\`\` + +Example request: "What's the current price of Bitcoin?" +Example response: +\`\`\`json +{ + "coinIds": "bitcoin", + "currency": ["usd"], + "include_market_cap": false, + "include_24hr_vol": false, + "include_24hr_change": false, + "include_last_updated_at": false +} +\`\`\` -Example responses: -For "What's the price of Bitcoin?": +Example request: "Show me ETH price and market cap in EUR with last update time" +Example response: \`\`\`json { - "coinId": "bitcoin", - "currency": "usd" + "coinIds": "ethereum", + "currency": ["eur"], + "include_market_cap": true, + "include_24hr_vol": false, + "include_24hr_change": false, + "include_last_updated_at": true } \`\`\` -For "Check ETH price in EUR": +Example request: "What's the current price of Bitcoin in USD, JPY and EUR?" +Example response: \`\`\`json { - "coinId": "ethereum", - "currency": "eur" + "coinIds": "bitcoin", + "currency": ["usd", "jpy", "eur"], + "include_market_cap": false, + "include_24hr_vol": false, + "include_24hr_change": false, + "include_last_updated_at": false } \`\`\` +Here are the recent user messages for context: {{recentMessages}} -Extract the cryptocurrency and currency information from the above messages and respond with the appropriate JSON.`; +Based on the conversation above, if the request is for cryptocurrency price data, extract the appropriate parameters and respond with a JSON object. If the request is not related to price data, respond with null.`; diff --git a/packages/plugin-coingecko/src/templates/trending.ts b/packages/plugin-coingecko/src/templates/trending.ts new file mode 100644 index 0000000000..073f68a0c0 --- /dev/null +++ b/packages/plugin-coingecko/src/templates/trending.ts @@ -0,0 +1,36 @@ +export const getTrendingTemplate = ` +Extract the following parameters for trending data: +- **include_nfts** (boolean): Whether to include NFTs in the response (default: true) +- **include_categories** (boolean): Whether to include categories in the response (default: true) + +Provide the values in the following JSON format: + +\`\`\`json +{ + "include_nfts": true, + "include_categories": true +} +\`\`\` + +Example request: "What's trending in crypto?" +Example response: +\`\`\`json +{ + "include_nfts": true, + "include_categories": true +} +\`\`\` + +Example request: "Show me trending coins only" +Example response: +\`\`\`json +{ + "include_nfts": false, + "include_categories": false +} +\`\`\` + +Here are the recent user messages for context: +{{recentMessages}} + +Based on the conversation above, if the request is for trending market data, extract the appropriate parameters and respond with a JSON object. If the request is not related to trending data, respond with null.`; \ No newline at end of file diff --git a/packages/plugin-coingecko/src/types.ts b/packages/plugin-coingecko/src/types.ts index c2ee9d725d..bf2eb42724 100644 --- a/packages/plugin-coingecko/src/types.ts +++ b/packages/plugin-coingecko/src/types.ts @@ -1,7 +1,8 @@ // Type definitions for CoinGecko plugin export interface CoinGeckoConfig { - apiKey?: string; + apiKey: string; + baseUrl?: string; } export interface PriceResponse { diff --git a/packages/plugin-coingecko/src/utils/coin.ts b/packages/plugin-coingecko/src/utils/coin.ts deleted file mode 100644 index 6a30d8510c..0000000000 --- a/packages/plugin-coingecko/src/utils/coin.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const COIN_ID_MAPPING = { - // Bitcoin variations - btc: "bitcoin", - bitcoin: "bitcoin", - // Ethereum variations - eth: "ethereum", - ethereum: "ethereum", - // USDC variations - usdc: "usd-coin", - "usd-coin": "usd-coin", - // Add more mappings as needed -} as const; - -/** - * Normalizes a coin name/symbol to its CoinGecko ID - * @param input The coin name or symbol to normalize - * @returns The normalized CoinGecko ID or null if not found - */ -export function normalizeCoinId(input: string): string | null { - const normalized = input.toLowerCase().trim(); - return COIN_ID_MAPPING[normalized] || null; -} diff --git a/packages/plugin-irys/.npmignore b/packages/plugin-irys/.npmignore new file mode 100644 index 0000000000..078562ecea --- /dev/null +++ b/packages/plugin-irys/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts \ No newline at end of file diff --git a/packages/plugin-irys/OrchestratorDiagram.png b/packages/plugin-irys/OrchestratorDiagram.png new file mode 100644 index 0000000000..1379266f79 Binary files /dev/null and b/packages/plugin-irys/OrchestratorDiagram.png differ diff --git a/packages/plugin-irys/README.md b/packages/plugin-irys/README.md new file mode 100644 index 0000000000..c2ef9b41cb --- /dev/null +++ b/packages/plugin-irys/README.md @@ -0,0 +1,319 @@ +# @elizaos/plugin-irys + +A plugin for ElizaOS that enables decentralized data storage and retrieval using Irys, a programmable datachain platform. + +## Overview + +This plugin integrates Irys functionality into ElizaOS, allowing agents to store and retrieve data in a decentralized manner. It provides a service for creating a decentralized knowledge base and enabling multi-agent collaboration. + +## Installation + +To install this plugin, run the following command: + +```bash +pnpm add @elizaos/plugin-irys +``` + +## Features + +- **Decentralized Data Storage**: Store data permanently on the Irys network +- **Data Retrieval**: Fetch stored data using GraphQL queries +- **Multi-Agent Support**: Enable data sharing and collaboration between agents +- **Ethereum Integration**: Built-in support for Ethereum wallet authentication + +## Configuration + +The plugin requires the following environment variables: + +- `EVM_WALLET_PRIVATE_KEY`: Your EVM wallet private key +- `AGENTS_WALLET_PUBLIC_KEYS`: The public keys of the agents that will be used to retrieve the data (string separated by commas) + +For this plugin to work, you need to have an EVM (Base network) wallet with a private key and public address. To prevent any security issues, we recommend using a dedicated wallet for this plugin. + +> **Important**: The wallet address needs to have Base Sepolia ETH tokens to store images/files and any data larger than 100KB. + +## How it works + +![Orchestrator Diagram](./OrchestratorDiagram.png) + +The system consists of three main components that work together to enable decentralized multi-agent operations: + +### 1. Providers +Providers are the data management layer of the system. They: +- Interact with the Orchestrator to store data +- Aggregate information from multiple sources to enhance context +- Support agents with enriched data for better decision-making + +### 2. Orchestrators +Orchestrators manage the flow of communication and requests. They: +- Interact with the Irys datachain to store and retrieve data +- Implement a tagging system for request categorization +- Validate data integrity and authenticity +- Coordinate the overall system workflow + +### 3. Workers +Workers are specialized agents that execute specific tasks. They: +- Perform autonomous operations (e.g., social media interactions, DeFi operations) +- Interact with Orchestrators to get contextual data from Providers +- Interact with Orchestrators to store execution results on the Irys datachain +- Maintain transparency by documenting all actions + +This architecture ensures a robust, transparent, and efficient system where: +- Data is securely stored and verified on the blockchain +- Requests are properly routed and managed +- Operations are executed autonomously +- All actions are traceable and accountable + +You can find more information about the system in the [A Decentralized Framework for Multi-Agent Systems Using Datachain Technology](https://trophe.net/article/A_Decentralized_Framework_for_Multi-Agent_Systems_Using_Datachain_Technology.pdf) paper. + +## Usage + +### Worker + +As a worker, you can store data on the Irys network using the `workerUploadDataOnIrys` function. You can use this function to store data from any source to document your actions. You can also use this function to store a request to get data from the Orchestrator to enhance your context. + +```typescript +const { IrysService } = require('@elizaos/plugin-irys'); + +const irysService : IrysService = runtime.getService(ServiceType.IRYS) +const data = "Provide Liquidity to the ETH pool on Stargate"; +const result = await irysService.workerUploadDataOnIrys( + data, + IrysDataType.OTHER, + IrysMessageType.DATA_STORAGE, + ["DeFi"], + ["Stargate", "LayerZero"] +); +console.log(`Data uploaded successfully at the following url: ${result.url}`); +``` + +To upload files or images : + +```typescript +const { IrysService } = require('@elizaos/plugin-irys'); + +const irysService : IrysService = runtime.getService(ServiceType.IRYS) +const userAttachmentToStore = state.recentMessagesData[1].content.attachments[0].url.replace("agent\\agent", "agent"); + +const result = await irysService.workerUploadDataOnIrys( + userAttachmentToStore, + IrysDataType.IMAGE, + IrysMessageType.DATA_STORAGE, + ["Social Media"], + ["X", "Twitter"] +); +console.log(`Data uploaded successfully at the following url: ${result.url}`); +``` + +To store a request to get data from the Orchestrator to enhance your context, you can use the `workerUploadDataOnIrys` function with the `IrysMessageType.REQUEST` message type. + +```typescript +const { IrysService } = require('@elizaos/plugin-irys'); + +const irysService : IrysService = runtime.getService(ServiceType.IRYS) +const data = "Which Pool farm has the highest APY on Stargate?"; +const result = await irysService.workerUploadDataOnIrys( + data, + IrysDataType.OTHER, + IrysMessageType.REQUEST, + ["DeFi"], + ["Stargate", "LayerZero"], + [0.5], // Validation Threshold - Not implemented yet + [1], // Minimum Providers + [false], // Test Provider - Not implemented yet + [0.5] // Reputation - Not implemented yet +); +console.log(`Data uploaded successfully at the following url: ${result.url}`); +console.log(`Response from the Orchestrator: ${result.data}`); +``` + +### Provider + +As a provider, you can store data on the Irys network using the `providerUploadDataOnIrys` function. The data you provide can be retrieved by the Orchestrator to enhance the context of the Worker. + +```typescript +const { IrysService } = require('@elizaos/plugin-irys'); + +const irysService : IrysService = runtime.getService(ServiceType.IRYS) +const data = "ETH Pool Farm APY : 6,86%"; +const result = await irysService.providerUploadDataOnIrys( + data, + IrysDataType.OTHER, + ["DeFi"], + ["Stargate", "LayerZero"] +); +console.log(`Data uploaded successfully at the following url: ${result.url}`); +``` + +To upload files or images : + +```typescript +const { IrysService } = require('@elizaos/plugin-irys'); + +const irysService : IrysService = runtime.getService(ServiceType.IRYS) +const userAttachmentToStore = state.recentMessagesData[1].content.attachments[0].url.replace("agent\\agent", "agent"); + +const result = await irysService.providerUploadDataOnIrys( + userAttachmentToStore, + IrysDataType.IMAGE, + ["Social Media"], + ["X", "Twitter"] +); +console.log(`Data uploaded successfully at the following url: ${result.url}`); +``` + +### Retrieving Data + +To retrieve data from the Irys network, you can use the `getDataFromAnAgent` function. This function will retrieve all data associated with the given wallet addresses, tags and timestamp. The function automatically detects the content type and returns either JSON data or file/image URLs accordingly. + +- For files and images: Returns the URL of the stored content +- For other data types: Returns a JSON object with the following structure: + +```typescript +{ + data: string, // The stored data + address: string // The address of the agent that stored the data +} +``` + +By using only the provider address you want to retrieve data from : + +```typescript +const { IrysService } = require('@elizaos/plugin-irys'); + +const irysService = runtime.getService(ServiceType.IRYS) +const agentsWalletPublicKeys = runtime.getSetting("AGENTS_WALLET_PUBLIC_KEYS").split(","); +const data = await irysService.getDataFromAnAgent(agentsWalletPublicKeys); +console.log(`Data retrieved successfully. Data: ${data}`); +``` + +By using tags and timestamp: + +```typescript +const { IrysService } = require('@elizaos/plugin-irys'); + +const irysService = runtime.getService(ServiceType.IRYS) +const tags = [ + { name: "Message-Type", values: [IrysMessageType.DATA_STORAGE] }, + { name: "Service-Category", values: ["DeFi"] }, + { name: "Protocol", values: ["Stargate", "LayerZero"] }, +]; +const timestamp = { from: 1710000000, to: 1710000000 }; +const data = await irysService.getDataFromAnAgent(null, tags, timestamp); +console.log(`Data retrieved successfully. Data: ${data}`); +``` + +If everything is null, the function will retrieve all data from the Irys network. + +## About Irys + +Irys is the first Layer 1 (L1) programmable datachain designed to optimize both data storage and execution. By integrating storage and execution, Irys enhances the utility of blockspace, enabling a broader spectrum of web services to operate on-chain. + +### Key Features of Irys + +- **Unified Platform**: Combines data storage and execution, allowing developers to eliminate dependencies and integrate efficient on-chain data seamlessly. +- **Cost-Effective Storage**: Optimized specifically for data storage, making it significantly cheaper to store data on-chain compared to traditional blockchains. +- **Programmable Datachain**: The IrysVM can utilize on-chain data during computations, enabling dynamic and real-time applications. +- **Decentralization**: Designed to minimize centralization risks by distributing control. +- **Free Storage for Small Data**: Storing less than 100KB of data is free. +- **GraphQL Querying**: Metadata stored on Irys can be queried using GraphQL. + +### GraphQL Query Examples + +The plugin uses GraphQL to retrieve transaction metadata. Here's an example query structure: + +```typescript +const QUERY = gql` + query($owners: [String!], $tags: [TagFilter!], $timestamp: TimestampFilter) { + transactions(owners: $owners, tags: $tags, timestamp: $timestamp) { + edges { + node { + id, + address + } + } + } + } +`; + +const variables = { + owners: owners, + tags: tags, + timestamp: timestamp +} + +const data: TransactionGQL = await graphQLClient.request(QUERY, variables); +``` + +## API Reference + +### IrysService + +The main service provided by this plugin implements the following interface: + +```typescript + +interface UploadIrysResult { + success: boolean; + url?: string; + error?: string; + data?: any; +} + +interface DataIrysFetchedFromGQL { + success: boolean; + data: any; + error?: string; +} + +interface GraphQLTag { + name: string; + values: any[]; +} + +const enum IrysMessageType { + REQUEST = "REQUEST", + DATA_STORAGE = "DATA_STORAGE", + REQUEST_RESPONSE = "REQUEST_RESPONSE", +} + +const enum IrysDataType { + FILE = "FILE", + IMAGE = "IMAGE", + OTHER = "OTHER", +} + +interface IrysTimestamp { + from: number; + to: number; +} + +interface IIrysService extends Service { + getDataFromAnAgent(agentsWalletPublicKeys: string[], tags: GraphQLTag[], timestamp: IrysTimestamp): Promise; + workerUploadDataOnIrys(data: any, dataType: IrysDataType, messageType: IrysMessageType, serviceCategory: string[], protocol: string[], validationThreshold: number[], minimumProviders: number[], testProvider: boolean[], reputation: number[]): Promise; + providerUploadDataOnIrys(data: any, dataType: IrysDataType, serviceCategory: string[], protocol: string[]): Promise; +} +``` + +#### Methods + +- `getDataFromAnAgent(agentsWalletPublicKeys: string[], tags: GraphQLTag[], timestamp: IrysTimestamp)`: Retrieves all data associated with the given parameters +- `workerUploadDataOnIrys(data: any, dataType: IrysDataType, messageType: IrysMessageType, serviceCategory: string[], protocol: string[], validationThreshold: number[], minimumProviders: number[], testProvider: boolean[], reputation: number[])`: Uploads data to Irys and returns the orchestrator response (request or data storage) +- `providerUploadDataOnIrys(data: any, dataType: IrysDataType, serviceCategory: string[], protocol: string[])`: Uploads data to Irys and returns orchestrator response (data storage) + +## Testing + +To run the tests, you can use the following command: + +```bash +pnpm test +``` + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## Ressources + +- [Irys Documentation](https://docs.irys.xyz/) +- [A Decentralized Framework for Multi-Agent Systems Using Datachain Technology](https://trophe.net/article/A_Decentralized_Framework_for_Multi-Agent_Systems_Using_Datachain_Technology.pdf) diff --git a/packages/plugin-irys/eslint.config.mjs b/packages/plugin-irys/eslint.config.mjs new file mode 100644 index 0000000000..92fe5bbebe --- /dev/null +++ b/packages/plugin-irys/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-irys/package.json b/packages/plugin-irys/package.json new file mode 100644 index 0000000000..15cd8a3904 --- /dev/null +++ b/packages/plugin-irys/package.json @@ -0,0 +1,23 @@ +{ + "name": "@elizaos/plugin-irys", + "version": "0.1.0-alpha.1", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@elizaos/core": "workspace:*", + "@irys/upload": "^0.0.14", + "@irys/upload-ethereum": "^0.0.14", + "graphql-request": "^4.0.0" + }, + "devDependencies": { + "tsup": "8.3.5", + "@types/node": "^20.0.0" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint --fix --cache .", + "test": "vitest run" + } +} diff --git a/packages/plugin-irys/src/index.ts b/packages/plugin-irys/src/index.ts new file mode 100644 index 0000000000..0cf83ac3ec --- /dev/null +++ b/packages/plugin-irys/src/index.ts @@ -0,0 +1,14 @@ +import { Plugin } from "@elizaos/core"; +import IrysService from "./services/irysService"; + +const irysPlugin: Plugin = { + name: "plugin-irys", + description: "Store and retrieve data on Irys to create a decentralized knowledge base and enable multi-agent collaboration", + actions: [], + providers: [], + evaluators: [], + clients: [], + services: [new IrysService()], +} + +export default irysPlugin; diff --git a/packages/plugin-irys/src/services/irysService.ts b/packages/plugin-irys/src/services/irysService.ts new file mode 100644 index 0000000000..e97bae6ee8 --- /dev/null +++ b/packages/plugin-irys/src/services/irysService.ts @@ -0,0 +1,345 @@ +import { + IAgentRuntime, + Service, + ServiceType, + IIrysService, + UploadIrysResult, + DataIrysFetchedFromGQL, + GraphQLTag, + IrysMessageType, + generateMessageResponse, + ModelClass, + IrysDataType, + IrysTimestamp, +} from "@elizaos/core"; +import { Uploader } from "@irys/upload"; +import { BaseEth } from "@irys/upload-ethereum"; +import { GraphQLClient, gql } from 'graphql-request'; +import crypto from 'crypto'; + +interface NodeGQL { + id: string; + address: string; +} + +interface TransactionsIdAddress { + success: boolean; + data: NodeGQL[]; + error?: string; +} + +interface TransactionGQL { + transactions: { + edges: { + node: { + id: string; + address: string; + } + }[] + } +} + +export class IrysService extends Service implements IIrysService { + static serviceType: ServiceType = ServiceType.IRYS; + + private runtime: IAgentRuntime | null = null; + private irysUploader: any | null = null; + private endpointForTransactionId: string = "https://uploader.irys.xyz/graphql"; + private endpointForData: string = "https://gateway.irys.xyz"; + + async initialize(runtime: IAgentRuntime): Promise { + console.log("Initializing IrysService"); + this.runtime = runtime; + } + + private async getTransactionId(owners: string[] = null, tags: GraphQLTag[] = null, timestamp: IrysTimestamp = null): Promise { + const graphQLClient = new GraphQLClient(this.endpointForTransactionId); + const QUERY = gql` + query($owners: [String!], $tags: [TagFilter!], $timestamp: TimestampFilter) { + transactions(owners: $owners, tags: $tags, timestamp: $timestamp) { + edges { + node { + id, + address + } + } + } + } + `; + try { + const variables = { + owners: owners, + tags: tags, + timestamp: timestamp + } + const data: TransactionGQL = await graphQLClient.request(QUERY, variables); + const listOfTransactions : NodeGQL[] = data.transactions.edges.map((edge: any) => edge.node); + console.log("Transaction IDs retrieved") + return { success: true, data: listOfTransactions }; + } catch (error) { + console.error("Error fetching transaction IDs", error); + return { success: false, data: [], error: "Error fetching transaction IDs" }; + } + } + + private async initializeIrysUploader(): Promise { + if (this.irysUploader) return true; + if (!this.runtime) return false; + + try { + const EVM_WALLET_PRIVATE_KEY = this.runtime.getSetting("EVM_WALLET_PRIVATE_KEY"); + if (!EVM_WALLET_PRIVATE_KEY) return false; + + const irysUploader = await Uploader(BaseEth).withWallet(EVM_WALLET_PRIVATE_KEY); + this.irysUploader = irysUploader; + return true; + } catch (error) { + console.error("Error initializing Irys uploader:", error); + return false; + } + } + + private async fetchDataFromTransactionId(transactionId: string): Promise { + console.log(`Fetching data from transaction ID: ${transactionId}`); + const response = await fetch(`${this.endpointForData}/${transactionId}`); + if (!response.ok) return { success: false, data: null, error: "Error fetching data from transaction ID" }; + return { + success: true, + data: response, + }; + } + private converToValues(value: any): any[] { + if (Array.isArray(value)) { + return value; + } + return [value]; + } + + private async orchestrateRequest(requestMessage: string, tags: GraphQLTag[], timestamp: IrysTimestamp = null): Promise { + let serviceCategory = tags.find((tag) => tag.name == "Service-Category")?.values; + let protocol = tags.find((tag) => tag.name == "Protocol")?.values; + let minimumProviders = Number(tags.find((tag) => tag.name == "Minimum-Providers")?.values); + /* + Further implementation of the orchestrator + { name: "Validation-Threshold", values: validationThreshold }, + { name: "Test-Provider", values: testProvider }, + { name: "Reputation", values: reputation }, + */ + const tagsToRetrieve : GraphQLTag[] = [ + { name: "Message-Type", values: [IrysMessageType.DATA_STORAGE] }, + { name: "Service-Category", values: this.converToValues(serviceCategory) }, + { name: "Protocol", values: this.converToValues(protocol) }, + ]; + const data = await this.getDataFromAnAgent(null, tagsToRetrieve, timestamp); + if (!data.success) return { success: false, data: null, error: data.error }; + const dataArray = data.data as Array; + try { + for (let i = 0; i < dataArray.length; i++) { + const node = dataArray[i]; + const templateRequest = ` + Determine the truthfulness of the relationship between the given context and text. + Context: ${requestMessage} + Text: ${node.data} + Return True or False + `; + const responseFromModel = await generateMessageResponse({ + runtime: this.runtime, + context: templateRequest, + modelClass: ModelClass.MEDIUM, + }); + console.log("RESPONSE FROM MODEL : ", responseFromModel) + if (!responseFromModel.success || ((responseFromModel.content?.toString().toLowerCase().includes('false')) && (!responseFromModel.content?.toString().toLowerCase().includes('true')))) { + dataArray.splice(i, 1); + i--; + } + } + } catch (error) { + if (error.message.includes("TypeError: Cannot read properties of undefined (reading 'settings')")) { + return { success: false, data: null, error: "Error in the orchestrator" }; + } + } + let responseTags: GraphQLTag[] = [ + { name: "Message-Type", values: [IrysMessageType.REQUEST_RESPONSE] }, + { name: "Service-Category", values: [serviceCategory] }, + { name: "Protocol", values: [protocol] }, + { name: "Request-Id", values: [tags.find((tag) => tag.name == "Request-Id")?.values[0]] }, + ]; + if (dataArray.length == 0) { + const response = await this.uploadDataOnIrys("No relevant data found from providers", responseTags, IrysMessageType.REQUEST_RESPONSE); + console.log("Response from Irys: ", response); + return { success: false, data: null, error: "No relevant data found from providers" }; + } + const listProviders = new Set(dataArray.map((provider: any) => provider.address)); + if (listProviders.size < minimumProviders) { + const response = await this.uploadDataOnIrys("Not enough providers", responseTags, IrysMessageType.REQUEST_RESPONSE); + console.log("Response from Irys: ", response); + return { success: false, data: null, error: "Not enough providers" }; + } + const listData = dataArray.map((provider: any) => provider.data); + const response = await this.uploadDataOnIrys(listData, responseTags, IrysMessageType.REQUEST_RESPONSE); + console.log("Response from Irys: ", response); + return { + success: true, + data: listData + } + } + + // Orchestrator + private async uploadDataOnIrys(data: any, tags: GraphQLTag[], messageType: IrysMessageType, timestamp: IrysTimestamp = null): Promise { + if (!(await this.initializeIrysUploader())) { + return { + success: false, + error: "Irys uploader not initialized", + }; + } + + // Transform tags to the correct format + const formattedTags = tags.map(tag => ({ + name: tag.name, + value: Array.isArray(tag.values) ? tag.values.join(',') : tag.values + })); + + const requestId = String(crypto.createHash('sha256').update(new Date().toISOString()).digest('hex')); + formattedTags.push({ + name: "Request-Id", + value: requestId + }); + try { + const dataToStore = { + data: data, + }; + const receipt = await this.irysUploader.upload(JSON.stringify(dataToStore), { tags: formattedTags }); + if (messageType == IrysMessageType.DATA_STORAGE || messageType == IrysMessageType.REQUEST_RESPONSE) { + return { success: true, url: `https://gateway.irys.xyz/${receipt.id}`}; + } else if (messageType == IrysMessageType.REQUEST) { + const response = await this.orchestrateRequest(data, tags, timestamp); + return { + success: response.success, + url: `https://gateway.irys.xyz/${receipt.id}`, + data: response.data, + error: response.error ? response.error : null + } + + } + return { success: true, url: `https://gateway.irys.xyz/${receipt.id}` }; + } catch (error) { + return { success: false, error: "Error uploading to Irys, " + error }; + } + } + + private async uploadFileOrImageOnIrys(data: string, tags: GraphQLTag[]): Promise { + if (!(await this.initializeIrysUploader())) { + return { + success: false, + error: "Irys uploader not initialized" + }; + } + + const formattedTags = tags.map(tag => ({ + name: tag.name, + value: Array.isArray(tag.values) ? tag.values.join(',') : tag.values + })); + + try { + const receipt = await this.irysUploader.uploadFile(data, { tags: formattedTags }); + return { success: true, url: `https://gateway.irys.xyz/${receipt.id}` }; + } catch (error) { + return { success: false, error: "Error uploading to Irys, " + error }; + } + } + + private normalizeArrayValues(arr: number[], min: number, max?: number): void { + for (let i = 0; i < arr.length; i++) { + arr[i] = Math.max(min, max !== undefined ? Math.min(arr[i], max) : arr[i]); + } + } + + private normalizeArraySize(arr: any[]): any { + if (arr.length == 1) { + return arr[0]; + } + return arr; + } + + async workerUploadDataOnIrys(data: any, dataType: IrysDataType, messageType: IrysMessageType, serviceCategory: string[], protocol: string[], validationThreshold: number[] = [], minimumProviders: number[] = [], testProvider: boolean[] = [], reputation: number[] = []): Promise { + this.normalizeArrayValues(validationThreshold, 0, 1); + this.normalizeArrayValues(minimumProviders, 0); + this.normalizeArrayValues(reputation, 0, 1); + + const tags = [ + { name: "Message-Type", values: messageType }, + { name: "Service-Category", values: this.normalizeArraySize(serviceCategory) }, + { name: "Protocol", values: this.normalizeArraySize(protocol) }, + ] as GraphQLTag[]; + + if (messageType == IrysMessageType.REQUEST) { + if (validationThreshold.length > 0) { + tags.push({ name: "Validation-Threshold", values: this.normalizeArraySize(validationThreshold) }); + } + if (minimumProviders.length > 0) { + tags.push({ name: "Minimum-Providers", values: this.normalizeArraySize(minimumProviders) }); + } + if (testProvider.length > 0) { + tags.push({ name: "Test-Provider", values: this.normalizeArraySize(testProvider) }); + } + if (reputation.length > 0) { + tags.push({ name: "Reputation", values: this.normalizeArraySize(reputation) }); + } + } + if (dataType == IrysDataType.FILE || dataType == IrysDataType.IMAGE) { + return await this.uploadFileOrImageOnIrys(data, tags); + } + + return await this.uploadDataOnIrys(data, tags, messageType); + } + + async providerUploadDataOnIrys(data: any, dataType: IrysDataType, serviceCategory: string[], protocol: string[]): Promise { + const tags = [ + { name: "Message-Type", values: [IrysMessageType.DATA_STORAGE] }, + { name: "Service-Category", values: serviceCategory }, + { name: "Protocol", values: protocol }, + ] as GraphQLTag[]; + + if (dataType == IrysDataType.FILE || dataType == IrysDataType.IMAGE) { + return await this.uploadFileOrImageOnIrys(data, tags); + } + + return await this.uploadDataOnIrys(data, tags, IrysMessageType.DATA_STORAGE); + } + + async getDataFromAnAgent(agentsWalletPublicKeys: string[] = null, tags: GraphQLTag[] = null, timestamp: IrysTimestamp = null): Promise { + try { + const transactionIdsResponse = await this.getTransactionId(agentsWalletPublicKeys, tags, timestamp); + if (!transactionIdsResponse.success) return { success: false, data: null, error: "Error fetching transaction IDs" }; + const transactionIdsAndResponse = transactionIdsResponse.data.map((node: NodeGQL) => node); + const dataPromises: Promise[] = transactionIdsAndResponse.map(async (node: NodeGQL) => { + const fetchDataFromTransactionIdResponse = await this.fetchDataFromTransactionId(node.id); + if (await fetchDataFromTransactionIdResponse.data.headers.get('content-type') == "application/octet-stream") { + let data = null; + const responseText = await fetchDataFromTransactionIdResponse.data.text(); + try { + data = JSON.parse(responseText); + } catch (error) { + data = responseText; + } + return { + data: data, + address: node.address + } + } + else { + return { + data: fetchDataFromTransactionIdResponse.data.url, + address: node.address + } + } + }); + const data = await Promise.all(dataPromises); + return { success: true, data: data }; + } catch (error) { + return { success: false, data: null, error: "Error fetching data from transaction IDs " + error }; + } + } +} + +export default IrysService; \ No newline at end of file diff --git a/packages/plugin-irys/tests/provider.test.ts b/packages/plugin-irys/tests/provider.test.ts new file mode 100644 index 0000000000..be6166ed31 --- /dev/null +++ b/packages/plugin-irys/tests/provider.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; +import { IrysService } from "../src/services/irysService"; +import { defaultCharacter, IrysDataType } from "@elizaos/core"; + +// Mock NodeCache +vi.mock("node-cache", () => { + return { + default: vi.fn().mockImplementation(() => ({ + set: vi.fn(), + get: vi.fn().mockReturnValue(null), + })), + }; +}); + +// Mock path module +vi.mock("path", async () => { + const actual = await vi.importActual("path"); + return { + ...actual, + join: vi.fn().mockImplementation((...args) => args.join("/")), + }; +}); + +// Mock the ICacheManager +const mockCacheManager = { + get: vi.fn().mockResolvedValue(null), + set: vi.fn(), + delete: vi.fn(), +}; + +describe("IrysService", () => { + let irysService; + let mockedRuntime; + + beforeEach(async () => { + vi.clearAllMocks(); + mockCacheManager.get.mockResolvedValue(null); + + mockedRuntime = { + character: defaultCharacter, + getSetting: vi.fn().mockImplementation((key: string) => { + if (key === "EVM_WALLET_PRIVATE_KEY") // TEST PRIVATE KEY + return "0xd6ed963c4eb8436b284f62636a621c164161ee25218b3be5ca4cad1261f8c390"; + return undefined; + }), + }; + irysService = new IrysService(); + await irysService.initialize(mockedRuntime); + }); + + afterEach(() => { + vi.clearAllTimers(); + }); + + describe("Store String on Irys", () => { + it("should store string on Irys", async () => { + const result = await irysService.providerUploadDataOnIrys("Hello World", IrysDataType.OTHER, ["test"], ["test"]); + console.log("Store String on Irys ERROR : ", result.error) + expect(result.success).toBe(true); + }); + }); +}); + diff --git a/packages/plugin-irys/tests/wallet.test.ts b/packages/plugin-irys/tests/wallet.test.ts new file mode 100644 index 0000000000..0c1ffc4a14 --- /dev/null +++ b/packages/plugin-irys/tests/wallet.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; +import { IrysService } from "../src/services/irysService"; +import { defaultCharacter } from "@elizaos/core"; + +// Mock NodeCache +vi.mock("node-cache", () => { + return { + default: vi.fn().mockImplementation(() => ({ + set: vi.fn(), + get: vi.fn().mockReturnValue(null), + })), + }; +}); + +// Mock path module +vi.mock("path", async () => { + const actual = await vi.importActual("path"); + return { + ...actual, + join: vi.fn().mockImplementation((...args) => args.join("/")), + }; +}); + +// Mock the ICacheManager +const mockCacheManager = { + get: vi.fn().mockResolvedValue(null), + set: vi.fn(), + delete: vi.fn(), +}; + +describe("IrysService", () => { + let irysService; + let mockedRuntime; + + beforeEach(async () => { + vi.clearAllMocks(); + mockCacheManager.get.mockResolvedValue(null); + + mockedRuntime = { + character: defaultCharacter, + getSetting: vi.fn().mockImplementation((key: string) => { + if (key === "EVM_WALLET_PRIVATE_KEY") // TEST PRIVATE KEY + return "0xd6ed963c4eb8436b284f62636a621c164161ee25218b3be5ca4cad1261f8c390"; + return undefined; + }), + }; + irysService = new IrysService(); + await irysService.initialize(mockedRuntime); + }); + + afterEach(() => { + vi.clearAllTimers(); + }); + + describe("Initialize IrysService", () => { + it("should initialize IrysService", async () => { + expect(irysService).toBeDefined(); + }); + + it("should initialize IrysUploader", async () => { + const result = await irysService.initializeIrysUploader(); + expect(result).toBe(true); + }); + }); +}); + diff --git a/packages/plugin-irys/tests/worker.test.ts b/packages/plugin-irys/tests/worker.test.ts new file mode 100644 index 0000000000..279be9cb41 --- /dev/null +++ b/packages/plugin-irys/tests/worker.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; +import { IrysService } from "../src/services/irysService"; +import { defaultCharacter, IrysDataType, IrysMessageType } from "@elizaos/core"; + +// Mock NodeCache +vi.mock("node-cache", () => { + return { + default: vi.fn().mockImplementation(() => ({ + set: vi.fn(), + get: vi.fn().mockReturnValue(null), + })), + }; +}); + +// Mock path module +vi.mock("path", async () => { + const actual = await vi.importActual("path"); + return { + ...actual, + join: vi.fn().mockImplementation((...args) => args.join("/")), + }; +}); + +// Mock the ICacheManager +const mockCacheManager = { + get: vi.fn().mockResolvedValue(null), + set: vi.fn(), + delete: vi.fn(), +}; + +describe("IrysService", () => { + let irysService; + let mockedRuntime; + + beforeEach(async () => { + + vi.clearAllMocks(); + mockCacheManager.get.mockResolvedValue(null); + + mockedRuntime = { + character: defaultCharacter, + getSetting: vi.fn().mockImplementation((key: string) => { + if (key === "EVM_WALLET_PRIVATE_KEY") // TEST PRIVATE KEY + return "0xd6ed963c4eb8436b284f62636a621c164161ee25218b3be5ca4cad1261f8c390"; + return undefined; + }), + }; + irysService = new IrysService(); + await irysService.initialize(mockedRuntime); + }); + + afterEach(() => { + vi.clearAllTimers(); + }); + + describe("Store String on Irys", () => { + it("should store string on Irys", async () => { + const result = await irysService.workerUploadDataOnIrys( + "Hello World", + IrysDataType.OTHER, + IrysMessageType.DATA_STORAGE, + ["test"], + ["test"] + ); + console.log("Store String on Irys ERROR : ", result.error) + expect(result.success).toBe(true); + }); + + it("should retrieve data from Irys", async () => { + const result = await irysService.getDataFromAnAgent(["0x7131780570930a0ef05ef7a66489111fc31e9538"], []); + console.log("should retrieve data from Irys ERROR : ", result.error) + expect(result.success).toBe(true); + expect(result.data.length).toBeGreaterThan(0); + }); + + it("should get a response from the orchestrator", async () => { + const result = await irysService.workerUploadDataOnIrys("Hello World", IrysDataType.OTHER, IrysMessageType.REQUEST, ["test"], ["test"]); + console.log("should get a response from the orchestrator ERROR : ", result.error) + expect(result.success).toBe(true); + expect(result.data.length).toBeGreaterThan(0); + }); + }); +}); + diff --git a/packages/plugin-irys/tsconfig.json b/packages/plugin-irys/tsconfig.json new file mode 100644 index 0000000000..2ef05a1844 --- /dev/null +++ b/packages/plugin-irys/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": [ + "node" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/packages/plugin-irys/tsup.config.ts b/packages/plugin-irys/tsup.config.ts new file mode 100644 index 0000000000..b5e4388b21 --- /dev/null +++ b/packages/plugin-irys/tsup.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + "zod", + // Add other modules you want to externalize + ], +}); diff --git a/packages/plugin-node/README.md b/packages/plugin-node/README.md index 7b6bfb1bcb..c0f367c1c5 100644 --- a/packages/plugin-node/README.md +++ b/packages/plugin-node/README.md @@ -80,7 +80,51 @@ Provides web scraping and content extraction capabilities using Playwright. ### ImageDescriptionService -Processes and analyzes images to generate descriptions. +Processes and analyzes images to generate descriptions. Supports multiple providers: + +- Local processing using Florence model +- OpenAI Vision API +- Google Gemini + +Configuration: + +```env +# For OpenAI Vision +OPENAI_API_KEY=your_openai_api_key + +# For Google Gemini +GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key +``` + +Provider selection: + +- If `imageVisionModelProvider` is set to `google/openai`, it will use this one. +- Else if `model` is set to `google/openai`, it will use this one. +- Default if nothing is set is OpenAI. + +The service automatically handles different image formats, including GIFs (first frame extraction). + +Features by provider: + +**Local (Florence):** + +- Basic image captioning +- Local processing without API calls + +**OpenAI Vision:** + +- Detailed image descriptions +- Text detection +- Object recognition + +**Google Gemini 1.5:** + +- High-quality image understanding +- Detailed descriptions with natural language +- Multi-modal context understanding +- Support for complex scenes and content + +The provider can be configured through the runtime settings, allowing easy switching between providers based on your needs. ### LlamaService diff --git a/packages/plugin-node/src/index.ts b/packages/plugin-node/src/index.ts index 17ef56e4d5..ec67170b72 100644 --- a/packages/plugin-node/src/index.ts +++ b/packages/plugin-node/src/index.ts @@ -2,7 +2,9 @@ export * from "./services/index.ts"; import { Plugin } from "@elizaos/core"; +import { describeImage } from "./actions/describe-image.ts"; import { + AwsS3Service, BrowserService, ImageDescriptionService, LlamaService, @@ -10,9 +12,7 @@ import { SpeechService, TranscriptionService, VideoService, - AwsS3Service, } from "./services/index.ts"; -import { describeImage } from "./actions/describe-image.ts"; export type NodePlugin = ReturnType; diff --git a/packages/plugin-node/src/services/image.ts b/packages/plugin-node/src/services/image.ts index 55c29db6d1..56a59c9056 100644 --- a/packages/plugin-node/src/services/image.ts +++ b/packages/plugin-node/src/services/image.ts @@ -1,10 +1,12 @@ -import { elizaLogger, getEndpoint, models } from "@elizaos/core"; -import { Service } from "@elizaos/core"; import { + elizaLogger, + getEndpoint, IAgentRuntime, + IImageDescriptionService, ModelProviderName, + models, + Service, ServiceType, - IImageDescriptionService, } from "@elizaos/core"; import { AutoProcessor, @@ -22,32 +24,54 @@ import gifFrames from "gif-frames"; import os from "os"; import path from "path"; -export class ImageDescriptionService - extends Service - implements IImageDescriptionService -{ - static serviceType: ServiceType = ServiceType.IMAGE_DESCRIPTION; +const IMAGE_DESCRIPTION_PROMPT = + "Describe this image and give it a title. The first line should be the title, and then a line break, then a detailed description of the image. Respond with the format 'title\\ndescription'"; - private modelId: string = "onnx-community/Florence-2-base-ft"; - private device: string = "gpu"; +interface ImageProvider { + initialize(): Promise; + describeImage( + imageData: Buffer, + mimeType: string + ): Promise<{ title: string; description: string }>; +} + +// Utility functions +const convertToBase64DataUrl = ( + imageData: Buffer, + mimeType: string +): string => { + const base64Data = imageData.toString("base64"); + return `data:${mimeType};base64,${base64Data}`; +}; + +const handleApiError = async ( + response: Response, + provider: string +): Promise => { + const responseText = await response.text(); + elizaLogger.error( + `${provider} API error:`, + response.status, + "-", + responseText + ); + throw new Error(`HTTP error! status: ${response.status}`); +}; + +const parseImageResponse = ( + text: string +): { title: string; description: string } => { + const [title, ...descriptionParts] = text.split("\n"); + return { title, description: descriptionParts.join("\n") }; +}; + +class LocalImageProvider implements ImageProvider { private model: PreTrainedModel | null = null; private processor: Florence2Processor | null = null; private tokenizer: PreTrainedTokenizer | null = null; - private initialized: boolean = false; - private runtime: IAgentRuntime | null = null; - private queue: string[] = []; - private processing: boolean = false; - - getInstance(): IImageDescriptionService { - return ImageDescriptionService.getInstance(); - } - - async initialize(runtime: IAgentRuntime): Promise { - elizaLogger.log("Initializing ImageDescriptionService"); - this.runtime = runtime; - } + private modelId: string = "onnx-community/Florence-2-base-ft"; - private async initializeLocalModel(): Promise { + async initialize(): Promise { env.allowLocalModels = false; env.allowRemoteModels = true; env.backends.onnx.logLevel = "fatal"; @@ -55,7 +79,6 @@ export class ImageDescriptionService env.backends.onnx.wasm.numThreads = 1; elizaLogger.info("Downloading Florence model..."); - this.model = await Florence2ForConditionalGeneration.from_pretrained( this.modelId, { @@ -77,8 +100,6 @@ export class ImageDescriptionService } ); - elizaLogger.success("Florence model downloaded successfully"); - elizaLogger.info("Downloading processor..."); this.processor = (await AutoProcessor.from_pretrained( this.modelId @@ -90,236 +111,229 @@ export class ImageDescriptionService } async describeImage( - imageUrl: string + imageData: Buffer ): Promise<{ title: string; description: string }> { - if (!this.initialized) { - const model = models[this.runtime?.character?.modelProvider]; + if (!this.model || !this.processor || !this.tokenizer) { + throw new Error("Model components not initialized"); + } - if (model === models[ModelProviderName.LLAMALOCAL]) { - await this.initializeLocalModel(); - } else { - this.modelId = "gpt-4o-mini"; - this.device = "cloud"; - } + const base64Data = imageData.toString("base64"); + const dataUrl = `data:image/jpeg;base64,${base64Data}`; + const image = await RawImage.fromURL(dataUrl); + const visionInputs = await this.processor(image); + const prompts = this.processor.construct_prompts(""); + const textInputs = this.tokenizer(prompts); + + elizaLogger.log("Generating image description"); + const generatedIds = (await this.model.generate({ + ...textInputs, + ...visionInputs, + max_new_tokens: 256, + })) as Tensor; + + const generatedText = this.tokenizer.batch_decode(generatedIds, { + skip_special_tokens: false, + })[0]; + + const result = this.processor.post_process_generation( + generatedText, + "", + image.size + ); - this.initialized = true; - } + const detailedCaption = result[""] as string; + return { title: detailedCaption, description: detailedCaption }; + } +} - if (this.device === "cloud") { - if (!this.runtime) { - throw new Error( - "Runtime is required for OpenAI image recognition" - ); - } - return this.recognizeWithOpenAI(imageUrl); - } +class OpenAIImageProvider implements ImageProvider { + constructor(private runtime: IAgentRuntime) {} - this.queue.push(imageUrl); - this.processQueue(); + async initialize(): Promise {} - return new Promise((resolve, _reject) => { - const checkQueue = () => { - const index = this.queue.indexOf(imageUrl); - if (index !== -1) { - setTimeout(checkQueue, 100); - } else { - resolve(this.processImage(imageUrl)); - } - }; - checkQueue(); + async describeImage( + imageData: Buffer, + mimeType: string + ): Promise<{ title: string; description: string }> { + const imageUrl = convertToBase64DataUrl(imageData, mimeType); + + const content = [ + { type: "text", text: IMAGE_DESCRIPTION_PROMPT }, + { type: "image_url", image_url: { url: imageUrl } }, + ]; + + const endpoint = + this.runtime.imageVisionModelProvider === ModelProviderName.OPENAI + ? getEndpoint(this.runtime.imageVisionModelProvider) + : "https://api.openai.com/v1"; + + const response = await fetch(endpoint + "/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.runtime.getSetting("OPENAI_API_KEY")}`, + }, + body: JSON.stringify({ + model: "gpt-4o-mini", + messages: [{ role: "user", content }], + max_tokens: 500, + }), }); + + if (!response.ok) { + await handleApiError(response, "OpenAI"); + } + + const data = await response.json(); + return parseImageResponse(data.choices[0].message.content); } +} - private async recognizeWithOpenAI( - imageUrl: string - ): Promise<{ title: string; description: string }> { - const isGif = imageUrl.toLowerCase().endsWith(".gif"); - let imageData: Buffer | null = null; +class GoogleImageProvider implements ImageProvider { + constructor(private runtime: IAgentRuntime) {} - try { - if (isGif) { - const { filePath } = - await this.extractFirstFrameFromGif(imageUrl); - imageData = fs.readFileSync(filePath); - } else if (fs.existsSync(imageUrl)) { - imageData = fs.readFileSync(imageUrl); - } else { - const response = await fetch(imageUrl); - if (!response.ok) { - throw new Error( - `Failed to fetch image: ${response.statusText}` - ); - } - imageData = Buffer.from(await response.arrayBuffer()); - } + async initialize(): Promise {} + + async describeImage( + imageData: Buffer, + mimeType: string + ): Promise<{ title: string; description: string }> { + const endpoint = getEndpoint(ModelProviderName.GOOGLE); + const apiKey = this.runtime.getSetting("GOOGLE_GENERATIVE_AI_API_KEY"); - if (!imageData || imageData.length === 0) { - throw new Error("Failed to fetch image data"); + const response = await fetch( + `${endpoint}/v1/models/gemini-1.5-pro:generateContent?key=${apiKey}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + contents: [ + { + parts: [ + { text: IMAGE_DESCRIPTION_PROMPT }, + { + inline_data: { + mime_type: mimeType, + data: imageData.toString("base64"), + }, + }, + ], + }, + ], + }), } + ); - const prompt = - "Describe this image and give it a title. The first line should be the title, and then a line break, then a detailed description of the image. Respond with the format 'title\ndescription'"; - const text = await this.requestOpenAI( - imageUrl, - imageData, - prompt, - isGif, - true - ); - - const [title, ...descriptionParts] = text.split("\n"); - return { - title, - description: descriptionParts.join("\n"), - }; - } catch (error) { - elizaLogger.error("Error in recognizeWithOpenAI:", error); - throw error; + if (!response.ok) { + await handleApiError(response, "Google Gemini"); } + + const data = await response.json(); + return parseImageResponse(data.candidates[0].content.parts[0].text); } +} - private async requestOpenAI( - imageUrl: string, - imageData: Buffer, - prompt: string, - isGif: boolean = false, - isLocalFile: boolean = false - ): Promise { - for (let attempt = 0; attempt < 3; attempt++) { - try { - const shouldUseBase64 = - (isGif || isLocalFile) && - !( - this.runtime.imageModelProvider === - ModelProviderName.OPENAI - ); - const mimeType = isGif - ? "png" - : path.extname(imageUrl).slice(1) || "jpeg"; - - const base64Data = imageData.toString("base64"); - const imageUrlToUse = shouldUseBase64 - ? `data:image/${mimeType};base64,${base64Data}` - : imageUrl; - - const content = [ - { type: "text", text: prompt }, - { - type: "image_url", - image_url: { - url: imageUrlToUse, - }, - }, - ]; - // If model provider is openai, use the endpoint, otherwise use the default openai endpoint. - const endpoint = - this.runtime.imageModelProvider === ModelProviderName.OPENAI - ? getEndpoint(this.runtime.imageModelProvider) - : "https://api.openai.com/v1"; - const response = await fetch(endpoint + "/chat/completions", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${this.runtime.getSetting("OPENAI_API_KEY")}`, - }, - body: JSON.stringify({ - model: "gpt-4o-mini", - messages: [{ role: "user", content }], - max_tokens: shouldUseBase64 ? 500 : 300, - }), - }); +export class ImageDescriptionService + extends Service + implements IImageDescriptionService +{ + static serviceType: ServiceType = ServiceType.IMAGE_DESCRIPTION; - if (!response.ok) { - const responseText = await response.text(); - elizaLogger.error( - "OpenAI API error:", - response.status, - "-", - responseText - ); - throw new Error(`HTTP error! status: ${response.status}`); - } + private initialized: boolean = false; + private runtime: IAgentRuntime | null = null; + private provider: ImageProvider | null = null; + + getInstance(): IImageDescriptionService { + return ImageDescriptionService.getInstance(); + } - const data = await response.json(); - return data.choices[0].message.content; - } catch (error) { + async initialize(runtime: IAgentRuntime): Promise { + elizaLogger.log("Initializing ImageDescriptionService"); + this.runtime = runtime; + } + + private async initializeProvider(): Promise { + if (!this.runtime) { + throw new Error("Runtime is required for image recognition"); + } + + const model = models[this.runtime?.character?.modelProvider]; + + if (this.runtime.imageVisionModelProvider) { + if ( + this.runtime.imageVisionModelProvider === + ModelProviderName.LLAMALOCAL + ) { + this.provider = new LocalImageProvider(); + elizaLogger.debug("Using llama local for vision model"); + } else if ( + this.runtime.imageVisionModelProvider === + ModelProviderName.GOOGLE + ) { + this.provider = new GoogleImageProvider(this.runtime); + elizaLogger.debug("Using google for vision model"); + } else if ( + this.runtime.imageVisionModelProvider === + ModelProviderName.OPENAI + ) { + this.provider = new OpenAIImageProvider(this.runtime); + elizaLogger.debug("Using openai for vision model"); + } else { elizaLogger.error( - "OpenAI request failed (attempt", - attempt + 1, - "):", - error + `Unsupported image vision model provider: ${this.runtime.imageVisionModelProvider}` ); - if (attempt === 2) throw error; } + } else if (model === models[ModelProviderName.LLAMALOCAL]) { + this.provider = new LocalImageProvider(); + elizaLogger.debug("Using llama local for vision model"); + } else if (model === models[ModelProviderName.GOOGLE]) { + this.provider = new GoogleImageProvider(this.runtime); + elizaLogger.debug("Using google for vision model"); + } else { + elizaLogger.debug("Using default openai for vision model"); + this.provider = new OpenAIImageProvider(this.runtime); } - throw new Error( - "Failed to recognize image with OpenAI after 3 attempts" - ); - } - private async processQueue(): Promise { - if (this.processing || this.queue.length === 0) return; - - this.processing = true; - while (this.queue.length > 0) { - const imageUrl = this.queue.shift(); - await this.processImage(imageUrl); - } - this.processing = false; + await this.provider.initialize(); + this.initialized = true; } - private async processImage( + private async loadImageData( imageUrl: string - ): Promise<{ title: string; description: string }> { - if (!this.model || !this.processor || !this.tokenizer) { - throw new Error("Model components not initialized"); - } - - elizaLogger.log("Processing image:", imageUrl); + ): Promise<{ data: Buffer; mimeType: string }> { const isGif = imageUrl.toLowerCase().endsWith(".gif"); - let imageToProcess = imageUrl; - - try { - if (isGif) { - elizaLogger.log("Extracting first frame from GIF"); - const { filePath } = - await this.extractFirstFrameFromGif(imageUrl); - imageToProcess = filePath; + let imageData: Buffer; + let mimeType: string; + + if (isGif) { + const { filePath } = await this.extractFirstFrameFromGif(imageUrl); + imageData = fs.readFileSync(filePath); + mimeType = "image/png"; + fs.unlinkSync(filePath); // Clean up temp file + } else { + if (fs.existsSync(imageUrl)) { + imageData = fs.readFileSync(imageUrl); + const ext = path.extname(imageUrl).slice(1); + mimeType = ext ? `image/${ext}` : "image/jpeg"; + } else { + const response = await fetch(imageUrl); + if (!response.ok) { + throw new Error( + `Failed to fetch image: ${response.statusText}` + ); + } + imageData = Buffer.from(await response.arrayBuffer()); + mimeType = response.headers.get("content-type") || "image/jpeg"; } + } - const image = await RawImage.fromURL(imageToProcess); - const visionInputs = await this.processor(image); - const prompts = - this.processor.construct_prompts(""); - const textInputs = this.tokenizer(prompts); - - elizaLogger.log("Generating image description"); - const generatedIds = (await this.model.generate({ - ...textInputs, - ...visionInputs, - max_new_tokens: 256, - })) as Tensor; - - const generatedText = this.tokenizer.batch_decode(generatedIds, { - skip_special_tokens: false, - })[0]; - - const result = this.processor.post_process_generation( - generatedText, - "", - image.size - ); - - const detailedCaption = result[""] as string; - return { title: detailedCaption, description: detailedCaption }; - } catch (error) { - elizaLogger.error("Error processing image:", error); - throw error; - } finally { - if (isGif && imageToProcess !== imageUrl) { - fs.unlinkSync(imageToProcess); - } + if (!imageData || imageData.length === 0) { + throw new Error("Failed to fetch image data"); } + + return { data: imageData, mimeType }; } private async extractFirstFrameFromGif( @@ -343,6 +357,22 @@ export class ImageDescriptionService writeStream.on("error", reject); }); } + + async describeImage( + imageUrl: string + ): Promise<{ title: string; description: string }> { + if (!this.initialized) { + await this.initializeProvider(); + } + + try { + const { data, mimeType } = await this.loadImageData(imageUrl); + return await this.provider!.describeImage(data, mimeType); + } catch (error) { + elizaLogger.error("Error in describeImage:", error); + throw error; + } + } } export default ImageDescriptionService; diff --git a/packages/plugin-node/src/services/index.ts b/packages/plugin-node/src/services/index.ts index 6e4be71cdf..554793d679 100644 --- a/packages/plugin-node/src/services/index.ts +++ b/packages/plugin-node/src/services/index.ts @@ -1,3 +1,4 @@ +import { AwsS3Service } from "./awsS3.ts"; import { BrowserService } from "./browser.ts"; import { ImageDescriptionService } from "./image.ts"; import { LlamaService } from "./llama.ts"; @@ -5,9 +6,9 @@ import { PdfService } from "./pdf.ts"; import { SpeechService } from "./speech.ts"; import { TranscriptionService } from "./transcription.ts"; import { VideoService } from "./video.ts"; -import { AwsS3Service } from "./awsS3.ts"; export { + AwsS3Service, BrowserService, ImageDescriptionService, LlamaService, @@ -15,5 +16,4 @@ export { SpeechService, TranscriptionService, VideoService, - AwsS3Service, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 741187355e..68ec523bda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -169,6 +169,9 @@ importers: '@elizaos/plugin-arthera': specifier: workspace:* version: link:../packages/plugin-arthera + '@elizaos/plugin-autonome': + specifier: workspace:* + version: link:../packages/plugin-autonome '@elizaos/plugin-avail': specifier: workspace:* version: link:../packages/plugin-avail @@ -1271,6 +1274,30 @@ importers: specifier: 7.1.0 version: 7.1.0 + packages/plugin-autonome: + dependencies: + '@coral-xyz/anchor': + specifier: 0.30.1 + version: 0.30.1(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@elizaos/core': + specifier: workspace:* + version: link:../core + '@elizaos/plugin-tee': + specifier: workspace:* + version: link:../plugin-tee + '@elizaos/plugin-trustdb': + specifier: workspace:* + version: link:../plugin-trustdb + axios: + specifier: ^1.7.9 + version: 1.7.9(debug@4.4.0) + form-data: + specifier: 4.0.1 + version: 4.0.1 + whatwg-url: + specifier: 7.1.0 + version: 7.1.0 + packages/plugin-avail: dependencies: '@elizaos/core': @@ -1718,6 +1745,28 @@ importers: specifier: 7.1.0 version: 7.1.0 + packages/plugin-irys: + dependencies: + '@elizaos/core': + specifier: workspace:* + version: link:../core + '@irys/upload': + specifier: ^0.0.14 + version: 0.0.14(arweave@1.15.5)(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@irys/upload-ethereum': + specifier: ^0.0.14 + version: 0.0.14(arweave@1.15.5)(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) + graphql-request: + specifier: ^4.0.0 + version: 4.3.0(encoding@0.1.13)(graphql@16.10.0) + devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.17.9 + tsup: + specifier: 8.3.5 + version: 8.3.5(@swc/core@1.10.7(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + packages/plugin-letzai: dependencies: '@elizaos/core': @@ -5843,6 +5892,25 @@ packages: '@ioredis/commands@1.2.0': resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + '@irys/arweave@0.0.2': + resolution: {integrity: sha512-ddE5h4qXbl0xfGlxrtBIwzflaxZUDlDs43TuT0u1OMfyobHul4AA1VEX72Rpzw2bOh4vzoytSqA1jCM7x9YtHg==} + + '@irys/bundles@0.0.1': + resolution: {integrity: sha512-yeQNzElERksFbfbNxJQsMkhtkI3+tNqIMZ/Wwxh76NVBmCnCP5huefOv7ET0MOO7TEQL+TqvKSqmFklYSvTyHw==} + + '@irys/query@0.0.9': + resolution: {integrity: sha512-uBIy8qeOQupUSBzR+1KU02JJXFp5Ue9l810PIbBF/ylUB8RTreUFkyyABZ7J3FUaOIXFYrT7WVFSJSzXM7P+8w==} + engines: {node: '>=16.10.0'} + + '@irys/upload-core@0.0.9': + resolution: {integrity: sha512-Ha4pX8jgYBA3dg5KHDPk+Am0QO+SmvnmgCwKa6uiDXZKuVr0neSx4V1OAHoP+As+j7yYgfChdsdrvsNzZGGehA==} + + '@irys/upload-ethereum@0.0.14': + resolution: {integrity: sha512-hzJkmuQ7JnHNhaunbBpwZSxrbchdiWCTkeFUYI4OZyRNFK1vdPfQ+fAiFBnqSTS8yuqlnN+6xad2b8gS+1JmSA==} + + '@irys/upload@0.0.14': + resolution: {integrity: sha512-6XdkyS5cVINcPjv1MzA6jDsawfG7Bw6sq5wilNx5B4X7nNotBPC3SuRrZs06G/0BTUj15W+TRO/tZTDWRUfZzA==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -7759,6 +7827,12 @@ packages: '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@randlabs/communication-bridge@1.0.1': + resolution: {integrity: sha512-CzS0U8IFfXNK7QaJFE4pjbxDGfPjbXBEsEaCn9FN15F+ouSAEUQkva3Gl66hrkBZOGexKFEWMwUHIDKpZ2hfVg==} + + '@randlabs/myalgo-connect@1.4.2': + resolution: {integrity: sha512-K9hEyUi7G8tqOp7kWIALJLVbGCByhilcy6123WfcorxWwiE1sbQupPyIU5f3YdQK6wMjBsyTWiLW52ZBMp7sXA==} + '@raydium-io/raydium-sdk-v2@0.1.82-alpha': resolution: {integrity: sha512-PScLnWZV5Y/igcvP4hbD/1ztzW2w5a2YStolu9A5VT6uB2q+izeo+SE7IqzZggyaReXyisjdkNGpB/kMdkdJGQ==} @@ -8762,6 +8836,10 @@ packages: '@supabase/supabase-js@2.46.2': resolution: {integrity: sha512-5FEzYMZhfIZrMWEqo5/dQincvrhM+DeMWH3/okeZrkBBW1AJxblOQhnhF4/dfNYK25oZ1O8dAnnxZ9gQqdr40w==} + '@supercharge/promise-pool@3.2.0': + resolution: {integrity: sha512-pj0cAALblTZBPtMltWOlZTQSLT07jIaFNeM8TWoJD1cQMgDB9mcMlVMoetiB35OzNJpqQ2b+QEtwiR9f20mADg==} + engines: {node: '>=8'} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -10123,6 +10201,10 @@ packages: resolution: {integrity: sha512-1aQJZX2Ax5X7Bq9j9Wkv0gczxexnkshlNNxTc0sD5DjAb+NIgfHkI3rpnjSgr6pK1s4V0Z7viBgE9/FHcIwkyw==} engines: {node: '>=8'} + algo-msgpack-with-bigint@2.1.1: + resolution: {integrity: sha512-F1tGh056XczEaEAqu7s+hlZUDWwOBT70Eq0lfMpBP2YguSQVyxRbprLq5rELXKQOyOaixTWYhMeMQMzP0U5FoQ==} + engines: {node: '>= 10'} + algoliasearch-helper@3.22.6: resolution: {integrity: sha512-F2gSb43QHyvZmvH/2hxIjbk/uFdO2MguQYTFP7J+RowMW1csjIODMobEnpLI8nbLQuzZnGZdIxl5Bpy1k9+CFQ==} peerDependencies: @@ -10135,6 +10217,10 @@ packages: resolution: {integrity: sha512-zrLtGhC63z3sVLDDKGW+SlCRN9eJHFTgdEmoAOpsVh6wgGL1GgTTDou7tpCBjevzgIvi3AIyDAQO3Xjbg5eqZg==} engines: {node: '>= 14.0.0'} + algosdk@1.24.1: + resolution: {integrity: sha512-9moZxdqeJ6GdE4N6fA/GlUP4LrbLZMYcYkt141J4Ss68OfEgH9qW0wBuZ3ZOKEx/xjc5bg7mLP2Gjg7nwrkmww==} + engines: {node: '>=14.0.0'} + amp-message@0.1.2: resolution: {integrity: sha512-JqutcFwoU1+jhv7ArgW38bqrE+LQdcRv4NxNw0mp0JHQyB6tXesWRjtYKlDgHRY2o3JE5UTaBGUK8kSWUdxWUg==} @@ -10225,6 +10311,9 @@ packages: aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + arconnect@0.4.2: + resolution: {integrity: sha512-Jkpd4QL3TVqnd3U683gzXmZUVqBUy17DdJDuL/3D9rkysLgX6ymJ2e+sR+xyZF5Rh42CBqDXWNMmCjBXeP7Gbw==} + are-docs-informative@0.0.2: resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} engines: {node: '>=14'} @@ -10317,9 +10406,21 @@ packages: resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} engines: {node: '>=8'} + arweave-stream-tx@1.2.2: + resolution: {integrity: sha512-bNt9rj0hbAEzoUZEF2s6WJbIz8nasZlZpxIw03Xm8fzb9gRiiZlZGW3lxQLjfc9Z0VRUWDzwtqoYeEoB/JDToQ==} + peerDependencies: + arweave: ^1.10.0 + + arweave@1.15.5: + resolution: {integrity: sha512-Zj3b8juz1ZtDaQDPQlzWyk2I4wZPx3RmcGq8pVJeZXl2Tjw0WRy5ueHPelxZtBLqCirGoZxZEAFRs6SZUSCBjg==} + engines: {node: '>=18'} + asn1.js@4.10.1: resolution: {integrity: sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==} + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} @@ -11798,6 +11899,9 @@ packages: csv-parse@5.6.0: resolution: {integrity: sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==} + csv-stringify@6.5.2: + resolution: {integrity: sha512-RFPahj0sXcmUyjrObAK+DOWtMvMIFV328n4qZJhgX3x2RqkQgOTU2mCUmiFR0CzM6AzChlRSUErjiJeEt8BaQA==} + csv-writer@1.6.0: resolution: {integrity: sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==} @@ -13028,6 +13132,10 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + extract-files@9.0.0: + resolution: {integrity: sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==} + engines: {node: ^10.17.0 || ^12.0.0 || >= 13.7.0} + extract-zip@2.0.1: resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} @@ -13312,6 +13420,10 @@ packages: resolution: {integrity: sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==} engines: {node: '>= 0.12'} + form-data@3.0.2: + resolution: {integrity: sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==} + engines: {node: '>= 6'} + form-data@4.0.1: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} @@ -13706,6 +13818,11 @@ packages: graphemesplit@2.4.4: resolution: {integrity: sha512-lKrpp1mk1NH26USxC/Asw4OHbhSQf5XfrWZ+CDv/dFVvd1j17kFgMotdJvOesmHkbFX9P9sBfpH8VogxOWLg8w==} + graphql-request@4.3.0: + resolution: {integrity: sha512-2v6hQViJvSsifK606AliqiNiijb1uwWp6Re7o0RTyH+uRTv/u7Uqm2g4Fjq/LgZIzARB38RZEvVBFOQOVdlBow==} + peerDependencies: + graphql: 14 - 16 + graphql-request@6.1.0: resolution: {integrity: sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==} peerDependencies: @@ -13883,6 +14000,9 @@ packages: hey-listen@1.0.8: resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} + hi-base32@0.5.1: + resolution: {integrity: sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==} + history@4.10.1: resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==} @@ -14857,6 +14977,9 @@ packages: js-sha3@0.8.0: resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} + js-sha512@0.8.0: + resolution: {integrity: sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==} + js-tiktoken@1.0.15: resolution: {integrity: sha512-65ruOWWXDEZHHbAo7EjOcNxOGasQKbL4Fq3jEr2xsCqSsoOo6VVSqzWQb6PRIqypFSDcma4jO90YP0w5X8qVXQ==} @@ -16166,6 +16289,9 @@ packages: resolution: {integrity: sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==} engines: {node: '>=10'} + multistream@4.1.0: + resolution: {integrity: sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==} + mustache@4.0.0: resolution: {integrity: sha512-FJgjyX/IVkbXBXYUwH+OYwQKqWpFPLaLVESd70yHjSDunwzV2hZOoTBvPf4KLoxesUzzyfTH6F784Uqd7Wm5yA==} engines: {npm: '>=1.4.0'} @@ -19879,6 +20005,9 @@ packages: resolution: {integrity: sha512-LQIHmHnuzfZgZWAf2HzL83TIIrD8NhhI0DVxqo9/FdOd4ilec+NTNZOlDZf7EwrTNoutccbsHjvWHYXLAtvxjw==} hasBin: true + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -21027,6 +21156,9 @@ packages: resolution: {integrity: sha512-sfAcO2yeSU0CSPFI/DmZp3FsFE9T+8913nv1xWBOyzODv13fwkn6Vl7HqxGpkr9F608M+8SuFId3s+BlZqfXww==} engines: {node: '>=4.0'} + vlq@2.0.4: + resolution: {integrity: sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA==} + vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} @@ -26571,6 +26703,102 @@ snapshots: '@ioredis/commands@1.2.0': {} + '@irys/arweave@0.0.2': + dependencies: + asn1.js: 5.4.1 + async-retry: 1.3.3 + axios: 1.7.9(debug@4.4.0) + base64-js: 1.5.1 + bignumber.js: 9.1.2 + transitivePeerDependencies: + - debug + + '@irys/bundles@0.0.1(arweave@1.15.5)(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10)': + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/providers': 5.7.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@ethersproject/signing-key': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/wallet': 5.7.0 + '@irys/arweave': 0.0.2 + '@noble/ed25519': 1.7.3 + base64url: 3.0.1 + bs58: 4.0.1 + keccak: 3.0.4 + secp256k1: 5.0.1 + optionalDependencies: + '@randlabs/myalgo-connect': 1.4.2 + algosdk: 1.24.1(encoding@0.1.13) + arweave-stream-tx: 1.2.2(arweave@1.15.5) + multistream: 4.1.0 + tmp-promise: 3.0.3 + transitivePeerDependencies: + - arweave + - bufferutil + - debug + - encoding + - utf-8-validate + + '@irys/query@0.0.9': + dependencies: + async-retry: 1.3.3 + axios: 1.7.9(debug@4.4.0) + transitivePeerDependencies: + - debug + + '@irys/upload-core@0.0.9(arweave@1.15.5)(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10)': + dependencies: + '@irys/bundles': 0.0.1(arweave@1.15.5)(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@irys/query': 0.0.9 + '@supercharge/promise-pool': 3.2.0 + async-retry: 1.3.3 + axios: 1.7.9(debug@4.4.0) + base64url: 3.0.1 + bignumber.js: 9.1.2 + transitivePeerDependencies: + - arweave + - bufferutil + - debug + - encoding + - utf-8-validate + + '@irys/upload-ethereum@0.0.14(arweave@1.15.5)(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10)': + dependencies: + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/contracts': 5.7.0 + '@ethersproject/providers': 5.7.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@ethersproject/wallet': 5.7.0 + '@irys/bundles': 0.0.1(arweave@1.15.5)(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@irys/upload': 0.0.14(arweave@1.15.5)(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@irys/upload-core': 0.0.9(arweave@1.15.5)(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) + bignumber.js: 9.1.2 + transitivePeerDependencies: + - arweave + - bufferutil + - debug + - encoding + - utf-8-validate + + '@irys/upload@0.0.14(arweave@1.15.5)(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10)': + dependencies: + '@irys/bundles': 0.0.1(arweave@1.15.5)(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@irys/upload-core': 0.0.9(arweave@1.15.5)(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) + async-retry: 1.3.3 + axios: 1.7.9(debug@4.4.0) + base64url: 3.0.1 + bignumber.js: 9.1.2 + csv-parse: 5.6.0 + csv-stringify: 6.5.2 + inquirer: 8.2.6 + mime-types: 2.1.35 + transitivePeerDependencies: + - arweave + - bufferutil + - debug + - encoding + - utf-8-validate + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -29845,6 +30073,14 @@ snapshots: '@radix-ui/rect@1.1.0': {} + '@randlabs/communication-bridge@1.0.1': + optional: true + + '@randlabs/myalgo-connect@1.4.2': + dependencies: + '@randlabs/communication-bridge': 1.0.1 + optional: true + '@raydium-io/raydium-sdk-v2@0.1.82-alpha(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.7.3)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 @@ -31206,7 +31442,7 @@ snapshots: dependencies: '@babel/runtime': 7.26.0 '@noble/curves': 1.8.0 - '@noble/hashes': 1.5.0 + '@noble/hashes': 1.7.0 '@solana/buffer-layout': 4.0.1 agentkeepalive: 4.6.0 bigint-buffer: 1.1.5 @@ -31482,6 +31718,8 @@ snapshots: - bufferutil - utf-8-validate + '@supercharge/promise-pool@3.2.0': {} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -33534,6 +33772,9 @@ snapshots: alawmulaw@6.0.0: {} + algo-msgpack-with-bigint@2.1.1: + optional: true + algoliasearch-helper@3.22.6(algoliasearch@4.24.0): dependencies: '@algolia/events': 4.0.1 @@ -33573,6 +33814,22 @@ snapshots: '@algolia/requester-fetch': 5.19.0 '@algolia/requester-node-http': 5.19.0 + algosdk@1.24.1(encoding@0.1.13): + dependencies: + algo-msgpack-with-bigint: 2.1.1 + buffer: 6.0.3 + cross-fetch: 3.2.0(encoding@0.1.13) + hi-base32: 0.5.1 + js-sha256: 0.9.0 + js-sha3: 0.8.0 + js-sha512: 0.8.0 + json-bigint: 1.0.0 + tweetnacl: 1.0.3 + vlq: 2.0.4 + transitivePeerDependencies: + - encoding + optional: true + amp-message@0.1.2: dependencies: amp: 0.3.1 @@ -33648,6 +33905,11 @@ snapshots: aproba@2.0.0: {} + arconnect@0.4.2: + dependencies: + arweave: 1.15.5 + optional: true + are-docs-informative@0.0.2: {} are-we-there-yet@2.0.0: @@ -33754,12 +34016,33 @@ snapshots: arrify@2.0.1: {} + arweave-stream-tx@1.2.2(arweave@1.15.5): + dependencies: + arweave: 1.15.5 + exponential-backoff: 3.1.1 + optional: true + + arweave@1.15.5: + dependencies: + arconnect: 0.4.2 + asn1.js: 5.4.1 + base64-js: 1.5.1 + bignumber.js: 9.1.2 + optional: true + asn1.js@4.10.1: dependencies: bn.js: 4.12.1 inherits: 2.0.4 minimalistic-assert: 1.0.1 + asn1.js@5.4.1: + dependencies: + bn.js: 4.12.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + asn1@0.2.6: dependencies: safer-buffer: 2.1.2 @@ -35732,6 +36015,8 @@ snapshots: csv-parse@5.6.0: {} + csv-stringify@6.5.2: {} + csv-writer@1.6.0: {} culvert@0.1.2: {} @@ -37463,9 +37748,11 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + extract-files@9.0.0: {} + extract-zip@2.0.1: dependencies: - debug: 4.3.4 + debug: 4.4.0(supports-color@5.5.0) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -37800,6 +38087,12 @@ snapshots: mime-types: 2.1.35 safe-buffer: 5.2.1 + form-data@3.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + form-data@4.0.1: dependencies: asynckit: 0.4.0 @@ -38333,6 +38626,15 @@ snapshots: js-base64: 3.7.7 unicode-trie: 2.0.0 + graphql-request@4.3.0(encoding@0.1.13)(graphql@16.10.0): + dependencies: + cross-fetch: 3.2.0(encoding@0.1.13) + extract-files: 9.0.0 + form-data: 3.0.2 + graphql: 16.10.0 + transitivePeerDependencies: + - encoding + graphql-request@6.1.0(encoding@0.1.13)(graphql@16.10.0): dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.10.0) @@ -38629,6 +38931,9 @@ snapshots: hey-listen@1.0.8: {} + hi-base32@0.5.1: + optional: true + history@4.10.1: dependencies: '@babel/runtime': 7.26.0 @@ -40099,6 +40404,9 @@ snapshots: js-sha3@0.8.0: {} + js-sha512@0.8.0: + optional: true + js-tiktoken@1.0.15: dependencies: base64-js: 1.5.1 @@ -40758,7 +41066,7 @@ snapshots: log-symbols@4.1.0: dependencies: - chalk: 4.1.0 + chalk: 4.1.2 is-unicode-supported: 0.1.0 log-symbols@6.0.0: @@ -41855,6 +42163,12 @@ snapshots: arrify: 2.0.1 minimatch: 3.0.5 + multistream@4.1.0: + dependencies: + once: 1.4.0 + readable-stream: 3.6.2 + optional: true + mustache@4.0.0: {} mustache@4.2.0: {} @@ -46391,6 +46705,11 @@ snapshots: dependencies: tldts-core: 6.1.71 + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.3 + optional: true + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 @@ -47943,6 +48262,9 @@ snapshots: ini: 1.3.8 js-git: 0.7.8 + vlq@2.0.4: + optional: true + vm-browserify@1.1.2: {} vscode-jsonrpc@8.2.0: {}