Add Langchain example for our docs. (#399)

These examples are the same we have for Python in our docs.
This commit is contained in:
Sergio Serrano 2025-05-13 20:30:48 -03:00 committed by GitHub
parent 45b83d3461
commit 8d0d77af10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 4850 additions and 0 deletions

View file

@ -0,0 +1,2 @@
OPENAI_API_KEY=sk-proj-1234567890
ARCADE_API_KEY=arc_1234567890

45
examples/langchain-ts/.gitignore vendored Normal file
View file

@ -0,0 +1,45 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment variables
.env
.env.local
.env.*.local
# Build output
dist/
build/
out/
# IDE and editor files
.idea/
.vscode/
*.swp
*.swo
.DS_Store
# Logs
logs/
*.log
# Testing
coverage/
# Temporary files
tmp/
temp/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# LangGraph API
.langgraph_api

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025, Arcade AI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,79 @@
import { Arcade } from "@arcadeai/arcadejs";
import {
GmailCreateDraft,
GmailGetMessage,
GmailGetThread,
GmailSearch,
GmailSendMessage,
} from "@langchain/community/tools/gmail";
import type { StructuredTool } from "@langchain/core/tools";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { ChatOpenAI } from "@langchain/openai";
// Initialize Arcade with API key from environment
const arcade = new Arcade();
// Replace this with your application's user ID (e.g. email address, UUID, etc.)
const USER_ID = "user@example.com";
// Start the authorization process for Gmail
// see all possible gmail scopes here:
// https://developers.google.com/gmail/api/auth/scopes
const authResponse = await arcade.auth.start(USER_ID, "google", {
scopes: ["https://www.googleapis.com/auth/gmail.readonly"],
});
// Prompt the user to authorize if not already completed
if (authResponse.status !== "completed") {
console.log("Please authorize the application in your browser:");
console.log(authResponse.url);
}
// Wait for the user to complete the authorization process, if necessary...
const completedAuth = await arcade.auth.waitForCompletion(authResponse);
if (!completedAuth.context?.token) {
throw new Error("Failed to get authorization token");
}
// Get Gmail tools with credentials
const gmailParam = {
credentials: {
accessToken: completedAuth.context.token,
},
};
const tools: StructuredTool[] = [
new GmailCreateDraft(gmailParam),
new GmailGetMessage(gmailParam),
new GmailGetThread(gmailParam),
new GmailSearch(gmailParam),
new GmailSendMessage(gmailParam),
];
// Initialize the language model and create an agent
const llm = new ChatOpenAI({ model: "gpt-4o" });
const agent_executor = createReactAgent({
llm,
tools,
});
// Define the user query
const exampleQuery = "Read my latest emails and summarize them.";
// Execute the agent with the user query
const events = await agent_executor.stream(
{
messages: [
{
role: "user",
content: exampleQuery,
},
],
},
{
streamMode: "values",
},
);
for await (const event of events) {
console.log(event.messages[event.messages.length - 1]);
}

View file

@ -0,0 +1,72 @@
import { Arcade } from "@arcadeai/arcadejs";
import { executeOrAuthorizeZodTool, toZod } from "@arcadeai/arcadejs/lib";
import { tool } from "@langchain/core/tools";
import { MemorySaver } from "@langchain/langgraph";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import { ChatOpenAI } from "@langchain/openai";
// 1) Initialize Arcade
const arcade = new Arcade();
// 2) Fetch Google toolkit from Arcade and prepare tools for LangGraph integration
const googleToolkit = await arcade.tools.list({ toolkit: "google", limit: 30 });
const arcadeTools = toZod({
tools: googleToolkit.items,
client: arcade,
userId: "<YOUR_SYSTEM_USER_ID>", // Replace this with your application's user ID (e.g. email address, UUID, etc.)
executeFactory: executeOrAuthorizeZodTool,
});
// Convert Arcade tools to LangGraph tools
const tools = arcadeTools.map(({ name, description, execute, parameters }) =>
tool(execute, {
name,
description,
schema: parameters,
}),
);
// 3) Create a ChatOpenAI model and bind the Arcade tools.
const model = new ChatOpenAI({
model: "gpt-4o",
apiKey: process.env.OPENAI_API_KEY,
});
const boundModel = model.bindTools(tools);
// 4) Use MemorySaver for checkpointing.
const memory = new MemorySaver();
// 5) Create a ReAct-style agent from the prebuilt function.
const graph = createReactAgent({
llm: boundModel,
tools,
checkpointer: memory,
});
// 6) Provide basic config and a user query.
// Note: user_id is required for the tool to be authorized
const config = {
configurable: {
thread_id: "1",
user_id: "user@example.com",
},
streamMode: "values" as const,
};
const user_input = {
messages: [
{
role: "user",
content: "List any new and important emails in my inbox.",
},
],
};
// 7) Stream the agent's output. If the tool is unauthorized, the agent will ask the user to authorize the tool.
try {
const stream = await graph.stream(user_input, config);
for await (const chunk of stream) {
console.log(chunk.messages[chunk.messages.length - 1]);
}
} catch (error) {
console.error("Error streaming response:", error);
}
// Once you login using the printed link, you can resume the agent.

View file

@ -0,0 +1,148 @@
import { pathToFileURL } from "node:url";
import { Arcade } from "@arcadeai/arcadejs";
import { toZod } from "@arcadeai/arcadejs/lib";
import type { AIMessage } from "@langchain/core/messages";
import { tool } from "@langchain/core/tools";
import { MessagesAnnotation, StateGraph } from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { ChatOpenAI } from "@langchain/openai";
// Initialize Arcade with API key from environment
const arcade = new Arcade();
// Replace with your application's user ID (e.g. email address, UUID, etc.)
const USER_ID = "user@example.com";
// Initialize tools from GitHub toolkit
const githubToolkit = await arcade.tools.list({ toolkit: "github", limit: 30 });
const arcadeTools = toZod({
tools: githubToolkit.items,
client: arcade,
userId: USER_ID,
});
// Convert Arcade tools to LangGraph tools
const tools = arcadeTools.map(({ name, description, execute, parameters }) =>
tool(execute, {
name,
description,
schema: parameters,
}),
);
// Initialize the prebuilt tool node
const toolNode = new ToolNode(tools);
// Create a language model instance and bind it with the tools
const model = new ChatOpenAI({
model: "gpt-4o",
apiKey: process.env.OPENAI_API_KEY,
});
const modelWithTools = model.bindTools(tools);
// Function to check if a tool requires authorization
async function requiresAuth(toolName: string): Promise<{
needsAuth: boolean;
id: string;
authUrl: string;
}> {
const authResponse = await arcade.tools.authorize({
tool_name: toolName,
user_id: USER_ID,
});
return {
needsAuth: authResponse.status === "pending",
id: authResponse.id ?? "",
authUrl: authResponse.url ?? "",
};
}
// Function to invoke the model and get a response
async function callAgent(
state: typeof MessagesAnnotation.State,
): Promise<typeof MessagesAnnotation.Update> {
const messages = state.messages;
const response = await modelWithTools.invoke(messages);
return { messages: [response] };
}
// Function to determine the next step in the workflow based on the last message
async function shouldContinue(
state: typeof MessagesAnnotation.State,
): Promise<string> {
const lastMessage = state.messages[state.messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
for (const toolCall of lastMessage.tool_calls) {
const { needsAuth } = await requiresAuth(toolCall.name);
if (needsAuth) {
return "authorization";
}
}
return "tools"; // Proceed to tool execution if no authorization is needed
}
return "__end__"; // End the workflow if no tool calls are present
}
// Function to handle authorization for tools that require it
async function authorize(
state: typeof MessagesAnnotation.State,
): Promise<typeof MessagesAnnotation.Update> {
const lastMessage = state.messages[state.messages.length - 1] as AIMessage;
for (const toolCall of lastMessage.tool_calls || []) {
const toolName = toolCall.name;
const { needsAuth, id, authUrl } = await requiresAuth(toolName);
if (needsAuth) {
// Prompt the user to visit the authorization URL
console.log(`Visit the following URL to authorize: ${authUrl}`);
// Wait for the user to complete the authorization
const response = await arcade.auth.waitForCompletion(id);
if (response.status !== "completed") {
throw new Error("Authorization failed");
}
}
}
return { messages: [] };
}
// Build the workflow graph
const workflow = new StateGraph(MessagesAnnotation)
.addNode("agent", callAgent)
.addNode("tools", toolNode)
.addNode("authorization", authorize)
.addEdge("__start__", "agent")
.addConditionalEdges("agent", shouldContinue, [
"authorization",
"tools",
"__end__",
])
.addEdge("authorization", "tools")
.addEdge("tools", "agent");
// Compile the graph
const graph = workflow.compile();
const main = async () => {
// Define the input messages from the user
const inputs = {
messages: [
{
role: "user",
content: "Star arcadeai/arcade-ai on github",
},
],
};
// Run the graph and stream the outputs
const stream = await graph.stream(inputs, { streamMode: "values" });
for await (const chunk of stream) {
// Print the last message in the chunk
console.log(chunk.messages[chunk.messages.length - 1].content);
}
};
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch(console.error);
}
export { graph };

View file

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

2741
examples/langchain-ts/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,27 @@
{
"name": "langchain-js",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"dev": "npx tsx --env-file=.env langgraph-arcade-minimal.ts",
"dev-auth": "npx tsx --env-file=.env langgraph-with-user-auth.ts",
"dev-auth-langchain": "npx tsx --env-file=.env langchain-tool-arcade-auth.ts",
"studio": "npx @langchain/langgraph-cli@latest dev"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.6.5",
"dependencies": {
"@arcadeai/arcadejs": "latest",
"@langchain/community": "^0.3.42",
"@langchain/core": "^0.3.55",
"@langchain/langgraph": "^0.2.72",
"@langchain/openai": "^0.5.10"
},
"devDependencies": {
"@types/node": "^22.15.17"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"rootDir": ".",
"types": ["node"],
"lib": ["ES2020"],
"declaration": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": ["./**/*.ts"],
"exclude": ["node_modules", "dist"]
}