From e69aeca365e173f3c5176c1661c6e0635008ad75 Mon Sep 17 00:00:00 2001 From: JSon Date: Fri, 20 Dec 2024 22:43:17 +0800 Subject: [PATCH 01/99] feat(plugin-nft-generation): add mint NFT with collection address action --- .../src/actions/mintNFTAction.ts | 381 ++++++++++++++++++ .../src/actions/nftCollectionGeneration.ts | 290 +++++++++++++ packages/plugin-nft-generation/src/index.ts | 189 +-------- .../src/provider/wallet/walletSolana.ts | 4 + 4 files changed, 679 insertions(+), 185 deletions(-) create mode 100644 packages/plugin-nft-generation/src/actions/mintNFTAction.ts create mode 100644 packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts diff --git a/packages/plugin-nft-generation/src/actions/mintNFTAction.ts b/packages/plugin-nft-generation/src/actions/mintNFTAction.ts new file mode 100644 index 0000000000..1551533715 --- /dev/null +++ b/packages/plugin-nft-generation/src/actions/mintNFTAction.ts @@ -0,0 +1,381 @@ +import { + Action, + composeContext, + Content, + elizaLogger, + generateObjectDeprecated, + getEmbeddingZeroVector, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + stringToUuid, +} from "@ai16z/eliza"; +import { createNFT } from "../handlers/createNFT.ts"; +import { verifyNFT } from "../handlers/verifyNFT.ts"; +import { sleep } from "../index.ts"; +import WalletSolana from "../provider/wallet/walletSolana.ts"; +import { PublicKey } from "@solana/web3.js"; + +const mintTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Example response: +\`\`\`json +{ + "collectionAddress": "D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", +} +\`\`\` + +{{message}} + +Given the recent messages, extract the following information about the requested mint nft: +- collection contract address + +Respond with a JSON markdown block containing only the extracted values.`; + +export interface MintContent extends Content { + collectionAddress: string; +} + +function isMintNFTContent( + runtime: IAgentRuntime, + content: any +): content is MintContent { + console.log("Content for mint", content); + return typeof content.collectionAddress === "string"; +} + +const mintNFTAction: Action = { + name: "MINT_NFT", + similes: [ + "NFT_MINTING", + "NFT_CREATION", + "CREATE_NFT", + "GENERATE_NFT", + "MINT_TOKEN", + "CREATE_TOKEN", + "MAKE_NFT", + "TOKEN_GENERATION", + ], + description: "Mint NFTs for the collection", + validate: async (runtime: IAgentRuntime, _message: Memory) => { + const AwsAccessKeyIdOk = !!runtime.getSetting("AWS_ACCESS_KEY_ID"); + const AwsSecretAccessKeyOk = !!runtime.getSetting( + "AWS_SECRET_ACCESS_KEY" + ); + const AwsRegionOk = !!runtime.getSetting("AWS_REGION"); + const AwsS3BucketOk = !!runtime.getSetting("AWS_S3_BUCKET"); + + return ( + AwsAccessKeyIdOk || + AwsSecretAccessKeyOk || + AwsRegionOk || + AwsS3BucketOk + ); + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + options: { [key: string]: unknown }, + callback: HandlerCallback + ) => { + try { + elizaLogger.log("Composing state for message:", message); + const userId = runtime.agentId; + const agentName = runtime.character.name; + const roomId = stringToUuid("nft_generate_room-" + agentName); + + const memory: Memory = { + agentId: userId, + userId, + roomId, + content: { + text: message.content.text, + source: "nft-generator", + }, + createdAt: Date.now(), + embedding: getEmbeddingZeroVector(), + }; + const state = await runtime.composeState(memory, { + message: message.content.text, + }); + + elizaLogger.log("state:", state); + + // Compose transfer context + const transferContext = composeContext({ + state, + template: mintTemplate, + }); + + const content = await generateObjectDeprecated({ + runtime, + context: transferContext, + modelClass: ModelClass.LARGE, + }); + + elizaLogger.log("generateObjectDeprecated:", transferContext); + + if (!isMintNFTContent(runtime, content)) { + elizaLogger.error("Invalid content for MINT_NFT action."); + if (callback) { + callback({ + text: "Unable to process mint request. Invalid content provided.", + content: { error: "Invalid mint content" }, + }); + } + return false; + } + + elizaLogger.log("mint content", content); + + const publicKey = runtime.getSetting("SOLANA_PUBLIC_KEY"); + const privateKey = runtime.getSetting("SOLANA_PRIVATE_KEY"); + + const wallet = new WalletSolana( + new PublicKey(publicKey), + privateKey + ); + + const collectionInfo = await wallet.fetchDigitalAsset( + content.collectionAddress + ); + elizaLogger.log("Collection Info", collectionInfo); + const metadata = collectionInfo.metadata; + if (metadata.collection?.["value"]) { + callback({ + text: `Unable to process mint request. Invalid collection address ${content.collectionAddress}.`, + content: { error: "Invalid collection address." }, + }); + return false; + } + if (metadata) { + elizaLogger.log("nft params", {}); + const nftRes = await createNFT({ + runtime, + collectionName: metadata.name, + collectionAddress: content.collectionAddress, + collectionAdminPublicKey: metadata.updateAuthority, + collectionFee: metadata.sellerFeeBasisPoints, + tokenId: 1, + }); + + elizaLogger.log("NFT Address:", nftRes); + + if (nftRes) { + callback({ + text: `Congratulations to you! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ \nCollection Address: ${content.collectionAddress}\n NFT Address: ${nftRes.address}\n NFT Link: ${nftRes.link}`, //caption.description, + attachments: [], + }); + await sleep(15000); + await verifyNFT({ + runtime, + collectionAddress: content.collectionAddress, + NFTAddress: nftRes.address, + }); + } else { + callback({ + text: `Mint NFT Error in ${content.collectionAddress}.`, + content: { error: "Mint NFT Error." }, + }); + return false; + } + } else { + callback({ + text: "Unable to process mint request. Invalid collection address.", + content: { error: "Invalid collection address." }, + }); + return false; + } + + // + // const userId = runtime.agentId; + // elizaLogger.log("User ID:", userId); + // + // const collectionAddressRes = await createCollection({ + // runtime, + // collectionName: runtime.character.name, + // }); + // + // const collectionInfo = collectionAddressRes.collectionInfo; + // + // elizaLogger.log("Collection Address:", collectionAddressRes); + + // + // elizaLogger.log("NFT Address:", nftRes); + // + // + // callback({ + // text: `Congratulations to you! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ \nCollection : ${collectionAddressRes.link}\n NFT: ${nftRes.link}`, //caption.description, + // attachments: [], + // }); + // await sleep(15000); + // await verifyNFT({ + // runtime, + // collectionAddress: collectionAddressRes.address, + // NFTAddress: nftRes.address, + // }); + return []; + } catch (e: any) { + console.log(e); + } + + // callback(); + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "mint nft for collection: D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", + }, + }, + { + user: "{{agentName}}", + content: { + text: "I've minted a new NFT in your specified collection.", + action: "MINT_NFT", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Could you create an NFT in collection D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS?", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Successfully minted your NFT in the specified collection.", + action: "MINT_NFT", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Please mint a new token in D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS collection", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Your NFT has been minted in the collection successfully.", + action: "MINT_NFT", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Generate NFT for D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", + }, + }, + { + user: "{{agentName}}", + content: { + text: "I've generated and minted your NFT in the collection.", + action: "MINT_NFT", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "I want to mint an NFT in collection D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Your NFT has been successfully minted in the collection.", + action: "MINT_NFT", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Create a new NFT token in D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS collection", + }, + }, + { + user: "{{agentName}}", + content: { + text: "The NFT has been created in your specified collection.", + action: "MINT_NFT", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Issue an NFT for collection D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", + }, + }, + { + user: "{{agentName}}", + content: { + text: "I've issued your NFT in the requested collection.", + action: "MINT_NFT", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Make a new NFT in D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Your new NFT has been minted in the collection.", + action: "MINT_NFT", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Can you mint an NFT for D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS collection?", + }, + }, + { + user: "{{agentName}}", + content: { + text: "I've completed minting your NFT in the collection.", + action: "MINT_NFT", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Add a new NFT to collection D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", + }, + }, + { + user: "{{agentName}}", + content: { + text: "A new NFT has been added to your collection.", + action: "MINT_NFT", + }, + }, + ], + ], +} as Action; + +export default mintNFTAction; diff --git a/packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts b/packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts new file mode 100644 index 0000000000..9973d73407 --- /dev/null +++ b/packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts @@ -0,0 +1,290 @@ +import { + Action, + elizaLogger, + HandlerCallback, + IAgentRuntime, + Memory, + State, +} from "@ai16z/eliza"; +import { createCollection } from "../handlers/createCollection.ts"; + +const nftCollectionGeneration: Action = { + name: "GENERATE_COLLECTION", + similes: [ + "COLLECTION_GENERATION", + "COLLECTION_GEN", + "CREATE_COLLECTION", + "MAKE_COLLECTION", + "GENERATE_COLLECTION", + ], + description: "Generate an NFT collection for the message", + validate: async (runtime: IAgentRuntime, _message: Memory) => { + const awsAccessKeyIdOk = !!runtime.getSetting("AWS_ACCESS_KEY_ID"); + const awsSecretAccessKeyOk = !!runtime.getSetting( + "AWS_SECRET_ACCESS_KEY" + ); + const awsRegionOk = !!runtime.getSetting("AWS_REGION"); + const awsS3BucketOk = !!runtime.getSetting("AWS_S3_BUCKET"); + const solanaPrivateKeyOk = !!runtime.getSetting("SOLANA_PRIVATE_KEY"); + const solanaPublicKeyOk = !!runtime.getSetting("SOLANA_PUBLIC_KEY"); + + return ( + awsAccessKeyIdOk || + awsSecretAccessKeyOk || + awsRegionOk || + awsS3BucketOk || + solanaPrivateKeyOk || + solanaPublicKeyOk + ); + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + options: { [key: string]: unknown }, + callback: HandlerCallback + ) => { + try { + elizaLogger.log("Composing state for message:", message); + const collectionAddressRes = await createCollection({ + runtime, + collectionName: runtime.character.name, + }); + elizaLogger.log("Collection Info:", collectionAddressRes); + if (callback) { + callback({ + text: `Congratulations to you! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ \nCollection Link : ${collectionAddressRes.link}\n Address: ${collectionAddressRes.address}`, //caption.description, + attachments: [], + }); + } + return []; + } catch (e: any) { + console.log(e); + throw e; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { text: "Generate a collection" }, + }, + { + user: "{{agentName}}", + content: { + text: "Here's the collection you requested.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Generate a collection using {{agentName}}" }, + }, + { + user: "{{agentName}}", + content: { + text: "We've successfully created a collection.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Create a collection using {{agentName}}" }, + }, + { + user: "{{agentName}}", + content: { + text: "Here's the collection you requested.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Build a Collection" }, + }, + { + user: "{{agentName}}", + content: { + text: "The collection has been successfully built.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Assemble a collection with {{agentName}}" }, + }, + { + user: "{{agentName}}", + content: { + text: "The collection has been assembled", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Make a collection" }, + }, + { + user: "{{agentName}}", + content: { + text: "The collection has been produced successfully.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Could you create a new collection for my photos?", + }, + }, + { + user: "{{agentName}}", + content: { + text: "I've created a new collection for your photos.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "I need a collection for organizing my music", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Your music collection has been generated.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Please set up a collection for my documents", + }, + }, + { + user: "{{agentName}}", + content: { + text: "I've set up a new collection for your documents.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Start a new collection for me" }, + }, + { + user: "{{agentName}}", + content: { + text: "Your new collection has been created.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "I'd like to make a collection of my recipes", + }, + }, + { + user: "{{agentName}}", + content: { + text: "I've generated a collection for your recipes.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Can you generate a collection for my artwork?", + }, + }, + { + user: "{{agentName}}", + content: { + text: "Your artwork collection has been generated.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Initialize a new collection please" }, + }, + { + user: "{{agentName}}", + content: { + text: "I've initialized a new collection for you.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Create a collection for my travel memories" }, + }, + { + user: "{{agentName}}", + content: { + text: "Your travel memories collection has been created.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Would you make a collection for my projects?", + }, + }, + { + user: "{{agentName}}", + content: { + text: "I've made a collection for your projects.", + action: "GENERATE_COLLECTION", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Set up a collection for my bookmarks" }, + }, + { + user: "{{agentName}}", + content: { + text: "Your bookmarks collection has been set up.", + action: "GENERATE_COLLECTION", + }, + }, + ], + ], +} as Action; + +export default nftCollectionGeneration; diff --git a/packages/plugin-nft-generation/src/index.ts b/packages/plugin-nft-generation/src/index.ts index 07a147ef0d..5540ec3a5b 100644 --- a/packages/plugin-nft-generation/src/index.ts +++ b/packages/plugin-nft-generation/src/index.ts @@ -1,16 +1,6 @@ -import { - Action, - elizaLogger, - HandlerCallback, - IAgentRuntime, - Memory, - Plugin, - State, -} from "@elizaos/core"; - -import { createCollection } from "./handlers/createCollection.ts"; -import { createNFT } from "./handlers/createNFT.ts"; -import { verifyNFT } from "./handlers/verifyNFT.ts"; +import { Plugin } from "@ai16z/eliza"; +import nftCollectionGeneration from "./actions/nftCollectionGeneration.ts"; +import mintNFTAction from "./actions/mintNFTAction.ts"; export * from "./provider/wallet/walletSolana.ts"; export * from "./api.ts"; @@ -21,181 +11,10 @@ export async function sleep(ms: number = 3000) { }); } -const nftCollectionGeneration: Action = { - name: "GENERATE_COLLECTION", - similes: [ - "COLLECTION_GENERATION", - "COLLECTION_GEN", - "CREATE_COLLECTION", - "MAKE_COLLECTION", - "GENERATE_COLLECTION", - ], - description: "Generate an NFT collection for the message", - validate: async (runtime: IAgentRuntime, _message: Memory) => { - const AwsAccessKeyIdOk = !!runtime.getSetting("AWS_ACCESS_KEY_ID"); - const AwsSecretAccessKeyOk = !!runtime.getSetting( - "AWS_SECRET_ACCESS_KEY" - ); - const AwsRegionOk = !!runtime.getSetting("AWS_REGION"); - const AwsS3BucketOk = !!runtime.getSetting("AWS_S3_BUCKET"); - - return ( - AwsAccessKeyIdOk || - AwsSecretAccessKeyOk || - AwsRegionOk || - AwsS3BucketOk - ); - }, - handler: async ( - runtime: IAgentRuntime, - message: Memory, - state: State, - options: { [key: string]: unknown }, - callback: HandlerCallback - ) => { - try { - elizaLogger.log("Composing state for message:", message); - const userId = runtime.agentId; - elizaLogger.log("User ID:", userId); - - const collectionAddressRes = await createCollection({ - runtime, - collectionName: runtime.character.name, - }); - - const collectionInfo = collectionAddressRes.collectionInfo; - - elizaLogger.log("Collection Address:", collectionAddressRes); - - const nftRes = await createNFT({ - runtime, - collectionName: collectionInfo.name, - collectionAddress: collectionAddressRes.address, - collectionAdminPublicKey: collectionInfo.adminPublicKey, - collectionFee: collectionInfo.fee, - tokenId: 1, - }); - - elizaLogger.log("NFT Address:", nftRes); - - callback({ - text: `Congratulations to you! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ \nCollection : ${collectionAddressRes.link}\n NFT: ${nftRes.link}`, //caption.description, - attachments: [], - }); - await sleep(15000); - await verifyNFT({ - runtime, - collectionAddress: collectionAddressRes.address, - NFTAddress: nftRes.address, - }); - return []; - } catch (e: any) { - console.log(e); - } - - // callback(); - }, - examples: [ - // TODO: We want to generate images in more abstract ways, not just when asked to generate an image - - [ - { - user: "{{user1}}", - content: { text: "Generate a collection" }, - }, - { - user: "{{agentName}}", - content: { - text: "Here's the collection you requested.", - action: "GENERATE_COLLECTION", - }, - }, - ], - [ - { - user: "{{user1}}", - content: { text: "Generate a collection using {{agentName}}" }, - }, - { - user: "{{agentName}}", - content: { - text: "We've successfully created a collection.", - action: "GENERATE_COLLECTION", - }, - }, - ], - [ - { - user: "{{user1}}", - content: { text: "Create a collection using {{agentName}}" }, - }, - { - user: "{{agentName}}", - content: { - text: "Here's the collection you requested.", - action: "GENERATE_COLLECTION", - }, - }, - ], - [ - { - user: "{{user1}}", - content: { text: "Build a Collection" }, - }, - { - user: "{{agentName}}", - content: { - text: "The collection has been successfully built.", - action: "GENERATE_COLLECTION", - }, - }, - ], - [ - { - user: "{{user1}}", - content: { text: "Assemble a collection with {{agentName}}" }, - }, - { - user: "{{agentName}}", - content: { - text: "The collection has been assembled", - action: "GENERATE_COLLECTION", - }, - }, - ], - [ - { - user: "{{user1}}", - content: { text: "Make a collection" }, - }, - { - user: "{{agentName}}", - content: { - text: "The collection has been produced successfully.", - action: "GENERATE_COLLECTION", - }, - }, - ], - [ - { - user: "{{user1}}", - content: { text: "Compile a collection" }, - }, - { - user: "{{agentName}}", - content: { - text: "The collection has been compiled.", - action: "GENERATE_COLLECTION", - }, - }, - ], - ], -} as Action; - export const nftGenerationPlugin: Plugin = { name: "nftCollectionGeneration", description: "Generate NFT Collections", - actions: [nftCollectionGeneration], + actions: [nftCollectionGeneration, mintNFTAction], evaluators: [], providers: [], }; diff --git a/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts b/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts index 2bfeb85ca6..74a605c9c2 100644 --- a/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts +++ b/packages/plugin-nft-generation/src/provider/wallet/walletSolana.ts @@ -10,6 +10,7 @@ import { createNft, findMetadataPda, mplTokenMetadata, + fetchDigitalAsset, updateV1, verifyCollectionV1, } from "@metaplex-foundation/mpl-token-metadata"; @@ -55,6 +56,9 @@ export class WalletSolana { this.umi = umi; } + async fetchDigitalAsset (address: string) { + return fetchDigitalAsset(this.umi, publicKey(address)) + } async getBalance() { const balance = await this.connection.getBalance(this.walletPublicKey); return { From 13b3fb11e90b880655fa68cd3769c5201d220dbb Mon Sep 17 00:00:00 2001 From: JSon Date: Fri, 20 Dec 2024 23:00:40 +0800 Subject: [PATCH 02/99] feat(plugin-nft-generation): change mintTemplate. --- .../src/actions/mintNFTAction.ts | 70 +++++++------------ 1 file changed, 24 insertions(+), 46 deletions(-) diff --git a/packages/plugin-nft-generation/src/actions/mintNFTAction.ts b/packages/plugin-nft-generation/src/actions/mintNFTAction.ts index 1551533715..a2a026a3aa 100644 --- a/packages/plugin-nft-generation/src/actions/mintNFTAction.ts +++ b/packages/plugin-nft-generation/src/actions/mintNFTAction.ts @@ -27,12 +27,15 @@ Example response: } \`\`\` -{{message}} +{{recentMessages}} Given the recent messages, extract the following information about the requested mint nft: - collection contract address -Respond with a JSON markdown block containing only the extracted values.`; +Respond with a JSON markdown block containing only the extracted values. + +Note: Make sure to extract the collection address from the most recent messages whenever possible.` + export interface MintContent extends Content { collectionAddress: string; @@ -87,22 +90,25 @@ const mintNFTAction: Action = { const agentName = runtime.character.name; const roomId = stringToUuid("nft_generate_room-" + agentName); - const memory: Memory = { - agentId: userId, - userId, - roomId, - content: { - text: message.content.text, - source: "nft-generator", - }, - createdAt: Date.now(), - embedding: getEmbeddingZeroVector(), - }; - const state = await runtime.composeState(memory, { - message: message.content.text, - }); - - elizaLogger.log("state:", state); + // const memory: Memory = { + // agentId: userId, + // userId, + // roomId, + // content: { + // text: message.content.text, + // source: "nft-generator", + // }, + // createdAt: Date.now(), + // embedding: getEmbeddingZeroVector(), + // }; + // const state = await runtime.composeState(memory, { + // message: message.content.text, + // }); + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } // Compose transfer context const transferContext = composeContext({ @@ -189,34 +195,6 @@ const mintNFTAction: Action = { }); return false; } - - // - // const userId = runtime.agentId; - // elizaLogger.log("User ID:", userId); - // - // const collectionAddressRes = await createCollection({ - // runtime, - // collectionName: runtime.character.name, - // }); - // - // const collectionInfo = collectionAddressRes.collectionInfo; - // - // elizaLogger.log("Collection Address:", collectionAddressRes); - - // - // elizaLogger.log("NFT Address:", nftRes); - // - // - // callback({ - // text: `Congratulations to you! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ \nCollection : ${collectionAddressRes.link}\n NFT: ${nftRes.link}`, //caption.description, - // attachments: [], - // }); - // await sleep(15000); - // await verifyNFT({ - // runtime, - // collectionAddress: collectionAddressRes.address, - // NFTAddress: nftRes.address, - // }); return []; } catch (e: any) { console.log(e); From 2c7c7ea22647e20c4048dd5d1349775d6fb415d8 Mon Sep 17 00:00:00 2001 From: JSon Date: Fri, 20 Dec 2024 23:08:32 +0800 Subject: [PATCH 03/99] chore --- .../src/actions/mintNFTAction.ts | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/packages/plugin-nft-generation/src/actions/mintNFTAction.ts b/packages/plugin-nft-generation/src/actions/mintNFTAction.ts index a2a026a3aa..ecb8a47d61 100644 --- a/packages/plugin-nft-generation/src/actions/mintNFTAction.ts +++ b/packages/plugin-nft-generation/src/actions/mintNFTAction.ts @@ -4,13 +4,11 @@ import { Content, elizaLogger, generateObjectDeprecated, - getEmbeddingZeroVector, HandlerCallback, IAgentRuntime, Memory, ModelClass, State, - stringToUuid, } from "@ai16z/eliza"; import { createNFT } from "../handlers/createNFT.ts"; import { verifyNFT } from "../handlers/verifyNFT.ts"; @@ -34,8 +32,7 @@ Given the recent messages, extract the following information about the requested Respond with a JSON markdown block containing only the extracted values. -Note: Make sure to extract the collection address from the most recent messages whenever possible.` - +Note: Make sure to extract the collection address from the most recent messages whenever possible.`; export interface MintContent extends Content { collectionAddress: string; @@ -86,24 +83,6 @@ const mintNFTAction: Action = { ) => { try { elizaLogger.log("Composing state for message:", message); - const userId = runtime.agentId; - const agentName = runtime.character.name; - const roomId = stringToUuid("nft_generate_room-" + agentName); - - // const memory: Memory = { - // agentId: userId, - // userId, - // roomId, - // content: { - // text: message.content.text, - // source: "nft-generator", - // }, - // createdAt: Date.now(), - // embedding: getEmbeddingZeroVector(), - // }; - // const state = await runtime.composeState(memory, { - // message: message.content.text, - // }); if (!state) { state = (await runtime.composeState(message)) as State; } else { From c9cac7aac1beef7148621b9823e342d2138bbf9d Mon Sep 17 00:00:00 2001 From: JSon Date: Sun, 22 Dec 2024 00:46:33 +0800 Subject: [PATCH 04/99] feat: 1.using generateObject and templated type safe schema. 2.naming convention camelCase. 3.create a template and type file --- .../src/actions/mintNFTAction.ts | 73 +++++++------------ .../plugin-nft-generation/src/templates.ts | 18 +++++ packages/plugin-nft-generation/src/types.ts | 11 +++ 3 files changed, 57 insertions(+), 45 deletions(-) create mode 100644 packages/plugin-nft-generation/src/templates.ts create mode 100644 packages/plugin-nft-generation/src/types.ts diff --git a/packages/plugin-nft-generation/src/actions/mintNFTAction.ts b/packages/plugin-nft-generation/src/actions/mintNFTAction.ts index ecb8a47d61..a6556b5cb7 100644 --- a/packages/plugin-nft-generation/src/actions/mintNFTAction.ts +++ b/packages/plugin-nft-generation/src/actions/mintNFTAction.ts @@ -1,9 +1,8 @@ import { Action, composeContext, - Content, elizaLogger, - generateObjectDeprecated, + generateObject, HandlerCallback, IAgentRuntime, Memory, @@ -15,34 +14,10 @@ import { verifyNFT } from "../handlers/verifyNFT.ts"; import { sleep } from "../index.ts"; import WalletSolana from "../provider/wallet/walletSolana.ts"; import { PublicKey } from "@solana/web3.js"; +import { mintNFTTemplate } from "../templates.ts"; +import { MintNFTContent, MintNFTSchema } from "../types.ts"; -const mintTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. - -Example response: -\`\`\`json -{ - "collectionAddress": "D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", -} -\`\`\` - -{{recentMessages}} - -Given the recent messages, extract the following information about the requested mint nft: -- collection contract address - -Respond with a JSON markdown block containing only the extracted values. - -Note: Make sure to extract the collection address from the most recent messages whenever possible.`; - -export interface MintContent extends Content { - collectionAddress: string; -} - -function isMintNFTContent( - runtime: IAgentRuntime, - content: any -): content is MintContent { - console.log("Content for mint", content); +function isMintNFTContent(content: any): content is MintNFTContent { return typeof content.collectionAddress === "string"; } @@ -60,18 +35,26 @@ const mintNFTAction: Action = { ], description: "Mint NFTs for the collection", validate: async (runtime: IAgentRuntime, _message: Memory) => { - const AwsAccessKeyIdOk = !!runtime.getSetting("AWS_ACCESS_KEY_ID"); - const AwsSecretAccessKeyOk = !!runtime.getSetting( + const awsAccessKeyIdOk = !!runtime.getSetting("AWS_ACCESS_KEY_ID"); + const awsSecretAccessKeyOk = !!runtime.getSetting( "AWS_SECRET_ACCESS_KEY" ); - const AwsRegionOk = !!runtime.getSetting("AWS_REGION"); - const AwsS3BucketOk = !!runtime.getSetting("AWS_S3_BUCKET"); + const awsRegionOk = !!runtime.getSetting("AWS_REGION"); + const awsS3BucketOk = !!runtime.getSetting("AWS_S3_BUCKET"); + const solanaAdminPrivateKeyOk = !!runtime.getSetting( + "SOLANA_ADMIN_PRIVATE_KEY" + ); + const solanaAdminPublicKeyOk = !!runtime.getSetting( + "SOLANA_ADMIN_PUBLIC_KEY" + ); return ( - AwsAccessKeyIdOk || - AwsSecretAccessKeyOk || - AwsRegionOk || - AwsS3BucketOk + awsAccessKeyIdOk || + awsSecretAccessKeyOk || + awsRegionOk || + awsS3BucketOk || + solanaAdminPrivateKeyOk || + solanaAdminPublicKeyOk ); }, handler: async ( @@ -92,18 +75,20 @@ const mintNFTAction: Action = { // Compose transfer context const transferContext = composeContext({ state, - template: mintTemplate, + template: mintNFTTemplate, }); - const content = await generateObjectDeprecated({ + const res = await generateObject({ runtime, context: transferContext, modelClass: ModelClass.LARGE, + schema: MintNFTSchema, }); + const content = res.object; - elizaLogger.log("generateObjectDeprecated:", transferContext); + elizaLogger.log("Generate Object:", content); - if (!isMintNFTContent(runtime, content)) { + if (!isMintNFTContent(content)) { elizaLogger.error("Invalid content for MINT_NFT action."); if (callback) { callback({ @@ -137,7 +122,6 @@ const mintNFTAction: Action = { return false; } if (metadata) { - elizaLogger.log("nft params", {}); const nftRes = await createNFT({ runtime, collectionName: metadata.name, @@ -176,10 +160,9 @@ const mintNFTAction: Action = { } return []; } catch (e: any) { - console.log(e); + elizaLogger.log(e); + throw e; } - - // callback(); }, examples: [ [ diff --git a/packages/plugin-nft-generation/src/templates.ts b/packages/plugin-nft-generation/src/templates.ts new file mode 100644 index 0000000000..8c6f9169ed --- /dev/null +++ b/packages/plugin-nft-generation/src/templates.ts @@ -0,0 +1,18 @@ + +export const mintNFTTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Example response: +\`\`\`json +{ + "collectionAddress": "D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", +} +\`\`\` + +{{recentMessages}} + +Given the recent messages, extract the following information about the requested mint nft: +- collection contract address + +Respond with a JSON markdown block containing only the extracted values. + +Note: Make sure to extract the collection address from the most recent messages whenever possible.`; diff --git a/packages/plugin-nft-generation/src/types.ts b/packages/plugin-nft-generation/src/types.ts new file mode 100644 index 0000000000..d422bdea21 --- /dev/null +++ b/packages/plugin-nft-generation/src/types.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; +import { Content } from "@ai16z/eliza"; + + +export interface MintNFTContent extends Content { + collectionAddress: string; +} + +export const MintNFTSchema = z.object({ + collectionAddress: z.string(), +}); From 53060702a01067acebfa6fcf3a1d1440a0915ab8 Mon Sep 17 00:00:00 2001 From: JSon Date: Sun, 22 Dec 2024 00:48:42 +0800 Subject: [PATCH 05/99] feat: Remove unnecessary logs --- packages/plugin-nft-generation/src/actions/mintNFTAction.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/plugin-nft-generation/src/actions/mintNFTAction.ts b/packages/plugin-nft-generation/src/actions/mintNFTAction.ts index a6556b5cb7..2d9f0fb2c9 100644 --- a/packages/plugin-nft-generation/src/actions/mintNFTAction.ts +++ b/packages/plugin-nft-generation/src/actions/mintNFTAction.ts @@ -99,7 +99,6 @@ const mintNFTAction: Action = { return false; } - elizaLogger.log("mint content", content); const publicKey = runtime.getSetting("SOLANA_PUBLIC_KEY"); const privateKey = runtime.getSetting("SOLANA_PRIVATE_KEY"); From c0470f25d81135d096da0f6725fa4aab94014861 Mon Sep 17 00:00:00 2001 From: JSon Date: Mon, 23 Dec 2024 10:39:42 +0800 Subject: [PATCH 06/99] feat: ai16z -> elizaos --- packages/plugin-nft-generation/src/actions/mintNFTAction.ts | 2 +- .../src/actions/nftCollectionGeneration.ts | 2 +- packages/plugin-nft-generation/src/index.ts | 2 +- packages/plugin-nft-generation/src/types.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/plugin-nft-generation/src/actions/mintNFTAction.ts b/packages/plugin-nft-generation/src/actions/mintNFTAction.ts index 2d9f0fb2c9..61f4d70984 100644 --- a/packages/plugin-nft-generation/src/actions/mintNFTAction.ts +++ b/packages/plugin-nft-generation/src/actions/mintNFTAction.ts @@ -8,7 +8,7 @@ import { Memory, ModelClass, State, -} from "@ai16z/eliza"; +} from "@elizaos/core"; import { createNFT } from "../handlers/createNFT.ts"; import { verifyNFT } from "../handlers/verifyNFT.ts"; import { sleep } from "../index.ts"; diff --git a/packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts b/packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts index 9973d73407..19e1fc3f88 100644 --- a/packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts +++ b/packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts @@ -5,7 +5,7 @@ import { IAgentRuntime, Memory, State, -} from "@ai16z/eliza"; +} from "@elizaos/core"; import { createCollection } from "../handlers/createCollection.ts"; const nftCollectionGeneration: Action = { diff --git a/packages/plugin-nft-generation/src/index.ts b/packages/plugin-nft-generation/src/index.ts index 5540ec3a5b..55f3fe0e3e 100644 --- a/packages/plugin-nft-generation/src/index.ts +++ b/packages/plugin-nft-generation/src/index.ts @@ -1,4 +1,4 @@ -import { Plugin } from "@ai16z/eliza"; +import { Plugin } from "@elizaos/core"; import nftCollectionGeneration from "./actions/nftCollectionGeneration.ts"; import mintNFTAction from "./actions/mintNFTAction.ts"; diff --git a/packages/plugin-nft-generation/src/types.ts b/packages/plugin-nft-generation/src/types.ts index d422bdea21..e00fe709d9 100644 --- a/packages/plugin-nft-generation/src/types.ts +++ b/packages/plugin-nft-generation/src/types.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { Content } from "@ai16z/eliza"; +import { Content } from "@elizaos/core"; export interface MintNFTContent extends Content { From 58a40d31dacc3de30e135a879705808d13b7210f Mon Sep 17 00:00:00 2001 From: JSon Date: Thu, 2 Jan 2025 07:52:20 +0800 Subject: [PATCH 07/99] feat: plugin-nft-generation - evm chain deploy contract. --- .../contract/CustomERC721.sol | 28 +++ packages/plugin-nft-generation/package.json | 6 +- .../src/actions/nftCollectionGeneration.ts | 218 +++++++++++++++--- packages/plugin-nft-generation/src/api.ts | 4 +- ...ollection.ts => createSolanaCollection.ts} | 55 +++-- .../plugin-nft-generation/src/templates.ts | 26 ++- packages/plugin-nft-generation/src/types.ts | 8 + .../src/utils/deployEVMContract.ts | 96 ++++++++ .../src/utils/generateERC721ContractCode.ts | 58 +++++ .../src/utils/verifyEVMContract.ts | 86 +++++++ packages/plugin-nft-generation/tsconfig.json | 2 +- packages/plugin-nft-generation/tsup.config.ts | 1 + 12 files changed, 528 insertions(+), 60 deletions(-) create mode 100644 packages/plugin-nft-generation/contract/CustomERC721.sol rename packages/plugin-nft-generation/src/handlers/{createCollection.ts => createSolanaCollection.ts} (74%) create mode 100644 packages/plugin-nft-generation/src/utils/deployEVMContract.ts create mode 100644 packages/plugin-nft-generation/src/utils/generateERC721ContractCode.ts create mode 100644 packages/plugin-nft-generation/src/utils/verifyEVMContract.ts diff --git a/packages/plugin-nft-generation/contract/CustomERC721.sol b/packages/plugin-nft-generation/contract/CustomERC721.sol new file mode 100644 index 0000000000..fa356af59f --- /dev/null +++ b/packages/plugin-nft-generation/contract/CustomERC721.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; + +contract NFTContractName is ERC721Enumerable { + uint256 public maxSupply; + uint256 public currentTokenId; + address public owner; + uint256 public royalty; + + constructor( + string memory _name, + string memory _symbol, + uint256 _maxSupply, + uint256 _royalty + ) ERC721(_name, _symbol) { + maxSupply = _maxSupply; + royalty = _royalty; + owner = msg.sender; + } + + function mint(address _to) public { + require(currentTokenId < maxSupply, "Max supply reached"); + currentTokenId++; + _mint(_to, currentTokenId); + } +} diff --git a/packages/plugin-nft-generation/package.json b/packages/plugin-nft-generation/package.json index 80e55d0604..bd61edb80b 100644 --- a/packages/plugin-nft-generation/package.json +++ b/packages/plugin-nft-generation/package.json @@ -12,12 +12,16 @@ "@metaplex-foundation/mpl-toolbox": "^0.9.4", "@metaplex-foundation/umi": "^0.9.2", "@metaplex-foundation/umi-bundle-defaults": "^0.9.2", + "@openzeppelin/contracts": "^5.1.0", "@solana-developers/helpers": "^2.5.6", "@solana/web3.js": "1.95.5", "bs58": "6.0.0", + "ethers": "6.13.4", "express": "4.21.1", "node-cache": "5.1.2", - "tsup": "8.3.5" + "solc": "^0.8.28", + "tsup": "8.3.5", + "viem": "^2.21.60" }, "scripts": { "build": "tsup --format esm --dts", diff --git a/packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts b/packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts index 19e1fc3f88..901915331e 100644 --- a/packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts +++ b/packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts @@ -1,12 +1,34 @@ import { Action, + composeContext, elizaLogger, + generateObject, HandlerCallback, IAgentRuntime, Memory, + ModelClass, State, } from "@elizaos/core"; -import { createCollection } from "../handlers/createCollection.ts"; +import { createCollectionMetadata } from "../handlers/createSolanaCollection.ts"; +import { CreateCollectionSchema } from "../types.ts"; +import { createCollectionTemplate } from "../templates.ts"; +import * as viemChains from "viem/chains"; +import WalletSolana from "../provider/wallet/walletSolana.ts"; +import { PublicKey } from "@solana/web3.js"; +import { createPublicClient, createWalletClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { + compileContract, + deployContract, + encodeConstructorArguments, + generateERC721ContractCode, +} from "../utils/deployEVMContract.ts"; +import { verifyEVMContract } from "../utils/verifyEVMContract.ts"; +// import { verifyEVMContract } from "../utils/verifyEVMContract.ts"; + +const _SupportedChainList = Object.keys(viemChains) as Array< + keyof typeof viemChains +>; const nftCollectionGeneration: Action = { name: "GENERATE_COLLECTION", @@ -46,17 +68,131 @@ const nftCollectionGeneration: Action = { ) => { try { elizaLogger.log("Composing state for message:", message); - const collectionAddressRes = await createCollection({ + + const state = await runtime.composeState(message); + + // Compose transfer context + const context = composeContext({ + state, + template: createCollectionTemplate, + }); + const chains = _SupportedChainList; + + const supportedChains: ( + | (typeof chains)[number] + | "solana" + | null + )[] = [...chains, "solana", null]; + const contextWithChains = context.replace( + "SUPPORTED_CHAINS", + supportedChains + .map((item) => (item ? `"${item}"` : item)) + .join("|") + ); + + const res = await generateObject({ runtime, - collectionName: runtime.character.name, + context: contextWithChains, + modelClass: ModelClass.LARGE, + schema: CreateCollectionSchema, }); - elizaLogger.log("Collection Info:", collectionAddressRes); - if (callback) { - callback({ - text: `Congratulations to you! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ \nCollection Link : ${collectionAddressRes.link}\n Address: ${collectionAddressRes.address}`, //caption.description, - attachments: [], + + const content = res.object as { + chainName: (typeof supportedChains)[number]; + }; + + console.log(111111, content); + if (content?.chainName === "solana") { + const collectionInfo = await createCollectionMetadata({ + runtime, + collectionName: runtime.character.name, + }); + if (!collectionInfo) return null; + const publicKey = runtime.getSetting("SOLANA_PUBLIC_KEY"); + const privateKey = runtime.getSetting("SOLANA_PRIVATE_KEY"); + const wallet = new WalletSolana( + new PublicKey(publicKey), + privateKey + ); + + const collectionAddressRes = await wallet.createCollection({ + ...collectionInfo, + }); + elizaLogger.log("Collection Info:", collectionAddressRes); + if (callback) { + callback({ + text: `Congratulations to you! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ \nCollection Link : ${collectionAddressRes.link}\n Address: ${collectionAddressRes.address}`, //caption.description, + attachments: [], + }); + } + } else if (chains.indexOf(content.chainName)) { + const privateKey = runtime.getSetting( + "WALLET_PRIVATE_KEY" + ) as `0x${string}`; + if (!privateKey) return null; + const rpcUrl = + viemChains[content.chainName].rpcUrls.default.http[0]; + const chain = viemChains[content.chainName]; // ๆ›ฟๆขไธบ็›ฎๆ ‡้“พ + const provider = http(rpcUrl); + const account = privateKeyToAccount(privateKey); + const walletClient = createWalletClient({ + account, + chain: chain, + transport: provider, + }); + + const publicClient = createPublicClient({ + chain: chain, + transport: provider, + }); + + // const collectionInfo = await createCollectionMetadata({ + // runtime, + // collectionName: runtime.character.name, + // }); + + const contractName = runtime.character.name.replace('.', '_'); + const contractSymbol = `${contractName.toUpperCase()[0]}`; + const contractMaxSupply = 5000; + const royalty = 0; + const params = [ + contractName, + contractSymbol, + contractMaxSupply, + royalty, + ]; + const sourceCode = generateERC721ContractCode(contractName); + + const { abi, bytecode, metadata } = compileContract( + contractName, + sourceCode + ); + elizaLogger.log("ABI and Bytecode generated."); + const contractAddress = await deployContract({ + walletClient, + publicClient, + abi, + bytecode, + args: params, }); + elizaLogger.log( + `Deployed contract address: ${contractAddress}` + ); + const constructorArgs = encodeConstructorArguments(abi, params); + await verifyEVMContract({ + contractAddress: contractAddress, + sourceCode, + metadata, + constructorArgs, + }); + if (callback) { + callback({ + text: `Congratulations to you! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ \nCollection Link : ${chain.blockExplorers.default.url}/address/${contractAddress}\n Address: ${contractAddress}`, //caption.description, + attachments: [], + }); + } } + return []; } catch (e: any) { console.log(e); @@ -67,7 +203,7 @@ const nftCollectionGeneration: Action = { [ { user: "{{user1}}", - content: { text: "Generate a collection" }, + content: { text: "Generate a collection on Solana" }, }, { user: "{{agentName}}", @@ -80,12 +216,14 @@ const nftCollectionGeneration: Action = { [ { user: "{{user1}}", - content: { text: "Generate a collection using {{agentName}}" }, + content: { + text: "Generate a collection using {{agentName}} on Solana", + }, }, { user: "{{agentName}}", content: { - text: "We've successfully created a collection.", + text: "We've successfully created a collection on Solana.", action: "GENERATE_COLLECTION", }, }, @@ -93,7 +231,9 @@ const nftCollectionGeneration: Action = { [ { user: "{{user1}}", - content: { text: "Create a collection using {{agentName}}" }, + content: { + text: "Create a collection using {{agentName}} on Solana", + }, }, { user: "{{agentName}}", @@ -106,7 +246,7 @@ const nftCollectionGeneration: Action = { [ { user: "{{user1}}", - content: { text: "Build a Collection" }, + content: { text: "Build a Collection on Solana" }, }, { user: "{{agentName}}", @@ -119,7 +259,9 @@ const nftCollectionGeneration: Action = { [ { user: "{{user1}}", - content: { text: "Assemble a collection with {{agentName}}" }, + content: { + text: "Assemble a collection with {{agentName}} on Solana", + }, }, { user: "{{agentName}}", @@ -132,7 +274,7 @@ const nftCollectionGeneration: Action = { [ { user: "{{user1}}", - content: { text: "Make a collection" }, + content: { text: "Make a collection on Solana" }, }, { user: "{{agentName}}", @@ -146,13 +288,13 @@ const nftCollectionGeneration: Action = { { user: "{{user1}}", content: { - text: "Could you create a new collection for my photos?", + text: "Could you create a new collection for my photos on Solana?", }, }, { user: "{{agentName}}", content: { - text: "I've created a new collection for your photos.", + text: "I've created a new collection for your photos on Solana.", action: "GENERATE_COLLECTION", }, }, @@ -161,13 +303,13 @@ const nftCollectionGeneration: Action = { { user: "{{user1}}", content: { - text: "I need a collection for organizing my music", + text: "I need a collection for organizing my music on Solana", }, }, { user: "{{agentName}}", content: { - text: "Your music collection has been generated.", + text: "Your music collection has been generated on Solana.", action: "GENERATE_COLLECTION", }, }, @@ -176,13 +318,13 @@ const nftCollectionGeneration: Action = { { user: "{{user1}}", content: { - text: "Please set up a collection for my documents", + text: "Please set up a collection for my documents on Solana", }, }, { user: "{{agentName}}", content: { - text: "I've set up a new collection for your documents.", + text: "I've set up a new collection for your documents on Solana.", action: "GENERATE_COLLECTION", }, }, @@ -190,12 +332,12 @@ const nftCollectionGeneration: Action = { [ { user: "{{user1}}", - content: { text: "Start a new collection for me" }, + content: { text: "Start a new collection for me on Solana" }, }, { user: "{{agentName}}", content: { - text: "Your new collection has been created.", + text: "Your new collection has been created on Solana.", action: "GENERATE_COLLECTION", }, }, @@ -204,13 +346,13 @@ const nftCollectionGeneration: Action = { { user: "{{user1}}", content: { - text: "I'd like to make a collection of my recipes", + text: "I'd like to make a collection of my recipes on Solana", }, }, { user: "{{agentName}}", content: { - text: "I've generated a collection for your recipes.", + text: "I've generated a collection for your recipes on Solana.", action: "GENERATE_COLLECTION", }, }, @@ -219,13 +361,13 @@ const nftCollectionGeneration: Action = { { user: "{{user1}}", content: { - text: "Can you generate a collection for my artwork?", + text: "Can you generate a collection for my artwork on Solana?", }, }, { user: "{{agentName}}", content: { - text: "Your artwork collection has been generated.", + text: "Your artwork collection has been generated on Solana.", action: "GENERATE_COLLECTION", }, }, @@ -233,12 +375,14 @@ const nftCollectionGeneration: Action = { [ { user: "{{user1}}", - content: { text: "Initialize a new collection please" }, + content: { + text: "Initialize a new collection please on Solana", + }, }, { user: "{{agentName}}", content: { - text: "I've initialized a new collection for you.", + text: "I've initialized a new collection for you on Solana.", action: "GENERATE_COLLECTION", }, }, @@ -246,12 +390,14 @@ const nftCollectionGeneration: Action = { [ { user: "{{user1}}", - content: { text: "Create a collection for my travel memories" }, + content: { + text: "Create a collection for my travel memories on Solana", + }, }, { user: "{{agentName}}", content: { - text: "Your travel memories collection has been created.", + text: "Your travel memories collection has been created on Solana.", action: "GENERATE_COLLECTION", }, }, @@ -260,13 +406,13 @@ const nftCollectionGeneration: Action = { { user: "{{user1}}", content: { - text: "Would you make a collection for my projects?", + text: "Would you make a collection for my projects on Solana?", }, }, { user: "{{agentName}}", content: { - text: "I've made a collection for your projects.", + text: "I've made a collection for your projects on Solana.", action: "GENERATE_COLLECTION", }, }, @@ -274,12 +420,14 @@ const nftCollectionGeneration: Action = { [ { user: "{{user1}}", - content: { text: "Set up a collection for my bookmarks" }, + content: { + text: "Set up a collection for my bookmarks on Solana", + }, }, { user: "{{agentName}}", content: { - text: "Your bookmarks collection has been set up.", + text: "Your bookmarks collection has been set up on Solana.", action: "GENERATE_COLLECTION", }, }, diff --git a/packages/plugin-nft-generation/src/api.ts b/packages/plugin-nft-generation/src/api.ts index 1501a5a370..002ec50691 100644 --- a/packages/plugin-nft-generation/src/api.ts +++ b/packages/plugin-nft-generation/src/api.ts @@ -1,7 +1,7 @@ import express from "express"; import { AgentRuntime } from "@elizaos/core"; -import { createCollection } from "./handlers/createCollection.ts"; +import { createSolanaCollection } from "./handlers/createSolanaCollection.ts"; import { createNFT, createNFTMetadata } from "./handlers/createNFT.ts"; import { verifyNFT } from "./handlers/verifyNFT.ts"; @@ -21,7 +21,7 @@ export function createNFTApiRouter( return; } try { - const collectionAddressRes = await createCollection({ + const collectionAddressRes = await createSolanaCollection({ runtime, collectionName: runtime.character.name, fee, diff --git a/packages/plugin-nft-generation/src/handlers/createCollection.ts b/packages/plugin-nft-generation/src/handlers/createSolanaCollection.ts similarity index 74% rename from packages/plugin-nft-generation/src/handlers/createCollection.ts rename to packages/plugin-nft-generation/src/handlers/createSolanaCollection.ts index 77cdd3d20d..17ddd376ed 100644 --- a/packages/plugin-nft-generation/src/handlers/createCollection.ts +++ b/packages/plugin-nft-generation/src/handlers/createSolanaCollection.ts @@ -15,12 +15,9 @@ import { } from "@elizaos/plugin-image-generation"; import { PublicKey } from "@solana/web3.js"; import WalletSolana from "../provider/wallet/walletSolana.ts"; +import { collectionImageTemplate } from "../templates.ts"; -const collectionImageTemplate = ` -Generate a logo with the text "{{collectionName}}", using orange as the main color, with a sci-fi and mysterious background theme -`; - -export async function createCollection({ +export async function createCollectionMetadata({ runtime, collectionName, fee, @@ -41,7 +38,6 @@ export async function createCollection({ roomId, content: { text: "", - source: "nft-generator", }, createdAt: Date.now(), @@ -79,8 +75,6 @@ export async function createCollection({ `/${collectionName}`, false ); - const publicKey = runtime.getSetting("SOLANA_PUBLIC_KEY"); - const privateKey = runtime.getSetting("SOLANA_PRIVATE_KEY"); const adminPublicKey = runtime.getSetting("SOLANA_ADMIN_PUBLIC_KEY"); const collectionInfo = { name: `${collectionName}`, @@ -100,19 +94,40 @@ export async function createCollection({ ); collectionInfo.uri = jsonFilePath.url; - const wallet = new WalletSolana(new PublicKey(publicKey), privateKey); - - const collectionAddressRes = await wallet.createCollection({ - ...collectionInfo, - }); + return collectionInfo; - return { - network: "solana", - address: collectionAddressRes.address, - link: collectionAddressRes.link, - collectionInfo, - }; } - return; + return null; +} + +export async function createSolanaCollection({ + runtime, + collectionName, + fee, +}: { + runtime: IAgentRuntime; + collectionName: string; + fee?: number; +}) { + const collectionInfo = await createCollectionMetadata({ + runtime, + collectionName, + fee, + }); + if (!collectionInfo) return null + const publicKey = runtime.getSetting("SOLANA_PUBLIC_KEY"); + const privateKey = runtime.getSetting("SOLANA_PRIVATE_KEY"); + const wallet = new WalletSolana(new PublicKey(publicKey), privateKey); + + const collectionAddressRes = await wallet.createCollection({ + ...collectionInfo, + }); + + return { + network: "solana", + address: collectionAddressRes.address, + link: collectionAddressRes.link, + collectionInfo, + }; } diff --git a/packages/plugin-nft-generation/src/templates.ts b/packages/plugin-nft-generation/src/templates.ts index 8c6f9169ed..bcf3e75d3f 100644 --- a/packages/plugin-nft-generation/src/templates.ts +++ b/packages/plugin-nft-generation/src/templates.ts @@ -1,4 +1,28 @@ +export const createCollectionTemplate = `Given the recent messages and wallet information below: + +{{recentMessages}} + +{{walletInfo}} + +Extract the following information about the requested transfer: +- Chain to execute on: Must be one of ["ethereum", "base", ...] (like in viem/chains) + +Respond with a JSON markdown block containing only the extracted values. All fields are required: + +\`\`\`json +{ + "chainName": SUPPORTED_CHAINS, +} +\`\`\` + +Note: Ensure to use the userโ€™s latest instruction to extract data; if it is not within the defined options, use null. + +`; + +export const collectionImageTemplate = ` +Generate a logo with the text "{{collectionName}}", using orange as the main color, with a sci-fi and mysterious background theme +`; export const mintNFTTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. Example response: @@ -15,4 +39,4 @@ Given the recent messages, extract the following information about the requested Respond with a JSON markdown block containing only the extracted values. -Note: Make sure to extract the collection address from the most recent messages whenever possible.`; +Note: Ensure to use the userโ€™s latest instruction to extract data; if it is not within the defined options, use null.`; diff --git a/packages/plugin-nft-generation/src/types.ts b/packages/plugin-nft-generation/src/types.ts index e00fe709d9..ec93468808 100644 --- a/packages/plugin-nft-generation/src/types.ts +++ b/packages/plugin-nft-generation/src/types.ts @@ -1,6 +1,8 @@ import { z } from "zod"; import { Content } from "@elizaos/core"; +import * as viemChains from "viem/chains"; +const _SupportedChainList = Object.keys(viemChains); export interface MintNFTContent extends Content { collectionAddress: string; @@ -9,3 +11,9 @@ export interface MintNFTContent extends Content { export const MintNFTSchema = z.object({ collectionAddress: z.string(), }); + +const supportedChainTuple = [..._SupportedChainList, 'solana'] as unknown as [string, ...string[]]; +export const CreateCollectionSchema = z.object({ + chainName: z.enum([...supportedChainTuple]).nullable(), +}); + diff --git a/packages/plugin-nft-generation/src/utils/deployEVMContract.ts b/packages/plugin-nft-generation/src/utils/deployEVMContract.ts new file mode 100644 index 0000000000..1e859ad754 --- /dev/null +++ b/packages/plugin-nft-generation/src/utils/deployEVMContract.ts @@ -0,0 +1,96 @@ +import { + createPublicClient, + createWalletClient, + encodeAbiParameters, + http, +} from "viem"; +import { fileURLToPath } from "url"; + +import { alienxHalTestnet } from "viem/chains"; +import { privateKeyToAccount } from "viem/accounts"; +import { compileWithImports } from "./generateERC721ContractCode.ts"; +import { verifyEVMContract } from "./verifyEVMContract.ts"; +import path from "path"; +import fs from "fs"; + +// ๅŠจๆ€็”Ÿๆˆ ERC-721 ๅˆ็บฆไปฃ็  +export function generateERC721ContractCode(NFTContractName) { + const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file + const __dirname = path.dirname(__filename); // get the name of the directory + const solPath = path.resolve(__dirname, "../contract/CustomERC721.sol"); + return fs + .readFileSync(solPath, "utf8") + .replace("NFTContractName", NFTContractName); +} + +// ไฝฟ็”จ Solidity ็ผ–่ฏ‘ๅ™จ็”Ÿๆˆ ABI ๅ’Œ Bytecode +export function compileContract(contractName, sourceCode) { + const res = compileWithImports(contractName, sourceCode); + const { abi, bytecode, metadata } = res; + return { abi, bytecode, metadata }; +} + +// ้ƒจ็ฝฒๅˆ็บฆ +export async function deployContract({ + walletClient, + publicClient, + abi, + bytecode, + args, +}) { + console.log("Deploying contract..."); + + const txHash = await walletClient.deployContract({ + abi: abi as any, + bytecode: bytecode as any, + args: args as any, + chain: undefined, + }); + + console.log(`Deployment transaction hash: ${txHash}`); + const receipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + console.log(`Contract deployed at address: ${receipt.contractAddress}`); + return receipt.contractAddress; +} + +// ่ฐƒ็”จ mint ๆ–นๆณ• +async function mintNFT({ + walletClient, + publicClient, + contractAddress, + abi, + recipient, +}: { + contractAddress: any; + abi: any; + recipient: any; + walletClient: any; + publicClient: any; +}) { + console.log("Minting NFT..."); + const txHash = await walletClient.writeContract({ + address: contractAddress as `0x${string}`, + abi: abi as any, + functionName: "mint", + args: [recipient] as any, + chain: undefined, + account: undefined, + }); + + console.log(`Mint transaction hash: ${txHash}`); + const receipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + console.log("Mint successful!"); + return receipt; +} + +// ็ผ–็ ๆž„้€ ๅ‡ฝๆ•ฐๅ‚ๆ•ฐ +export function encodeConstructorArguments(abi, args) { + const argsData = encodeAbiParameters(abi[0].inputs, args); + + return argsData.slice(2); +} + diff --git a/packages/plugin-nft-generation/src/utils/generateERC721ContractCode.ts b/packages/plugin-nft-generation/src/utils/generateERC721ContractCode.ts new file mode 100644 index 0000000000..588c144aae --- /dev/null +++ b/packages/plugin-nft-generation/src/utils/generateERC721ContractCode.ts @@ -0,0 +1,58 @@ +import solc from "solc"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +// Load OpenZeppelin contract source code +export function loadOpenZeppelinFile(contractPath) { + const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file + const __dirname = path.dirname(__filename); // get the name of the directory + + const fullPath = path.resolve(__dirname, '../../../', "node_modules", contractPath); + return fs.readFileSync(fullPath, "utf8"); +} + +// Dynamic import callback for Solidity +export function importResolver(importPath) { + if (importPath.startsWith("@openzeppelin/")) { + return { + contents: loadOpenZeppelinFile(importPath), + }; + } + return { error: "File not found" }; +} + +// Compile contract with custom import callback +export function compileWithImports(contractName, sourceCode) { + const input = { + language: "Solidity", + sources: { + [`${contractName}.sol`]: { + content: sourceCode, + }, + }, + settings: { + outputSelection: { + "*": { + "*": ["*"], + }, + }, + }, + }; + + const output = JSON.parse( + solc.compile(JSON.stringify(input), { import: importResolver }) + ); + + if (output.errors) { + output.errors.forEach((err) => console.error(err)); + } + const contractFile = output.contracts[`${contractName}.sol`][`${contractName}`]; + + const metadata = JSON.parse(contractFile.metadata); + return { + abi: contractFile.abi, + bytecode: contractFile.evm.bytecode.object, + metadata + }; +} diff --git a/packages/plugin-nft-generation/src/utils/verifyEVMContract.ts b/packages/plugin-nft-generation/src/utils/verifyEVMContract.ts new file mode 100644 index 0000000000..1ae3b2b2ae --- /dev/null +++ b/packages/plugin-nft-generation/src/utils/verifyEVMContract.ts @@ -0,0 +1,86 @@ +import axios from "axios"; +import { + loadOpenZeppelinFile, +} from "./generateERC721ContractCode.ts"; + +function getSources(metadata, sourceCode) { + const fileName = Object.keys(metadata.settings.compilationTarget)[0] + const obj = { + [fileName]: { + content: sourceCode, + }, + }; + const keys = Object.keys(metadata.sources); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + console.log(key, fileName); + if (key !== fileName) { + obj[key] = { + content: loadOpenZeppelinFile(key), + }; + } + } + return obj; +} + +export async function verifyEVMContract({ + contractAddress, + sourceCode, + metadata, + constructorArgs = "", +}) { + const apiEndpoint = "https://hal-explorer.alienxchain.io/api"; + const verificationData = { + module: "contract", + action: "verifysourcecode", + sourceCode: JSON.stringify({ + language: "Solidity", + sources: getSources(metadata, sourceCode), + settings: { + optimizer: { + enabled: metadata.settings.optimizer?.enabled, + runs: metadata.settings.optimizer?.runs, + }, + }, + }), + codeformat: "solidity-standard-json-input", + contractaddress: contractAddress, + contractname: "test", + compilerversion: `v${metadata.compiler.version}`, + optimizationUsed: metadata.settings.optimizer?.enabled ? 1 : 0, + runs: metadata.settings.optimizer?.runs || 200, + constructorArguements: constructorArgs, // Remove '0x' prefix + }; + + try { + const response = await axios.post(apiEndpoint, verificationData); + if (response.data.status === "1") { + const guid = response.data.result; + + // Check verification status + const checkStatus = async () => { + const statusResponse = await axios.get(apiEndpoint, { + params: { + module: "contract", + action: "checkverifystatus", + guid: guid, + }, + }); + console.log(111, statusResponse.data); + return statusResponse.data; + }; + + // Poll for completion + let status; + do { + await new Promise((resolve) => setTimeout(resolve, 3000)); + status = await checkStatus(); + } while (status.result === "Pending in queue"); + + return status; + } + return response.data; + } catch (error) { + throw new Error(`Verification failed: ${error.message}`); + } +} diff --git a/packages/plugin-nft-generation/tsconfig.json b/packages/plugin-nft-generation/tsconfig.json index 834c4dce26..abd2932121 100644 --- a/packages/plugin-nft-generation/tsconfig.json +++ b/packages/plugin-nft-generation/tsconfig.json @@ -10,4 +10,4 @@ "include": [ "src/**/*.ts" ] -} \ No newline at end of file +} diff --git a/packages/plugin-nft-generation/tsup.config.ts b/packages/plugin-nft-generation/tsup.config.ts index 1a96f24afa..f2d8f4c321 100644 --- a/packages/plugin-nft-generation/tsup.config.ts +++ b/packages/plugin-nft-generation/tsup.config.ts @@ -16,6 +16,7 @@ export default defineConfig({ "http", "agentkeepalive", "safe-buffer", + "axios" // Add other modules you want to externalize ], }); From a66628e004b41800f1bef2262c61233af89ea725 Mon Sep 17 00:00:00 2001 From: AIFlow_ML Date: Fri, 3 Jan 2025 16:59:52 +0700 Subject: [PATCH 08/99] fix(adapter-postgres): #1687 Add vector embedding validation and tests --- .../adapter-postgres/src/__tests__/README.md | 71 +++ .../src/__tests__/docker-compose.test.yml | 16 + .../src/__tests__/run_tests.sh | 84 ++++ .../src/__tests__/vector-extension.test.ts | 433 ++++++++++++++++++ packages/adapter-postgres/src/index.ts | 24 +- 5 files changed, 627 insertions(+), 1 deletion(-) create mode 100644 packages/adapter-postgres/src/__tests__/README.md create mode 100644 packages/adapter-postgres/src/__tests__/docker-compose.test.yml create mode 100644 packages/adapter-postgres/src/__tests__/run_tests.sh create mode 100644 packages/adapter-postgres/src/__tests__/vector-extension.test.ts diff --git a/packages/adapter-postgres/src/__tests__/README.md b/packages/adapter-postgres/src/__tests__/README.md new file mode 100644 index 0000000000..98896ff4f2 --- /dev/null +++ b/packages/adapter-postgres/src/__tests__/README.md @@ -0,0 +1,71 @@ +# PostgreSQL Adapter Tests + +This directory contains tests for the PostgreSQL adapter with vector extension support. + +## Prerequisites + +- Docker installed and running +- Node.js and pnpm installed +- Bash shell (for Unix/Mac) or Git Bash (for Windows) + +## Test Environment + +The tests run against a PostgreSQL instance with the `pgvector` extension enabled. We use Docker to ensure a consistent test environment: + +- PostgreSQL 16 with pgvector extension +- Test database: `eliza_test` +- Port: 5433 (to avoid conflicts with local PostgreSQL) +- Vector dimensions: 1536 (OpenAI compatible) + +## Running Tests + +The easiest way to run tests is using the provided script: + +```bash +./run_tests.sh +``` + +This script will: +1. Start the PostgreSQL container with vector extension +2. Wait for the database to be ready +3. Run the test suite + +## Manual Setup + +If you prefer to run tests manually: + +1. Start the test database: + ```bash + docker compose -f docker-compose.test.yml up -d + ``` + +2. Wait for the database to be ready (about 30 seconds) + +3. Run tests: + ```bash + pnpm vitest vector-extension.test.ts + ``` + +## Test Structure + +- `vector-extension.test.ts`: Main test suite for vector operations +- `docker-compose.test.yml`: Docker configuration for test database +- `run_tests.sh`: Helper script to run tests + +## Troubleshooting + +1. If tests fail with connection errors: + - Check if Docker is running + - Verify port 5433 is available + - Wait a bit longer for database initialization + +2. If vector operations fail: + - Check if pgvector extension is properly loaded + - Verify schema initialization + - Check vector dimensions match (1536 for OpenAI) + +## Notes + +- Tests automatically clean up after themselves +- Each test run starts with a fresh database +- Vector extension is initialized as part of the schema setup \ No newline at end of file diff --git a/packages/adapter-postgres/src/__tests__/docker-compose.test.yml b/packages/adapter-postgres/src/__tests__/docker-compose.test.yml new file mode 100644 index 0000000000..7a589ec192 --- /dev/null +++ b/packages/adapter-postgres/src/__tests__/docker-compose.test.yml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json +version: '3.8' +services: + postgres-test: + image: pgvector/pgvector:pg16 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: eliza_test + ports: + - "5433:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 \ No newline at end of file diff --git a/packages/adapter-postgres/src/__tests__/run_tests.sh b/packages/adapter-postgres/src/__tests__/run_tests.sh new file mode 100644 index 0000000000..6a18b681eb --- /dev/null +++ b/packages/adapter-postgres/src/__tests__/run_tests.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +# Color output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Get script directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +SCHEMA_PATH="$SCRIPT_DIR/../../schema.sql" + +echo -e "${YELLOW}Starting PostgreSQL test environment...${NC}" + +# Determine Docker Compose command +if [[ "$OSTYPE" == "darwin"* ]]; then + DOCKER_COMPOSE_CMD="docker compose" +else + DOCKER_COMPOSE_CMD="docker-compose" +fi + +# Stop any existing containers +echo -e "${YELLOW}Cleaning up existing containers...${NC}" +$DOCKER_COMPOSE_CMD -f docker-compose.test.yml down + +# Start fresh container +echo -e "${YELLOW}Starting PostgreSQL container...${NC}" +$DOCKER_COMPOSE_CMD -f docker-compose.test.yml up -d + +# Function to check if PostgreSQL is ready +check_postgres() { + $DOCKER_COMPOSE_CMD -f docker-compose.test.yml exec -T postgres-test pg_isready -U postgres +} + +# Wait for PostgreSQL to be ready +echo -e "${YELLOW}Waiting for PostgreSQL to be ready...${NC}" +RETRIES=30 +until check_postgres || [ $RETRIES -eq 0 ]; do + echo -e "${YELLOW}Waiting for PostgreSQL to be ready... ($RETRIES attempts left)${NC}" + RETRIES=$((RETRIES-1)) + sleep 1 +done + +if [ $RETRIES -eq 0 ]; then + echo -e "${RED}Failed to connect to PostgreSQL${NC}" + $DOCKER_COMPOSE_CMD -f docker-compose.test.yml logs + exit 1 +fi + +echo -e "${GREEN}PostgreSQL is ready!${NC}" + +# Load schema +echo -e "${YELLOW}Loading database schema...${NC}" +if [ ! -f "$SCHEMA_PATH" ]; then + echo -e "${RED}Schema file not found at: $SCHEMA_PATH${NC}" + exit 1 +fi + +$DOCKER_COMPOSE_CMD -f docker-compose.test.yml exec -T postgres-test psql -U postgres -d eliza_test -f - < "$SCHEMA_PATH" +if [ $? -ne 0 ]; then + echo -e "${RED}Failed to load schema${NC}" + exit 1 +fi +echo -e "${GREEN}Schema loaded successfully!${NC}" + +# Run the tests +echo -e "${YELLOW}Running tests...${NC}" +pnpm vitest vector-extension.test.ts + +# Capture test exit code +TEST_EXIT_CODE=$? + +# Clean up +echo -e "${YELLOW}Cleaning up test environment...${NC}" +$DOCKER_COMPOSE_CMD -f docker-compose.test.yml down + +# Exit with test exit code +if [ $TEST_EXIT_CODE -eq 0 ]; then + echo -e "${GREEN}Tests completed successfully!${NC}" +else + echo -e "${RED}Tests failed!${NC}" +fi + +exit $TEST_EXIT_CODE \ No newline at end of file diff --git a/packages/adapter-postgres/src/__tests__/vector-extension.test.ts b/packages/adapter-postgres/src/__tests__/vector-extension.test.ts new file mode 100644 index 0000000000..7ced587371 --- /dev/null +++ b/packages/adapter-postgres/src/__tests__/vector-extension.test.ts @@ -0,0 +1,433 @@ +import { PostgresDatabaseAdapter } from '../index'; +import pg from 'pg'; +import fs from 'fs'; +import path from 'path'; +import { describe, test, expect, beforeEach, afterEach, vi, beforeAll } from 'vitest'; +import { DatabaseAdapter, elizaLogger, type Memory, type Content, EmbeddingProvider } from '@elizaos/core'; + +// Increase test timeout +vi.setConfig({ testTimeout: 15000 }); + +// Mock the @elizaos/core module +vi.mock('@elizaos/core', () => ({ + elizaLogger: { + error: vi.fn().mockImplementation(console.error), + info: vi.fn().mockImplementation(console.log), + success: vi.fn().mockImplementation(console.log), + debug: vi.fn().mockImplementation(console.log), + warn: vi.fn().mockImplementation(console.warn), + }, + getEmbeddingConfig: () => ({ + provider: 'OpenAI', + dimensions: 1536, + model: 'text-embedding-3-small' + }), + DatabaseAdapter: class { + protected circuitBreaker = { + execute: async (operation: () => Promise) => operation() + }; + protected async withCircuitBreaker(operation: () => Promise) { + return this.circuitBreaker.execute(operation); + } + }, + EmbeddingProvider: { + OpenAI: 'OpenAI', + Ollama: 'Ollama', + BGE: 'BGE' + } +})); + +// Helper function to parse vector string from PostgreSQL +const parseVectorString = (vectorStr: string): number[] => { + if (!vectorStr) return []; + // Remove brackets and split by comma + return vectorStr.replace(/[\[\]]/g, '').split(',').map(Number); +}; + +describe('PostgresDatabaseAdapter - Vector Extension Validation', () => { + let adapter: PostgresDatabaseAdapter; + let testClient: pg.PoolClient; + let testPool: pg.Pool; + + const initializeDatabase = async (client: pg.PoolClient) => { + elizaLogger.info('Initializing database with schema...'); + try { + // Set app settings for vector dimension + await client.query(` + ALTER DATABASE eliza_test SET app.use_openai_embedding = 'true'; + ALTER DATABASE eliza_test SET app.use_ollama_embedding = 'false'; + `); + + // Read and execute schema file + const schemaPath = path.resolve(__dirname, '../../schema.sql'); + const schema = fs.readFileSync(schemaPath, 'utf8'); + await client.query(schema); + + // Verify schema setup + const { rows: vectorExt } = await client.query(` + SELECT * FROM pg_extension WHERE extname = 'vector' + `); + elizaLogger.info('Vector extension status:', { isInstalled: vectorExt.length > 0 }); + + const { rows: dimension } = await client.query('SELECT get_embedding_dimension()'); + elizaLogger.info('Vector dimension:', { dimension: dimension[0].get_embedding_dimension }); + + // Verify search path + const { rows: searchPath } = await client.query('SHOW search_path'); + elizaLogger.info('Search path:', { searchPath: searchPath[0].search_path }); + + } catch (error) { + elizaLogger.error(`Database initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }; + + const cleanDatabase = async (client: pg.PoolClient) => { + elizaLogger.info('Starting database cleanup...'); + try { + await client.query('DROP TABLE IF EXISTS relationships CASCADE'); + await client.query('DROP TABLE IF EXISTS participants CASCADE'); + await client.query('DROP TABLE IF EXISTS logs CASCADE'); + await client.query('DROP TABLE IF EXISTS goals CASCADE'); + await client.query('DROP TABLE IF EXISTS memories CASCADE'); + await client.query('DROP TABLE IF EXISTS rooms CASCADE'); + await client.query('DROP TABLE IF EXISTS accounts CASCADE'); + await client.query('DROP TABLE IF EXISTS cache CASCADE'); + await client.query('DROP EXTENSION IF EXISTS vector CASCADE'); + await client.query('DROP SCHEMA IF EXISTS extensions CASCADE'); + elizaLogger.success('Database cleanup completed successfully'); + } catch (error) { + elizaLogger.error(`Database cleanup failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }; + + beforeAll(async () => { + elizaLogger.info('Setting up test database...'); + const setupPool = new pg.Pool({ + host: 'localhost', + port: 5433, + database: 'eliza_test', + user: 'postgres', + password: 'postgres' + }); + + const setupClient = await setupPool.connect(); + try { + await cleanDatabase(setupClient); + await initializeDatabase(setupClient); + } finally { + await setupClient.release(); + await setupPool.end(); + } + }); + + beforeEach(async () => { + elizaLogger.info('Setting up test environment...'); + try { + // Setup test database connection + testPool = new pg.Pool({ + host: 'localhost', + port: 5433, + database: 'eliza_test', + user: 'postgres', + password: 'postgres' + }); + + testClient = await testPool.connect(); + elizaLogger.debug('Database connection established'); + + await cleanDatabase(testClient); + elizaLogger.debug('Database cleaned'); + + adapter = new PostgresDatabaseAdapter({ + host: 'localhost', + port: 5433, + database: 'eliza_test', + user: 'postgres', + password: 'postgres' + }); + elizaLogger.success('Test environment setup completed'); + } catch (error) { + elizaLogger.error(`Test environment setup failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }); + + afterEach(async () => { + elizaLogger.info('Cleaning up test environment...'); + try { + await cleanDatabase(testClient); + await testClient?.release(); + await testPool?.end(); + await adapter?.close(); + elizaLogger.success('Test environment cleanup completed'); + } catch (error) { + elizaLogger.error(`Test environment cleanup failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }); + + describe('Schema and Extension Management', () => { + test('should initialize with vector extension', async () => { + elizaLogger.info('Testing vector extension initialization...'); + try { + // Act + elizaLogger.debug('Initializing adapter...'); + await adapter.init(); + elizaLogger.success('Adapter initialized successfully'); + + // Assert + elizaLogger.debug('Verifying vector extension existence...'); + const { rows } = await testClient.query(` + SELECT 1 FROM pg_extension WHERE extname = 'vector' + `); + expect(rows.length).toBe(1); + elizaLogger.success('Vector extension verified successfully'); + } catch (error) { + elizaLogger.error(`Vector extension test failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }); + + test('should handle missing rooms table', async () => { + elizaLogger.info('Testing rooms table creation...'); + try { + // Act + elizaLogger.debug('Initializing adapter...'); + await adapter.init(); + elizaLogger.success('Adapter initialized successfully'); + + // Assert + elizaLogger.debug('Verifying rooms table existence...'); + const { rows } = await testClient.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'rooms' + ); + `); + expect(rows[0].exists).toBe(true); + elizaLogger.success('Rooms table verified successfully'); + } catch (error) { + elizaLogger.error(`Rooms table test failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }); + + test('should not reapply schema when everything exists', async () => { + elizaLogger.info('Testing schema reapplication prevention...'); + try { + // Arrange + elizaLogger.debug('Setting up initial schema...'); + await adapter.init(); + elizaLogger.success('Initial schema setup completed'); + + const spy = vi.spyOn(fs, 'readFileSync'); + elizaLogger.debug('File read spy installed'); + + // Act + elizaLogger.debug('Attempting schema reapplication...'); + await adapter.init(); + elizaLogger.success('Second initialization completed'); + + // Assert + expect(spy).not.toHaveBeenCalled(); + elizaLogger.success('Verified schema was not reapplied'); + spy.mockRestore(); + } catch (error) { + elizaLogger.error(`Schema reapplication test failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }); + + test('should handle transaction rollback on error', async () => { + elizaLogger.info('Testing transaction rollback...'); + try { + // Arrange + elizaLogger.debug('Setting up file read error simulation...'); + const spy = vi.spyOn(fs, 'readFileSync').mockImplementation(() => { + elizaLogger.warn('Simulating schema read error'); + throw new Error('Schema read error'); + }); + + // Act & Assert + elizaLogger.debug('Attempting initialization with error...'); + await expect(adapter.init()).rejects.toThrow('Schema read error'); + elizaLogger.success('Error thrown as expected'); + + // Verify no tables were created + elizaLogger.debug('Verifying rollback...'); + const { rows } = await testClient.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'rooms' + ); + `); + expect(rows[0].exists).toBe(false); + elizaLogger.success('Rollback verified successfully'); + spy.mockRestore(); + } catch (error) { + elizaLogger.error(`Transaction rollback test failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }); + }); + + // Memory Operations tests will be updated in the next iteration + describe('Memory Operations with Vector', () => { + const TEST_UUID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; + const TEST_TABLE = 'test_memories'; + + beforeEach(async () => { + elizaLogger.info('Setting up memory operations test...'); + try { + // Ensure clean state and proper initialization + await adapter.init(); + + // Verify vector extension and search path + await testClient.query(` + SET search_path TO public, extensions; + SELECT set_config('app.use_openai_embedding', 'true', false); + `); + + // Create necessary account and room first + await testClient.query('BEGIN'); + try { + await testClient.query(` + INSERT INTO accounts (id, email) + VALUES ($1, 'test@test.com') + ON CONFLICT (id) DO NOTHING + `, [TEST_UUID]); + + await testClient.query(` + INSERT INTO rooms (id) + VALUES ($1) + ON CONFLICT (id) DO NOTHING + `, [TEST_UUID]); + + await testClient.query('COMMIT'); + } catch (error) { + await testClient.query('ROLLBACK'); + throw error; + } + + } catch (error) { + elizaLogger.error('Memory operations setup failed:', { + error: error instanceof Error ? error.message : String(error) + }); + throw error; + } + }); + + test('should create and retrieve memory with vector embedding', async () => { + // Arrange + const content: Content = { + text: 'test content' + }; + + const memory: Memory = { + id: TEST_UUID, + content, + embedding: new Array(1536).fill(0.1), + unique: true, + userId: TEST_UUID, + agentId: TEST_UUID, + roomId: TEST_UUID, + createdAt: Date.now() + }; + + // Act + await testClient.query('BEGIN'); + try { + await adapter.createMemory(memory, TEST_TABLE); + await testClient.query('COMMIT'); + } catch (error) { + await testClient.query('ROLLBACK'); + throw error; + } + + // Verify the embedding dimension + const { rows: [{ get_embedding_dimension }] } = await testClient.query('SELECT get_embedding_dimension()'); + expect(get_embedding_dimension).toBe(1536); + + // Retrieve and verify + const retrieved = await adapter.getMemoryById(TEST_UUID); + expect(retrieved).toBeDefined(); + const parsedEmbedding = typeof retrieved?.embedding === 'string' ? parseVectorString(retrieved.embedding) : retrieved?.embedding; + expect(Array.isArray(parsedEmbedding)).toBe(true); + expect(parsedEmbedding).toHaveLength(1536); + expect(retrieved?.content).toEqual(content); + }); + + test('should search memories by embedding', async () => { + // Arrange + const content: Content = { text: 'test content' }; + const embedding = new Array(1536).fill(0.1); + const memory: Memory = { + id: TEST_UUID, + content, + embedding, + unique: true, + userId: TEST_UUID, + agentId: TEST_UUID, + roomId: TEST_UUID, + createdAt: Date.now() + }; + + // Create memory within transaction + await testClient.query('BEGIN'); + try { + await adapter.createMemory(memory, TEST_TABLE); + await testClient.query('COMMIT'); + } catch (error) { + await testClient.query('ROLLBACK'); + throw error; + } + + // Act + const results = await adapter.searchMemoriesByEmbedding(embedding, { + tableName: TEST_TABLE, + roomId: TEST_UUID, + match_threshold: 0.8, + count: 1 + }); + + // Assert + expect(results).toBeDefined(); + expect(Array.isArray(results)).toBe(true); + expect(results.length).toBeGreaterThan(0); + const parsedEmbedding = typeof results[0].embedding === 'string' ? parseVectorString(results[0].embedding) : results[0].embedding; + expect(parsedEmbedding).toHaveLength(1536); + }); + + test('should handle invalid embedding dimensions', async () => { + // Arrange + const content: Content = { + text: 'test content' + }; + + const memory: Memory = { + id: TEST_UUID, + content, + embedding: new Array(100).fill(0.1), // Wrong dimension + unique: true, + userId: TEST_UUID, + agentId: TEST_UUID, + roomId: TEST_UUID, + createdAt: Date.now() + }; + + // Act & Assert + await testClient.query('BEGIN'); + try { + await expect(adapter.createMemory(memory, TEST_TABLE)) + .rejects + .toThrow('Invalid embedding dimension: expected 1536, got 100'); + await testClient.query('ROLLBACK'); + } catch (error) { + await testClient.query('ROLLBACK'); + throw error; + } + }, { timeout: 30000 }); // Increased timeout for retry attempts + }); +}); \ No newline at end of file diff --git a/packages/adapter-postgres/src/index.ts b/packages/adapter-postgres/src/index.ts index f1942b9fef..48210999b9 100644 --- a/packages/adapter-postgres/src/index.ts +++ b/packages/adapter-postgres/src/index.ts @@ -183,6 +183,27 @@ export class PostgresDatabaseAdapter }, "query"); } + private async validateVectorSetup(): Promise { + try { + const vectorExt = await this.query(` + SELECT 1 FROM pg_extension WHERE extname = 'vector' + `); + const hasVector = vectorExt.rows.length > 0; + + if (!hasVector) { + elizaLogger.error("Vector extension not found in database"); + return false; + } + + return true; + } catch (error) { + elizaLogger.error("Failed to validate vector extension:", { + error: error instanceof Error ? error.message : String(error) + }); + return false; + } + } + async init() { await this.testConnection(); @@ -211,7 +232,8 @@ export class PostgresDatabaseAdapter ); `); - if (!rows[0].exists) { + if (!rows[0].exists || !await this.validateVectorSetup()) { + elizaLogger.info("Applying database schema - tables or vector extension missing"); const schema = fs.readFileSync( path.resolve(__dirname, "../schema.sql"), "utf8" From 7d854f2dc3e2ea215ffee20db8551caed428da96 Mon Sep 17 00:00:00 2001 From: AIFlow_ML Date: Fri, 3 Jan 2025 17:13:39 +0700 Subject: [PATCH 09/99] fix(shell): Improve shell script exit code handling --- .../src/__tests__/run_tests.sh | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) mode change 100644 => 100755 packages/adapter-postgres/src/__tests__/run_tests.sh diff --git a/packages/adapter-postgres/src/__tests__/run_tests.sh b/packages/adapter-postgres/src/__tests__/run_tests.sh old mode 100644 new mode 100755 index 6a18b681eb..f9acc13c13 --- a/packages/adapter-postgres/src/__tests__/run_tests.sh +++ b/packages/adapter-postgres/src/__tests__/run_tests.sh @@ -56,8 +56,8 @@ if [ ! -f "$SCHEMA_PATH" ]; then exit 1 fi -$DOCKER_COMPOSE_CMD -f docker-compose.test.yml exec -T postgres-test psql -U postgres -d eliza_test -f - < "$SCHEMA_PATH" -if [ $? -ne 0 ]; then +# Fix: Check exit code directly instead of using $? +if ! $DOCKER_COMPOSE_CMD -f docker-compose.test.yml exec -T postgres-test psql -U postgres -d eliza_test -f - < "$SCHEMA_PATH"; then echo -e "${RED}Failed to load schema${NC}" exit 1 fi @@ -65,20 +65,14 @@ echo -e "${GREEN}Schema loaded successfully!${NC}" # Run the tests echo -e "${YELLOW}Running tests...${NC}" -pnpm vitest vector-extension.test.ts +if ! pnpm vitest vector-extension.test.ts; then + echo -e "${RED}Tests failed!${NC}" + $DOCKER_COMPOSE_CMD -f docker-compose.test.yml down + exit 1 +fi -# Capture test exit code -TEST_EXIT_CODE=$? +echo -e "${GREEN}Tests completed successfully!${NC}" # Clean up echo -e "${YELLOW}Cleaning up test environment...${NC}" -$DOCKER_COMPOSE_CMD -f docker-compose.test.yml down - -# Exit with test exit code -if [ $TEST_EXIT_CODE -eq 0 ]; then - echo -e "${GREEN}Tests completed successfully!${NC}" -else - echo -e "${RED}Tests failed!${NC}" -fi - -exit $TEST_EXIT_CODE \ No newline at end of file +$DOCKER_COMPOSE_CMD -f docker-compose.test.yml down \ No newline at end of file From 0c3c3f32e5e9876071eec5a07bf6e6d1a0d44e08 Mon Sep 17 00:00:00 2001 From: Luka Petrovic Date: Fri, 3 Jan 2025 11:23:04 +0100 Subject: [PATCH 10/99] feat: add coingecko plugin --- packages/plugin-coingecko/.env.test | 1 + packages/plugin-coingecko/.npmignore | 6 + packages/plugin-coingecko/package.json | 17 ++ .../src/__tests__/getPrice.test.ts | 142 +++++++++++++ .../plugin-coingecko/src/actions/getPrice.ts | 195 ++++++++++++++++++ packages/plugin-coingecko/src/environment.ts | 30 +++ packages/plugin-coingecko/src/index.ts | 12 ++ packages/plugin-coingecko/src/types.ts | 22 ++ packages/plugin-coingecko/tsconfig.json | 10 + packages/plugin-coingecko/tsup.config.ts | 11 + 10 files changed, 446 insertions(+) create mode 100644 packages/plugin-coingecko/.env.test create mode 100644 packages/plugin-coingecko/.npmignore create mode 100644 packages/plugin-coingecko/package.json create mode 100644 packages/plugin-coingecko/src/__tests__/getPrice.test.ts create mode 100644 packages/plugin-coingecko/src/actions/getPrice.ts create mode 100644 packages/plugin-coingecko/src/environment.ts create mode 100644 packages/plugin-coingecko/src/index.ts create mode 100644 packages/plugin-coingecko/src/types.ts create mode 100644 packages/plugin-coingecko/tsconfig.json create mode 100644 packages/plugin-coingecko/tsup.config.ts diff --git a/packages/plugin-coingecko/.env.test b/packages/plugin-coingecko/.env.test new file mode 100644 index 0000000000..dafea90cce --- /dev/null +++ b/packages/plugin-coingecko/.env.test @@ -0,0 +1 @@ +COINGECKO_API_KEY=your_test_api_key_here \ No newline at end of file diff --git a/packages/plugin-coingecko/.npmignore b/packages/plugin-coingecko/.npmignore new file mode 100644 index 0000000000..0468b4b364 --- /dev/null +++ b/packages/plugin-coingecko/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts diff --git a/packages/plugin-coingecko/package.json b/packages/plugin-coingecko/package.json new file mode 100644 index 0000000000..fb1fe8b830 --- /dev/null +++ b/packages/plugin-coingecko/package.json @@ -0,0 +1,17 @@ +{ + "name": "@elizaos/plugin-coingecko", + "version": "0.1.7-alpha.2", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@elizaos/core": "workspace:*", + "axios": "^1.6.7", + "tsup": "^8.3.5" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "test": "vitest run" + } +} \ No newline at end of file diff --git a/packages/plugin-coingecko/src/__tests__/getPrice.test.ts b/packages/plugin-coingecko/src/__tests__/getPrice.test.ts new file mode 100644 index 0000000000..ed072b3e65 --- /dev/null +++ b/packages/plugin-coingecko/src/__tests__/getPrice.test.ts @@ -0,0 +1,142 @@ +import { defaultCharacter } from "@elizaos/core"; +import axios from "axios"; +import { + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; +import getPriceAction from "../actions/getPrice"; + +// Mock axios +vi.mock("axios", () => ({ + default: { + get: vi.fn(), + }, +})); + +describe("getPrice Action", () => { + let mockedRuntime; + + beforeAll(() => { + mockedRuntime = { + character: defaultCharacter, + getSetting: vi.fn().mockReturnValue("test-api-key"), + composeState: vi.fn().mockResolvedValue({ + recentMessages: [ + { + id: "1", + timestamp: Date.now(), + content: { + text: "What is the price of Bitcoin?", + }, + user: "user1", + }, + ], + }), + updateRecentMessageState: vi + .fn() + .mockImplementation((state) => state), + }; + }); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.clearAllTimers(); + }); + + describe("Price Checking", () => { + it("should validate configuration successfully", async () => { + const result = await getPriceAction.validate(mockedRuntime, { + id: "1", + timestamp: Date.now(), + content: { text: "Check BTC price" }, + user: "user1", + }); + + expect(result).toBe(true); + }); + + it("should handle price fetch successfully", async () => { + const mockAxiosResponse = { + data: { + bitcoin: { + usd: 50000, + }, + }, + }; + + (axios.get as any).mockResolvedValueOnce(mockAxiosResponse); + + const mockCallback = vi.fn(); + + const result = await getPriceAction.handler( + mockedRuntime, + { + id: "1", + timestamp: Date.now(), + content: { text: "Check BTC price" }, + user: "user1", + }, + { + recentMessages: [ + { + id: "1", + timestamp: Date.now(), + content: { text: "Check BTC price" }, + user: "user1", + }, + ], + }, + {}, + mockCallback + ); + + expect(result).toBe(true); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining("50000 USD"), + content: { price: 50000, currency: "usd" }, + }); + }); + + it("should handle API errors gracefully", async () => { + (axios.get as any).mockRejectedValueOnce(new Error("API Error")); + + const mockCallback = vi.fn(); + + const result = await getPriceAction.handler( + mockedRuntime, + { + id: "1", + timestamp: Date.now(), + content: { text: "Check BTC price" }, + user: "user1", + }, + { + recentMessages: [ + { + id: "1", + timestamp: Date.now(), + content: { text: "Check BTC price" }, + user: "user1", + }, + ], + }, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining("Error fetching price"), + content: { error: "API Error" }, + }); + }); + }); +}); diff --git a/packages/plugin-coingecko/src/actions/getPrice.ts b/packages/plugin-coingecko/src/actions/getPrice.ts new file mode 100644 index 0000000000..852742bd26 --- /dev/null +++ b/packages/plugin-coingecko/src/actions/getPrice.ts @@ -0,0 +1,195 @@ +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 { validateCoingeckoConfig } from "../environment"; + +const GetPriceSchema = z.object({ + coinId: z.string(), + currency: z.string().default("usd"), +}); + +export interface GetPriceContent extends Content { + coinId: string; + currency: string; +} + +export function isGetPriceContent( + content: GetPriceContent +): content is GetPriceContent { + return ( + typeof content.coinId === "string" && + typeof content.currency === "string" + ); +} + +const getPriceTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Here are several frequently used coin IDs. Use these for the corresponding tokens: +- bitcoin/btc: bitcoin +- ethereum/eth: ethereum +- usdc: usd-coin + +Example response: +\`\`\`json +{ + "coinId": "bitcoin", + "currency": "usd" +} +\`\`\` + +{{recentMessages}} + +Given the recent messages, extract the following information about the requested price check: +- Coin ID +- Currency (defaults to USD) + +Respond with a JSON markdown block containing only the extracted values.`; + +export default { + name: "GET_PRICE", + similes: [ + "CHECK_PRICE", + "PRICE_CHECK", + "GET_CRYPTO_PRICE", + "CHECK_CRYPTO_PRICE", + ], + validate: async (runtime: IAgentRuntime, message: Memory) => { + await validateCoingeckoConfig(runtime); + return true; + }, + description: "Get the current price of a cryptocurrency from CoinGecko", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + elizaLogger.log("Starting CoinGecko GET_PRICE handler..."); + + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + const priceContext = composeContext({ + state, + template: getPriceTemplate, + }); + + const content = ( + await generateObject({ + runtime, + context: priceContext, + modelClass: ModelClass.SMALL, + schema: GetPriceSchema, + }) + ).object as unknown as GetPriceContent; + + if (!isGetPriceContent(content)) { + console.error("Invalid content for GET_PRICE action."); + if (callback) { + callback({ + text: "Unable to process price check request. Invalid content provided.", + content: { error: "Invalid price check content" }, + }); + } + return false; + } + + try { + const config = await validateCoingeckoConfig(runtime); + const response = await axios.get( + `https://api.coingecko.com/api/v3/simple/price`, + { + params: { + ids: content.coinId, + vs_currencies: content.currency, + x_cg_demo_api_key: config.COINGECKO_API_KEY, + }, + } + ); + + const price = response.data[content.coinId][content.currency]; + elizaLogger.success( + `Price retrieved successfully! ${content.coinId}: ${price} ${content.currency.toUpperCase()}` + ); + + if (callback) { + callback({ + text: `The current price of ${content.coinId} is ${price} ${content.currency.toUpperCase()}`, + content: { price, currency: content.currency }, + }); + } + + return true; + } catch (error) { + elizaLogger.error("Error fetching price:", error); + if (callback) { + callback({ + text: `Error fetching price: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } + }, + + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "What's the current price of Bitcoin?", + }, + }, + { + user: "{{agent}}", + content: { + text: "Let me check the current Bitcoin price for you.", + action: "GET_PRICE", + }, + }, + { + user: "{{agent}}", + content: { + text: "The current price of Bitcoin is 65,432.21 USD", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { + text: "Check ETH price in EUR", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll check the current Ethereum price in EUR.", + action: "GET_PRICE", + }, + }, + { + user: "{{agent}}", + content: { + text: "The current price of Ethereum is 2,345.67 EUR", + }, + }, + ], + ] as ActionExample[][], +} as Action; diff --git a/packages/plugin-coingecko/src/environment.ts b/packages/plugin-coingecko/src/environment.ts new file mode 100644 index 0000000000..276658e371 --- /dev/null +++ b/packages/plugin-coingecko/src/environment.ts @@ -0,0 +1,30 @@ +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"), +}); + +export type CoingeckoConfig = z.infer; + +export async function validateCoingeckoConfig( + runtime: IAgentRuntime +): Promise { + try { + const config = { + COINGECKO_API_KEY: runtime.getSetting("COINGECKO_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; + } +} diff --git a/packages/plugin-coingecko/src/index.ts b/packages/plugin-coingecko/src/index.ts new file mode 100644 index 0000000000..b2962f1072 --- /dev/null +++ b/packages/plugin-coingecko/src/index.ts @@ -0,0 +1,12 @@ +import { Plugin } from "@elizaos/core"; +import getPrice from "./actions/getPrice"; + +export const coingeckoPlugin: Plugin = { + name: "coingecko", + description: "CoinGecko Plugin for Eliza", + actions: [getPrice], + evaluators: [], + providers: [], +}; + +export default coingeckoPlugin; diff --git a/packages/plugin-coingecko/src/types.ts b/packages/plugin-coingecko/src/types.ts new file mode 100644 index 0000000000..c2ee9d725d --- /dev/null +++ b/packages/plugin-coingecko/src/types.ts @@ -0,0 +1,22 @@ +// Type definitions for CoinGecko plugin + +export interface CoinGeckoConfig { + apiKey?: string; +} + +export interface PriceResponse { + [key: string]: { + [currency: string]: number; + }; +} + +export interface MarketData { + id: string; + symbol: string; + name: string; + current_price: number; + market_cap: number; + market_cap_rank: number; + price_change_percentage_24h: number; + total_volume: number; +} diff --git a/packages/plugin-coingecko/tsconfig.json b/packages/plugin-coingecko/tsconfig.json new file mode 100644 index 0000000000..73993deaaf --- /dev/null +++ b/packages/plugin-coingecko/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-coingecko/tsup.config.ts b/packages/plugin-coingecko/tsup.config.ts new file mode 100644 index 0000000000..3e30481b3a --- /dev/null +++ b/packages/plugin-coingecko/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], + dts: true, + external: ["dotenv", "fs", "path", "https", "http"] +}); From 67922dc4ab353f19430cb1640365f877a6b9afdc Mon Sep 17 00:00:00 2001 From: Luka Petrovic Date: Fri, 3 Jan 2025 12:22:08 +0100 Subject: [PATCH 11/99] test: removed deprecated test --- .../src/__tests__/getPrice.test.ts | 142 ------------------ pnpm-lock.yaml | 62 +++++--- 2 files changed, 44 insertions(+), 160 deletions(-) delete mode 100644 packages/plugin-coingecko/src/__tests__/getPrice.test.ts diff --git a/packages/plugin-coingecko/src/__tests__/getPrice.test.ts b/packages/plugin-coingecko/src/__tests__/getPrice.test.ts deleted file mode 100644 index ed072b3e65..0000000000 --- a/packages/plugin-coingecko/src/__tests__/getPrice.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { defaultCharacter } from "@elizaos/core"; -import axios from "axios"; -import { - afterEach, - beforeAll, - beforeEach, - describe, - expect, - it, - vi, -} from "vitest"; -import getPriceAction from "../actions/getPrice"; - -// Mock axios -vi.mock("axios", () => ({ - default: { - get: vi.fn(), - }, -})); - -describe("getPrice Action", () => { - let mockedRuntime; - - beforeAll(() => { - mockedRuntime = { - character: defaultCharacter, - getSetting: vi.fn().mockReturnValue("test-api-key"), - composeState: vi.fn().mockResolvedValue({ - recentMessages: [ - { - id: "1", - timestamp: Date.now(), - content: { - text: "What is the price of Bitcoin?", - }, - user: "user1", - }, - ], - }), - updateRecentMessageState: vi - .fn() - .mockImplementation((state) => state), - }; - }); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - afterEach(() => { - vi.clearAllTimers(); - }); - - describe("Price Checking", () => { - it("should validate configuration successfully", async () => { - const result = await getPriceAction.validate(mockedRuntime, { - id: "1", - timestamp: Date.now(), - content: { text: "Check BTC price" }, - user: "user1", - }); - - expect(result).toBe(true); - }); - - it("should handle price fetch successfully", async () => { - const mockAxiosResponse = { - data: { - bitcoin: { - usd: 50000, - }, - }, - }; - - (axios.get as any).mockResolvedValueOnce(mockAxiosResponse); - - const mockCallback = vi.fn(); - - const result = await getPriceAction.handler( - mockedRuntime, - { - id: "1", - timestamp: Date.now(), - content: { text: "Check BTC price" }, - user: "user1", - }, - { - recentMessages: [ - { - id: "1", - timestamp: Date.now(), - content: { text: "Check BTC price" }, - user: "user1", - }, - ], - }, - {}, - mockCallback - ); - - expect(result).toBe(true); - expect(mockCallback).toHaveBeenCalledWith({ - text: expect.stringContaining("50000 USD"), - content: { price: 50000, currency: "usd" }, - }); - }); - - it("should handle API errors gracefully", async () => { - (axios.get as any).mockRejectedValueOnce(new Error("API Error")); - - const mockCallback = vi.fn(); - - const result = await getPriceAction.handler( - mockedRuntime, - { - id: "1", - timestamp: Date.now(), - content: { text: "Check BTC price" }, - user: "user1", - }, - { - recentMessages: [ - { - id: "1", - timestamp: Date.now(), - content: { text: "Check BTC price" }, - user: "user1", - }, - ], - }, - {}, - mockCallback - ); - - expect(result).toBe(false); - expect(mockCallback).toHaveBeenCalledWith({ - text: expect.stringContaining("Error fetching price"), - content: { error: "API Error" }, - }); - }); - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e208d46a5..84fef3a878 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1093,6 +1093,18 @@ importers: specifier: 8.3.5 version: 8.3.5(@swc/core@1.10.4(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.7.0) + packages/plugin-coingecko: + dependencies: + '@elizaos/core': + specifier: workspace:* + version: link:../core + axios: + specifier: ^1.6.7 + version: 1.7.9 + tsup: + specifier: ^8.3.5 + version: 8.3.5(@swc/core@1.10.1(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.1) + packages/plugin-conflux: dependencies: '@elizaos/core': @@ -19601,7 +19613,7 @@ snapshots: '@acuminous/bitsyntax@0.1.2': dependencies: buffer-more-ints: 1.0.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0 safe-buffer: 5.1.2 transitivePeerDependencies: - supports-color @@ -21534,7 +21546,7 @@ snapshots: dependencies: '@scure/bip32': 1.6.0 abitype: 1.0.8(typescript@5.6.3)(zod@3.23.8) - axios: 1.7.9(debug@4.4.0) + axios: 1.7.9 axios-mock-adapter: 1.22.0(axios@1.7.9) axios-retry: 4.5.0(axios@1.7.9) bip32: 4.0.0 @@ -23369,7 +23381,7 @@ snapshots: '@eslint/config-array@0.19.1': dependencies: '@eslint/object-schema': 2.1.5 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -23395,7 +23407,7 @@ snapshots: '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0 espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -28792,7 +28804,7 @@ snapshots: '@typescript-eslint/types': 8.16.0 '@typescript-eslint/typescript-estree': 8.16.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 8.16.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0 eslint: 9.16.0(jiti@2.4.2) optionalDependencies: typescript: 5.6.3 @@ -28825,7 +28837,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.16.0(typescript@5.6.3) '@typescript-eslint/utils': 8.16.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.6.3) - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0 eslint: 9.16.0(jiti@2.4.2) ts-api-utils: 1.4.3(typescript@5.6.3) optionalDependencies: @@ -28856,7 +28868,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.16.0 '@typescript-eslint/visitor-keys': 8.16.0 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -29646,7 +29658,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -30038,13 +30050,13 @@ snapshots: axios-mock-adapter@1.22.0(axios@1.7.9): dependencies: - axios: 1.7.9(debug@4.4.0) + axios: 1.7.9 fast-deep-equal: 3.1.3 is-buffer: 2.0.5 axios-retry@4.5.0(axios@1.7.9): dependencies: - axios: 1.7.9(debug@4.4.0) + axios: 1.7.9 is-retry-allowed: 2.2.0 axios@0.21.4: @@ -30055,7 +30067,7 @@ snapshots: axios@0.27.2: dependencies: - follow-redirects: 1.15.9(debug@4.4.0) + follow-redirects: 1.15.9 form-data: 4.0.1 transitivePeerDependencies: - debug @@ -30084,6 +30096,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.7.9: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axios@1.7.9(debug@4.4.0): dependencies: follow-redirects: 1.15.9(debug@4.4.0) @@ -32119,6 +32139,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.0: + dependencies: + ms: 2.1.3 + debug@4.4.0(supports-color@5.5.0): dependencies: ms: 2.1.3 @@ -33018,7 +33042,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0 escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 eslint-visitor-keys: 4.2.0 @@ -33604,6 +33628,8 @@ snapshots: async: 0.2.10 which: 1.3.1 + follow-redirects@1.15.9: {} + follow-redirects@1.15.9(debug@4.3.7): optionalDependencies: debug: 4.3.7 @@ -34682,7 +34708,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -34738,14 +34764,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0 transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -41113,7 +41139,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.3 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0 socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -42122,7 +42148,7 @@ snapshots: tuf-js@2.2.1: dependencies: '@tufjs/models': 2.0.1 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0 make-fetch-happen: 13.0.1 transitivePeerDependencies: - supports-color @@ -42914,7 +42940,7 @@ snapshots: '@vitest/spy': 2.1.5 '@vitest/utils': 2.1.5 chai: 5.1.2 - debug: 4.4.0(supports-color@8.1.1) + debug: 4.4.0 expect-type: 1.1.0 magic-string: 0.30.17 pathe: 1.1.2 From c5d6367a71ff10ab227a22826a5279c141b64e6a Mon Sep 17 00:00:00 2001 From: Luka Petrovic Date: Fri, 3 Jan 2025 15:16:39 +0100 Subject: [PATCH 12/99] feat: add readme.md, fix getprice --- agent/package.json | 149 +++++++++--------- agent/src/index.ts | 28 ++-- packages/plugin-coingecko/README.md | 49 ++++++ .../plugin-coingecko/src/actions/getPrice.ts | 123 ++++++--------- .../plugin-coingecko/src/templates/price.ts | 31 ++++ packages/plugin-coingecko/src/utils/coin.ts | 22 +++ pnpm-lock.yaml | 55 +++---- 7 files changed, 266 insertions(+), 191 deletions(-) create mode 100644 packages/plugin-coingecko/README.md create mode 100644 packages/plugin-coingecko/src/templates/price.ts create mode 100644 packages/plugin-coingecko/src/utils/coin.ts diff --git a/agent/package.json b/agent/package.json index 48b3f4270a..a3172b1e52 100644 --- a/agent/package.json +++ b/agent/package.json @@ -1,75 +1,76 @@ { - "name": "@elizaos/agent", - "version": "0.1.7-alpha.2", - "main": "src/index.ts", - "type": "module", - "scripts": { - "start": "node --loader ts-node/esm src/index.ts", - "dev": "node --loader ts-node/esm src/index.ts", - "check-types": "tsc --noEmit", - "test": "jest" - }, - "nodemonConfig": { - "watch": [ - "src", - "../core/dist" - ], - "ext": "ts,json", - "exec": "node --enable-source-maps --loader ts-node/esm src/index.ts" - }, - "dependencies": { - "@elizaos/adapter-postgres": "workspace:*", - "@elizaos/adapter-redis": "workspace:*", - "@elizaos/adapter-sqlite": "workspace:*", - "@elizaos/client-auto": "workspace:*", - "@elizaos/client-direct": "workspace:*", - "@elizaos/client-discord": "workspace:*", - "@elizaos/client-farcaster": "workspace:*", - "@elizaos/client-lens": "workspace:*", - "@elizaos/client-telegram": "workspace:*", - "@elizaos/client-twitter": "workspace:*", - "@elizaos/client-slack": "workspace:*", - "@elizaos/core": "workspace:*", - "@elizaos/plugin-0g": "workspace:*", - "@elizaos/plugin-abstract": "workspace:*", - "@elizaos/plugin-aptos": "workspace:*", - "@elizaos/plugin-bootstrap": "workspace:*", - "@elizaos/plugin-intiface": "workspace:*", - "@elizaos/plugin-coinbase": "workspace:*", - "@elizaos/plugin-conflux": "workspace:*", - "@elizaos/plugin-evm": "workspace:*", - "@elizaos/plugin-echochambers": "workspace:*", - "@elizaos/plugin-flow": "workspace:*", - "@elizaos/plugin-gitbook": "workspace:*", - "@elizaos/plugin-story": "workspace:*", - "@elizaos/plugin-goat": "workspace:*", - "@elizaos/plugin-icp": "workspace:*", - "@elizaos/plugin-image-generation": "workspace:*", - "@elizaos/plugin-nft-generation": "workspace:*", - "@elizaos/plugin-node": "workspace:*", - "@elizaos/plugin-solana": "workspace:*", - "@elizaos/plugin-starknet": "workspace:*", - "@elizaos/plugin-ton": "workspace:*", - "@elizaos/plugin-sui": "workspace:*", - "@elizaos/plugin-tee": "workspace:*", - "@elizaos/plugin-multiversx": "workspace:*", - "@elizaos/plugin-near": "workspace:*", - "@elizaos/plugin-zksync-era": "workspace:*", - "@elizaos/plugin-twitter": "workspace:*", - "@elizaos/plugin-cronoszkevm": "workspace:*", - "@elizaos/plugin-3d-generation": "workspace:*", - "@elizaos/plugin-fuel": "workspace:*", - "@elizaos/plugin-avalanche": "workspace:*", - "@elizaos/plugin-web-search": "workspace:*", - "readline": "1.3.0", - "ws": "8.18.0", - "yargs": "17.7.2" - }, - "devDependencies": { - "@types/jest": "^29.5.14", - "jest": "^29.7.0", - "ts-jest": "^29.2.5", - "ts-node": "10.9.2", - "tsup": "8.3.5" - } -} + "name": "@elizaos/agent", + "version": "0.1.7-alpha.2", + "main": "src/index.ts", + "type": "module", + "scripts": { + "start": "node --loader ts-node/esm src/index.ts", + "dev": "node --loader ts-node/esm src/index.ts", + "check-types": "tsc --noEmit", + "test": "jest" + }, + "nodemonConfig": { + "watch": [ + "src", + "../core/dist" + ], + "ext": "ts,json", + "exec": "node --enable-source-maps --loader ts-node/esm src/index.ts" + }, + "dependencies": { + "@elizaos/adapter-postgres": "workspace:*", + "@elizaos/adapter-redis": "workspace:*", + "@elizaos/adapter-sqlite": "workspace:*", + "@elizaos/client-auto": "workspace:*", + "@elizaos/client-direct": "workspace:*", + "@elizaos/client-discord": "workspace:*", + "@elizaos/client-farcaster": "workspace:*", + "@elizaos/client-lens": "workspace:*", + "@elizaos/client-telegram": "workspace:*", + "@elizaos/client-twitter": "workspace:*", + "@elizaos/client-slack": "workspace:*", + "@elizaos/core": "workspace:*", + "@elizaos/plugin-0g": "workspace:*", + "@elizaos/plugin-abstract": "workspace:*", + "@elizaos/plugin-aptos": "workspace:*", + "@elizaos/plugin-bootstrap": "workspace:*", + "@elizaos/plugin-intiface": "workspace:*", + "@elizaos/plugin-coinbase": "workspace:*", + "@elizaos/plugin-coingecko": "workspace:*", + "@elizaos/plugin-conflux": "workspace:*", + "@elizaos/plugin-evm": "workspace:*", + "@elizaos/plugin-echochambers": "workspace:*", + "@elizaos/plugin-flow": "workspace:*", + "@elizaos/plugin-gitbook": "workspace:*", + "@elizaos/plugin-story": "workspace:*", + "@elizaos/plugin-goat": "workspace:*", + "@elizaos/plugin-icp": "workspace:*", + "@elizaos/plugin-image-generation": "workspace:*", + "@elizaos/plugin-nft-generation": "workspace:*", + "@elizaos/plugin-node": "workspace:*", + "@elizaos/plugin-solana": "workspace:*", + "@elizaos/plugin-starknet": "workspace:*", + "@elizaos/plugin-ton": "workspace:*", + "@elizaos/plugin-sui": "workspace:*", + "@elizaos/plugin-tee": "workspace:*", + "@elizaos/plugin-multiversx": "workspace:*", + "@elizaos/plugin-near": "workspace:*", + "@elizaos/plugin-zksync-era": "workspace:*", + "@elizaos/plugin-twitter": "workspace:*", + "@elizaos/plugin-cronoszkevm": "workspace:*", + "@elizaos/plugin-3d-generation": "workspace:*", + "@elizaos/plugin-fuel": "workspace:*", + "@elizaos/plugin-avalanche": "workspace:*", + "@elizaos/plugin-web-search": "workspace:*", + "readline": "1.3.0", + "ws": "8.18.0", + "yargs": "17.7.2" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "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 53058cf4ec..2179101b0c 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -1,4 +1,5 @@ import { PostgresDatabaseAdapter } from "@elizaos/adapter-postgres"; +import { RedisClient } from "@elizaos/adapter-redis"; import { SqliteDatabaseAdapter } from "@elizaos/adapter-sqlite"; import { AutoClientInterface } from "@elizaos/client-auto"; import { DiscordClientInterface } from "@elizaos/client-discord"; @@ -10,31 +11,32 @@ import { TwitterClientInterface } from "@elizaos/client-twitter"; import { AgentRuntime, CacheManager, + CacheStore, Character, + Client, Clients, DbCacheAdapter, defaultCharacter, elizaLogger, FsCacheAdapter, IAgentRuntime, + ICacheManager, IDatabaseAdapter, IDatabaseCacheAdapter, ModelProviderName, settings, stringToUuid, validateCharacterConfig, - CacheStore, - Client, - ICacheManager, - parseBooleanFromText, } from "@elizaos/core"; -import { RedisClient } from "@elizaos/adapter-redis"; import { zgPlugin } from "@elizaos/plugin-0g"; import { bootstrapPlugin } from "@elizaos/plugin-bootstrap"; import createGoatPlugin from "@elizaos/plugin-goat"; // import { intifacePlugin } from "@elizaos/plugin-intiface"; import { DirectClient } from "@elizaos/client-direct"; +import { ThreeDGenerationPlugin } from "@elizaos/plugin-3d-generation"; +import { abstractPlugin } from "@elizaos/plugin-abstract"; import { aptosPlugin } from "@elizaos/plugin-aptos"; +import { avalanchePlugin } from "@elizaos/plugin-avalanche"; import { advancedTradePlugin, coinbaseCommercePlugin, @@ -43,33 +45,31 @@ import { tradePlugin, webhookPlugin, } from "@elizaos/plugin-coinbase"; +import { coingeckoPlugin } from "@elizaos/plugin-coingecko"; import { confluxPlugin } from "@elizaos/plugin-conflux"; +import { cronosZkEVMPlugin } from "@elizaos/plugin-cronoszkevm"; +import { echoChamberPlugin } from "@elizaos/plugin-echochambers"; import { evmPlugin } from "@elizaos/plugin-evm"; -import { storyPlugin } from "@elizaos/plugin-story"; import { flowPlugin } from "@elizaos/plugin-flow"; import { fuelPlugin } from "@elizaos/plugin-fuel"; import { imageGenerationPlugin } from "@elizaos/plugin-image-generation"; -import { ThreeDGenerationPlugin } from "@elizaos/plugin-3d-generation"; import { multiversxPlugin } from "@elizaos/plugin-multiversx"; import { nearPlugin } from "@elizaos/plugin-near"; import { nftGenerationPlugin } from "@elizaos/plugin-nft-generation"; import { createNodePlugin } from "@elizaos/plugin-node"; import { solanaPlugin } from "@elizaos/plugin-solana"; +import { storyPlugin } from "@elizaos/plugin-story"; import { suiPlugin } from "@elizaos/plugin-sui"; import { TEEMode, teePlugin } from "@elizaos/plugin-tee"; import { tonPlugin } from "@elizaos/plugin-ton"; -import { zksyncEraPlugin } from "@elizaos/plugin-zksync-era"; -import { cronosZkEVMPlugin } from "@elizaos/plugin-cronoszkevm"; -import { abstractPlugin } from "@elizaos/plugin-abstract"; -import { avalanchePlugin } from "@elizaos/plugin-avalanche"; import { webSearchPlugin } from "@elizaos/plugin-web-search"; -import { echoChamberPlugin } from "@elizaos/plugin-echochambers"; +import { zksyncEraPlugin } from "@elizaos/plugin-zksync-era"; import Database from "better-sqlite3"; import fs from "fs"; +import net from "net"; import path from "path"; import { fileURLToPath } from "url"; import yargs from "yargs"; -import net from "net"; const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file const __dirname = path.dirname(__filename); // get the name of the directory @@ -585,6 +585,8 @@ export async function createAgent( ? webhookPlugin : null, goatPlugin, + getSecret(character, "COINGECKO_API_KEY") ? coingeckoPlugin : null, + getSecret(character, "EVM_PROVIDER_URL") ? goatPlugin : null, getSecret(character, "ABSTRACT_PRIVATE_KEY") ? abstractPlugin : null, diff --git a/packages/plugin-coingecko/README.md b/packages/plugin-coingecko/README.md new file mode 100644 index 0000000000..ded984b61c --- /dev/null +++ b/packages/plugin-coingecko/README.md @@ -0,0 +1,49 @@ +# Plugin CoinGecko + +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. + +## Installation + +```bash +pnpm add @elizaos/plugin-coingecko +``` + +## Configuration + +Set up your environment with the required CoinGecko API key: + +| Variable Name | Description | +| ------------------- | ---------------------- | +| `COINGECKO_API_KEY` | Your CoinGecko API key | + +## Usage + +```typescript +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. + +Examples: + +- "What's the current price of Bitcoin?" +- "Check ETH price in EUR" +- "What's USDC worth?" + +## License + +MIT diff --git a/packages/plugin-coingecko/src/actions/getPrice.ts b/packages/plugin-coingecko/src/actions/getPrice.ts index 852742bd26..deb923b2e9 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, - generateObject, + generateObjectDeprecated, HandlerCallback, IAgentRuntime, Memory, @@ -12,51 +12,15 @@ import { type Action, } from "@elizaos/core"; import axios from "axios"; -import { z } from "zod"; import { validateCoingeckoConfig } from "../environment"; - -const GetPriceSchema = z.object({ - coinId: z.string(), - currency: z.string().default("usd"), -}); +import { getPriceTemplate } from "../templates/price"; +import { normalizeCoinId } from "../utils/coin"; export interface GetPriceContent extends Content { coinId: string; currency: string; } -export function isGetPriceContent( - content: GetPriceContent -): content is GetPriceContent { - return ( - typeof content.coinId === "string" && - typeof content.currency === "string" - ); -} - -const getPriceTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. - -Here are several frequently used coin IDs. Use these for the corresponding tokens: -- bitcoin/btc: bitcoin -- ethereum/eth: ethereum -- usdc: usd-coin - -Example response: -\`\`\`json -{ - "coinId": "bitcoin", - "currency": "usd" -} -\`\`\` - -{{recentMessages}} - -Given the recent messages, extract the following information about the requested price check: -- Coin ID -- Currency (defaults to USD) - -Respond with a JSON markdown block containing only the extracted values.`; - export default { name: "GET_PRICE", similes: [ @@ -79,65 +43,82 @@ export default { ): Promise => { elizaLogger.log("Starting CoinGecko GET_PRICE handler..."); + // Initialize or update state if (!state) { state = (await runtime.composeState(message)) as State; } else { state = await runtime.updateRecentMessageState(state); } - const priceContext = composeContext({ - state, - template: getPriceTemplate, - }); - - const content = ( - await generateObject({ + try { + // Compose price check context + elizaLogger.log("Composing price context..."); + const priceContext = composeContext({ + state, + template: getPriceTemplate, + }); + + elizaLogger.log("Composing content..."); + const content = (await generateObjectDeprecated({ runtime, context: priceContext, - modelClass: ModelClass.SMALL, - schema: GetPriceSchema, - }) - ).object as unknown as GetPriceContent; + modelClass: ModelClass.LARGE, + })) as unknown as GetPriceContent; - if (!isGetPriceContent(content)) { - console.error("Invalid content for GET_PRICE action."); - if (callback) { - callback({ - text: "Unable to process price check request. Invalid content provided.", - content: { error: "Invalid price check content" }, - }); + // Validate content structure first + if (!content || typeof content !== "object") { + throw new Error("Invalid response format from model"); } - return false; - } - try { + // Get and validate coin ID + const coinId = content.coinId + ? normalizeCoinId(content.coinId) + : null; + if (!coinId) { + throw new Error( + `Unsupported or invalid cryptocurrency: ${content.coinId}` + ); + } + + // Normalize currency + const currency = (content.currency || "usd").toLowerCase(); + + // Fetch price from CoinGecko const config = await validateCoingeckoConfig(runtime); + elizaLogger.log(`Fetching price for ${coinId} in ${currency}...`); + const response = await axios.get( `https://api.coingecko.com/api/v3/simple/price`, { params: { - ids: content.coinId, - vs_currencies: content.currency, + ids: coinId, + vs_currencies: currency, x_cg_demo_api_key: config.COINGECKO_API_KEY, }, } ); - const price = response.data[content.coinId][content.currency]; + if (!response.data[coinId]?.[currency]) { + throw new Error( + `No price data available for ${coinId} in ${currency}` + ); + } + + const price = response.data[coinId][currency]; elizaLogger.success( - `Price retrieved successfully! ${content.coinId}: ${price} ${content.currency.toUpperCase()}` + `Price retrieved successfully! ${coinId}: ${price} ${currency.toUpperCase()}` ); if (callback) { callback({ - text: `The current price of ${content.coinId} is ${price} ${content.currency.toUpperCase()}`, - content: { price, currency: content.currency }, + text: `The current price of ${coinId} is ${price} ${currency.toUpperCase()}`, + content: { price, currency }, }); } return true; } catch (error) { - elizaLogger.error("Error fetching price:", error); + elizaLogger.error("Error in GET_PRICE handler:", error); if (callback) { callback({ text: `Error fetching price: ${error.message}`, @@ -159,14 +140,14 @@ export default { { user: "{{agent}}", content: { - text: "Let me check the current Bitcoin price for you.", + text: "I'll check the current Bitcoin price for you.", action: "GET_PRICE", }, }, { user: "{{agent}}", content: { - text: "The current price of Bitcoin is 65,432.21 USD", + text: "The current price of bitcoin is {{dynamic}} USD", }, }, ], @@ -180,14 +161,14 @@ export default { { user: "{{agent}}", content: { - text: "I'll check the current Ethereum price in EUR.", + text: "I'll check the current Ethereum price in EUR for you.", action: "GET_PRICE", }, }, { user: "{{agent}}", content: { - text: "The current price of Ethereum is 2,345.67 EUR", + text: "The current price of ethereum is {{dynamic}} EUR", }, }, ], diff --git a/packages/plugin-coingecko/src/templates/price.ts b/packages/plugin-coingecko/src/templates/price.ts new file mode 100644 index 0000000000..e30175c6bf --- /dev/null +++ b/packages/plugin-coingecko/src/templates/price.ts @@ -0,0 +1,31 @@ +export const getPriceTemplate = `Given the message, extract information about the cryptocurrency price check request. Look for coin name/symbol and currency. + +Common coin mappings: +- BTC/Bitcoin -> "bitcoin" +- ETH/Ethereum -> "ethereum" +- USDC -> "usd-coin" + +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) + +Example responses: +For "What's the price of Bitcoin?": +\`\`\`json +{ + "coinId": "bitcoin", + "currency": "usd" +} +\`\`\` + +For "Check ETH price in EUR": +\`\`\`json +{ + "coinId": "ethereum", + "currency": "eur" +} +\`\`\` + +{{recentMessages}} + +Extract the cryptocurrency and currency information from the above messages and respond with the appropriate JSON.`; diff --git a/packages/plugin-coingecko/src/utils/coin.ts b/packages/plugin-coingecko/src/utils/coin.ts new file mode 100644 index 0000000000..6a30d8510c --- /dev/null +++ b/packages/plugin-coingecko/src/utils/coin.ts @@ -0,0 +1,22 @@ +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/pnpm-lock.yaml b/pnpm-lock.yaml index 84fef3a878..597bfa7e52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -159,6 +159,9 @@ importers: '@elizaos/plugin-coinbase': specifier: workspace:* version: link:../packages/plugin-coinbase + '@elizaos/plugin-coingecko': + specifier: workspace:* + version: link:../packages/plugin-coingecko '@elizaos/plugin-conflux': specifier: workspace:* version: link:../packages/plugin-conflux @@ -1100,7 +1103,7 @@ importers: version: link:../core axios: specifier: ^1.6.7 - version: 1.7.9 + version: 1.7.9(debug@4.4.0) tsup: specifier: ^8.3.5 version: 8.3.5(@swc/core@1.10.1(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.1) @@ -19613,7 +19616,7 @@ snapshots: '@acuminous/bitsyntax@0.1.2': dependencies: buffer-more-ints: 1.0.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) safe-buffer: 5.1.2 transitivePeerDependencies: - supports-color @@ -21546,7 +21549,7 @@ snapshots: dependencies: '@scure/bip32': 1.6.0 abitype: 1.0.8(typescript@5.6.3)(zod@3.23.8) - axios: 1.7.9 + axios: 1.7.9(debug@4.4.0) axios-mock-adapter: 1.22.0(axios@1.7.9) axios-retry: 4.5.0(axios@1.7.9) bip32: 4.0.0 @@ -23381,7 +23384,7 @@ snapshots: '@eslint/config-array@0.19.1': dependencies: '@eslint/object-schema': 2.1.5 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -23407,7 +23410,7 @@ snapshots: '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -28804,7 +28807,7 @@ snapshots: '@typescript-eslint/types': 8.16.0 '@typescript-eslint/typescript-estree': 8.16.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 8.16.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) eslint: 9.16.0(jiti@2.4.2) optionalDependencies: typescript: 5.6.3 @@ -28837,7 +28840,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.16.0(typescript@5.6.3) '@typescript-eslint/utils': 8.16.0(eslint@9.16.0(jiti@2.4.2))(typescript@5.6.3) - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) eslint: 9.16.0(jiti@2.4.2) ts-api-utils: 1.4.3(typescript@5.6.3) optionalDependencies: @@ -28868,7 +28871,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.16.0 '@typescript-eslint/visitor-keys': 8.16.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -29658,7 +29661,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -30050,13 +30053,13 @@ snapshots: axios-mock-adapter@1.22.0(axios@1.7.9): dependencies: - axios: 1.7.9 + axios: 1.7.9(debug@4.4.0) fast-deep-equal: 3.1.3 is-buffer: 2.0.5 axios-retry@4.5.0(axios@1.7.9): dependencies: - axios: 1.7.9 + axios: 1.7.9(debug@4.4.0) is-retry-allowed: 2.2.0 axios@0.21.4: @@ -30067,7 +30070,7 @@ snapshots: axios@0.27.2: dependencies: - follow-redirects: 1.15.9 + follow-redirects: 1.15.9(debug@4.4.0) form-data: 4.0.1 transitivePeerDependencies: - debug @@ -30096,14 +30099,6 @@ snapshots: transitivePeerDependencies: - debug - axios@1.7.9: - dependencies: - follow-redirects: 1.15.9 - form-data: 4.0.1 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - axios@1.7.9(debug@4.4.0): dependencies: follow-redirects: 1.15.9(debug@4.4.0) @@ -32139,10 +32134,6 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.0: - dependencies: - ms: 2.1.3 - debug@4.4.0(supports-color@5.5.0): dependencies: ms: 2.1.3 @@ -33042,7 +33033,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 eslint-visitor-keys: 4.2.0 @@ -33628,8 +33619,6 @@ snapshots: async: 0.2.10 which: 1.3.1 - follow-redirects@1.15.9: {} - follow-redirects@1.15.9(debug@4.3.7): optionalDependencies: debug: 4.3.7 @@ -34708,7 +34697,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -34764,14 +34753,14 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -41139,7 +41128,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -42148,7 +42137,7 @@ snapshots: tuf-js@2.2.1: dependencies: '@tufjs/models': 2.0.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) make-fetch-happen: 13.0.1 transitivePeerDependencies: - supports-color @@ -42940,7 +42929,7 @@ snapshots: '@vitest/spy': 2.1.5 '@vitest/utils': 2.1.5 chai: 5.1.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@8.1.1) expect-type: 1.1.0 magic-string: 0.30.17 pathe: 1.1.2 From 39e0cef9d1fea636c1333770ea434a525694bc39 Mon Sep 17 00:00:00 2001 From: JSon Date: Thu, 2 Jan 2025 22:46:06 +0800 Subject: [PATCH 13/99] feat: plugin-nft-generation support evm chain mint nft --- .../src/actions/mintNFTAction.ts | 209 ++++++++++++------ .../src/actions/nftCollectionGeneration.ts | 4 +- .../plugin-nft-generation/src/templates.ts | 7 +- packages/plugin-nft-generation/src/types.ts | 4 +- .../src/utils/deployEVMContract.ts | 29 +-- .../src/utils/verifyEVMContract.ts | 4 +- 6 files changed, 157 insertions(+), 100 deletions(-) diff --git a/packages/plugin-nft-generation/src/actions/mintNFTAction.ts b/packages/plugin-nft-generation/src/actions/mintNFTAction.ts index 61f4d70984..ab268b5fd3 100644 --- a/packages/plugin-nft-generation/src/actions/mintNFTAction.ts +++ b/packages/plugin-nft-generation/src/actions/mintNFTAction.ts @@ -16,9 +16,16 @@ import WalletSolana from "../provider/wallet/walletSolana.ts"; import { PublicKey } from "@solana/web3.js"; import { mintNFTTemplate } from "../templates.ts"; import { MintNFTContent, MintNFTSchema } from "../types.ts"; +import * as viemChains from "viem/chains"; +import { createPublicClient, createWalletClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { mintNFT } from "../utils/deployEVMContract.ts"; +const _SupportedChainList = Object.keys(viemChains) as Array< + keyof typeof viemChains +>; function isMintNFTContent(content: any): content is MintNFTContent { - return typeof content.collectionAddress === "string"; + return typeof content.collectionAddress === "string" && typeof content.collectionAddress === "string"; } const mintNFTAction: Action = { @@ -72,19 +79,35 @@ const mintNFTAction: Action = { state = await runtime.updateRecentMessageState(state); } - // Compose transfer context - const transferContext = composeContext({ + const context = composeContext({ state, template: mintNFTTemplate, }); + const chains = _SupportedChainList; + + const supportedChains: ( + | (typeof chains)[number] + | "solana" + | null + )[] = [...chains, "solana", null]; + const contextWithChains = context.replace( + "SUPPORTED_CHAINS", + supportedChains + .map((item) => (item ? `"${item}"` : item)) + .join("|") + ); + const res = await generateObject({ runtime, - context: transferContext, + context: contextWithChains, modelClass: ModelClass.LARGE, schema: MintNFTSchema, }); - const content = res.object; + const content = res.object as { + collectionAddress: string, + chainName: (typeof supportedChains)[number]; + }; elizaLogger.log("Generate Object:", content); @@ -99,63 +122,111 @@ const mintNFTAction: Action = { return false; } + if (content?.chainName === "solana") { + const publicKey = runtime.getSetting("SOLANA_PUBLIC_KEY"); + const privateKey = runtime.getSetting("SOLANA_PRIVATE_KEY"); - const publicKey = runtime.getSetting("SOLANA_PUBLIC_KEY"); - const privateKey = runtime.getSetting("SOLANA_PRIVATE_KEY"); + const wallet = new WalletSolana( + new PublicKey(publicKey), + privateKey + ); - const wallet = new WalletSolana( - new PublicKey(publicKey), - privateKey - ); - - const collectionInfo = await wallet.fetchDigitalAsset( - content.collectionAddress - ); - elizaLogger.log("Collection Info", collectionInfo); - const metadata = collectionInfo.metadata; - if (metadata.collection?.["value"]) { - callback({ - text: `Unable to process mint request. Invalid collection address ${content.collectionAddress}.`, - content: { error: "Invalid collection address." }, - }); - return false; - } - if (metadata) { - const nftRes = await createNFT({ - runtime, - collectionName: metadata.name, - collectionAddress: content.collectionAddress, - collectionAdminPublicKey: metadata.updateAuthority, - collectionFee: metadata.sellerFeeBasisPoints, - tokenId: 1, - }); - - elizaLogger.log("NFT Address:", nftRes); - - if (nftRes) { + const collectionInfo = await wallet.fetchDigitalAsset( + content.collectionAddress + ); + elizaLogger.log("Collection Info", collectionInfo); + const metadata = collectionInfo.metadata; + if (metadata.collection?.["value"]) { callback({ - text: `Congratulations to you! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ \nCollection Address: ${content.collectionAddress}\n NFT Address: ${nftRes.address}\n NFT Link: ${nftRes.link}`, //caption.description, - attachments: [], + text: `Unable to process mint request. Invalid collection address ${content.collectionAddress}.`, + content: { error: "Invalid collection address." }, }); - await sleep(15000); - await verifyNFT({ + return false; + } + if (metadata) { + const nftRes = await createNFT({ runtime, + collectionName: metadata.name, collectionAddress: content.collectionAddress, - NFTAddress: nftRes.address, + collectionAdminPublicKey: metadata.updateAuthority, + collectionFee: metadata.sellerFeeBasisPoints, + tokenId: 1, }); + + elizaLogger.log("NFT Address:", nftRes); + + if (nftRes) { + callback({ + text: `Congratulations to you! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ \nCollection Address: ${content.collectionAddress}\n NFT Address: ${nftRes.address}\n NFT Link: ${nftRes.link}`, //caption.description, + attachments: [], + }); + await sleep(15000); + await verifyNFT({ + runtime, + collectionAddress: content.collectionAddress, + NFTAddress: nftRes.address, + }); + } else { + callback({ + text: `Mint NFT Error in ${content.collectionAddress}.`, + content: { error: "Mint NFT Error." }, + }); + return false; + } } else { callback({ - text: `Mint NFT Error in ${content.collectionAddress}.`, - content: { error: "Mint NFT Error." }, + text: "Unable to process mint request. Invalid collection address.", + content: { error: "Invalid collection address." }, }); return false; } - } else { - callback({ - text: "Unable to process mint request. Invalid collection address.", - content: { error: "Invalid collection address." }, + } else if (chains.indexOf(content.chainName)) { + const privateKey = runtime.getSetting( + "WALLET_PRIVATE_KEY" + ) as `0x${string}`; + if (!privateKey) return null; + const rpcUrl = + viemChains[content.chainName].rpcUrls.default.http[0]; + const chain = viemChains[content.chainName]; // ๆ›ฟๆขไธบ็›ฎๆ ‡้“พ + const provider = http(rpcUrl); + const account = privateKeyToAccount(privateKey); + const walletClient = createWalletClient({ + account, + chain: chain, + transport: provider, }); - return false; + + const publicClient = createPublicClient({ + chain: chain, + transport: provider, + }); + await mintNFT({ + walletClient, + publicClient, + contractAddress: content.collectionAddress, + abi: [ + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + recipient: account.address, + }) + if (callback) { + callback({ + text: `Congratulations to you! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ \nCollection Address: ${content.collectionAddress}\n `, //caption.description, + attachments: [], + }); + } } return []; } catch (e: any) { @@ -168,13 +239,13 @@ const mintNFTAction: Action = { { user: "{{user1}}", content: { - text: "mint nft for collection: D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", + text: "mint nft for collection: D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS on Solana", }, }, { user: "{{agentName}}", content: { - text: "I've minted a new NFT in your specified collection.", + text: "I've minted a new NFT in your specified collection on Solana.", action: "MINT_NFT", }, }, @@ -183,13 +254,13 @@ const mintNFTAction: Action = { { user: "{{user1}}", content: { - text: "Could you create an NFT in collection D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS?", + text: "Could you create an NFT in collection D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS on Solana?", }, }, { user: "{{agentName}}", content: { - text: "Successfully minted your NFT in the specified collection.", + text: "Successfully minted your NFT in the specified collection on Solana.", action: "MINT_NFT", }, }, @@ -198,13 +269,13 @@ const mintNFTAction: Action = { { user: "{{user1}}", content: { - text: "Please mint a new token in D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS collection", + text: "Please mint a new token in D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS collection on Solana", }, }, { user: "{{agentName}}", content: { - text: "Your NFT has been minted in the collection successfully.", + text: "Your NFT has been minted in the collection successfully on Solana.", action: "MINT_NFT", }, }, @@ -213,13 +284,13 @@ const mintNFTAction: Action = { { user: "{{user1}}", content: { - text: "Generate NFT for D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", + text: "Generate NFT for D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS on Solana", }, }, { user: "{{agentName}}", content: { - text: "I've generated and minted your NFT in the collection.", + text: "I've generated and minted your NFT in the collection on Solana.", action: "MINT_NFT", }, }, @@ -228,13 +299,13 @@ const mintNFTAction: Action = { { user: "{{user1}}", content: { - text: "I want to mint an NFT in collection D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", + text: "I want to mint an NFT in collection D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS on Solana", }, }, { user: "{{agentName}}", content: { - text: "Your NFT has been successfully minted in the collection.", + text: "Your NFT has been successfully minted in the collection on Solana.", action: "MINT_NFT", }, }, @@ -243,13 +314,13 @@ const mintNFTAction: Action = { { user: "{{user1}}", content: { - text: "Create a new NFT token in D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS collection", + text: "Create a new NFT token in D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS collection on Solana", }, }, { user: "{{agentName}}", content: { - text: "The NFT has been created in your specified collection.", + text: "The NFT has been created in your specified collection on Solana.", action: "MINT_NFT", }, }, @@ -258,13 +329,13 @@ const mintNFTAction: Action = { { user: "{{user1}}", content: { - text: "Issue an NFT for collection D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", + text: "Issue an NFT for collection D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS on Solana", }, }, { user: "{{agentName}}", content: { - text: "I've issued your NFT in the requested collection.", + text: "I've issued your NFT in the requested collection on Solana.", action: "MINT_NFT", }, }, @@ -273,13 +344,13 @@ const mintNFTAction: Action = { { user: "{{user1}}", content: { - text: "Make a new NFT in D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", + text: "Make a new NFT in D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS on Solana", }, }, { user: "{{agentName}}", content: { - text: "Your new NFT has been minted in the collection.", + text: "Your new NFT has been minted in the collection on Solana.", action: "MINT_NFT", }, }, @@ -288,13 +359,13 @@ const mintNFTAction: Action = { { user: "{{user1}}", content: { - text: "Can you mint an NFT for D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS collection?", + text: "Can you mint an NFT for D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS collection on Solana?", }, }, { user: "{{agentName}}", content: { - text: "I've completed minting your NFT in the collection.", + text: "I've completed minting your NFT in the collection on Solana.", action: "MINT_NFT", }, }, @@ -303,13 +374,13 @@ const mintNFTAction: Action = { { user: "{{user1}}", content: { - text: "Add a new NFT to collection D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", + text: "Add a new NFT to collection D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS on Solana", }, }, { user: "{{agentName}}", content: { - text: "A new NFT has been added to your collection.", + text: "A new NFT has been added to your collection on Solana.", action: "MINT_NFT", }, }, diff --git a/packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts b/packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts index 901915331e..daaf29f5e3 100644 --- a/packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts +++ b/packages/plugin-nft-generation/src/actions/nftCollectionGeneration.ts @@ -24,7 +24,6 @@ import { generateERC721ContractCode, } from "../utils/deployEVMContract.ts"; import { verifyEVMContract } from "../utils/verifyEVMContract.ts"; -// import { verifyEVMContract } from "../utils/verifyEVMContract.ts"; const _SupportedChainList = Object.keys(viemChains) as Array< keyof typeof viemChains @@ -101,7 +100,6 @@ const nftCollectionGeneration: Action = { chainName: (typeof supportedChains)[number]; }; - console.log(111111, content); if (content?.chainName === "solana") { const collectionInfo = await createCollectionMetadata({ runtime, @@ -179,11 +177,13 @@ const nftCollectionGeneration: Action = { `Deployed contract address: ${contractAddress}` ); const constructorArgs = encodeConstructorArguments(abi, params); + const blockExplorers = chain.blockExplorers?.default await verifyEVMContract({ contractAddress: contractAddress, sourceCode, metadata, constructorArgs, + apiEndpoint: (blockExplorers as typeof blockExplorers & { apiUrl?: string })?.apiUrl || `${chain.blockExplorers.default.url}/api`, }); if (callback) { callback({ diff --git a/packages/plugin-nft-generation/src/templates.ts b/packages/plugin-nft-generation/src/templates.ts index bcf3e75d3f..e3833cede6 100644 --- a/packages/plugin-nft-generation/src/templates.ts +++ b/packages/plugin-nft-generation/src/templates.ts @@ -6,7 +6,7 @@ export const createCollectionTemplate = `Given the recent messages and wallet in {{walletInfo}} Extract the following information about the requested transfer: -- Chain to execute on: Must be one of ["ethereum", "base", ...] (like in viem/chains) +- chainName to execute on: Must be one of ["ethereum", "base", ...] (like in viem/chains) Respond with a JSON markdown block containing only the extracted values. All fields are required: @@ -24,11 +24,11 @@ export const collectionImageTemplate = ` Generate a logo with the text "{{collectionName}}", using orange as the main color, with a sci-fi and mysterious background theme `; export const mintNFTTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. - -Example response: +Respond with a JSON markdown block containing only the extracted values. All fields are required: \`\`\`json { "collectionAddress": "D8j4ubQ3MKwmAqiJw83qT7KQNKjhsuoC7zJJdJa5BkvS", + "chainName": SUPPORTED_CHAINS, } \`\`\` @@ -37,6 +37,5 @@ Example response: Given the recent messages, extract the following information about the requested mint nft: - collection contract address -Respond with a JSON markdown block containing only the extracted values. Note: Ensure to use the userโ€™s latest instruction to extract data; if it is not within the defined options, use null.`; diff --git a/packages/plugin-nft-generation/src/types.ts b/packages/plugin-nft-generation/src/types.ts index ec93468808..99b74b1b0f 100644 --- a/packages/plugin-nft-generation/src/types.ts +++ b/packages/plugin-nft-generation/src/types.ts @@ -3,16 +3,18 @@ import { Content } from "@elizaos/core"; import * as viemChains from "viem/chains"; const _SupportedChainList = Object.keys(viemChains); +const supportedChainTuple = [..._SupportedChainList, 'solana'] as unknown as [string, ...string[]]; export interface MintNFTContent extends Content { collectionAddress: string; + chainName: string; } export const MintNFTSchema = z.object({ collectionAddress: z.string(), + chainName: z.enum([...supportedChainTuple]).nullable(), }); -const supportedChainTuple = [..._SupportedChainList, 'solana'] as unknown as [string, ...string[]]; export const CreateCollectionSchema = z.object({ chainName: z.enum([...supportedChainTuple]).nullable(), }); diff --git a/packages/plugin-nft-generation/src/utils/deployEVMContract.ts b/packages/plugin-nft-generation/src/utils/deployEVMContract.ts index 1e859ad754..044b800fd6 100644 --- a/packages/plugin-nft-generation/src/utils/deployEVMContract.ts +++ b/packages/plugin-nft-generation/src/utils/deployEVMContract.ts @@ -1,15 +1,6 @@ -import { - createPublicClient, - createWalletClient, - encodeAbiParameters, - http, -} from "viem"; +import { encodeAbiParameters } from "viem"; import { fileURLToPath } from "url"; - -import { alienxHalTestnet } from "viem/chains"; -import { privateKeyToAccount } from "viem/accounts"; import { compileWithImports } from "./generateERC721ContractCode.ts"; -import { verifyEVMContract } from "./verifyEVMContract.ts"; import path from "path"; import fs from "fs"; @@ -41,10 +32,9 @@ export async function deployContract({ console.log("Deploying contract..."); const txHash = await walletClient.deployContract({ - abi: abi as any, - bytecode: bytecode as any, - args: args as any, - chain: undefined, + abi, + bytecode, + args, }); console.log(`Deployment transaction hash: ${txHash}`); @@ -56,7 +46,7 @@ export async function deployContract({ } // ่ฐƒ็”จ mint ๆ–นๆณ• -async function mintNFT({ +export async function mintNFT({ walletClient, publicClient, contractAddress, @@ -71,12 +61,10 @@ async function mintNFT({ }) { console.log("Minting NFT..."); const txHash = await walletClient.writeContract({ - address: contractAddress as `0x${string}`, - abi: abi as any, + address: contractAddress, + abi: abi, functionName: "mint", - args: [recipient] as any, - chain: undefined, - account: undefined, + args: [recipient], }); console.log(`Mint transaction hash: ${txHash}`); @@ -93,4 +81,3 @@ export function encodeConstructorArguments(abi, args) { return argsData.slice(2); } - diff --git a/packages/plugin-nft-generation/src/utils/verifyEVMContract.ts b/packages/plugin-nft-generation/src/utils/verifyEVMContract.ts index 1ae3b2b2ae..91d264af5d 100644 --- a/packages/plugin-nft-generation/src/utils/verifyEVMContract.ts +++ b/packages/plugin-nft-generation/src/utils/verifyEVMContract.ts @@ -13,7 +13,6 @@ function getSources(metadata, sourceCode) { const keys = Object.keys(metadata.sources); for (let i = 0; i < keys.length; i++) { const key = keys[i]; - console.log(key, fileName); if (key !== fileName) { obj[key] = { content: loadOpenZeppelinFile(key), @@ -28,8 +27,8 @@ export async function verifyEVMContract({ sourceCode, metadata, constructorArgs = "", + apiEndpoint }) { - const apiEndpoint = "https://hal-explorer.alienxchain.io/api"; const verificationData = { module: "contract", action: "verifysourcecode", @@ -66,7 +65,6 @@ export async function verifyEVMContract({ guid: guid, }, }); - console.log(111, statusResponse.data); return statusResponse.data; }; From 77ea4c32e5e99f60b9b224d2ae71b40353df15e3 Mon Sep 17 00:00:00 2001 From: Joey Date: Mon, 6 Jan 2025 18:34:28 +0100 Subject: [PATCH 14/99] upd: initialized empty remix app --- client/.eslintrc.cjs | 84 + client/.gitignore | 25 +- client/README.md | 40 + client/app/entry.client.tsx | 18 + client/app/entry.server.tsx | 140 + client/app/root.tsx | 45 + client/app/routes/_index.tsx | 138 + client/app/tailwind.css | 12 + client/components.json | 21 - client/eslint.config.js | 28 - client/index.html | 13 - client/package.json | 88 +- client/postcss.config.js | 8 +- client/public/favicon.ico | Bin 0 -> 16958 bytes client/public/logo-dark.png | Bin 0 -> 80332 bytes client/public/logo-light.png | Bin 0 -> 5906 bytes client/public/vite.svg | 1 - client/src/Agent.tsx | 10 - client/src/Agents.tsx | 35 - client/src/App.css | 41 - client/src/App.tsx | 12 - client/src/Character.tsx | 7 - client/src/Chat.tsx | 162 - client/src/Layout.tsx | 12 - client/src/api/index.ts | 2 - client/src/api/mutations/index.ts | 1 - .../src/api/mutations/sendMessageMutation.ts | 60 - client/src/api/queries/index.ts | 1 - client/src/api/queries/queries.ts | 3 - client/src/api/queries/useGetAgentsQuery.ts | 23 - client/src/api/routes.ts | 4 - client/src/api/types.ts | 13 - client/src/assets/react.svg | 1 - client/src/components/app-sidebar.tsx | 60 - client/src/components/theme-toggle.tsx | 19 - client/src/components/ui/button.tsx | 57 - client/src/components/ui/card.tsx | 83 - client/src/components/ui/input.tsx | 22 - client/src/components/ui/separator.tsx | 33 - client/src/components/ui/sheet.tsx | 136 - client/src/components/ui/sidebar.tsx | 786 ----- client/src/components/ui/skeleton.tsx | 15 - client/src/components/ui/tooltip.tsx | 32 - client/src/hooks/use-mobile.tsx | 23 - client/src/hooks/use-theme.tsx | 32 - client/src/index.css | 109 - client/src/lib/utils.ts | 6 - client/src/main.tsx | 27 - client/src/router.tsx | 32 - client/src/vite-env.d.ts | 1 - client/tailwind.config.js | 57 - client/tailwind.config.ts | 22 + client/tsconfig.app.json | 28 - client/tsconfig.json | 45 +- client/tsconfig.node.json | 22 - client/vite.config.ts | 53 +- pnpm-lock.yaml | 3101 ++++++++++++----- 57 files changed, 2789 insertions(+), 3060 deletions(-) create mode 100644 client/.eslintrc.cjs create mode 100644 client/README.md create mode 100644 client/app/entry.client.tsx create mode 100644 client/app/entry.server.tsx create mode 100644 client/app/root.tsx create mode 100644 client/app/routes/_index.tsx create mode 100644 client/app/tailwind.css delete mode 100644 client/components.json delete mode 100644 client/eslint.config.js delete mode 100644 client/index.html create mode 100644 client/public/favicon.ico create mode 100644 client/public/logo-dark.png create mode 100644 client/public/logo-light.png delete mode 100644 client/public/vite.svg delete mode 100644 client/src/Agent.tsx delete mode 100644 client/src/Agents.tsx delete mode 100644 client/src/App.css delete mode 100644 client/src/App.tsx delete mode 100644 client/src/Character.tsx delete mode 100644 client/src/Chat.tsx delete mode 100644 client/src/Layout.tsx delete mode 100644 client/src/api/index.ts delete mode 100644 client/src/api/mutations/index.ts delete mode 100644 client/src/api/mutations/sendMessageMutation.ts delete mode 100644 client/src/api/queries/index.ts delete mode 100644 client/src/api/queries/queries.ts delete mode 100644 client/src/api/queries/useGetAgentsQuery.ts delete mode 100644 client/src/api/routes.ts delete mode 100644 client/src/api/types.ts delete mode 100644 client/src/assets/react.svg delete mode 100644 client/src/components/app-sidebar.tsx delete mode 100644 client/src/components/theme-toggle.tsx delete mode 100644 client/src/components/ui/button.tsx delete mode 100644 client/src/components/ui/card.tsx delete mode 100644 client/src/components/ui/input.tsx delete mode 100644 client/src/components/ui/separator.tsx delete mode 100644 client/src/components/ui/sheet.tsx delete mode 100644 client/src/components/ui/sidebar.tsx delete mode 100644 client/src/components/ui/skeleton.tsx delete mode 100644 client/src/components/ui/tooltip.tsx delete mode 100644 client/src/hooks/use-mobile.tsx delete mode 100644 client/src/hooks/use-theme.tsx delete mode 100644 client/src/index.css delete mode 100644 client/src/lib/utils.ts delete mode 100644 client/src/main.tsx delete mode 100644 client/src/router.tsx delete mode 100644 client/src/vite-env.d.ts delete mode 100644 client/tailwind.config.js create mode 100644 client/tailwind.config.ts delete mode 100644 client/tsconfig.app.json delete mode 100644 client/tsconfig.node.json diff --git a/client/.eslintrc.cjs b/client/.eslintrc.cjs new file mode 100644 index 0000000000..4f6f59eee1 --- /dev/null +++ b/client/.eslintrc.cjs @@ -0,0 +1,84 @@ +/** + * This is intended to be a basic starting point for linting in your app. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, + }, + ignorePatterns: ["!**/.server", "!**/.client"], + + // Base config + extends: ["eslint:recommended"], + + overrides: [ + // React + { + files: ["**/*.{js,jsx,ts,tsx}"], + plugins: ["react", "jsx-a11y"], + extends: [ + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended", + ], + settings: { + react: { + version: "detect", + }, + formComponents: ["Form"], + linkComponents: [ + { name: "Link", linkAttribute: "to" }, + { name: "NavLink", linkAttribute: "to" }, + ], + "import/resolver": { + typescript: {}, + }, + }, + }, + + // Typescript + { + files: ["**/*.{ts,tsx}"], + plugins: ["@typescript-eslint", "import"], + parser: "@typescript-eslint/parser", + settings: { + "import/internal-regex": "^~/", + "import/resolver": { + node: { + extensions: [".ts", ".tsx"], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + ], + }, + + // Node + { + files: [".eslintrc.cjs"], + env: { + node: true, + }, + }, + ], +}; diff --git a/client/.gitignore b/client/.gitignore index a547bf36d8..80ec311f4f 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -1,24 +1,5 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - node_modules -dist -dist-ssr -*.local -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? +/.cache +/build +.env diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000000..6c4d2168fa --- /dev/null +++ b/client/README.md @@ -0,0 +1,40 @@ +# Welcome to Remix! + +- ๐Ÿ“– [Remix docs](https://remix.run/docs) + +## Development + +Run the dev server: + +```shellscript +npm run dev +``` + +## Deployment + +First, build your app for production: + +```sh +npm run build +``` + +Then run the app in production mode: + +```sh +npm start +``` + +Now you'll need to pick a host to deploy it to. + +### DIY + +If you're familiar with deploying Node applications, the built-in Remix app server is production-ready. + +Make sure to deploy the output of `npm run build` + +- `build/server` +- `build/client` + +## Styling + +This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever css framework you prefer. See the [Vite docs on css](https://vitejs.dev/guide/features.html#css) for more information. diff --git a/client/app/entry.client.tsx b/client/app/entry.client.tsx new file mode 100644 index 0000000000..94d5dc0de0 --- /dev/null +++ b/client/app/entry.client.tsx @@ -0,0 +1,18 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` โœจ + * For more information, see https://remix.run/file-conventions/entry.client + */ + +import { RemixBrowser } from "@remix-run/react"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +startTransition(() => { + hydrateRoot( + document, + + + + ); +}); diff --git a/client/app/entry.server.tsx b/client/app/entry.server.tsx new file mode 100644 index 0000000000..45db3229c6 --- /dev/null +++ b/client/app/entry.server.tsx @@ -0,0 +1,140 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` โœจ + * For more information, see https://remix.run/file-conventions/entry.server + */ + +import { PassThrough } from "node:stream"; + +import type { AppLoadContext, EntryContext } from "@remix-run/node"; +import { createReadableStreamFromReadable } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import { isbot } from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + // This is ignored so we can keep it in the template for visibility. Feel + // free to delete this parameter in your app if you're not using it! + // eslint-disable-next-line @typescript-eslint/no-unused-vars + loadContext: AppLoadContext +) { + return isbot(request.headers.get("user-agent") || "") + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/client/app/root.tsx b/client/app/root.tsx new file mode 100644 index 0000000000..61c8b983d2 --- /dev/null +++ b/client/app/root.tsx @@ -0,0 +1,45 @@ +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; +import type { LinksFunction } from "@remix-run/node"; + +import "./tailwind.css"; + +export const links: LinksFunction = () => [ + { rel: "preconnect", href: "https://fonts.googleapis.com" }, + { + rel: "preconnect", + href: "https://fonts.gstatic.com", + crossOrigin: "anonymous", + }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", + }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} diff --git a/client/app/routes/_index.tsx b/client/app/routes/_index.tsx new file mode 100644 index 0000000000..13a5c00468 --- /dev/null +++ b/client/app/routes/_index.tsx @@ -0,0 +1,138 @@ +import type { MetaFunction } from "@remix-run/node"; + +export const meta: MetaFunction = () => { + return [ + { title: "New Remix App" }, + { name: "description", content: "Welcome to Remix!" }, + ]; +}; + +export default function Index() { + return ( +
+
+
+

+ Welcome to Remix +

+
+ Remix + Remix +
+
+ +
+
+ ); +} + +const resources = [ + { + href: "https://remix.run/start/quickstart", + text: "Quick Start (5 min)", + icon: ( + + + + ), + }, + { + href: "https://remix.run/start/tutorial", + text: "Tutorial (30 min)", + icon: ( + + + + ), + }, + { + href: "https://remix.run/docs", + text: "Remix Docs", + icon: ( + + + + ), + }, + { + href: "https://rmx.as/discord", + text: "Join Discord", + icon: ( + + + + ), + }, +]; diff --git a/client/app/tailwind.css b/client/app/tailwind.css new file mode 100644 index 0000000000..303fe158fc --- /dev/null +++ b/client/app/tailwind.css @@ -0,0 +1,12 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body { + @apply bg-white dark:bg-gray-950; + + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} diff --git a/client/components.json b/client/components.json deleted file mode 100644 index 9efb29d672..0000000000 --- a/client/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.js", - "css": "src/index.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} diff --git a/client/eslint.config.js b/client/eslint.config.js deleted file mode 100644 index 9d1c0c63b1..0000000000 --- a/client/eslint.config.js +++ /dev/null @@ -1,28 +0,0 @@ -import js from "@eslint/js"; -import globals from "globals"; -import reactHooks from "eslint-plugin-react-hooks"; -import reactRefresh from "eslint-plugin-react-refresh"; -import tseslint from "typescript-eslint"; - -export default tseslint.config( - { ignores: ["dist"] }, - { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ["**/*.{ts,tsx}"], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - plugins: { - "react-hooks": reactHooks, - "react-refresh": reactRefresh, - }, - rules: { - ...reactHooks.configs.recommended.rules, - "react-refresh/only-export-components": [ - "warn", - { allowConstantExport: true }, - ], - }, - } -); diff --git a/client/index.html b/client/index.html deleted file mode 100644 index 342f887293..0000000000 --- a/client/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Eliza - - -
- - - diff --git a/client/package.json b/client/package.json index ba963ec4e4..ec44645443 100644 --- a/client/package.json +++ b/client/package.json @@ -1,47 +1,43 @@ { - "name": "eliza-client", - "private": true, - "version": "0.1.7", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "check-types": "tsc --noEmit", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "@elizaos/core": "workspace:*", - "@radix-ui/react-dialog": "1.1.2", - "@radix-ui/react-separator": "1.1.0", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-tooltip": "1.1.4", - "@tanstack/react-query": "5.61.0", - "class-variance-authority": "0.7.1", - "clsx": "2.1.1", - "lucide-react": "0.460.0", - "react": "18.3.1", - "react-dom": "18.3.1", - "react-router-dom": "6.22.1", - "tailwind-merge": "2.5.5", - "tailwindcss-animate": "1.0.7", - "vite-plugin-top-level-await": "1.4.4", - "vite-plugin-wasm": "3.3.0" - }, - "devDependencies": { - "@eslint/js": "9.16.0", - "@types/node": "22.8.4", - "@types/react": "18.3.12", - "@types/react-dom": "18.3.1", - "@vitejs/plugin-react": "4.3.3", - "autoprefixer": "10.4.20", - "eslint-plugin-react-hooks": "5.0.0", - "eslint-plugin-react-refresh": "0.4.14", - "globals": "15.11.0", - "postcss": "8.4.49", - "tailwindcss": "3.4.15", - "typescript": "5.6.3", - "typescript-eslint": "8.11.0", - "vite": "link:@tanstack/router-plugin/vite" - } -} + "name": "client", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "build": "remix vite:build", + "dev": "remix vite:dev", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "remix-serve ./build/server/index.js", + "typecheck": "tsc" + }, + "dependencies": { + "@remix-run/node": "^2.15.2", + "@remix-run/react": "^2.15.2", + "@remix-run/serve": "^2.15.2", + "isbot": "^4.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@remix-run/dev": "^2.15.2", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "autoprefixer": "^10.4.19", + "eslint": "^8.38.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.4", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" + }, + "engines": { + "node": ">=20.0.0" + } +} \ No newline at end of file diff --git a/client/postcss.config.js b/client/postcss.config.js index 49c0612d5c..2aa7205d4b 100644 --- a/client/postcss.config.js +++ b/client/postcss.config.js @@ -1,6 +1,6 @@ export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, }; diff --git a/client/public/favicon.ico b/client/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8830cf6821b354114848e6354889b8ecf6d2bc61 GIT binary patch literal 16958 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 EKbET!0{{R3 literal 0 HcmV?d00001 diff --git a/client/public/logo-dark.png b/client/public/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..b24c7aee3a86dad7eb334ba6fba94e5b18b84215 GIT binary patch literal 80332 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 BAPKNXC&B9%2=3YBbo!Y+L4D-GS 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�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 F{{et{IHv#r literal 0 HcmV?d00001 diff --git a/client/public/vite.svg b/client/public/vite.svg deleted file mode 100644 index e7b8dfb1b2..0000000000 --- a/client/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/Agent.tsx b/client/src/Agent.tsx deleted file mode 100644 index f3094f14eb..0000000000 --- a/client/src/Agent.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export default function Agent() { - return ( -
    -

    - Select an option from the sidebar to configure, view, or chat - with your ELIZA agent -

    -
    - ); -} diff --git a/client/src/Agents.tsx b/client/src/Agents.tsx deleted file mode 100644 index 55dc8e1ee1..0000000000 --- a/client/src/Agents.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { useNavigate } from "react-router-dom"; -import { useGetAgentsQuery } from "@/api"; -import "./App.css"; - -function Agents() { - const navigate = useNavigate(); - const { data: agents, isLoading } = useGetAgentsQuery(); - - return ( -
    -

    Select your agent:

    - - {isLoading ? ( -
    Loading agents...
    - ) : ( -
    - {agents?.map((agent) => ( - - ))} -
    - )} -
    - ); -} - -export default Agents; diff --git a/client/src/App.css b/client/src/App.css deleted file mode 100644 index d6055f0d02..0000000000 --- a/client/src/App.css +++ /dev/null @@ -1,41 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/client/src/App.tsx b/client/src/App.tsx deleted file mode 100644 index c5b0826f12..0000000000 --- a/client/src/App.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import "./App.css"; -import Agents from "./Agents"; - -function App() { - return ( -
    - -
    - ); -} - -export default App; diff --git a/client/src/Character.tsx b/client/src/Character.tsx deleted file mode 100644 index bdb53882ad..0000000000 --- a/client/src/Character.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function Character() { - return ( -
    -

    WIP

    -
    - ); -} diff --git a/client/src/Chat.tsx b/client/src/Chat.tsx deleted file mode 100644 index 6158d639db..0000000000 --- a/client/src/Chat.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import type { TextResponse } from "@/api"; -import { useSendMessageMutation } from "@/api"; -import { ImageIcon } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import { useParams } from "react-router-dom"; -import "./App.css"; - -export default function Chat() { - const { agentId } = useParams(); - const [input, setInput] = useState(""); - const [messages, setMessages] = useState([]); - const [selectedFile, setSelectedFile] = useState(null); - const fileInputRef = useRef(null); - const messagesEndRef = useRef(null); - const { mutate: sendMessage, isPending } = useSendMessageMutation({ - setMessages, - setSelectedFile, - }); - - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }; - - useEffect(() => { - scrollToBottom(); - }, [messages]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if ((!input.trim() && !selectedFile) || !agentId) return; - - // Add user message immediately to state - const userMessage: TextResponse = { - text: input, - user: "user", - attachments: selectedFile - ? [ - { - url: URL.createObjectURL(selectedFile), - contentType: selectedFile.type, - title: selectedFile.name, - }, - ] - : undefined, - }; - setMessages((prev) => [...prev, userMessage]); - - sendMessage({ text: input, agentId, selectedFile }); - setInput(""); - }; - - const handleFileSelect = () => { - fileInputRef.current?.click(); - }; - - const handleFileChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file && file.type.startsWith("image/")) { - setSelectedFile(file); - } - }; - - return ( -
    -
    -
    - {messages.length > 0 ? ( - messages.map((message, index) => ( -
    -
    -                                    {message.text}
    -                                    {message.attachments?.map(
    -                                        (attachment, i) =>
    -                                            attachment.contentType.startsWith(
    -                                                "image/"
    -                                            ) && (
    -                                                {
    -                                            )
    -                                    )}
    -                                
    -
    - )) - ) : ( -
    - No messages yet. Start a conversation! -
    - )} -
    -
    -
    - -
    -
    -
    - - setInput(e.target.value)} - placeholder="Type a message..." - className="flex-1" - disabled={isPending} - /> - - -
    - {selectedFile && ( -
    - Selected file: {selectedFile.name} -
    - )} -
    -
    -
    - ); -} diff --git a/client/src/Layout.tsx b/client/src/Layout.tsx deleted file mode 100644 index 70c79f7403..0000000000 --- a/client/src/Layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { SidebarProvider } from "@/components/ui/sidebar"; -import { AppSidebar } from "@/components/app-sidebar"; -import { Outlet } from "react-router-dom"; - -export default function Layout() { - return ( - - - - - ); -} diff --git a/client/src/api/index.ts b/client/src/api/index.ts deleted file mode 100644 index 0c2adeab02..0000000000 --- a/client/src/api/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./mutations"; -export * from "./queries"; diff --git a/client/src/api/mutations/index.ts b/client/src/api/mutations/index.ts deleted file mode 100644 index ca9f0653dc..0000000000 --- a/client/src/api/mutations/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./sendMessageMutation"; diff --git a/client/src/api/mutations/sendMessageMutation.ts b/client/src/api/mutations/sendMessageMutation.ts deleted file mode 100644 index 500e19d2e1..0000000000 --- a/client/src/api/mutations/sendMessageMutation.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { CustomMutationResult } from "../types"; - -import { useMutation } from "@tanstack/react-query"; -import { ROUTES } from "../routes"; -import { SetStateAction } from "react"; - -export type TextResponse = { - text: string; - user: string; - attachments?: { url: string; contentType: string; title: string }[]; -}; - -type SendMessageMutationProps = { - text: string; - agentId: string; - selectedFile: File | null; -}; - -type Props = Required<{ - setMessages: (value: SetStateAction) => void; - setSelectedFile: (value: SetStateAction) => void; -}>; - -export const useSendMessageMutation = ({ - setMessages, - setSelectedFile, -}: Props): CustomMutationResult => { - const mutation = useMutation({ - mutationFn: async ({ - text, - agentId, - selectedFile, - }: SendMessageMutationProps) => { - const formData = new FormData(); - formData.append("text", text); - formData.append("userId", "user"); - formData.append("roomId", `default-room-${agentId}`); - - if (selectedFile) { - formData.append("file", selectedFile); - } - - const res = await fetch(ROUTES.sendMessage(agentId), { - method: "POST", - body: formData, - }); - - return res.json() as Promise; - }, - onSuccess: (data) => { - setMessages((prev) => [...prev, ...data]); - setSelectedFile(null); - }, - onError: (error) => { - console.error("[useSendMessageMutation]:", error); - }, - }); - - return mutation; -}; diff --git a/client/src/api/queries/index.ts b/client/src/api/queries/index.ts deleted file mode 100644 index 1b1c08c1e9..0000000000 --- a/client/src/api/queries/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./useGetAgentsQuery"; diff --git a/client/src/api/queries/queries.ts b/client/src/api/queries/queries.ts deleted file mode 100644 index 40253fe29d..0000000000 --- a/client/src/api/queries/queries.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum Queries { - AGENTS = "agents", -} diff --git a/client/src/api/queries/useGetAgentsQuery.ts b/client/src/api/queries/useGetAgentsQuery.ts deleted file mode 100644 index 88f91ff7e7..0000000000 --- a/client/src/api/queries/useGetAgentsQuery.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import type { CustomQueryResult } from "../types"; -import { Queries } from "./queries"; -import { ROUTES } from "../routes"; - -export type Agent = { - id: string; - name: string; -}; - -export const useGetAgentsQuery = (): CustomQueryResult => { - return useQuery({ - queryKey: [Queries.AGENTS], - queryFn: async () => { - const res = await fetch(ROUTES.getAgents()); - const data = await res.json(); - return data.agents as Agent[]; - }, - retry: (failureCount) => failureCount < 3, - staleTime: 5 * 60 * 1000, // 5 minutes - refetchOnWindowFocus: false, - }); -}; diff --git a/client/src/api/routes.ts b/client/src/api/routes.ts deleted file mode 100644 index 1005a61a72..0000000000 --- a/client/src/api/routes.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const ROUTES = { - sendMessage: (agentId: string): string => `/api/${agentId}/message`, - getAgents: (): string => `/api/agents`, -}; diff --git a/client/src/api/types.ts b/client/src/api/types.ts deleted file mode 100644 index 286daf64b5..0000000000 --- a/client/src/api/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { UseMutationResult, UseQueryResult } from "@tanstack/react-query"; - -export type CustomMutationResult = UseMutationResult< - TData, - Error, - TArgs, - unknown ->; - -export type CustomQueryResult = Omit< - UseQueryResult, - "data" | "refetch" | "promise" -> & { data: TData; refetch: () => void; promise: unknown }; diff --git a/client/src/assets/react.svg b/client/src/assets/react.svg deleted file mode 100644 index 6c87de9bb3..0000000000 --- a/client/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/components/app-sidebar.tsx b/client/src/components/app-sidebar.tsx deleted file mode 100644 index 9fc8918427..0000000000 --- a/client/src/components/app-sidebar.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Calendar, Inbox } from "lucide-react"; -import { useParams } from "react-router-dom"; -import { ThemeToggle } from "@/components/theme-toggle"; - -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/components/ui/sidebar"; - -// Menu items. -const items = [ - { - title: "Chat", - url: "chat", - icon: Inbox, - }, - { - title: "Character Overview", - url: "character", - icon: Calendar, - }, -]; - -export function AppSidebar() { - const { agentId } = useParams(); - - return ( - - - - Application - - - {items.map((item) => ( - - -
    - - {item.title} - - - - ))} - - - - - - - - - ); -} diff --git a/client/src/components/theme-toggle.tsx b/client/src/components/theme-toggle.tsx deleted file mode 100644 index 91677a4a78..0000000000 --- a/client/src/components/theme-toggle.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Moon, Sun } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { useTheme } from "@/hooks/use-theme"; - -export function ThemeToggle() { - const { theme, setTheme } = useTheme(); - - return ( - - ); -} diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx deleted file mode 100644 index 8c1b26a165..0000000000 --- a/client/src/components/ui/button.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", - outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - icon: "h-9 w-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean; -} - -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - return ( - - ); - } -); -Button.displayName = "Button"; - -export { Button, buttonVariants }; diff --git a/client/src/components/ui/card.tsx b/client/src/components/ui/card.tsx deleted file mode 100644 index 25157bf2d3..0000000000 --- a/client/src/components/ui/card.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
    -)); -Card.displayName = "Card"; - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
    -)); -CardHeader.displayName = "CardHeader"; - -const CardTitle = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
    -)); -CardTitle.displayName = "CardTitle"; - -const CardDescription = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
    -)); -CardDescription.displayName = "CardDescription"; - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
    -)); -CardContent.displayName = "CardContent"; - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
    -)); -CardFooter.displayName = "CardFooter"; - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardDescription, - CardContent, -}; diff --git a/client/src/components/ui/input.tsx b/client/src/components/ui/input.tsx deleted file mode 100644 index 9661e332a7..0000000000 --- a/client/src/components/ui/input.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -const Input = React.forwardRef>( - ({ className, type, ...props }, ref) => { - return ( - - ); - } -); -Input.displayName = "Input"; - -export { Input }; diff --git a/client/src/components/ui/separator.tsx b/client/src/components/ui/separator.tsx deleted file mode 100644 index 2af4ec891e..0000000000 --- a/client/src/components/ui/separator.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as SeparatorPrimitive from "@radix-ui/react-separator"; - -import { cn } from "@/lib/utils"; - -const Separator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->( - ( - { className, orientation = "horizontal", decorative = true, ...props }, - ref - ) => ( - - ) -); -Separator.displayName = SeparatorPrimitive.Root.displayName; - -export { Separator }; diff --git a/client/src/components/ui/sheet.tsx b/client/src/components/ui/sheet.tsx deleted file mode 100644 index e18e295c73..0000000000 --- a/client/src/components/ui/sheet.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import * as React from "react"; -import * as SheetPrimitive from "@radix-ui/react-dialog"; -import { cva, type VariantProps } from "class-variance-authority"; -import { X } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const Sheet = SheetPrimitive.Root; - -const SheetTrigger = SheetPrimitive.Trigger; - -const SheetClose = SheetPrimitive.Close; - -const SheetPortal = SheetPrimitive.Portal; - -const SheetOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; - -const sheetVariants = cva( - "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", - { - variants: { - side: { - top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", - bottom: "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", - left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", - right: "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", - }, - }, - defaultVariants: { - side: "right", - }, - } -); - -interface SheetContentProps - extends React.ComponentPropsWithoutRef, - VariantProps {} - -const SheetContent = React.forwardRef< - React.ElementRef, - SheetContentProps ->(({ side = "right", className, children, ...props }, ref) => ( - - - - - - Close - - {children} - - -)); -SheetContent.displayName = SheetPrimitive.Content.displayName; - -const SheetHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
    -); -SheetHeader.displayName = "SheetHeader"; - -const SheetFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
    -); -SheetFooter.displayName = "SheetFooter"; - -const SheetTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -SheetTitle.displayName = SheetPrimitive.Title.displayName; - -const SheetDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -SheetDescription.displayName = SheetPrimitive.Description.displayName; - -export { - Sheet, - SheetPortal, - SheetOverlay, - SheetTrigger, - SheetClose, - SheetContent, - SheetHeader, - SheetFooter, - SheetTitle, - SheetDescription, -}; diff --git a/client/src/components/ui/sidebar.tsx b/client/src/components/ui/sidebar.tsx deleted file mode 100644 index ab5862ab35..0000000000 --- a/client/src/components/ui/sidebar.tsx +++ /dev/null @@ -1,786 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { VariantProps, cva } from "class-variance-authority"; -import { PanelLeft } from "lucide-react"; - -import { useIsMobile } from "@/hooks/use-mobile"; -import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Separator } from "@/components/ui/separator"; -import { Sheet, SheetContent } from "@/components/ui/sheet"; -import { Skeleton } from "@/components/ui/skeleton"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; - -const SIDEBAR_COOKIE_NAME = "sidebar:state"; -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; -const SIDEBAR_WIDTH = "16rem"; -const SIDEBAR_WIDTH_MOBILE = "18rem"; -const SIDEBAR_WIDTH_ICON = "3rem"; -const SIDEBAR_KEYBOARD_SHORTCUT = "b"; - -type SidebarContext = { - state: "expanded" | "collapsed"; - open: boolean; - setOpen: (open: boolean) => void; - openMobile: boolean; - setOpenMobile: (open: boolean) => void; - isMobile: boolean; - toggleSidebar: () => void; -}; - -const SidebarContext = React.createContext(null); - -function useSidebar() { - const context = React.useContext(SidebarContext); - if (!context) { - throw new Error("useSidebar must be used within a SidebarProvider."); - } - - return context; -} - -const SidebarProvider = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - defaultOpen?: boolean; - open?: boolean; - onOpenChange?: (open: boolean) => void; - } ->( - ( - { - defaultOpen = true, - open: openProp, - onOpenChange: setOpenProp, - className, - style, - children, - ...props - }, - ref - ) => { - const isMobile = useIsMobile(); - const [openMobile, setOpenMobile] = React.useState(false); - - // This is the internal state of the sidebar. - // We use openProp and setOpenProp for control from outside the component. - const [_open, _setOpen] = React.useState(defaultOpen); - const open = openProp ?? _open; - const setOpen = React.useCallback( - (value: boolean | ((value: boolean) => boolean)) => { - const openState = - typeof value === "function" ? value(open) : value; - if (setOpenProp) { - setOpenProp(openState); - } else { - _setOpen(openState); - } - - // This sets the cookie to keep the sidebar state. - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; - }, - [setOpenProp, open] - ); - - // Helper to toggle the sidebar. - const toggleSidebar = React.useCallback(() => { - return isMobile - ? setOpenMobile((open) => !open) - : setOpen((open) => !open); - }, [isMobile, setOpen, setOpenMobile]); - - // Adds a keyboard shortcut to toggle the sidebar. - React.useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault(); - toggleSidebar(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [toggleSidebar]); - - // We add a state so that we can do data-state="expanded" or "collapsed". - // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? "expanded" : "collapsed"; - - const contextValue = React.useMemo( - () => ({ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - toggleSidebar, - }), - [ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - toggleSidebar, - ] - ); - - return ( - - -
    - {children} -
    -
    -
    - ); - } -); -SidebarProvider.displayName = "SidebarProvider"; - -const Sidebar = React.forwardRef< - HTMLDivElement, - React.ComponentProps<"div"> & { - side?: "left" | "right"; - variant?: "sidebar" | "floating" | "inset"; - collapsible?: "offcanvas" | "icon" | "none"; - } ->( - ( - { - side = "left", - variant = "sidebar", - collapsible = "offcanvas", - className, - children, - ...props - }, - ref - ) => { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); - - if (collapsible === "none") { - return ( -
    - {children} -
    - ); - } - - if (isMobile) { - return ( - - -
    - {children} -
    -
    -
    - ); - } - - return ( -
    - {/* This is what handles the sidebar gap on desktop */} -
    - -
    - ); - } -); -Sidebar.displayName = "Sidebar"; - -const SidebarTrigger = React.forwardRef< - React.ElementRef, - React.ComponentProps ->(({ className, onClick, ...props }, ref) => { - const { toggleSidebar } = useSidebar(); - - return ( - - ); -}); -SidebarTrigger.displayName = "SidebarTrigger"; - -const SidebarRail = React.forwardRef< - HTMLButtonElement, - React.ComponentProps<"button"> ->(({ className, ...props }, ref) => { - const { toggleSidebar } = useSidebar(); - - return ( - +
    + ); +} \ No newline at end of file diff --git a/client/app/tailwind.css b/client/app/tailwind.css index 303fe158fc..be310705e9 100644 --- a/client/app/tailwind.css +++ b/client/app/tailwind.css @@ -2,11 +2,68 @@ @tailwind components; @tailwind utilities; -html, -body { - @apply bg-white dark:bg-gray-950; +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 5.9% 10%; + --radius: 0.5rem; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } - @media (prefers-color-scheme: dark) { - color-scheme: dark; + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; } } diff --git a/client/app/types/index.ts b/client/app/types/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/client/components.json b/client/components.json new file mode 100644 index 0000000000..d4e662d533 --- /dev/null +++ b/client/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/tailwind.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "~/components", + "utils": "~/lib/utils", + "ui": "~/components/ui", + "lib": "~/lib", + "hooks": "~/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/client/package.json b/client/package.json index ec44645443..dedb4fa566 100644 --- a/client/package.json +++ b/client/package.json @@ -11,12 +11,20 @@ "typecheck": "tsc" }, "dependencies": { + "@elizaos/core": "workspace:*", + "@radix-ui/react-slot": "^1.1.1", "@remix-run/node": "^2.15.2", "@remix-run/react": "^2.15.2", "@remix-run/serve": "^2.15.2", + "@tanstack/react-query": "^5.62.15", + "class-variance-authority": "^0.7.1", + "clsx": "2.1.1", "isbot": "^4.1.0", + "lucide-react": "^0.469.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { "@remix-run/dev": "^2.15.2", diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts index 5f06ad4ba5..79c018f5eb 100644 --- a/client/tailwind.config.ts +++ b/client/tailwind.config.ts @@ -1,22 +1,70 @@ import type { Config } from "tailwindcss"; export default { - content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"], + darkMode: ["class"], + content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"], theme: { - extend: { - fontFamily: { - sans: [ - "Inter", - "ui-sans-serif", - "system-ui", - "sans-serif", - "Apple Color Emoji", - "Segoe UI Emoji", - "Segoe UI Symbol", - "Noto Color Emoji", - ], - }, - }, + extend: { + fontFamily: { + sans: [ + 'Inter', + 'ui-sans-serif', + 'system-ui', + 'sans-serif', + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji' + ] + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + } + } }, - plugins: [], + plugins: [require("tailwindcss-animate")], } satisfies Config; diff --git a/client/vite.config.ts b/client/vite.config.ts index e4e8cefc3b..e0986efb4a 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -3,22 +3,22 @@ import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; declare module "@remix-run/node" { - interface Future { - v3_singleFetch: true; - } + interface Future { + v3_singleFetch: true; + } } export default defineConfig({ - plugins: [ - remix({ - future: { - v3_fetcherPersist: true, - v3_relativeSplatPath: true, - v3_throwAbortReason: true, - v3_singleFetch: true, - v3_lazyRouteDiscovery: true, - }, - }), - tsconfigPaths(), - ], + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + v3_singleFetch: true, + v3_lazyRouteDiscovery: true, + }, + }), + tsconfigPaths(), + ], }); From c73df1171b7112b3bb8dc117d911a9fd5ec10cb9 Mon Sep 17 00:00:00 2001 From: Joey Date: Mon, 6 Jan 2025 19:44:02 +0100 Subject: [PATCH 16/99] upd: created initial sidebar for Agents --- client/app/components/app-sidebar.tsx | 49 ++ client/app/components/ui/avatar.tsx | 48 ++ client/app/components/ui/breadcrumb.tsx | 115 ++++ client/app/components/ui/collapsible.tsx | 9 + client/app/components/ui/input.tsx | 22 + client/app/components/ui/separator.tsx | 29 + client/app/components/ui/sheet.tsx | 138 ++++ client/app/components/ui/sidebar.tsx | 762 +++++++++++++++++++++++ client/app/components/ui/skeleton.tsx | 15 + client/app/components/ui/tooltip.tsx | 32 + client/app/hooks/use-mobile.tsx | 19 + client/app/lib/api.ts | 2 +- client/app/root.tsx | 16 +- client/app/routes/_index.tsx | 11 +- client/app/tailwind.css | 16 + client/package.json | 6 + client/tailwind.config.ts | 10 + 17 files changed, 1287 insertions(+), 12 deletions(-) create mode 100644 client/app/components/app-sidebar.tsx create mode 100644 client/app/components/ui/avatar.tsx create mode 100644 client/app/components/ui/breadcrumb.tsx create mode 100644 client/app/components/ui/collapsible.tsx create mode 100644 client/app/components/ui/input.tsx create mode 100644 client/app/components/ui/separator.tsx create mode 100644 client/app/components/ui/sheet.tsx create mode 100644 client/app/components/ui/sidebar.tsx create mode 100644 client/app/components/ui/skeleton.tsx create mode 100644 client/app/components/ui/tooltip.tsx create mode 100644 client/app/hooks/use-mobile.tsx diff --git a/client/app/components/app-sidebar.tsx b/client/app/components/app-sidebar.tsx new file mode 100644 index 0000000000..6bcec7a43c --- /dev/null +++ b/client/app/components/app-sidebar.tsx @@ -0,0 +1,49 @@ +import { useQuery } from "@tanstack/react-query"; +import { Calendar, Home, Inbox, Search, Settings } from "lucide-react"; + +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "~/components/ui/sidebar"; +import { apiClient } from "~/lib/api"; + +export function AppSidebar() { + const query = useQuery({ + queryKey: ["agents"], + queryFn: () => apiClient.getAgents(), + }); + + const agents = query?.data?.agents; + + return ( + + + + Agents + + + {agents?.map((agent) => ( + + +
    +
    + {agent?.name?.substring(0,2)} +
    + {agent.name} +
    +
    +
    + ))} +
    +
    +
    +
    +
    + ); +} diff --git a/client/app/components/ui/avatar.tsx b/client/app/components/ui/avatar.tsx new file mode 100644 index 0000000000..706f1778ba --- /dev/null +++ b/client/app/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "~/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/client/app/components/ui/breadcrumb.tsx b/client/app/components/ui/breadcrumb.tsx new file mode 100644 index 0000000000..2ca8b012a1 --- /dev/null +++ b/client/app/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "~/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>