LangChain
This page shows how to register tools from the tool reference with LangChain and bind them to a chat model.
Run LangChain on the server, not in the browser. The chat model requires API keys (for example, OPENAI_API_KEY), and exposing these client-side allows attackers to steal them. LangChain’s OpenAI integration blocks browser usage by default for this reason. For more information, see the LangChain security documentation(opens in a new tab).
Reaching the browser from the server
Nutrient Web SDK runs in the browser, but LangChain runs on the server. The SDK instance isn’t available server-side, so tool execute functions cannot call SDK methods directly. Instead, each tool delegates execution to the browser and waits for the result.
LangChain and LangGraph don’t have a built-in mechanism for this. There are two common approaches, outlined below.
WebSocket bridge
Build a bidirectional channel between the server and the browser. When the agent calls a tool, the execute function sends the tool name and arguments over WebSocket, waits for the browser to execute the SDK method, and returns the result. This is custom infrastructure; LangChain doesn’t provide it.
LangGraph interrupt pattern
LangGraph’s interrupt() function pauses graph execution and surfaces a payload to the caller. Originally designed for human-in-the-loop approval workflows, it provides the mechanics needed to delegate tool execution to the browser:
- The agent calls a tool on the server.
- The tool calls
interrupt()with the tool name and arguments. - The server sends the interrupt payload to the browser (over WebSocket, SSE, or polling).
- The browser executes the SDK method and sends the result back.
- The server resumes the graph with
Command({ resume: result }).
This repurposes the interrupt mechanism beyond its intended use case. It works, but it adds complexity: Graph state is persisted on every interrupt, parallel tool calls require careful handling, and the interrupt/resume lifecycle is designed for infrequent human decisions rather than rapid automated tool execution. For more details, refer to the LangGraph interrupts documentation(opens in a new tab).
Registering tools (server)
Each tool runs on the server. Since the SDK instance isn’t available, the execute function delegates to the browser via whichever bridge you chose above.
The example below uses a callBrowser function that sends the tool call to the browser and returns the result. Replace this with your WebSocket bridge or LangGraph interrupt implementation:
import { tool } from "@langchain/core/tools";import { z } from "zod";
// `callBrowser` sends a tool call to the browser// and returns the result. Implement this with// your WebSocket bridge or LangGraph interrupt.function createDocumentTools( callBrowser: ( toolName: string, args: Record<string, unknown>, ) => Promise<unknown>,) { return [ tool( async ({ pageIndex }) => { const result = await callBrowser( "get_annotations", { pageIndex }, ); return JSON.stringify(result); }, { name: "get_annotations", description: "Get all annotations on a given page. Returns " + "type, content, bounding box, and ID.", schema: z.object({ pageIndex: z .number() .describe("Zero-based page index"), }), }, ), tool( async (args) => { const result = await callBrowser( "create_annotation", args, ); return JSON.stringify(result); }, { name: "create_annotation", description: "Add a text annotation to a page. Call " + "get_page_info first to understand dimensions.", schema: z.object({ pageIndex: z.number(), text: z.string(), left: z.number(), top: z.number(), width: z.number(), height: z.number(), }), }, ), ];}Executing tools (browser)
On the browser side, listen for tool calls from the server and dispatch them to execute wrappers. Each wrapper follows the same pattern as the tool reference:
import NutrientViewer from "@nutrient-sdk/viewer";
// Execute wrappers — match the patterns from the tool reference.async function getAnnotations({ pageIndex }, instance) { const annotations = await instance.getAnnotations(pageIndex); return annotations.toJS();}
async function createAnnotation( { pageIndex, text, left, top, width, height }, instance,) { const annotation = new NutrientViewer.Annotations.TextAnnotation({ pageIndex, text: { format: "plain", value: text }, boundingBox: new NutrientViewer.Geometry.Rect({ left, top, width, height, }), }); const [created] = await instance.create(annotation); return { id: created.id };}
async function handleToolCall( instance: NutrientViewer.Instance, toolName: string, args: Record<string, unknown>,) { switch (toolName) { case "get_annotations": return getAnnotations(args, instance);
case "create_annotation": return createAnnotation(args, instance); }}Binding tools to a model
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({ model: "gpt-4o" });const tools = createDocumentTools(callBrowser);const agent = model.bindTools(tools);
const response = await agent.invoke([ { role: "user", content: "Find all mentions of 'revenue'", },]);Add tools for any SDK method from the tool reference by adding another tool() call to the server factory and a matching case to the browser handler.