|
| 1 | +import { createAgent, HumanMessage, tool, MemorySaver } from "langchain"; |
| 2 | +import { humanInTheLoopMiddleware } from "langchain/middleware"; |
| 3 | +import { Command } from "@langchain/langgraph"; |
| 4 | +import { z } from "zod"; |
| 5 | + |
| 6 | +const checkpointSaver = new MemorySaver(); |
| 7 | + |
| 8 | +// Define a safe tool (no approval needed) |
| 9 | +const calculateTool = tool( |
| 10 | + async ({ a, b, operation }: { a: number; b: number; operation: string }) => { |
| 11 | + console.log( |
| 12 | + `π οΈ calculator tool called with args: ${a}, ${b}, ${operation}` |
| 13 | + ); |
| 14 | + switch (operation) { |
| 15 | + case "add": |
| 16 | + return `${a} + ${b} = ${a + b}`; |
| 17 | + case "multiply": |
| 18 | + return `${a} * ${b} = ${a * b}`; |
| 19 | + default: |
| 20 | + return "Unknown operation"; |
| 21 | + } |
| 22 | + }, |
| 23 | + { |
| 24 | + name: "calculator", |
| 25 | + description: "Perform basic math operations", |
| 26 | + schema: z.object({ |
| 27 | + a: z.number().describe("First number"), |
| 28 | + b: z.number().describe("Second number"), |
| 29 | + operation: z.enum(["add", "multiply"]).describe("Math operation"), |
| 30 | + }), |
| 31 | + } |
| 32 | +); |
| 33 | + |
| 34 | +// Define a tool that requires approval |
| 35 | +const writeFileTool = tool( |
| 36 | + async ({ filename, content }: { filename: string; content: string }) => { |
| 37 | + console.log( |
| 38 | + `π οΈ write_file tool called with args: ${filename}, ${content}` |
| 39 | + ); |
| 40 | + // Simulate file writing |
| 41 | + return `Successfully wrote ${content.length} characters to ${filename}`; |
| 42 | + }, |
| 43 | + { |
| 44 | + name: "write_file", |
| 45 | + description: "Write content to a file", |
| 46 | + schema: z.object({ |
| 47 | + filename: z.string().describe("Name of the file"), |
| 48 | + content: z.string().describe("Content to write"), |
| 49 | + }), |
| 50 | + } |
| 51 | +); |
| 52 | + |
| 53 | +// Configure HITL middleware |
| 54 | +const hitlMiddleware = humanInTheLoopMiddleware({ |
| 55 | + toolConfigs: { |
| 56 | + write_file: { |
| 57 | + requireApproval: true, |
| 58 | + description: "β οΈ File write operation requires approval", |
| 59 | + }, |
| 60 | + calculator: { |
| 61 | + requireApproval: false, // Math is safe |
| 62 | + }, |
| 63 | + }, |
| 64 | +}); |
| 65 | + |
| 66 | +// Create agent with HITL middleware |
| 67 | +const agent = createAgent({ |
| 68 | + model: "openai:gpt-4o-mini", |
| 69 | + checkpointSaver, |
| 70 | + prompt: |
| 71 | + "You are a helpful assistant. Use the tools provided to help the user.", |
| 72 | + tools: [calculateTool, writeFileTool], |
| 73 | + middlewares: [hitlMiddleware] as const, |
| 74 | +}); |
| 75 | +const config = { |
| 76 | + configurable: { |
| 77 | + thread_id: "123", |
| 78 | + }, |
| 79 | +}; |
| 80 | + |
| 81 | +console.log("π HITL Tool Approval Example"); |
| 82 | +console.log("=============================\n"); |
| 83 | + |
| 84 | +// Example 1: Safe tool - no approval needed |
| 85 | +console.log("π Example 1: Using calculator (auto-approved)"); |
| 86 | +const mathResult = await agent.invoke( |
| 87 | + { |
| 88 | + messages: [new HumanMessage("Calculate 42 * 17")], |
| 89 | + }, |
| 90 | + config |
| 91 | +); |
| 92 | +console.log("Result:", mathResult.messages.at(-1)?.content); |
| 93 | + |
| 94 | +// Example 2: Tool requiring approval |
| 95 | +console.log("\nπ Example 2: Writing to file (requires approval)"); |
| 96 | +console.log("User: Write 'Hello World' to greeting.txt\n"); |
| 97 | + |
| 98 | +// This will pause at the HITL middleware for approval |
| 99 | +const initialResult = await agent.invoke( |
| 100 | + { |
| 101 | + messages: [new HumanMessage("Write 'Hello World' to greeting.txt")], |
| 102 | + }, |
| 103 | + config |
| 104 | +); |
| 105 | + |
| 106 | +// Check if the agent is paused (waiting for approval) |
| 107 | +const state = await agent.graph.getState(config); |
| 108 | +if (state.next && state.next.length > 0) { |
| 109 | + console.log("βΈοΈ Interrupted for approval!"); |
| 110 | + |
| 111 | + // Get the interrupt data from the task |
| 112 | + const task = state.tasks?.[0]; |
| 113 | + if (task?.interrupts && task.interrupts.length > 0) { |
| 114 | + const requests = task.interrupts[0].value; |
| 115 | + console.log("Tool:", requests[0].action); |
| 116 | + console.log("Args:", JSON.stringify(requests[0].args, null, 2)); |
| 117 | + |
| 118 | + console.log("\nβΉοΈ In a real application, you would:"); |
| 119 | + console.log(" - Show this to the user"); |
| 120 | + console.log(" - Get their response (accept/edit/ignore/manual)"); |
| 121 | + console.log( |
| 122 | + " - Resume with: agent.invoke(new Command({ resume: response }))" |
| 123 | + ); |
| 124 | + |
| 125 | + console.log("\nβ
Simulating user approval...\n"); |
| 126 | + |
| 127 | + // Resume with approval |
| 128 | + const resumedResult = await agent.invoke( |
| 129 | + // @ts-expect-error |
| 130 | + new Command({ |
| 131 | + resume: [{ type: "accept" }], // Approve the tool call |
| 132 | + }), |
| 133 | + config |
| 134 | + ); |
| 135 | + |
| 136 | + console.log("Result:", resumedResult.messages.at(-1)?.content); |
| 137 | + } |
| 138 | +} else { |
| 139 | + console.log("Agent completed without interruption"); |
| 140 | + console.log("Result:", initialResult.messages.at(-1)?.content); |
| 141 | +} |
0 commit comments