-
Notifications
You must be signed in to change notification settings - Fork 80
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add AI agent setup with LangChain (#688)
* 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
Showing
10 changed files
with
405 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
}), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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({}), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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({}), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.