Update LangGraph example (#398)
This commit is contained in:
parent
b9afa1b5cf
commit
45b83d3461
4 changed files with 69 additions and 142 deletions
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in a new issue