Add LangGraph-TS example (#356)
This PR adds a new example showcasing how to integrate Arcade tools with LangGraph.js to create a ReAct agent. The example is based on the [LangChain React Agent JS](https://github.com/langchain-ai/react-agent-js/tree/main) repository.
This commit is contained in:
parent
8f11fce4b4
commit
3a45d5fec0
9 changed files with 442 additions and 0 deletions
69
examples/langgraph-ts/.gitignore
vendored
Normal file
69
examples/langgraph-ts/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# Dependencies
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.example
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
|
||||
# OS specific
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# pnpm specific
|
||||
.pnpm-debug.log
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
|
||||
# Cache
|
||||
.cache/
|
||||
.turbo/
|
||||
|
||||
# LangGraph API
|
||||
.langgraph_api/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
*.bak
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# Local development
|
||||
.local/
|
||||
107
examples/langgraph-ts/README.md
Normal file
107
examples/langgraph-ts/README.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# Arcade LangGraph.js Agent
|
||||
|
||||
This project is based on the [LangChain React Agent JS](https://github.com/langchain-ai/react-agent-js/tree/main) repository.
|
||||
|
||||

|
||||
|
||||
This template showcases a LangGraph.js agent integrated with Arcade tools, designed for LangGraph Studio. The agent uses the ReAct pattern to execute API calls and access various tools through the Arcade API.
|
||||
|
||||
## What it does
|
||||
|
||||
The Arcade LangGraph agent:
|
||||
|
||||
1. Takes a user **query** as input
|
||||
2. Reasons about the query and decides on an action using Arcade tools
|
||||
3. Executes the chosen action through the Arcade API
|
||||
4. Observes the result of the action
|
||||
5. Repeats steps 2-4 until it can provide a final answer
|
||||
|
||||
This approach creates a flexible agent that can interact with multiple services like Google, GitHub, and other external tools through Arcade's unified API.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Clone this repository
|
||||
|
||||
2. Create a `.env` file:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. Add your API keys to the `.env` file:
|
||||
|
||||
```
|
||||
OPENAI_API_KEY=your-openai-api-key
|
||||
ARCADE_API_KEY=your-arcade-api-key
|
||||
ARCADE_BASE_URL=https://api.arcade.dev
|
||||
```
|
||||
|
||||
4. Install dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
5. Run the development server:
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## How it works
|
||||
|
||||
The core logic is defined in `src/graph.ts`
|
||||
|
||||
1. Loads Arcade tools for multiple toolkits (e.g., Google, GitHub)
|
||||
2. Creates a model with the tools bound to it
|
||||
3. Routes messages between tool calls and model reasoning
|
||||
4. Compiles everything into a graph you can invoke and deploy
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `src/graph.ts` - Main graph implementation showing how to create and use a LangGraph agent with Arcade tools
|
||||
- `src/arcade.ts` - Utility functions for integrating Arcade with LangGraph and converting Arcade tools to LangChain tools
|
||||
- `src/configuration.ts` - Configurable parameters for the agent
|
||||
- `src/prompts.ts` - Default prompts used by the agent
|
||||
- `src/utils.ts` - Helper functions for loading models
|
||||
|
||||
## Customization
|
||||
|
||||
To use different Arcade toolkits or queries:
|
||||
|
||||
1. Modify the `toolkits` array in `getArcadeTools()` in `src/graph.ts` to include the desired toolkits (e.g., `["google", "github", "notion"]`)
|
||||
2. Change the default model in `src/configuration.ts`
|
||||
3. Update the system prompt in `src/prompts.ts`
|
||||
|
||||
Currently supported Arcade toolkits:
|
||||
|
||||
- GitHub
|
||||
- Google
|
||||
- Notion
|
||||
- Reddit
|
||||
- X
|
||||
- And more
|
||||
|
||||
You can check out our [Integrations](https://docs.arcade.dev/integrations) documentation for more information on how to integrate with other tools.
|
||||
|
||||
You can also create your own custom tools and integrate them with LangGraph. Check out our [Custom Tools](https://docs.arcade.dev/home/custom-tools) documentation for more information.
|
||||
|
||||
## Development
|
||||
|
||||
While iterating on your graph, you can edit past state and rerun your app from past states to debug specific nodes. Local changes will be automatically applied via hot reload.
|
||||
|
||||
You can create an entirely new thread, clearing previous history, using the `+` button in the top right of the LangGraph Studio interface.
|
||||
|
||||
## Authorization Flow
|
||||
|
||||
The integration handles authorization requirements for Arcade tools:
|
||||
|
||||
- When a tool requires authentication, an authorization URL is generated
|
||||
- This URL can be presented to the user to complete the authorization process
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js (v18+)
|
||||
- pnpm
|
||||
- Arcade account with [API key](https://docs.arcade.dev/home/api-keys)
|
||||
- OpenAI API key or other compatible LLM
|
||||
9
examples/langgraph-ts/langgraph.json
Normal file
9
examples/langgraph-ts/langgraph.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"node_version": "20",
|
||||
"dockerfile_lines": [],
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./src/graph.ts:graph"
|
||||
},
|
||||
"env": ".env"
|
||||
}
|
||||
28
examples/langgraph-ts/package.json
Normal file
28
examples/langgraph-ts/package.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "arcade-langgraph",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "npx @langchain/langgraph-cli dev"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.8.0",
|
||||
"dependencies": {
|
||||
"@arcadeai/arcadejs": "1.2.1",
|
||||
"@dmitryrechkin/json-schema-to-zod": "^1.0.1",
|
||||
"@langchain/community": "^0.3.27",
|
||||
"@langchain/core": "^0.3.37",
|
||||
"@langchain/langgraph": "^0.2.43",
|
||||
"langchain": "^0.3.14",
|
||||
"@langchain/openai": "^0.5.4",
|
||||
"dotenv": "^16.4.7",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0"
|
||||
}
|
||||
}
|
||||
103
examples/langgraph-ts/src/arcade.ts
Normal file
103
examples/langgraph-ts/src/arcade.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { Arcade } from "@arcadeai/arcadejs";
|
||||
import { JSONSchemaToZod } from "@dmitryrechkin/json-schema-to-zod";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { z } from "zod";
|
||||
|
||||
export const arcadeClient = new Arcade({
|
||||
baseURL: process.env.ARCADE_BASE_URL,
|
||||
apiKey: process.env.ARCADE_API_KEY,
|
||||
});
|
||||
|
||||
const arcadeToolMinimumSchema = z.object({
|
||||
function: z.object({
|
||||
name: z.string(),
|
||||
parameters: z.record(z.any()),
|
||||
description: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
function isAuthorizationRequiredError(error: Error) {
|
||||
return (
|
||||
error?.name === "PermissionDeniedError" ||
|
||||
error?.message?.includes("permission denied") ||
|
||||
error?.message?.includes("authorization required")
|
||||
);
|
||||
}
|
||||
|
||||
async function getAuthorizationResponse(toolName: string, user_id: string) {
|
||||
return await arcadeClient.tools.authorize({
|
||||
tool_name: toolName,
|
||||
user_id,
|
||||
});
|
||||
}
|
||||
|
||||
type LangChainTool = ReturnType<typeof tool>;
|
||||
|
||||
export const getArcadeTools = async ({
|
||||
toolkits,
|
||||
user_id,
|
||||
}: {
|
||||
toolkits?: string[];
|
||||
user_id: string;
|
||||
}): Promise<LangChainTool[]> => {
|
||||
// If no toolkits provided, fetch all tools
|
||||
if (!toolkits || toolkits.length === 0) {
|
||||
const tools = await arcadeClient.tools.formatted.list({
|
||||
format: "openai",
|
||||
});
|
||||
return processTools(tools.items, user_id);
|
||||
}
|
||||
|
||||
// Fetch tools for each toolkit and merge them
|
||||
const toolkitPromises = toolkits.map((toolkit) =>
|
||||
arcadeClient.tools.formatted.list({
|
||||
toolkit,
|
||||
format: "openai",
|
||||
})
|
||||
);
|
||||
|
||||
const toolkitResults = await Promise.all(toolkitPromises);
|
||||
const allTools = toolkitResults.flatMap((result) => result.items);
|
||||
|
||||
// Remove duplicates based on tool name
|
||||
const uniqueTools = Array.from(
|
||||
new Map(allTools.map((item) => [item.function.name, item])).values()
|
||||
);
|
||||
|
||||
return processTools(uniqueTools, user_id);
|
||||
};
|
||||
|
||||
// Helper function to process tools and create LangChain tools
|
||||
const processTools = (tools: unknown[], user_id: string): LangChainTool[] => {
|
||||
const validTools = tools
|
||||
.filter((item) => arcadeToolMinimumSchema.safeParse(item).success)
|
||||
.map((item) => arcadeToolMinimumSchema.parse(item));
|
||||
|
||||
return validTools.map((item) => {
|
||||
const { name, description, parameters } = item.function;
|
||||
const zodSchema = JSONSchemaToZod.convert(parameters);
|
||||
|
||||
return tool(
|
||||
async (input: unknown) => {
|
||||
try {
|
||||
return await arcadeClient.tools.execute({
|
||||
tool_name: name,
|
||||
input: input as Record<string, unknown>,
|
||||
user_id,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && isAuthorizationRequiredError(error)) {
|
||||
const response = await getAuthorizationResponse(name, user_id);
|
||||
return {
|
||||
authorization_required: true,
|
||||
url: response.url,
|
||||
message: "Forward this url to the user for authorization",
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
{ name, description, schema: zodSchema },
|
||||
) as unknown as LangChainTool;
|
||||
});
|
||||
};
|
||||
32
examples/langgraph-ts/src/configuration.ts
Normal file
32
examples/langgraph-ts/src/configuration.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import type { RunnableConfig } from "@langchain/core/runnables";
|
||||
/**
|
||||
* Define the configurable parameters for the agent.
|
||||
*/
|
||||
import { Annotation } from "@langchain/langgraph";
|
||||
import { SYSTEM_PROMPT_TEMPLATE } from "./prompts.js";
|
||||
|
||||
export const ConfigurationSchema = Annotation.Root({
|
||||
/**
|
||||
* The system prompt to be used by the agent.
|
||||
*/
|
||||
systemPromptTemplate: Annotation<string>,
|
||||
|
||||
/**
|
||||
* The name of the language model to be used by the agent.
|
||||
*/
|
||||
model: Annotation<string>,
|
||||
});
|
||||
|
||||
export function ensureConfiguration(
|
||||
config: RunnableConfig,
|
||||
): typeof ConfigurationSchema.State {
|
||||
/**
|
||||
* Ensure the defaults are populated.
|
||||
*/
|
||||
const configurable = config.configurable ?? {};
|
||||
return {
|
||||
systemPromptTemplate:
|
||||
configurable.systemPromptTemplate ?? SYSTEM_PROMPT_TEMPLATE,
|
||||
model: configurable.model ?? "gpt-4o-mini",
|
||||
};
|
||||
}
|
||||
87
examples/langgraph-ts/src/graph.ts
Normal file
87
examples/langgraph-ts/src/graph.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import type { AIMessage } from "@langchain/core/messages";
|
||||
import type { RunnableConfig } from "@langchain/core/runnables";
|
||||
import { MessagesAnnotation, StateGraph } from "@langchain/langgraph";
|
||||
import { ToolNode } from "@langchain/langgraph/prebuilt";
|
||||
import { getArcadeTools } from "./arcade.ts";
|
||||
import { ConfigurationSchema, ensureConfiguration } from "./configuration.ts";
|
||||
import { loadChatModel } from "./utils.ts";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
|
||||
|
||||
// Replace this with your application's user ID (e.g. email address, UUID, etc.)
|
||||
const USER_ID = "user@example.com";
|
||||
// Get the Arcade tools, you can customize the toolkit (e.g. "github", "notion", "google", etc.)
|
||||
const arcadeTools = await getArcadeTools({
|
||||
toolkits: ["google", "github"],
|
||||
user_id: USER_ID,
|
||||
});
|
||||
// Define the function that calls the model
|
||||
async function callModel(
|
||||
state: typeof MessagesAnnotation.State,
|
||||
config: RunnableConfig,
|
||||
): Promise<typeof MessagesAnnotation.Update> {
|
||||
/** Call the LLM powering our agent. **/
|
||||
const configuration = ensureConfiguration(config);
|
||||
|
||||
/**
|
||||
* Initialize the model and bind the tools
|
||||
*/
|
||||
const model = new ChatOpenAI({
|
||||
model: configuration.model,
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
}).bindTools(arcadeTools);
|
||||
|
||||
const response = await model.invoke([
|
||||
{
|
||||
role: "system",
|
||||
content: configuration.systemPromptTemplate.replace(
|
||||
"{system_time}",
|
||||
new Date().toISOString(),
|
||||
),
|
||||
},
|
||||
...state.messages,
|
||||
]);
|
||||
|
||||
// We return a list, because this will get added to the existing list
|
||||
return { messages: [response] };
|
||||
}
|
||||
|
||||
// Define the function that determines whether to continue or not
|
||||
function routeModelOutput(state: typeof MessagesAnnotation.State): string {
|
||||
const messages = state.messages;
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
// If the LLM is invoking tools, route there.
|
||||
if ((lastMessage as AIMessage)?.tool_calls?.length) {
|
||||
return "tools";
|
||||
}
|
||||
// Otherwise end the graph.
|
||||
|
||||
return "__end__";
|
||||
}
|
||||
|
||||
// Define a new graph. We use the prebuilt MessagesAnnotation to define state:
|
||||
// https://langchain-ai.github.io/langgraphjs/concepts/low_level/#messagesannotation
|
||||
const workflow = new StateGraph(MessagesAnnotation, ConfigurationSchema)
|
||||
// Define the two nodes we will cycle between
|
||||
.addNode("callModel", callModel)
|
||||
.addNode("tools", new ToolNode(arcadeTools))
|
||||
// Set the entrypoint as `callModel`
|
||||
// This means that this node is the first one called
|
||||
.addEdge("__start__", "callModel")
|
||||
.addConditionalEdges(
|
||||
// First, we define the edges' source node. We use `callModel`.
|
||||
// This means these are the edges taken after the `callModel` node is called.
|
||||
"callModel",
|
||||
// Next, we pass in the function that will determine the sink node(s), which
|
||||
// will be called after the source node is called.
|
||||
routeModelOutput,
|
||||
)
|
||||
// This means that after `tools` is called, `callModel` node is called next.
|
||||
.addEdge("tools", "callModel");
|
||||
|
||||
// Finally, we compile it!
|
||||
// This compiles it into a graph you can invoke and deploy.
|
||||
export const graph = workflow.compile({
|
||||
interruptBefore: [], // if you want to update the state before calling the tools
|
||||
interruptAfter: [],
|
||||
});
|
||||
7
examples/langgraph-ts/src/prompts.ts
Normal file
7
examples/langgraph-ts/src/prompts.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/**
|
||||
* Default prompts used by the agent.
|
||||
*/
|
||||
|
||||
export const SYSTEM_PROMPT_TEMPLATE = `You are a helpful AI assistant.
|
||||
|
||||
System time: {system_time}`;
|
||||
BIN
examples/langgraph-ts/studio.png
Normal file
BIN
examples/langgraph-ts/studio.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 651 KiB |
Loading…
Reference in a new issue