Skip to content
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

Merged
merged 6 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ HEURIST_IMAGE_MODEL=
# EVM
EVM_PRIVATE_KEY=
EVM_PUBLIC_KEY=
EVM_PROVIDER_URL=

# Solana
SOLANA_PRIVATE_KEY=
Expand Down
12 changes: 12 additions & 0 deletions packages/plugin-goat/README.md
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
21 changes: 21 additions & 0 deletions packages/plugin-goat/package.json
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"
}
}
186 changes: 186 additions & 0 deletions packages/plugin-goat/src/actions.ts
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({
Copy link
Collaborator

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

Copy link
Contributor Author

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 🙏

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,
});
}
29 changes: 29 additions & 0 deletions packages/plugin-goat/src/index.ts
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
31 changes: 31 additions & 0 deletions packages/plugin-goat/src/wallet.ts
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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be hardcoded or can we configure it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be added as an .env variable but we would need a map from string to viem object which would still require you to change the code if you want to use a chain that is not contemplated in the map.

I centralized the definition inside provider.ts to make it easier to update 😄

Would that work?

transport: http(provider),
});
return viem(walletClient);
}
9 changes: 9 additions & 0 deletions packages/plugin-goat/tsconfig.json
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"]
}
21 changes: 21 additions & 0 deletions packages/plugin-goat/tsup.config.ts
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"
],
});
Loading