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 { 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 { 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 { 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-mcp 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 };