Update LangGraph example (#398)

This commit is contained in:
Sergio Serrano 2025-05-12 19:50:18 -03:00 committed by GitHub
parent b9afa1b5cf
commit 45b83d3461
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 69 additions and 142 deletions

View file

@ -33,7 +33,6 @@ This approach creates a flexible agent that can interact with multiple services
```
OPENAI_API_KEY=your-openai-api-key
ARCADE_API_KEY=your-arcade-api-key
ARCADE_BASE_URL=https://api.arcade.dev
```
4. Install dependencies:
@ -52,7 +51,7 @@ This approach creates a flexible agent that can interact with multiple services
The core logic is defined in `src/graph.ts`
1. Loads Arcade tools for multiple toolkits (e.g., Google, GitHub)
1. Loads Arcade tools for multiple toolkits (e.g., Google)
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
@ -60,16 +59,14 @@ The core logic is defined in `src/graph.ts`
## 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"]`)
1. Modify the `toolkit` string in `arcade.tools.list` in `src/graph.ts` to include the desired toolkit (e.g., `["google", "github", "notion"]`)
2. Change the default model in `src/configuration.ts`
3. Update the system prompt in `src/prompts.ts`

View file

@ -1,28 +1,27 @@
{
"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"
}
"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.10.0",
"dependencies": {
"@arcadeai/arcadejs": "latest",
"@langchain/community": "^0.3.42",
"@langchain/core": "^0.3.55",
"@langchain/langgraph": "^0.2.71",
"langchain": "^0.3.24",
"@langchain/openai": "^0.5.10",
"dotenv": "^16.5.0",
"zod": "^3.24.4"
},
"devDependencies": {
"@types/node": "^22.15.17"
}
}

View file

@ -1,103 +0,0 @@
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;
});
};

View file

@ -1,20 +1,54 @@
import { Arcade } from "@arcadeai/arcadejs";
import { executeOrAuthorizeZodTool, toZod } from "@arcadeai/arcadejs/lib";
import type { AIMessage } from "@langchain/core/messages";
import type { RunnableConfig } from "@langchain/core/runnables";
import { type Tool, tool } from "@langchain/core/tools";
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";
import { ConfigurationSchema, ensureConfiguration } from "./configuration.ts";
// Initialize Arcade
const arcade = new Arcade();
// 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,
const googleToolkit = await arcade.tools.list({ toolkit: "google", limit: 30 });
/**
* LangGraph requires tools to be defined using Zod, a TypeScript-first schema validation library
* that has become the standard for runtime type checking. Zod is particularly valuable because it:
* - Provides runtime type safety and validation
* - Offers excellent TypeScript integration with automatic type inference
* - Has a simple, declarative API for defining schemas
* - Is widely adopted in the TypeScript ecosystem
*
* Arcade provides `toZod` to convert our tools into Zod format, making them compatible
* with LangGraph.
*
* The `executeOrAuthorizeZodTool` helper function simplifies authorization.
* It checks if the tool requires authorization: if so, it returns an authorization URL,
* otherwise, it runs the tool directly without extra boilerplate.
*
* Learn more: https://docs.arcade.dev/home/use-tools/get-tool-definitions#get-zod-tool-definitions
*/
const arcadeTools = toZod({
tools: googleToolkit.items,
client: arcade,
userId: USER_ID,
executeFactory: executeOrAuthorizeZodTool, // Checks if tool is authorized and executes it, or returns authorization URL if needed
});
// Convert Arcade tools to LangChain tools
const tools = arcadeTools.map(({ name, description, execute, parameters }) =>
tool(execute, {
name,
description,
schema: parameters,
}),
);
// Define the function that calls the model
async function callModel(
state: typeof MessagesAnnotation.State,
@ -29,7 +63,7 @@ async function callModel(
const model = new ChatOpenAI({
model: configuration.model,
apiKey: process.env.OPENAI_API_KEY,
}).bindTools(arcadeTools);
}).bindTools(tools);
const response = await model.invoke([
{
@ -64,7 +98,7 @@ function routeModelOutput(state: typeof MessagesAnnotation.State): string {
const workflow = new StateGraph(MessagesAnnotation, ConfigurationSchema)
// Define the two nodes we will cycle between
.addNode("callModel", callModel)
.addNode("tools", new ToolNode(arcadeTools))
.addNode("tools", new ToolNode(tools))
// Set the entrypoint as `callModel`
// This means that this node is the first one called
.addEdge("__start__", "callModel")