-
Notifications
You must be signed in to change notification settings - Fork 5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add goat plugin #736
Changes from 2 commits
e4a240c
3a8c45a
bad0099
2d7bdbf
4bed62a
9408709
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.7", | ||
"@goat-sdk/plugin-erc20": "0.1.5", | ||
"@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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
import { | ||
type WalletClient, | ||
type Plugin, | ||
getDeferredTools, | ||
parametersToJsonExample, | ||
addParametersToDescription, | ||
type ChainForWalletClient, | ||
type DeferredTool, | ||
} from "@goat-sdk/core"; | ||
import { | ||
type Action, | ||
generateText, | ||
type HandlerCallback, | ||
type IAgentRuntime, | ||
type Memory, | ||
ModelClass, | ||
type State, | ||
composeContext, | ||
generateObject, | ||
} from "@ai16z/eliza"; | ||
|
||
type GetOnChainActionsParams<TWalletClient extends WalletClient> = { | ||
chain: ChainForWalletClient<TWalletClient>; | ||
getWalletClient: (runtime: IAgentRuntime) => Promise<TWalletClient>; | ||
plugins: Plugin<TWalletClient>[]; | ||
supportsSmartWallets?: boolean; | ||
}; | ||
|
||
/** | ||
* Get all the on chain actions for the given wallet client and plugins | ||
* | ||
* @param params | ||
* @returns | ||
*/ | ||
export async function getOnChainActions<TWalletClient extends WalletClient>({ | ||
getWalletClient, | ||
plugins, | ||
chain, | ||
supportsSmartWallets, | ||
}: GetOnChainActionsParams<TWalletClient>): Promise<Action[]> { | ||
const tools = await getDeferredTools<TWalletClient>({ | ||
plugins, | ||
wordForTool: "action", | ||
chain, | ||
supportsSmartWallets, | ||
}); | ||
|
||
return tools | ||
.map((action) => ({ | ||
...action, | ||
name: action.name.toUpperCase(), | ||
})) | ||
.map((tool) => createAction(tool, getWalletClient)) | ||
} | ||
|
||
function createAction<TWalletClient extends WalletClient>( | ||
tool: DeferredTool<TWalletClient>, | ||
getWalletClient: (runtime: IAgentRuntime) => Promise<TWalletClient> | ||
): Action { | ||
return { | ||
name: tool.name, | ||
similes: [], | ||
description: tool.description, | ||
validate: async () => true, | ||
handler: async ( | ||
runtime: IAgentRuntime, | ||
message: Memory, | ||
state: State | undefined, | ||
options?: Record<string, unknown>, | ||
callback?: HandlerCallback | ||
): Promise<boolean> => { | ||
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 | ||
); | ||
|
||
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<TWalletClient extends WalletClient>( | ||
tool: DeferredTool<TWalletClient>, | ||
state: State | ||
): string { | ||
const contextTemplate = `Respond with a JSON markdown block containing only the extracted values for action "${ | ||
tool.name | ||
}". Use null for any values that cannot be determined. | ||
|
||
Example response: | ||
\`\`\`json | ||
${parametersToJsonExample(tool.parameters)} | ||
\`\`\` | ||
|
||
{{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 | ||
): Promise<unknown> { | ||
return generateObject({ | ||
runtime, | ||
context, | ||
modelClass: ModelClass.SMALL, | ||
}); | ||
} | ||
|
||
function composeResponseContext<TWalletClient extends WalletClient>( | ||
tool: DeferredTool<TWalletClient>, | ||
result: unknown, | ||
state: State | ||
): string { | ||
const responseTemplate = ` | ||
The action "${tool.name}" was executed successfully. | ||
Here is the result: | ||
${JSON.stringify(result)} | ||
|
||
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<string> { | ||
return generateText({ | ||
runtime, | ||
context, | ||
modelClass: ModelClass.SMALL, | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import type { Plugin } from '@ai16z/eliza' | ||
import { base } from 'viem/chains'; | ||
import { getOnChainActions } from './actions'; | ||
import { erc20, USDC } from '@goat-sdk/plugin-erc20'; | ||
import { getWalletClient } from './wallet'; | ||
import { sendETH } from '@goat-sdk/core'; | ||
|
||
export const goatPlugin: Plugin = { | ||
name: "[GOAT] Onchain Actions", | ||
description: "Base integration plugin", | ||
providers: [], | ||
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] })], | ||
// Add the chain you want to use | ||
chain: { | ||
type: "evm", | ||
id: base.id, | ||
}, | ||
})), | ||
], | ||
}; | ||
|
||
export default goatPlugin |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { createWalletClient, http } from "viem"; | ||
import { privateKeyToAccount } from "viem/accounts"; | ||
import { base } from "viem/chains"; | ||
|
||
import { type IAgentRuntime } from "@ai16z/eliza"; | ||
import { viem } from "@goat-sdk/wallet-viem"; | ||
|
||
/** | ||
* 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: base, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this be hardcoded or can we configure it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could be added as an I centralized the definition inside Would that work? |
||
transport: http(provider), | ||
}); | ||
return viem(walletClient); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"compilerOptions": { | ||
"outDir": "dist", | ||
"rootDir": "./src", | ||
"declaration": true | ||
}, | ||
"include": ["src"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
], | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would recommend generateObjectV2 instead as it is much more deterministic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat! 😄 Just moved to that!! Thanks for the rec 🙏