From 45b83d34611631fda09079a5f6cb499d652713fb Mon Sep 17 00:00:00 2001 From: Sergio Serrano <35855882+sdserranog@users.noreply.github.com> Date: Mon, 12 May 2025 19:50:18 -0300 Subject: [PATCH] Update LangGraph example (#398) --- examples/langgraph-ts/README.md | 7 +- examples/langgraph-ts/package.json | 51 +++++++------- examples/langgraph-ts/src/arcade.ts | 103 ---------------------------- examples/langgraph-ts/src/graph.ts | 50 +++++++++++--- 4 files changed, 69 insertions(+), 142 deletions(-) delete mode 100644 examples/langgraph-ts/src/arcade.ts diff --git a/examples/langgraph-ts/README.md b/examples/langgraph-ts/README.md index 21aee807..ae41c11c 100644 --- a/examples/langgraph-ts/README.md +++ b/examples/langgraph-ts/README.md @@ -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` diff --git a/examples/langgraph-ts/package.json b/examples/langgraph-ts/package.json index fb8a563c..55ce7bdb 100644 --- a/examples/langgraph-ts/package.json +++ b/examples/langgraph-ts/package.json @@ -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" + } } diff --git a/examples/langgraph-ts/src/arcade.ts b/examples/langgraph-ts/src/arcade.ts deleted file mode 100644 index 37103ebb..00000000 --- a/examples/langgraph-ts/src/arcade.ts +++ /dev/null @@ -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; - -export const getArcadeTools = async ({ - toolkits, - user_id, -}: { - toolkits?: string[]; - user_id: string; -}): Promise => { - // 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, - 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; - }); -}; diff --git a/examples/langgraph-ts/src/graph.ts b/examples/langgraph-ts/src/graph.ts index ccea40c5..820cde6e 100644 --- a/examples/langgraph-ts/src/graph.ts +++ b/examples/langgraph-ts/src/graph.ts @@ -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")