Skip to content

Commit

Permalink
Add AI agent setup with LangChain (#688)
Browse files Browse the repository at this point in the history
* Add AI agent setup with LangChain

* Add tutorial

* Fix annotation in .env.example

* Add screenshot

* Remove unused env variable

* Wording edits

* Implement requested changes
  • Loading branch information
louis-md authored Feb 11, 2025
1 parent 292c065 commit 64b82d8
Show file tree
Hide file tree
Showing 10 changed files with 405 additions and 1 deletion.
15 changes: 14 additions & 1 deletion .github/scripts/generateCodeExamples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,20 @@ const repos = [
'/app.json',
'/.env-sample'
]
}
},
{
organization: '5afe',
repo: 'safe-ai-agent-tutorial',
destination: './examples/ai-agent',
branch: 'main',
files: [
'/agent.ts',
'/tools/safe.ts',
'/tools/prices.ts',
'/tools/math.ts',
'/.env.example'
]
},
// {
// organization: '5afe',
// repo: 'safe-7579-tutorial',
Expand Down
3 changes: 3 additions & 0 deletions .github/styles/config/vocabularies/default/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
[Aa]urora
[Aa]rbitrum
[Aa]ccessor
[Aa]utomation
[Bb]ackend
[Bb]inance
[Bb]lockchain
Expand All @@ -34,9 +35,11 @@
[Gg]elato
[Gg]oerli
[Gg]nosis
[Hh]ackathon
[Ll]inea
[Ll]inea
[Ll]ogics
[Ll][Ll][Mm]
[Mm]ainnet
[Mm]iddleware
[Mm]onerium
Expand Down
Binary file added assets/ai-agent-setup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions examples/ai-agent/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
AGENT_PRIVATE_KEY="0x..."
AGENT_ADDRESS="0x..."

# Optional:
OPENAI_API_KEY="sk-..."
LANGCHAIN_API_KEY="lsv2_..."
LANGCHAIN_CALLBACKS_BACKGROUND="true"
LANGCHAIN_TRACING_V2="true"
LANGCHAIN_PROJECT="Safe Agent Tutorial"

72 changes: 72 additions & 0 deletions examples/ai-agent/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { ChatOllama } from "@langchain/ollama";
// import { ChatOpenAI } from "@langchain/openai";
import { MemorySaver } from "@langchain/langgraph";
import { HumanMessage } from "@langchain/core/messages";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { tool } from "@langchain/core/tools";

import {
deployNewSafe,
deployNewSafeMetadata,
getEthBalance,
getEthBalanceMetadata,
} from "./tools/safe";
import { getEthPriceUsd, getEthPriceUsdMetadata } from "./tools/prices";
import { multiply, multiplyMetadata } from "./tools/math";

const main = async () => {
// Define the tools for the agent to use
const agentTools = [
tool(getEthBalance, getEthBalanceMetadata),
tool(getEthPriceUsd, getEthPriceUsdMetadata),
tool(multiply, multiplyMetadata),
tool(deployNewSafe, deployNewSafeMetadata),
];

// Initialize the agent with a model running locally:
const agentModel = new ChatOllama({ model: "mistral-nemo" }); // Feel free to try different models. For the full list: https://ollama.com/search?c=tools

// Or if your prefer using OpenAI (you will need to provide an OPENAI_API_KEY in the .env file.):
// const agentModel = new ChatOpenAI({ temperature: 0, model: "o3-mini" });

const agentCheckpointer = new MemorySaver(); // Initialize memory to persist state between graph runs

const agent = createReactAgent({
llm: agentModel,
tools: agentTools,
checkpointSaver: agentCheckpointer,
});

// Let's chat!
const agentFinalState = await agent.invoke(
{
messages: [
new HumanMessage(
"what is the current balance of the Safe Multisig at the address 0x220866B1A2219f40e72f5c628B65D54268cA3A9D on chain id 1? Please answer in ETH and its total value in USD."
),
],
},
{ configurable: { thread_id: "42" } }
);

console.log(
agentFinalState.messages[agentFinalState.messages.length - 1].content
);

// You can continue the conversation by adding more messages:
// const agentNextState = await agent.invoke(
// {
// messages: [
// new HumanMessage("Could you deploy a new Safe multisig on Sepolia?"),
// ],
// },
// { configurable: { thread_id: "42" } }
// );

// console.log("--- Prompt #2 ---");
// console.log(
// agentNextState.messages[agentNextState.messages.length - 1].content
// );
};

main();
14 changes: 14 additions & 0 deletions examples/ai-agent/tools/math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z } from "zod";

export const multiply = ({ a, b }: { a: number; b: number }): string => {
return `The result of ${a} multiplied by ${b} is ${a * b}.`;
};

export const multiplyMetadata = {
name: "multiply",
description: "Call when you need to multiply two numbers together.",
schema: z.object({
a: z.number(),
b: z.number(),
}),
};
27 changes: 27 additions & 0 deletions examples/ai-agent/tools/prices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { z } from "zod";

export const getEthPriceUsd = async (): Promise<string> => {
const fetchedPrice = await fetch(
"https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd",
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
).catch((error) => {
throw new Error("Error fetching data from the tx service:" + error);
});

const ethPriceData = await fetchedPrice.json();
const ethPriceUsd = ethPriceData?.ethereum?.usd;

return `The price of 1ETH is ${ethPriceUsd.toLocaleString("en-US")}USD at today's prices.`;
};

export const getEthPriceUsdMetadata = {
name: "getEthPriceUsd",
description:
"Call to get the price of ETH in USD.",
schema: z.object({}),
};
91 changes: 91 additions & 0 deletions examples/ai-agent/tools/safe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { z } from "zod";
import Safe from "@safe-global/protocol-kit";
import { createPublicClient, http } from "viem";
import { sepolia } from "viem/chains";

export const getEthBalance = async ({ address, chainId }) => {
if (chainId !== "1") throw new Error("Chain ID not supported.");
if (!address.startsWith("0x") || address.length !== 42) {
throw new Error("Invalid address.");
}

const fetchedEthBalance = await fetch(
`https://safe-transaction-mainnet.safe.global/api/v1/safes/${address}/balances/`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
).catch((error) => {
throw new Error("Error fetching data from the tx service:" + error);
});

const ethBalanceData = await fetchedEthBalance.json();
const weiBalance = ethBalanceData.find(
(element) => element?.tokenAddress === null && element?.token === null
)?.balance;
const ethBalance = BigInt(weiBalance) / BigInt(10 ** 18); // Convert from wei to eth

return `The current balance of the Safe Multisig at address ${address} is ${ethBalance.toLocaleString(
"en-US"
)} ETH.`;
};

export const deployNewSafe = async () => {
const saltNonce = Math.trunc(Math.random() * 10 ** 10).toString(); // Random 10-digit integer
const protocolKit = await Safe.init({
provider: "https://rpc.ankr.com/eth_sepolia",
signer: process.env.AGENT_PRIVATE_KEY,
predictedSafe: {
safeAccountConfig: {
owners: [process.env.AGENT_ADDRESS as string],
threshold: 1,
},
safeDeploymentConfig: {
saltNonce,
},
},
});

const safeAddress = await protocolKit.getAddress();

const deploymentTransaction =
await protocolKit.createSafeDeploymentTransaction();

const safeClient = await protocolKit.getSafeProvider().getExternalSigner();

const transactionHash = await safeClient?.sendTransaction({
to: deploymentTransaction.to,
value: BigInt(deploymentTransaction.value),
data: deploymentTransaction.data as `0x${string}`,
chain: sepolia,
});

const publicClient = createPublicClient({
chain: sepolia,
transport: http(),
});

await publicClient?.waitForTransactionReceipt({
hash: transactionHash as `0x${string}`,
});

return `A new Safe multisig was successfully deployed on Sepolia. You can see it live at https://app.safe.global/home?safe=sep:${safeAddress}. The saltNonce used was ${saltNonce}.`;
};

export const getEthBalanceMetadata = {
name: "getEthBalance",
description:
"Call to get the balance in ETH of a Safe Multisig for a given address and chain ID.",
schema: z.object({
address: z.string(),
chainId: z.enum(["1"]),
}),
};

export const deployNewSafeMetadata = {
name: "deployNewSafe",
description: "Call to deploy a new 1-1 Safe Multisig on Sepolia.",
schema: z.object({}),
};
1 change: 1 addition & 0 deletions pages/home/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"title": "AI"
},
"ai-overview": "Overview",
"ai-agent-setup": "Setup an AI agent",
"ai-agent-quickstarts": "Quickstart Guides",
"ai-agent-actions": "Action Guides",
"-- Help": {
Expand Down
Loading

0 comments on commit 64b82d8

Please sign in to comment.