diff --git a/.env.example b/.env.example index 7d04667929e..4905550ad15 100644 --- a/.env.example +++ b/.env.example @@ -86,6 +86,7 @@ HEURIST_IMAGE_MODEL= # EVM EVM_PRIVATE_KEY= +EVM_PROVIDER_URL= # Solana SOLANA_PRIVATE_KEY= diff --git a/packages/plugin-goat/README.md b/packages/plugin-goat/README.md new file mode 100644 index 00000000000..fcac78c5737 --- /dev/null +++ b/packages/plugin-goat/README.md @@ -0,0 +1,12 @@ +# Goat Plugin +Example plugin setup of how you can integrate [Goat](https://ohmygoat.dev/) tools and plugins with Eliza. + +Adds onchain capabilities to your agent to send and check balances of ETH and USDC. Add all the capabilities you need by adding more plugins! + +## Setup +1. Configure your wallet (key pair, smart wallet, etc. see all available wallets at [https://ohmygoat.dev/wallets](https://ohmygoat.dev/wallets)) +2. Add the plugins you need (uniswap, zora, polymarket, etc. see all available plugins at [https://ohmygoat.dev/chains-wallets-plugins](https://ohmygoat.dev/chains-wallets-plugins)) +3. Select a chain (see all available chains at [https://ohmygoat.dev/chains](https://ohmygoat.dev/chains)) +4. Import and add the plugin to your Eliza agent +5. Build the project +6. Add the necessary environment variables to set up your wallet and plugins diff --git a/packages/plugin-goat/package.json b/packages/plugin-goat/package.json new file mode 100644 index 00000000000..88c73c51040 --- /dev/null +++ b/packages/plugin-goat/package.json @@ -0,0 +1,21 @@ +{ + "name": "@ai16z/plugin-goat", + "version": "0.0.1", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@ai16z/eliza": "workspace:*", + "@goat-sdk/core": "0.3.8", + "@goat-sdk/plugin-erc20": "0.1.6", + "@goat-sdk/wallet-viem": "0.1.3", + "tsup": "^8.3.5", + "viem": "^2.21.45" + }, + "scripts": { + "build": "tsup --format esm --dts" + }, + "peerDependencies": { + "whatwg-url": "7.1.0" + } +} diff --git a/packages/plugin-goat/src/actions.ts b/packages/plugin-goat/src/actions.ts new file mode 100644 index 00000000000..a69ab6378e6 --- /dev/null +++ b/packages/plugin-goat/src/actions.ts @@ -0,0 +1,200 @@ +import { + type WalletClient, + type Plugin, + getDeferredTools, + addParametersToDescription, + type ChainForWalletClient, + type DeferredTool, +} from "@goat-sdk/core"; +import { + type Action, + generateText, + type HandlerCallback, + type IAgentRuntime, + type Memory, + ModelClass, + type State, + composeContext, + generateObjectV2, +} from "@ai16z/eliza"; + +type GetOnChainActionsParams = { + chain: ChainForWalletClient; + getWalletClient: (runtime: IAgentRuntime) => Promise; + plugins: Plugin[]; + supportsSmartWallets?: boolean; +}; + +/** + * Get all the on chain actions for the given wallet client and plugins + * + * @param params + * @returns + */ +export async function getOnChainActions({ + getWalletClient, + plugins, + chain, + supportsSmartWallets, +}: GetOnChainActionsParams): Promise { + const tools = await getDeferredTools({ + plugins, + wordForTool: "action", + chain, + supportsSmartWallets, + }); + + return tools + .map((action) => ({ + ...action, + name: action.name.toUpperCase(), + })) + .map((tool) => createAction(tool, getWalletClient)); +} + +function createAction( + tool: DeferredTool, + getWalletClient: (runtime: IAgentRuntime) => Promise +): Action { + return { + name: tool.name, + similes: [], + description: tool.description, + validate: async () => true, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State | undefined, + options?: Record, + callback?: HandlerCallback + ): Promise => { + try { + const walletClient = await getWalletClient(runtime); + let currentState = + state ?? (await runtime.composeState(message)); + currentState = + await runtime.updateRecentMessageState(currentState); + + const parameterContext = composeParameterContext( + tool, + currentState + ); + const parameters = await generateParameters( + runtime, + parameterContext, + tool + ); + + const parsedParameters = tool.parameters.safeParse(parameters); + if (!parsedParameters.success) { + callback?.({ + text: `Invalid parameters for action ${tool.name}: ${parsedParameters.error.message}`, + content: { error: parsedParameters.error.message }, + }); + return false; + } + + const result = await tool.method( + walletClient, + parsedParameters.data + ); + const responseContext = composeResponseContext( + tool, + result, + currentState + ); + const response = await generateResponse( + runtime, + responseContext + ); + + callback?.({ text: response, content: result }); + return true; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + callback?.({ + text: `Error executing action ${tool.name}: ${errorMessage}`, + content: { error: errorMessage }, + }); + return false; + } + }, + examples: [], + }; +} + +function composeParameterContext( + tool: DeferredTool, + state: State +): string { + const contextTemplate = `{{recentMessages}} + +Given the recent messages, extract the following information for the action "${tool.name}": +${addParametersToDescription("", tool.parameters)} +`; + return composeContext({ state, template: contextTemplate }); +} + +async function generateParameters( + runtime: IAgentRuntime, + context: string, + tool: DeferredTool +): Promise { + const { object } = await generateObjectV2({ + runtime, + context, + modelClass: ModelClass.SMALL, + schema: tool.parameters, + }); + + return object; +} + +function composeResponseContext( + tool: DeferredTool, + result: unknown, + state: State +): string { + const responseTemplate = ` + # Action Examples +{{actionExamples}} +(Action examples are for reference only. Do not use the information from them in your response.) + +# Knowledge +{{knowledge}} + +# Task: Generate dialog and actions for the character {{agentName}}. +About {{agentName}}: +{{bio}} +{{lore}} + +{{providers}} + +{{attachments}} + +# Capabilities +Note that {{agentName}} is capable of reading/seeing/hearing various forms of media, including images, videos, audio, plaintext and PDFs. Recent attachments have been included above under the "Attachments" section. + +The action "${tool.name}" was executed successfully. +Here is the result: +${JSON.stringify(result)} + +{{actions}} + +Respond to the message knowing that the action was successful and these were the previous messages: +{{recentMessages}} + `; + return composeContext({ state, template: responseTemplate }); +} + +async function generateResponse( + runtime: IAgentRuntime, + context: string +): Promise { + return generateText({ + runtime, + context, + modelClass: ModelClass.SMALL, + }); +} diff --git a/packages/plugin-goat/src/index.ts b/packages/plugin-goat/src/index.ts new file mode 100644 index 00000000000..281d95a1621 --- /dev/null +++ b/packages/plugin-goat/src/index.ts @@ -0,0 +1,27 @@ +import type { Plugin } from '@ai16z/eliza' +import { getOnChainActions } from './actions'; +import { erc20, USDC } from '@goat-sdk/plugin-erc20'; +import { chain, getWalletClient, walletProvider } from './provider'; +import { sendETH } from '@goat-sdk/core'; + +export const goatPlugin: Plugin = { + name: "[GOAT] Onchain Actions", + description: "Base integration plugin", + providers: [walletProvider], + evaluators: [], + services: [], + actions: [ + ...(await getOnChainActions({ + getWalletClient, + // Add plugins here based on what actions you want to use + // See all available plugins at https://ohmygoat.dev/chains-wallets-plugins#plugins + plugins: [sendETH(), erc20({ tokens: [USDC] })], + chain: { + type: "evm", + id: chain.id, + }, + })), + ], +}; + +export default goatPlugin diff --git a/packages/plugin-goat/src/provider.ts b/packages/plugin-goat/src/provider.ts new file mode 100644 index 00000000000..20b4356b6f1 --- /dev/null +++ b/packages/plugin-goat/src/provider.ts @@ -0,0 +1,54 @@ +import { createWalletClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { base } from "viem/chains"; + +import { Memory, Provider, State, type IAgentRuntime } from "@ai16z/eliza"; +import { viem } from "@goat-sdk/wallet-viem"; + + +// Add the chain you want to use, remember to update also +// the EVM_PROVIDER_URL to the correct one for the chain +export const chain = base; + +/** + * Create a wallet client for the given runtime. + * + * You can change it to use a different wallet client such as Crossmint smart wallets or others. + * + * See all available wallet clients at https://ohmygoat.dev/wallets + * + * @param runtime + * @returns Wallet client + */ +export async function getWalletClient(runtime: IAgentRuntime) { + const privateKey = runtime.getSetting("EVM_PRIVATE_KEY"); + if (!privateKey) throw new Error("EVM_PRIVATE_KEY not configured"); + + const provider = runtime.getSetting("EVM_PROVIDER_URL"); + if (!provider) throw new Error("EVM_PROVIDER_URL not configured"); + + const walletClient = createWalletClient({ + account: privateKeyToAccount(privateKey as `0x${string}`), + chain: chain, + transport: http(provider), + }); + return viem(walletClient); +} + +export const walletProvider: Provider = { + async get( + runtime: IAgentRuntime, + message: Memory, + state?: State + ): Promise { + try { + const walletClient = await getWalletClient(runtime); + const address = walletClient.getAddress(); + const balance = await walletClient.balanceOf(address); + return `EVM Wallet Address: ${address}\nBalance: ${balance} ETH`; + } catch (error) { + console.error("Error in EVM wallet provider:", error); + return null; + } + }, +}; diff --git a/packages/plugin-goat/tsconfig.json b/packages/plugin-goat/tsconfig.json new file mode 100644 index 00000000000..a29f5acc13b --- /dev/null +++ b/packages/plugin-goat/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "./src", + "declaration": true + }, + "include": ["src"] +} diff --git a/packages/plugin-goat/tsup.config.ts b/packages/plugin-goat/tsup.config.ts new file mode 100644 index 00000000000..b0c1a8a9f46 --- /dev/null +++ b/packages/plugin-goat/tsup.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + "viem", + "@lifi/sdk" + ], +});