arcade-mcp/examples/agent_frameworks/langgraph-ts/src/graph.ts
Eric Gustin d7107c107d
Update examples (#601)
* Reorganize the examples folder
* Add two mcp server examples. A local filesystem server and a simple
'starter' server.
2025-10-03 17:37:22 -07:00

121 lines
4.4 KiB
TypeScript

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<typeof MessagesAnnotation.Update> {
/** 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: [],
});