import { Arcade } from "@arcadeai/arcadejs"; import { executeOrAuthorizeZodTool, toZod } from "@arcadeai/arcadejs/lib"; import type { AIMessage } from "@langchain/core/messages"; import type { RunnableConfig } from "@langchain/core/runnables"; import { type Tool, tool } from "@langchain/core/tools"; import { MessagesAnnotation, StateGraph } from "@langchain/langgraph"; import { ToolNode } from "@langchain/langgraph/prebuilt"; import { ChatOpenAI } from "@langchain/openai"; import { ConfigurationSchema, ensureConfiguration } from "./configuration.ts"; // Initialize Arcade const arcade = new Arcade(); // 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", "gmail", etc.) const gmailToolkit = await arcade.tools.list({ toolkit: "gmail", limit: 30 }); /** * LangGraph requires tools to be defined using Zod, a TypeScript-first schema validation library * that has become the standard for runtime type checking. Zod is particularly valuable because it: * - Provides runtime type safety and validation * - Offers excellent TypeScript integration with automatic type inference * - Has a simple, declarative API for defining schemas * - Is widely adopted in the TypeScript ecosystem * * Arcade provides `toZod` to convert our tools into Zod format, making them compatible * with LangGraph. * * The `executeOrAuthorizeZodTool` helper function simplifies authorization. * It checks if the tool requires authorization: if so, it returns an authorization URL, * otherwise, it runs the tool directly without extra boilerplate. * * Learn more: https://docs.arcade.dev/home/use-tools/get-tool-definitions#get-zod-tool-definitions */ const arcadeTools = toZod({ tools: gmailToolkit.items, client: arcade, userId: USER_ID, executeFactory: executeOrAuthorizeZodTool, // Checks if tool is authorized and executes it, or returns authorization URL if needed }); // Convert Arcade tools to LangChain tools const tools = arcadeTools.map(({ name, description, execute, parameters }) => tool(execute, { name, description, schema: parameters, }), ); // Define the function that calls the model async function callModel( state: typeof MessagesAnnotation.State, config: RunnableConfig, ): Promise { /** 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(tools); 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(tools)) // 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: [], });