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