Skip to content

Commit

Permalink
Merge pull request #773 from ai16z/shaw/add-goat
Browse files Browse the repository at this point in the history
Integrate goat plugin
  • Loading branch information
lalalune authored Dec 1, 2024
2 parents f1abcbb + 9abeb1c commit 719b0a7
Show file tree
Hide file tree
Showing 12 changed files with 418 additions and 197 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ HEURIST_IMAGE_MODEL=

# EVM
EVM_PRIVATE_KEY=
EVM_PROVIDER_URL=

# Solana
SOLANA_PRIVATE_KEY=
Expand Down
1 change: 1 addition & 0 deletions agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@ai16z/plugin-node": "workspace:*",
"@ai16z/plugin-solana": "workspace:*",
"@ai16z/plugin-0g": "workspace:*",
"@ai16z/plugin-goat": "workspace:*",
"@ai16z/plugin-starknet": "workspace:*",
"@ai16z/plugin-icp": "workspace:*",
"@ai16z/plugin-tee": "workspace:*",
Expand Down
20 changes: 2 additions & 18 deletions agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
validateCharacterConfig,
} from "@ai16z/eliza";
import { zgPlugin } from "@ai16z/plugin-0g";
import { goatPlugin } from "@ai16z/plugin-goat";
import { bootstrapPlugin } from "@ai16z/plugin-bootstrap";
// import { buttplugPlugin } from "@ai16z/plugin-buttplug";
import {
Expand Down Expand Up @@ -90,24 +91,6 @@ export async function loadCharacters(
.map((filePath) => filePath.trim());
const loadedCharacters = [];

// Add logging here
elizaLogger.info("Character loading details:", {
characterPaths,
cwd: process.cwd(),
dirname: __dirname,
fullPath: path.resolve(
process.cwd(),
"characters/8bitoracle.laozi.character.json"
),
exists: fs.existsSync(
path.resolve(
process.cwd(),
"characters/8bitoracle.laozi.character.json"
)
),
dirContents: fs.readdirSync(process.cwd()),
});

if (characterPaths?.length > 0) {
for (const characterPath of characterPaths) {
let content = null;
Expand Down Expand Up @@ -393,6 +376,7 @@ export function createAgent(
? [coinbaseMassPaymentsPlugin, tradePlugin]
: []),
getSecret(character, "WALLET_SECRET_SALT") ? teePlugin : null,
getSecret(character, "ALCHEMY_API_KEY") ? goatPlugin : null,
].filter(Boolean),
providers: [],
actions: [],
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.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"
}
}
200 changes: 200 additions & 0 deletions packages/plugin-goat/src/actions.ts
Original file line number Diff line number Diff line change
@@ -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<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,
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<TWalletClient extends WalletClient>(
tool: DeferredTool<TWalletClient>,
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<TWalletClient extends WalletClient>(
runtime: IAgentRuntime,
context: string,
tool: DeferredTool<TWalletClient>
): Promise<unknown> {
const { object } = await generateObjectV2({
runtime,
context,
modelClass: ModelClass.SMALL,
schema: tool.parameters,
});

return object;
}

function composeResponseContext<TWalletClient extends WalletClient>(
tool: DeferredTool<TWalletClient>,
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<string> {
return generateText({
runtime,
context,
modelClass: ModelClass.SMALL,
});
}
27 changes: 27 additions & 0 deletions packages/plugin-goat/src/index.ts
Original file line number Diff line number Diff line change
@@ -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
54 changes: 54 additions & 0 deletions packages/plugin-goat/src/provider.ts
Original file line number Diff line number Diff line change
@@ -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<string | null> {
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;
}
},
};
11 changes: 11 additions & 0 deletions packages/plugin-goat/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../core/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "./src",
"declaration": true
},
"include": [
"src"
]
}
Loading

0 comments on commit 719b0a7

Please sign in to comment.