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:
Sergio Serrano 2025-04-09 21:08:18 -03:00 committed by GitHub
parent 8f11fce4b4
commit 3a45d5fec0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 442 additions and 0 deletions

69
examples/langgraph-ts/.gitignore vendored Normal file
View 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/

View 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.
![LangGraph Studio](studio.png)
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

View file

@ -0,0 +1,9 @@
{
"node_version": "20",
"dockerfile_lines": [],
"dependencies": ["."],
"graphs": {
"agent": "./src/graph.ts:graph"
},
"env": ".env"
}

View 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"
}
}

View 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;
});
};

View 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",
};
}

View 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: [],
});

View 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}`;

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 KiB