Add Langchain example for our docs. (#399)
These examples are the same we have for Python in our docs.
This commit is contained in:
parent
45b83d3461
commit
8d0d77af10
11 changed files with 4850 additions and 0 deletions
2
examples/langchain-ts/.env.example
Normal file
2
examples/langchain-ts/.env.example
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
OPENAI_API_KEY=sk-proj-1234567890
|
||||
ARCADE_API_KEY=arc_1234567890
|
||||
45
examples/langchain-ts/.gitignore
vendored
Normal file
45
examples/langchain-ts/.gitignore
vendored
Normal 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
|
||||
21
examples/langchain-ts/LICENSE
Normal file
21
examples/langchain-ts/LICENSE
Normal 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.
|
||||
79
examples/langchain-ts/langchain-tool-arcade-auth.ts
Normal file
79
examples/langchain-ts/langchain-tool-arcade-auth.ts
Normal 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]);
|
||||
}
|
||||
72
examples/langchain-ts/langgraph-arcade-minimal.ts
Normal file
72
examples/langchain-ts/langgraph-arcade-minimal.ts
Normal 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.
|
||||
148
examples/langchain-ts/langgraph-with-user-auth.ts
Normal file
148
examples/langchain-ts/langgraph-with-user-auth.ts
Normal 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 };
|
||||
9
examples/langchain-ts/langgraph.json
Normal file
9
examples/langchain-ts/langgraph.json
Normal 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
2741
examples/langchain-ts/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
27
examples/langchain-ts/package.json
Normal file
27
examples/langchain-ts/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1686
examples/langchain-ts/pnpm-lock.yaml
Normal file
1686
examples/langchain-ts/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
20
examples/langchain-ts/tsconfig.json
Normal file
20
examples/langchain-ts/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Loading…
Reference in a new issue