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