Skip to content

Commit 3f750bc

Browse files
pokeyhntrl
andauthored
feat(examples): Add supervisor example (#9235)
Co-authored-by: Hunter Lovell <[email protected]>
1 parent 619ae64 commit 3f750bc

File tree

1 file changed

+382
-0
lines changed

1 file changed

+382
-0
lines changed
Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
/**
2+
* Complete Personal Assistant Supervisor Example with Human-in-the-Loop
3+
*
4+
* This example demonstrates:
5+
* 1. The tool calling pattern for multi-agent systems
6+
* 2. Human-in-the-loop review of sensitive actions
7+
* 3. Approve/edit/reject decisions for tool calls
8+
*
9+
* A supervisor agent coordinates specialized sub-agents (calendar and email)
10+
* that are wrapped as tools, with human approval for sensitive operations.
11+
*
12+
* This example is designed to accompany the supervisor tutorial:
13+
*
14+
* https://docs.langchain.com/oss/javascript/langchain/supervisor
15+
*/
16+
17+
import { ChatAnthropic } from "@langchain/anthropic";
18+
import { HumanMessage } from "@langchain/core/messages";
19+
import {
20+
Command,
21+
getCurrentTaskInput,
22+
MemorySaver,
23+
} from "@langchain/langgraph";
24+
import {
25+
type BuiltInState,
26+
createAgent,
27+
humanInTheLoopMiddleware,
28+
tool,
29+
} from "langchain";
30+
import { z } from "zod";
31+
32+
// ============================================================================
33+
// Step 1: Define low-level API tools (stubbed)
34+
// ============================================================================
35+
36+
const createCalendarEvent = tool(
37+
async ({ title, startTime, endTime, attendees, location }) => {
38+
// Stub: In practice, this would call Google Calendar API, Outlook API, etc.
39+
return `Event created: ${title} from ${startTime} to ${endTime} with ${attendees.length} attendees at ${location}`;
40+
},
41+
{
42+
name: "create_calendar_event",
43+
description: "Create a calendar event. Requires exact ISO datetime format.",
44+
schema: z.object({
45+
title: z.string(),
46+
startTime: z.string().describe("ISO format: '2024-01-15T14:00:00'"),
47+
endTime: z.string().describe("ISO format: '2024-01-15T15:00:00'"),
48+
attendees: z.array(z.string()).describe("email addresses"),
49+
location: z.string().optional().default(""),
50+
}),
51+
}
52+
);
53+
54+
const sendEmail = tool(
55+
async ({ to, subject, body, cc }) => {
56+
// Stub: In practice, this would call SendGrid, Gmail API, etc.
57+
return [
58+
`Email sent to ${to.join(", ")}`,
59+
`- Subject: ${subject}`,
60+
`- Body: ${body}`,
61+
`- CC: ${cc.join(", ")}`,
62+
].join("\n");
63+
},
64+
{
65+
name: "send_email",
66+
description:
67+
"Send an email via email API. Requires properly formatted addresses.",
68+
schema: z.object({
69+
to: z.array(z.string()).describe("email addresses"),
70+
subject: z.string(),
71+
body: z.string(),
72+
cc: z.array(z.string()).optional().default([]),
73+
}),
74+
}
75+
);
76+
77+
const getAvailableTimeSlots = tool(
78+
async () => {
79+
// Stub: In practice, this would query calendar APIs
80+
return ["09:00", "14:00", "16:00"];
81+
},
82+
{
83+
name: "get_available_time_slots",
84+
description:
85+
"Check calendar availability for given attendees on a specific date.",
86+
schema: z.object({
87+
attendees: z.array(z.string()),
88+
date: z.string().describe("ISO format: '2024-01-15'"),
89+
durationMinutes: z.number(),
90+
}),
91+
}
92+
);
93+
94+
// ============================================================================
95+
// Step 2: Create specialized sub-agents with human-in-the-loop middleware
96+
// ============================================================================
97+
98+
const llm = new ChatAnthropic({
99+
model: "claude-sonnet-4-5-20250929",
100+
});
101+
102+
const CALENDAR_AGENT_PROMPT = `
103+
You are a calendar scheduling assistant.
104+
Parse natural language scheduling requests (e.g., 'next Tuesday at 2pm')
105+
into proper ISO datetime formats.
106+
Use get_available_time_slots to check availability when needed.
107+
Use create_calendar_event to schedule events.
108+
Always confirm what was scheduled in your final response.
109+
`.trim();
110+
111+
const EMAIL_AGENT_PROMPT = `
112+
You are an email assistant.
113+
Compose professional emails based on natural language requests.
114+
Extract recipient information and craft appropriate subject lines and body text.
115+
Use send_email to send the message.
116+
Always confirm what was sent in your final response.
117+
`.trim();
118+
119+
const calendarAgent = createAgent({
120+
model: llm,
121+
tools: [createCalendarEvent, getAvailableTimeSlots],
122+
systemPrompt: CALENDAR_AGENT_PROMPT,
123+
middleware: [
124+
humanInTheLoopMiddleware({
125+
interruptOn: { create_calendar_event: true },
126+
descriptionPrefix: "Calendar event pending approval",
127+
}),
128+
],
129+
});
130+
131+
const emailAgent = createAgent({
132+
model: llm,
133+
tools: [sendEmail],
134+
systemPrompt: EMAIL_AGENT_PROMPT,
135+
middleware: [
136+
humanInTheLoopMiddleware({
137+
interruptOn: { send_email: true },
138+
descriptionPrefix: "Outbound email pending approval",
139+
}),
140+
],
141+
});
142+
143+
// ============================================================================
144+
// Step 3: Wrap sub-agents as tools for the supervisor
145+
// ============================================================================
146+
147+
const scheduleEvent = tool(
148+
async ({ request }, config) => {
149+
// Customize context received by sub-agent
150+
// Access full thread messages from the config
151+
const currentMessages = getCurrentTaskInput<BuiltInState>(config).messages;
152+
153+
const originalUserMessage = currentMessages.find(HumanMessage.isInstance);
154+
155+
const prompt = `
156+
You are assisting with the following user inquiry:
157+
158+
${originalUserMessage?.content || "No context available"}
159+
160+
You are tasked with the following sub-request:
161+
162+
${request}
163+
`.trim();
164+
165+
const result = await calendarAgent.invoke({
166+
messages: [{ role: "user", content: prompt }],
167+
});
168+
const lastMessage = result.messages[result.messages.length - 1];
169+
return lastMessage.text;
170+
},
171+
{
172+
name: "schedule_event",
173+
description: `
174+
Schedule calendar events using natural language.
175+
176+
Use this when the user wants to create, modify, or check calendar appointments.
177+
Handles date/time parsing, availability checking, and event creation.
178+
179+
Input: Natural language scheduling request (e.g., 'meeting with design team next Tuesday at 2pm')
180+
`.trim(),
181+
schema: z.object({
182+
request: z.string().describe("Natural language scheduling request"),
183+
}),
184+
}
185+
);
186+
187+
const manageEmail = tool(
188+
async ({ request }, config) => {
189+
// Customize context received by sub-agent
190+
// Access full thread messages from the config
191+
const currentMessages = getCurrentTaskInput<BuiltInState>(config).messages;
192+
193+
const originalUserMessage = currentMessages.find(HumanMessage.isInstance);
194+
195+
const prompt = `
196+
You are assisting with the following user inquiry:
197+
198+
${originalUserMessage?.content || "No context available"}
199+
200+
You are tasked with the following sub-request:
201+
202+
${request}
203+
`.trim();
204+
205+
const result = await emailAgent.invoke({
206+
messages: [{ role: "user", content: prompt }],
207+
});
208+
const lastMessage = result.messages[result.messages.length - 1];
209+
return lastMessage.text;
210+
},
211+
{
212+
name: "manage_email",
213+
description: `
214+
Send emails using natural language.
215+
216+
Use this when the user wants to send notifications, reminders, or any email communication.
217+
Handles recipient extraction, subject generation, and email composition.
218+
219+
Input: Natural language email request (e.g., 'send them a reminder about the meeting')
220+
`.trim(),
221+
schema: z.object({
222+
request: z.string().describe("Natural language email request"),
223+
}),
224+
}
225+
);
226+
227+
// ============================================================================
228+
// Step 4: Create the supervisor agent with checkpointer
229+
// ============================================================================
230+
231+
const SUPERVISOR_PROMPT = `
232+
You are a helpful personal assistant.
233+
You can schedule calendar events and send emails.
234+
Break down user requests into appropriate tool calls and coordinate the results.
235+
When a request involves multiple actions, use multiple tools in sequence.
236+
`.trim();
237+
238+
const supervisorAgent = createAgent({
239+
model: llm,
240+
tools: [scheduleEvent, manageEmail],
241+
systemPrompt: SUPERVISOR_PROMPT,
242+
checkpointer: new MemorySaver(),
243+
});
244+
245+
// ============================================================================
246+
// Step 5: Demonstrate the complete workflow with human-in-the-loop
247+
// ============================================================================
248+
249+
async function main() {
250+
const query =
251+
"Schedule a meeting with the design team ([email protected], [email protected]) " +
252+
"on January 28, 2025 at 2pm for 1 hour titled 'Design Review', " +
253+
"and send them an email reminder about reviewing the new mockups.";
254+
255+
const config = { configurable: { thread_id: "6" } };
256+
257+
console.log("User Request:", query);
258+
console.log(`\n${"=".repeat(80)}\n`);
259+
260+
// Initial stream - will interrupt for human approval
261+
console.log("=== Initial Request (will interrupt for approval) ===\n");
262+
263+
const interrupts: any[] = [];
264+
const stream = await supervisorAgent.stream(
265+
{ messages: [{ role: "user", content: query }] },
266+
config
267+
);
268+
269+
for await (const step of stream) {
270+
for (const update of Object.values(step)) {
271+
if (update && typeof update === "object" && "messages" in update) {
272+
for (const message of (update as any).messages) {
273+
console.log(message.prettyPrint());
274+
}
275+
} else if (Array.isArray(update)) {
276+
const interrupt = update[0];
277+
interrupts.push(interrupt);
278+
console.log(`\nINTERRUPTED: ${interrupt.id}`);
279+
}
280+
}
281+
}
282+
283+
// Inspect the interrupts
284+
console.log(`\n${"=".repeat(80)}\n`);
285+
console.log("=== Inspecting Interrupts ===\n");
286+
287+
for (const interrupt of interrupts) {
288+
for (const request of interrupt.value.actionRequests) {
289+
console.log(`INTERRUPTED: ${interrupt.id}`);
290+
console.log(`${request.description}\n`);
291+
}
292+
}
293+
294+
// Build resume decisions: approve calendar, edit email subject
295+
console.log(`${"=".repeat(80)}\n`);
296+
console.log("=== Resuming with Decisions ===");
297+
console.log("- Approving calendar event");
298+
console.log("- Editing email subject to 'Mockups reminder'\n");
299+
300+
const resume: Record<string, any> = {};
301+
for (const interrupt of interrupts) {
302+
// Check which interrupt this is by inspecting the tool
303+
const actionRequest = interrupt.value.actionRequests[0];
304+
305+
if (actionRequest.name === "send_email") {
306+
// Edit email subject
307+
const editedAction = { ...actionRequest };
308+
editedAction.arguments.subject = "Mockups reminder";
309+
resume[interrupt.id] = {
310+
decisions: [{ type: "edit", editedAction }],
311+
};
312+
} else {
313+
// Approve everything else
314+
resume[interrupt.id] = { decisions: [{ type: "approve" }] };
315+
}
316+
}
317+
318+
const resumeStream = await supervisorAgent.stream(
319+
new Command({ resume }),
320+
config
321+
);
322+
323+
const moreInterrupts: any[] = [];
324+
for await (const step of resumeStream) {
325+
for (const update of Object.values(step)) {
326+
if (update && typeof update === "object" && "messages" in update) {
327+
for (const message of (update as any).messages) {
328+
console.log(message.prettyPrint());
329+
}
330+
} else if (Array.isArray(update)) {
331+
const interrupt = update[0];
332+
moreInterrupts.push(interrupt);
333+
console.log(`\nINTERRUPTED: ${interrupt.id}`);
334+
}
335+
}
336+
}
337+
338+
// Handle any additional interrupts (e.g., for the email)
339+
if (moreInterrupts.length > 0) {
340+
console.log(`\n${"=".repeat(80)}\n`);
341+
console.log("=== Additional Interrupts (Email) ===\n");
342+
343+
for (const interrupt of moreInterrupts) {
344+
for (const request of interrupt.value.actionRequests) {
345+
console.log(`INTERRUPTED: ${interrupt.id}`);
346+
console.log(`${request.description}\n`);
347+
}
348+
}
349+
350+
console.log(`${"=".repeat(80)}\n`);
351+
console.log("=== Approving Email ===\n");
352+
353+
// Approve the email interrupt
354+
const finalResume: Record<string, any> = {};
355+
for (const interrupt of moreInterrupts) {
356+
finalResume[interrupt.id] = { decisions: [{ type: "approve" }] };
357+
}
358+
359+
const finalStream = await supervisorAgent.stream(
360+
new Command({ resume: finalResume }),
361+
config
362+
);
363+
364+
for await (const step of finalStream) {
365+
for (const update of Object.values(step)) {
366+
if (update && typeof update === "object" && "messages" in update) {
367+
for (const message of (update as any).messages) {
368+
console.log(message.prettyPrint());
369+
}
370+
}
371+
}
372+
}
373+
}
374+
375+
console.log(`\n${"=".repeat(80)}`);
376+
console.log(
377+
"\n✅ Complete! The supervisor coordinated both agents with human approval."
378+
);
379+
}
380+
381+
// Run the example
382+
main().catch(console.error);

0 commit comments

Comments
 (0)