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 OPENAI_API_KEY=your-openai-api-key
ARCADE_API_KEY=your-arcade-api-key ARCADE_API_KEY=your-arcade-api-key
ARCADE_BASE_URL=https://api.arcade.dev
``` ```
4. Install dependencies: 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` 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 2. Creates a model with the tools bound to it
3. Routes messages between tool calls and model reasoning 3. Routes messages between tool calls and model reasoning
4. Compiles everything into a graph you can invoke and deploy 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 ## Project Structure
- `src/graph.ts` - Main graph implementation showing how to create and use a LangGraph agent with Arcade tools - `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/configuration.ts` - Configurable parameters for the agent
- `src/prompts.ts` - Default prompts used by the agent - `src/prompts.ts` - Default prompts used by the agent
- `src/utils.ts` - Helper functions for loading models
## Customization ## Customization
To use different Arcade toolkits or queries: 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` 2. Change the default model in `src/configuration.ts`
3. Update the system prompt in `src/prompts.ts` 3. Update the system prompt in `src/prompts.ts`

View file

@ -1,28 +1,27 @@
{ {
"name": "arcade-langgraph", "name": "arcade-langgraph",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "npx @langchain/langgraph-cli dev" "dev": "npx @langchain/langgraph-cli dev"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"packageManager": "pnpm@10.8.0", "packageManager": "pnpm@10.10.0",
"dependencies": { "dependencies": {
"@arcadeai/arcadejs": "1.2.1", "@arcadeai/arcadejs": "latest",
"@dmitryrechkin/json-schema-to-zod": "^1.0.1", "@langchain/community": "^0.3.42",
"@langchain/community": "^0.3.27", "@langchain/core": "^0.3.55",
"@langchain/core": "^0.3.37", "@langchain/langgraph": "^0.2.71",
"@langchain/langgraph": "^0.2.43", "langchain": "^0.3.24",
"langchain": "^0.3.14", "@langchain/openai": "^0.5.10",
"@langchain/openai": "^0.5.4", "dotenv": "^16.5.0",
"dotenv": "^16.4.7", "zod": "^3.24.4"
"zod": "^3.24.2" },
}, "devDependencies": {
"devDependencies": { "@types/node": "^22.15.17"
"@types/node": "^22.14.0" }
}
} }

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 { AIMessage } from "@langchain/core/messages";
import type { RunnableConfig } from "@langchain/core/runnables"; import type { RunnableConfig } from "@langchain/core/runnables";
import { type Tool, tool } from "@langchain/core/tools";
import { MessagesAnnotation, StateGraph } from "@langchain/langgraph"; import { MessagesAnnotation, StateGraph } from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt"; 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 { 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.) // Replace this with your application's user ID (e.g. email address, UUID, etc.)
const USER_ID = "user@example.com"; const USER_ID = "user@example.com";
// Get the Arcade tools, you can customize the toolkit (e.g. "github", "notion", "google", etc.) // Get the Arcade tools, you can customize the toolkit (e.g. "github", "notion", "google", etc.)
const arcadeTools = await getArcadeTools({ const googleToolkit = await arcade.tools.list({ toolkit: "google", limit: 30 });
toolkits: ["google", "github"],
user_id: USER_ID, /**
* 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 // Define the function that calls the model
async function callModel( async function callModel(
state: typeof MessagesAnnotation.State, state: typeof MessagesAnnotation.State,
@ -29,7 +63,7 @@ async function callModel(
const model = new ChatOpenAI({ const model = new ChatOpenAI({
model: configuration.model, model: configuration.model,
apiKey: process.env.OPENAI_API_KEY, apiKey: process.env.OPENAI_API_KEY,
}).bindTools(arcadeTools); }).bindTools(tools);
const response = await model.invoke([ const response = await model.invoke([
{ {
@ -64,7 +98,7 @@ function routeModelOutput(state: typeof MessagesAnnotation.State): string {
const workflow = new StateGraph(MessagesAnnotation, ConfigurationSchema) const workflow = new StateGraph(MessagesAnnotation, ConfigurationSchema)
// Define the two nodes we will cycle between // Define the two nodes we will cycle between
.addNode("callModel", callModel) .addNode("callModel", callModel)
.addNode("tools", new ToolNode(arcadeTools)) .addNode("tools", new ToolNode(tools))
// Set the entrypoint as `callModel` // Set the entrypoint as `callModel`
// This means that this node is the first one called // This means that this node is the first one called
.addEdge("__start__", "callModel") .addEdge("__start__", "callModel")