Langgraph (#78)

# Pull Request Description

## Related Issue
Fixes #61

## Changes Made
This PR adds the following changes:
- Added a new example demonstrating LangGraph integration with Solana
Agent Kit
- Implemented a multi-agent system with specialized agents for different
tasks
- Added state management and routing logic using LangGraph's StateGraph
- Integrated Tavily search capabilities for enhanced general-purpose
queries
- Added support for token swaps using Jupiter DEX
- Implemented TypeScript-based project structure with full type safety
- Added comprehensive documentation and architecture diagram

## Implementation Details
- Created a directed workflow using StateGraph with the following
components:
  - Manager Agent: Handles query classification and routing
- General Agent: Processes basic queries with Tavily search integration
  - Transfer/Swap Agent: Handles token transfers and DEX operations
  - Read Agent: Manages blockchain data queries
- Implemented state management using LangGraph annotations for query
classification
- Added environment-based configuration for API keys and RPC endpoints
- Integrated with external dependencies:
  - @langchain/community v0.3.20
  - @langchain/core v0.3.26
  - @langchain/langgraph v0.2.36
  - solana-agent-kit v1.3.0
- Set up TypeScript configuration with ESM support

## Architecture

<img width="801" alt="Screenshot 2024-12-27 at 5 59 26 PM"
src="https://github.com/user-attachments/assets/a90597ac-3bfc-47e1-b1de-5a17a3de106b"
/>


## Transaction executed by agent 
Example transaction: 


## Additional Notes
- The implementation follows a modular architecture as shown in the
architecture diagram
- Supports both read and write operations on the Solana blockchain
- Includes comprehensive error handling and type safety
- Provides flexible configuration through environment variables
- Project structure follows best practices with clear separation of
concerns
- Includes example queries for testing different agent pathways

## Checklist
- [x] I have tested these changes locally
- [x] I have updated the documentation
- [x] I have added a transaction link
- [x] I have added the prompt used to test it
This commit is contained in:
ARYAN
2024-12-28 02:25:28 +05:30
committed by GitHub
25 changed files with 5017 additions and 4 deletions

View File

@@ -202,6 +202,23 @@ const price = await agent.pythFetchPrice(
console.log("Price in BTC/USD:", price);
```
## Examples
### LangGraph Multi-Agent System
The repository includes an advanced example of building a multi-agent system using LangGraph and Solana Agent Kit. Located in `examples/agent-kit-langgraph`, this example demonstrates:
- Multi-agent architecture using LangGraph's StateGraph
- Specialized agents for different tasks:
- General purpose agent for basic queries
- Transfer/Swap agent for transaction operations
- Read agent for blockchain data queries
- Manager agent for routing and orchestration
- Fully typed TypeScript implementation
- Environment-based configuration
Check out the [LangGraph example](examples/agent-kit-langgraph) for a complete implementation of an advanced Solana agent system.
## Dependencies
The toolkit relies on several key Solana and Metaplex libraries:

View File

@@ -0,0 +1,4 @@
OPENAI_API_KEY=
RPC_URL=
SOLANA_PRIVATE_KEY=
TAVILY_API_KEY= #Optional: for search functionality

11
examples/agent-kit-langgraph/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
# Dependencies
node_modules/
package-lock.json
# Environment variables
.env
.env.local
# Build output
dist/
build/

View File

@@ -0,0 +1,87 @@
# Agent Kit LangGraph Example
This example demonstrates how to build an advanced Solana agent using LangGraph and the Solana Agent Kit. It showcases a multi-agent system that can handle various Solana-related tasks through a directed workflow.
![Solana Agent Kit LangGraph Architecture](./assets/architecture.png)
## Features
- Multi-agent architecture using LangGraph's StateGraph
- Specialized agents for different tasks:
- General purpose agent for basic queries (with optional Tavily search integration)
- Transfer/Swap agent for transaction operations
- Read agent for blockchain data queries
- Manager agent for routing and orchestration
- Environment-based configuration
- TypeScript implementation with full type safety
## Prerequisites
- Node.js (v16 or higher)
- pnpm package manager
- Solana development environment
## Installation
1. Clone the repository and navigate to the example directory:
```bash
cd examples/agent-kit-langgraph
```
2. Install dependencies:
```bash
pnpm install
```
3. Configure environment variables:
```bash
cp .env.example .env
```
Edit the `.env` file with your configuration:
- Add your OpenAI API key
- Add your Tavily API key (optional, enables web search capabilities)
- Configure any other required environment variables
## Project Structure
```
src/
├── agents/ # Individual agent implementations
├── helper/ # Helper utilities and examples
├── prompts/ # Agent prompts and templates
├── tools/ # Custom tools for agents
└── utils/ # Utility functions and configurations
```
## Usage
To run the example:
```bash
pnpm dev src/index.ts
```
The example demonstrates a workflow where:
1. The manager agent receives the initial query
2. Based on the query type, it routes to the appropriate specialized agent:
- General queries → Generalist Agent
- Transfer/Swap operations → TransferSwap Agent
- Blockchain data queries → Read Agent
## Dependencies
- `@langchain/community`: LangChain community tools and utilities
- Includes Tavily search integration for enhanced query responses
- `@langchain/core`: Core LangChain functionality
- `@langchain/langgraph`: Graph-based agent workflows
- `solana-agent-kit`: Solana Agent Kit for blockchain interactions
- `zod`: Runtime type checking
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
ISC License

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

View File

@@ -0,0 +1,9 @@
{
"node_version": "20",
"dockerfile_lines": [],
"dependencies": ["."],
"graphs": {
"solanaAgent": "./src/index.ts:graph"
},
"env": ".env"
}

View File

@@ -0,0 +1,31 @@
{
"name": "agent-kit-langgraph",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "tsx"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@langchain/community": "^0.3.20",
"@langchain/core": "^0.3.26",
"@langchain/langgraph": "^0.2.36",
"dotenv": "^16.4.7",
"solana-agent-kit": "^1.3.0",
"zod": "^3.24.1"
},
"devDependencies": {
"ts-node": "^10.9.2",
"tsx": "^4.19.2",
"typescript": "^5.0.0"
},
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node"
}
}

4462
examples/agent-kit-langgraph/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { gpt4o } from "../utils/model";
import { solanaAgentState } from "../utils/state";
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
// Initialize tools array
const searchTools = [];
// Only add Tavily search if API key is available
if (process.env.TAVILY_API_KEY) {
searchTools.push(new TavilySearchResults());
}
const generalAgent = createReactAgent({
llm: gpt4o,
tools: searchTools,
});
export const generalistNode = async (state: typeof solanaAgentState.State) => {
const { messages } = state;
const result = await generalAgent.invoke({ messages });
return { messages: [...result.messages] };
};

View File

@@ -0,0 +1,23 @@
import { prompt, parser } from "../prompts/manager";
import { RunnableSequence } from "@langchain/core/runnables";
import { solanaAgentState } from "../utils/state";
import { gpt4o } from "../utils/model";
const chain = RunnableSequence.from([prompt, gpt4o, parser]);
export const managerNode = async (state: typeof solanaAgentState.State) => {
const { messages } = state;
const result = await chain.invoke({
formatInstructions: parser.getFormatInstructions(),
messages: messages,
});
const { isSolanaReadQuery, isSolanaWriteQuery, isGeneralQuery } = result;
return {
isSolanaReadQuery,
isSolanaWriteQuery,
isGeneralQuery,
};
};

View File

@@ -0,0 +1,22 @@
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { gpt4o } from "../utils/model";
import { solanaAgentState } from "../utils/state";
import { HumanMessage } from "@langchain/core/messages";
import { agentKit } from "../utils/solanaAgent";
import {
SolanaBalanceTool,
SolanaFetchPriceTool,
} from "solana-agent-kit/dist/langchain";
const readAgent = createReactAgent({
llm: gpt4o,
tools: [new SolanaBalanceTool(agentKit), new SolanaFetchPriceTool(agentKit)],
});
export const readNode = async (state: typeof solanaAgentState.State) => {
const { messages } = state;
const result = await readAgent.invoke({ messages });
return { messages: [...result.messages] };
};

View File

@@ -0,0 +1,25 @@
import { gpt4o } from "../utils/model";
import { agentKit } from "../utils/solanaAgent";
import { solanaAgentState } from "../utils/state";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { SolanaTransferTool } from "solana-agent-kit/dist/langchain";
import { transferSwapPrompt } from "../prompts/transferSwap";
import { swapTool } from "../tools/swap";
const transferOrSwapAgent = createReactAgent({
stateModifier: transferSwapPrompt,
llm: gpt4o,
tools: [new SolanaTransferTool(agentKit), swapTool],
});
export const transferSwapNode = async (
state: typeof solanaAgentState.State,
) => {
const { messages } = state;
const result = await transferOrSwapAgent.invoke({
messages,
});
return result;
};

View File

@@ -0,0 +1,9 @@
import { HumanMessage } from "@langchain/core/messages";
export const generalQuestion = [
new HumanMessage("Who is the president of Ecuador?"),
];
export const solanaReadQuery = [new HumanMessage("what is the price of SOL")];
export const solanaWriteQuery = [new HumanMessage("swap 0.1 usdc to sol")];

View File

@@ -0,0 +1,50 @@
export const tokenList = [
{
name: "USDC",
ticker: "USDC",
decimal: 6,
mintAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
},
{
name: "USDT",
ticker: "USDT",
decimal: 6,
mintAddress: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
},
{
name: "USDS",
ticker: "USDS",
decimal: 6,
mintAddress: "USDSwr9ApdHk5bvJKMjzff41FfuX8bSxdKcR81vTwcA",
},
{
name: "SOL",
ticker: "SOL",
decimal: 9,
mintAddress: "So11111111111111111111111111111111111111112",
},
{
name: "jitoSOL",
ticker: "jitoSOL",
decimal: 9,
mintAddress: "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn",
},
{
name: "bSOL",
ticker: "bSOL",
decimal: 9,
mintAddress: "bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1",
},
{
name: "mSOL",
ticker: "mSOL",
decimal: 9,
mintAddress: "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So",
},
{
name: "BONK",
ticker: "BONK",
decimal: 9,
mintAddress: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
},
];

View File

@@ -0,0 +1,28 @@
import { StateGraph } from "@langchain/langgraph";
import { solanaAgentState } from "./utils/state";
import { generalistNode } from "./agents/generalAgent";
import { transferSwapNode } from "./agents/transferOrSwap";
import { managerNode } from "./agents/manager";
import { readNode } from "./agents/readAgent";
import { START, END } from "@langchain/langgraph";
import { managerRouter } from "./utils/route";
import { HumanMessage } from "@langchain/core/messages";
const workflow = new StateGraph(solanaAgentState)
.addNode("generalist", generalistNode)
.addNode("manager", managerNode)
.addNode("transferSwap", transferSwapNode)
.addNode("read", readNode)
.addEdge(START, "manager")
.addConditionalEdges("manager", managerRouter)
.addEdge("generalist", END)
.addEdge("transferSwap", END)
.addEdge("read", END);
export const graph = workflow.compile();
const result = await graph.invoke({
messages: [new HumanMessage("what is the price of SOL")],
});
console.log(result);

View File

@@ -0,0 +1,48 @@
import { StructuredOutputParser } from "@langchain/core/output_parsers";
import { z } from "zod";
import { PromptTemplate } from "@langchain/core/prompts";
export const parser = StructuredOutputParser.fromZodSchema(
z.object({
isSolanaReadQuery: z
.boolean()
.describe("Query requires reading data from Solana blockchain"),
isSolanaWriteQuery: z
.boolean()
.describe("Query requires writing/modifying data on Solana blockchain"),
isGeneralQuery: z
.boolean()
.describe("Query is about non-blockchain topics"),
}),
);
export const prompt = PromptTemplate.fromTemplate(
`
You are the Chief Routing Officer for a multi-blockchain agent network. Your role is to:
1. Analyze and classify incoming queries
2. Determine if the query requires Solana read operations, write operations, or is general
Format your response according to:
{formatInstructions}
Classification Guidelines:
- Solana Read Operations include:
* Checking account balances
* Viewing NFT metadata
* Getting program data
* Querying transaction history
* Checking token prices or holdings
- Solana Write Operations include:
* Creating or updating programs
* Sending tokens or SOL
* Minting NFTs
* Creating accounts
* Any transaction that modifies blockchain state
- General queries include:
* Non-blockchain topics
* Internet searches
* General knowledge questions
\n {messages} \n
`,
);

View File

@@ -0,0 +1,43 @@
import {
ChatPromptTemplate,
MessagesPlaceholder,
} from "@langchain/core/prompts";
import { tokenList } from "../helper/tokenList";
// Convert token list to a more readable format for the prompt
const formattedTokenList = tokenList
.map(
(token) =>
`- ${token.name} (${token.ticker}) - Decimals: ${token.decimal} - Address: ${token.mintAddress}`,
)
.join("\n ");
export const transferSwapPrompt = ChatPromptTemplate.fromMessages([
[
"system",
`You are an agent that is an expert in Solana transactions, specialized in token transfers and swaps. You can execute these transactions using the available tools based on user input.
When processing token amounts:
1. Use EXACTLY the decimal amount specified by the user without any modifications
2. Do not round or adjust the numbers
3. Maintain precise decimal places as provided in the user input
For transfers:
- User must specify the token, amount, and recipient address
- The same token will be used for input and output
For swaps:
- User must specify the input token, output token, and amount to swap
- Input and output tokens must be different
- Select tokens from this list of supported tokens:
${formattedTokenList}
Example amounts:
If you say "0.01 SOL", I will use exactly 0.01 (not 0.010 or 0.0100)
If you say "1.234 USDC", I will use exactly 1.234 (not 1.23 or 1.2340)
For swaps, have the slippage be 200 bps
`,
],
new MessagesPlaceholder("messages"),
]);

View File

@@ -0,0 +1,45 @@
import { agentKit } from "../utils/solanaAgent";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { PublicKey } from "@solana/web3.js";
export const swapTool = tool(
async ({ outputMint, inputAmount, inputMint, inputDecimal }) => {
try {
const inputAmountWithDecimals = inputAmount * 10 ** inputDecimal;
const outputMintAddress = new PublicKey(outputMint);
const inputMintAddress = new PublicKey(inputMint);
console.log(inputAmountWithDecimals, outputMintAddress, inputMintAddress);
const tx = await agentKit.trade(
outputMintAddress,
inputAmountWithDecimals,
inputMintAddress,
200,
);
return tx;
} catch (error) {
console.error(error);
return "error";
}
},
{
name: "swap",
description:
"call to swap/trade tokens from one token to the other using Jupiter exchange",
schema: z.object({
outputMint: z
.string()
.describe("The mint address of destination token to be swapped to"),
inputAmount: z
.number()
.describe(
"the input amount of the token to be swapped without adding any decimals",
),
inputMint: z.string().describe("The mint address of the origin token "),
inputDecimal: z
.number()
.describe("The decimal of the input token that is being traded"),
}),
},
);

View File

@@ -0,0 +1,12 @@
import { ChatOpenAI } from "@langchain/openai";
import "dotenv/config";
export const gpt4o = new ChatOpenAI({
modelName: "gpt-4o",
apiKey: process.env.OPENAI_API_KEY!,
});
export const gpt4oMini = new ChatOpenAI({
modelName: "gpt-4o-mini",
apiKey: process.env.OPENAI_API_KEY!,
});

View File

@@ -0,0 +1,16 @@
import { solanaAgentState } from "./state";
import { END } from "@langchain/langgraph";
export const managerRouter = (state: typeof solanaAgentState.State) => {
const { isSolanaReadQuery, isSolanaWriteQuery, isGeneralQuery } = state;
if (isGeneralQuery) {
return "generalist";
} else if (isSolanaWriteQuery) {
return "transferSwap";
} else if (isSolanaReadQuery) {
return "read";
} else {
return END;
}
};

View File

@@ -0,0 +1,9 @@
import { SolanaAgentKit, createSolanaTools } from "solana-agent-kit";
export const agentKit = new SolanaAgentKit(
process.env.SOLANA_PRIVATE_KEY!,
process.env.RPC_URL!,
process.env.OPENAI_API_KEY!,
);
export const solanaTools = createSolanaTools(agentKit);

View File

@@ -0,0 +1,25 @@
import { Annotation } from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";
import { messagesStateReducer } from "@langchain/langgraph";
export const solanaAgentState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: messagesStateReducer,
default: () => [],
}),
isSolanaReadQuery: Annotation<boolean>({
reducer: (x, y) => y ?? x ?? false,
default: () => false,
}),
isSolanaWriteQuery: Annotation<boolean>({
reducer: (x, y) => y ?? x ?? false,
default: () => false,
}),
isGeneralQuery: Annotation<boolean>({
reducer: (x, y) => y ?? x ?? false,
default: () => false,
}),
});

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"outDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

View File

@@ -23,9 +23,9 @@
"dependencies": {
"@bonfida/spl-name-service": "^3.0.7",
"@coral-xyz/anchor": "0.29",
"@langchain/core": "^0.3.18",
"@langchain/core": "^0.3.26",
"@langchain/groq": "^0.1.2",
"@langchain/langgraph": "^0.2.27",
"@langchain/langgraph": "^0.2.34",
"@langchain/openai": "^0.3.13",
"@lightprotocol/compressed-token": "^0.17.1",
"@lightprotocol/stateless.js": "^0.17.1",

4
pnpm-lock.yaml generated
View File

@@ -15,13 +15,13 @@ importers:
specifier: '0.29'
version: 0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
'@langchain/core':
specifier: ^0.3.18
specifier: ^0.3.26
version: 0.3.26(openai@4.77.0(zod@3.24.1))
'@langchain/groq':
specifier: ^0.1.2
version: 0.1.2(@langchain/core@0.3.26(openai@4.77.0(zod@3.24.1)))
'@langchain/langgraph':
specifier: ^0.2.27
specifier: ^0.2.34
version: 0.2.34(@langchain/core@0.3.26(openai@4.77.0(zod@3.24.1)))
'@langchain/openai':
specifier: ^0.3.13