create-langgraph-app 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create.js +484 -88
- package/package.json +1 -1
package/dist/create.js
CHANGED
|
@@ -72,18 +72,14 @@ function generateEnv(config) {
|
|
|
72
72
|
}
|
|
73
73
|
function generatePackageJson(config) {
|
|
74
74
|
const deps = {
|
|
75
|
-
"@langchain/core": "^1.
|
|
76
|
-
"@langchain/langgraph": "^1.
|
|
77
|
-
"@langchain/langgraph-supervisor": "^1.0.3",
|
|
75
|
+
"@langchain/core": "^1.2.3",
|
|
76
|
+
"@langchain/langgraph": "^1.4.8",
|
|
78
77
|
"@langchain/mcp-adapters": "^1.1.3",
|
|
79
78
|
dotenv: "^17.4.2",
|
|
80
|
-
fastify: "^5.
|
|
81
|
-
langchain: "^1.3
|
|
79
|
+
fastify: "^5.10.0",
|
|
80
|
+
langchain: "^1.5.3",
|
|
82
81
|
zod: "^4.4.3",
|
|
83
82
|
};
|
|
84
|
-
if (config.patterns.includes("swarm")) {
|
|
85
|
-
deps["@langchain/langgraph-swarm"] = "^1.0.2";
|
|
86
|
-
}
|
|
87
83
|
if (config.patterns.includes("rag")) {
|
|
88
84
|
deps["@langchain/textsplitters"] = "^1.0.1";
|
|
89
85
|
}
|
|
@@ -105,6 +101,7 @@ function generatePackageJson(config) {
|
|
|
105
101
|
dev: "tsx src/index.ts",
|
|
106
102
|
"dev:http": "tsx src/server.ts",
|
|
107
103
|
test: "vitest run",
|
|
104
|
+
"test:watch": "vitest",
|
|
108
105
|
typecheck: "tsc --noEmit",
|
|
109
106
|
},
|
|
110
107
|
dependencies: Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b))),
|
|
@@ -130,7 +127,7 @@ function generateTsConfig() {
|
|
|
130
127
|
types: ["node"],
|
|
131
128
|
outDir: "dist",
|
|
132
129
|
},
|
|
133
|
-
include: ["src"],
|
|
130
|
+
include: ["src", "tests"],
|
|
134
131
|
}, null, 2) + "\n";
|
|
135
132
|
}
|
|
136
133
|
function generateEnvConfig(config) {
|
|
@@ -219,50 +216,372 @@ export function getLlm(opts?: LlmOptions): Promise<BaseChatModel> {
|
|
|
219
216
|
`;
|
|
220
217
|
}
|
|
221
218
|
function generateAgentFactory() {
|
|
222
|
-
return `import type
|
|
223
|
-
import type {
|
|
224
|
-
import {
|
|
219
|
+
return `import { createAgent, type ResponseFormat, type TypedToolStrategy } from "langchain";
|
|
220
|
+
import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
|
221
|
+
import type { BaseCheckpointSaver } from "@langchain/langgraph-checkpoint";
|
|
222
|
+
|
|
223
|
+
type CreateAgentParams = Parameters<typeof createAgent>[0];
|
|
225
224
|
|
|
226
225
|
export interface MakeAgentParams {
|
|
227
226
|
name: string;
|
|
228
227
|
llm: BaseChatModel;
|
|
229
|
-
tools?:
|
|
228
|
+
tools?: CreateAgentParams["tools"];
|
|
230
229
|
system?: string;
|
|
231
|
-
responseFormat?:
|
|
230
|
+
responseFormat?: ResponseFormat | TypedToolStrategy<Record<string, unknown>>;
|
|
231
|
+
/**
|
|
232
|
+
* Only the outermost agent of a multi-agent setup should get a
|
|
233
|
+
* checkpointer — subagents inherit it at runtime, which is what lets
|
|
234
|
+
* interrupt() calls bubble up to the top-level graph.
|
|
235
|
+
*/
|
|
236
|
+
checkpointer?: BaseCheckpointSaver;
|
|
232
237
|
}
|
|
233
238
|
|
|
234
|
-
|
|
235
|
-
|
|
239
|
+
/** Wraps LangChain's createAgent with a simpler interface. */
|
|
240
|
+
export function makeAgent({
|
|
241
|
+
name,
|
|
242
|
+
llm,
|
|
243
|
+
tools = [],
|
|
244
|
+
system,
|
|
245
|
+
responseFormat,
|
|
246
|
+
checkpointer,
|
|
247
|
+
}: MakeAgentParams) {
|
|
248
|
+
return createAgent({
|
|
236
249
|
name,
|
|
237
|
-
llm,
|
|
250
|
+
model: llm,
|
|
238
251
|
tools,
|
|
239
|
-
...(system ? {
|
|
252
|
+
...(system ? { systemPrompt: system } : {}),
|
|
240
253
|
...(responseFormat ? { responseFormat } : {}),
|
|
254
|
+
...(checkpointer ? { checkpointer } : {}),
|
|
241
255
|
});
|
|
242
256
|
}
|
|
257
|
+
|
|
258
|
+
export type AgentGraph = ReturnType<typeof makeAgent>;
|
|
243
259
|
`;
|
|
244
260
|
}
|
|
245
261
|
function generateSupervisorHelper() {
|
|
246
|
-
return `import
|
|
247
|
-
import
|
|
248
|
-
import {
|
|
249
|
-
import {
|
|
262
|
+
return `import { z } from "zod";
|
|
263
|
+
import { tool } from "@langchain/core/tools";
|
|
264
|
+
import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
|
265
|
+
import type { BaseCheckpointSaver } from "@langchain/langgraph-checkpoint";
|
|
266
|
+
import { MemorySaver } from "@langchain/langgraph";
|
|
267
|
+
import { makeAgent, type AgentGraph } from "./factory";
|
|
250
268
|
|
|
251
|
-
|
|
269
|
+
/**
|
|
270
|
+
* Supervisor via the "subagents" pattern: a main agent coordinates workers
|
|
271
|
+
* by calling them as tools. Subagents are stateless and run in isolated
|
|
272
|
+
* context windows; only the supervisor gets a checkpointer, so interrupt()
|
|
273
|
+
* inside a subagent tool pauses the whole graph.
|
|
274
|
+
*/
|
|
275
|
+
|
|
276
|
+
export interface SubagentSpec {
|
|
277
|
+
/** Tool name the supervisor calls, e.g. "math_expert". */
|
|
278
|
+
name: string;
|
|
279
|
+
/** Tells the supervisor's model when to delegate to this agent. */
|
|
280
|
+
description: string;
|
|
281
|
+
agent: AgentGraph;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Wraps a compiled agent as a tool the supervisor can call. */
|
|
285
|
+
export function subagentTool({ name, description, agent }: SubagentSpec) {
|
|
286
|
+
return tool(
|
|
287
|
+
async ({ task }) => {
|
|
288
|
+
const result = await agent.invoke({
|
|
289
|
+
messages: [{ role: "user", content: task }],
|
|
290
|
+
});
|
|
291
|
+
const last = result.messages.at(-1);
|
|
292
|
+
if (!last) return "(no response)";
|
|
293
|
+
return typeof last.content === "string"
|
|
294
|
+
? last.content
|
|
295
|
+
: JSON.stringify(last.content);
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name,
|
|
299
|
+
description,
|
|
300
|
+
schema: z.object({
|
|
301
|
+
task: z
|
|
302
|
+
.string()
|
|
303
|
+
.describe("A self-contained task with all context the agent needs"),
|
|
304
|
+
}),
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
}
|
|
252
308
|
|
|
253
|
-
export interface MakeSupervisorParams
|
|
309
|
+
export interface MakeSupervisorParams {
|
|
310
|
+
subagents: SubagentSpec[];
|
|
311
|
+
llm: BaseChatModel;
|
|
312
|
+
supervisorName?: string;
|
|
313
|
+
prompt?: string;
|
|
254
314
|
checkpointer?: BaseCheckpointSaver;
|
|
255
|
-
store?: BaseStore;
|
|
256
315
|
}
|
|
257
316
|
|
|
258
|
-
export function makeSupervisor({
|
|
259
|
-
|
|
317
|
+
export function makeSupervisor({
|
|
318
|
+
subagents,
|
|
319
|
+
llm,
|
|
320
|
+
supervisorName = "supervisor",
|
|
321
|
+
prompt,
|
|
322
|
+
checkpointer,
|
|
323
|
+
}: MakeSupervisorParams) {
|
|
324
|
+
const defaultPrompt =
|
|
325
|
+
"You coordinate a team of specialists. Delegate work to them via " +
|
|
326
|
+
\`your tools (\${subagents.map((s) => s.name).join(", ")}) and answer \` +
|
|
327
|
+
"the user only once the delegated work is done.";
|
|
328
|
+
|
|
329
|
+
return makeAgent({
|
|
330
|
+
name: supervisorName,
|
|
331
|
+
llm,
|
|
332
|
+
tools: subagents.map(subagentTool),
|
|
333
|
+
system: prompt ?? defaultPrompt,
|
|
260
334
|
checkpointer: checkpointer ?? new MemorySaver(),
|
|
261
|
-
store: store ?? new InMemoryStore(),
|
|
262
335
|
});
|
|
263
336
|
}
|
|
264
337
|
`;
|
|
265
338
|
}
|
|
339
|
+
// Maps a selected pattern to the app it generates, so the server and demos
|
|
340
|
+
// only reference files that actually exist.
|
|
341
|
+
const PATTERN_APPS = {
|
|
342
|
+
supervisor: { route: "supervisor", fn: "createSupervisorApp", path: "./apps/supervisor" },
|
|
343
|
+
swarm: { route: "swarm", fn: "createSwarmApp", path: "./apps/swarm" },
|
|
344
|
+
hitl: { route: "interrupt", fn: "createInterruptApp", path: "./apps/interrupt" },
|
|
345
|
+
structured: { route: "analyst", fn: "createAnalystApp", path: "./apps/analyst" },
|
|
346
|
+
rag: { route: "rag", fn: "createRagApp", path: "./apps/rag" },
|
|
347
|
+
};
|
|
348
|
+
function selectedApps(config) {
|
|
349
|
+
return config.patterns.map((p) => PATTERN_APPS[p]).filter(Boolean);
|
|
350
|
+
}
|
|
351
|
+
function generateServer(config) {
|
|
352
|
+
const apps = selectedApps(config);
|
|
353
|
+
const imports = apps.map((a) => `import { ${a.fn} } from "${a.path}";`).join("\n");
|
|
354
|
+
const entries = apps.map((a) => ` "${a.route}": asApp(await ${a.fn}()),`).join("\n");
|
|
355
|
+
return `import "./config/env";
|
|
356
|
+
import { fastify, type FastifyError, type FastifyReply, type FastifyRequest } from "fastify";
|
|
357
|
+
import { Command } from "@langchain/langgraph";
|
|
358
|
+
import { AIMessageChunk, type BaseMessage } from "@langchain/core/messages";
|
|
359
|
+
import { PORT } from "./config/env";
|
|
360
|
+
${imports}
|
|
361
|
+
|
|
362
|
+
// The apps are a mix of agents and graphs whose generic types don't unify;
|
|
363
|
+
// this is the structural slice the routes actually need.
|
|
364
|
+
interface AppGraph {
|
|
365
|
+
invoke(input: unknown, config?: unknown): Promise<{ messages: BaseMessage[] } & Record<string, unknown>>;
|
|
366
|
+
stream(input: unknown, config?: unknown): Promise<AsyncIterable<[unknown, { langgraph_node?: string } | undefined]>>;
|
|
367
|
+
getState(config: unknown): Promise<{ values?: unknown; next?: string[]; tasks?: unknown[] } | undefined>;
|
|
368
|
+
}
|
|
369
|
+
const asApp = (g: unknown) => g as AppGraph;
|
|
370
|
+
|
|
371
|
+
function httpError(status: number, message: string) {
|
|
372
|
+
return Object.assign(new Error(message), { statusCode: status });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Reject malformed bodies before they reach the agent. */
|
|
376
|
+
function validateMessages(raw: unknown): { role: string; content: string }[] {
|
|
377
|
+
if (raw === undefined) return [];
|
|
378
|
+
if (!Array.isArray(raw)) throw httpError(400, '"messages" must be an array');
|
|
379
|
+
return raw.map((m, i) => {
|
|
380
|
+
const { role, content } = (m ?? {}) as Record<string, unknown>;
|
|
381
|
+
if (typeof role !== "string" || typeof content !== "string") {
|
|
382
|
+
throw httpError(400, \`messages[\${i}] needs string "role" and "content"\`);
|
|
383
|
+
}
|
|
384
|
+
return { role, content };
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const server = fastify({ logger: false });
|
|
389
|
+
|
|
390
|
+
server.setErrorHandler(async (error: FastifyError, _req: FastifyRequest, reply: FastifyReply) => {
|
|
391
|
+
const status = error.statusCode ?? 500;
|
|
392
|
+
return reply.status(status).send({ error: error.message });
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
async function start() {
|
|
396
|
+
const apps: Record<string, AppGraph> = {
|
|
397
|
+
${entries}
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const getApp = (name: string) => {
|
|
401
|
+
const app = apps[name];
|
|
402
|
+
if (!app) throw httpError(404, \`Unknown app: "\${name}". Available: \${Object.keys(apps).join(", ")}\`);
|
|
403
|
+
return app;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
server.post<{ Params: { app: string } }>("/:app/invoke", async (req, reply) => {
|
|
407
|
+
const app = getApp(req.params.app);
|
|
408
|
+
const body = (req.body ?? {}) as { messages?: unknown; thread_id?: string };
|
|
409
|
+
const messages = validateMessages(body.messages);
|
|
410
|
+
const thread_id = body.thread_id ?? "default";
|
|
411
|
+
|
|
412
|
+
const result = await app.invoke({ messages }, { configurable: { thread_id } });
|
|
413
|
+
const last = result.messages.at(-1);
|
|
414
|
+
return reply.send({
|
|
415
|
+
messages: result.messages,
|
|
416
|
+
structuredResponse: (result as Record<string, unknown>).structuredResponse ?? null,
|
|
417
|
+
lastMessage: typeof last?.content === "string" ? last.content : JSON.stringify(last?.content),
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
server.post<{ Params: { app: string } }>("/:app/stream", async (req, reply) => {
|
|
422
|
+
const app = getApp(req.params.app);
|
|
423
|
+
const body = (req.body ?? {}) as { messages?: unknown; thread_id?: string };
|
|
424
|
+
const messages = validateMessages(body.messages);
|
|
425
|
+
const thread_id = body.thread_id ?? "default";
|
|
426
|
+
|
|
427
|
+
reply.raw.writeHead(200, {
|
|
428
|
+
"Content-Type": "text/event-stream",
|
|
429
|
+
"Cache-Control": "no-cache",
|
|
430
|
+
Connection: "keep-alive",
|
|
431
|
+
});
|
|
432
|
+
const send = (e: unknown) => reply.raw.write(\`data: \${JSON.stringify(e)}\\n\\n\`);
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
const stream = await app.stream({ messages }, { configurable: { thread_id }, streamMode: "messages" });
|
|
436
|
+
for await (const [chunk, metadata] of stream) {
|
|
437
|
+
if (chunk instanceof AIMessageChunk) {
|
|
438
|
+
send({ type: "token", content: chunk.content, node: metadata?.langgraph_node ?? "unknown" });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
send({ type: "done" });
|
|
442
|
+
} catch (err) {
|
|
443
|
+
send({ type: "error", content: err instanceof Error ? err.message : "Stream failed" });
|
|
444
|
+
} finally {
|
|
445
|
+
reply.raw.end();
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
server.post<{ Params: { app: string } }>("/:app/resume", async (req, reply) => {
|
|
450
|
+
const app = getApp(req.params.app);
|
|
451
|
+
const body = (req.body ?? {}) as { thread_id?: string; decision?: string };
|
|
452
|
+
if (body.decision === undefined) {
|
|
453
|
+
return reply.status(400).send({ error: '"decision" field is required' });
|
|
454
|
+
}
|
|
455
|
+
const result = await app.invoke(new Command({ resume: body.decision }), {
|
|
456
|
+
configurable: { thread_id: body.thread_id ?? "default" },
|
|
457
|
+
});
|
|
458
|
+
const last = result.messages.at(-1);
|
|
459
|
+
return reply.send({
|
|
460
|
+
messages: result.messages,
|
|
461
|
+
lastMessage: typeof last?.content === "string" ? last.content : JSON.stringify(last?.content),
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
server.get<{ Params: { app: string; threadId: string } }>("/:app/threads/:threadId", async (req, reply) => {
|
|
466
|
+
const app = getApp(req.params.app);
|
|
467
|
+
const state = await app.getState({ configurable: { thread_id: req.params.threadId } });
|
|
468
|
+
if (!state?.values) return reply.status(404).send({ error: "Thread not found" });
|
|
469
|
+
return reply.send({ values: state.values, next: state.next ?? [], tasks: state.tasks ?? [] });
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
server.get("/health", async () => ({ status: "ok", apps: Object.keys(apps) }));
|
|
473
|
+
|
|
474
|
+
// Container runtimes send SIGTERM on stop — drain instead of dying mid-request.
|
|
475
|
+
let shuttingDown = false;
|
|
476
|
+
for (const signal of ["SIGTERM", "SIGINT"] as const) {
|
|
477
|
+
process.on(signal, () => {
|
|
478
|
+
if (shuttingDown) return;
|
|
479
|
+
shuttingDown = true;
|
|
480
|
+
server.close().then(() => process.exit(0)).catch(() => process.exit(1));
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
await server.listen({ port: PORT, host: "0.0.0.0" });
|
|
485
|
+
console.log(\`Server running at http://localhost:\${PORT}\`);
|
|
486
|
+
console.log(\`Apps: \${Object.keys(apps).join(", ")}\`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
start().catch((err) => {
|
|
490
|
+
console.error("Failed to start server:", err);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
});
|
|
493
|
+
`;
|
|
494
|
+
}
|
|
495
|
+
function generateScriptedModel() {
|
|
496
|
+
return `import { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
|
497
|
+
import { AIMessage, type BaseMessage } from "@langchain/core/messages";
|
|
498
|
+
import type { ChatResult } from "@langchain/core/outputs";
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* A model that replays a queued list of responses, so tests can drive real
|
|
502
|
+
* graphs offline — no API key, no network, fully deterministic.
|
|
503
|
+
*/
|
|
504
|
+
export class ScriptedToolCallingModel extends BaseChatModel {
|
|
505
|
+
private queue: AIMessage[];
|
|
506
|
+
|
|
507
|
+
constructor(queue: AIMessage[]) {
|
|
508
|
+
super({});
|
|
509
|
+
this.queue = [...queue];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
_llmType(): string {
|
|
513
|
+
return "scripted-tool-calling";
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
override bindTools(): this {
|
|
517
|
+
return this;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async _generate(_messages: BaseMessage[]): Promise<ChatResult> {
|
|
521
|
+
const message = this.queue.shift();
|
|
522
|
+
if (!message) throw new Error("Scripted model ran out of responses");
|
|
523
|
+
return { generations: [{ message, text: "" }] };
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
`;
|
|
527
|
+
}
|
|
528
|
+
function generateAgentTest(config) {
|
|
529
|
+
const hasSupervisor = config.patterns.includes("supervisor");
|
|
530
|
+
const supervisorTest = hasSupervisor
|
|
531
|
+
? `
|
|
532
|
+
it("delegates to a subagent and returns its answer", async () => {
|
|
533
|
+
const llm = new ScriptedToolCallingModel([
|
|
534
|
+
// supervisor delegates
|
|
535
|
+
new AIMessage({
|
|
536
|
+
content: "",
|
|
537
|
+
tool_calls: [{ id: "c1", name: "worker", args: { task: "do the thing" } }],
|
|
538
|
+
}),
|
|
539
|
+
// subagent answers
|
|
540
|
+
new AIMessage("worker done"),
|
|
541
|
+
// supervisor wraps up
|
|
542
|
+
new AIMessage("All finished."),
|
|
543
|
+
]);
|
|
544
|
+
|
|
545
|
+
const worker = makeAgent({ name: "worker", llm, tools: [] });
|
|
546
|
+
const app = await makeSupervisor({
|
|
547
|
+
subagents: [{ name: "worker", description: "Does the thing.", agent: worker }],
|
|
548
|
+
llm,
|
|
549
|
+
checkpointer: new MemorySaver(),
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
const result = await app.invoke(
|
|
553
|
+
{ messages: [{ role: "user", content: "do the thing" }] },
|
|
554
|
+
{ configurable: { thread_id: "t1" } }
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
expect(result.messages.at(-1)?.content).toBe("All finished.");
|
|
558
|
+
// the supervisor sees the subagent's final answer, not its internals
|
|
559
|
+
const toolMessages = result.messages.filter((m) => m.getType() === "tool");
|
|
560
|
+
expect(toolMessages).toHaveLength(1);
|
|
561
|
+
expect(toolMessages[0].content).toBe("worker done");
|
|
562
|
+
});
|
|
563
|
+
`
|
|
564
|
+
: "";
|
|
565
|
+
const supervisorImports = hasSupervisor
|
|
566
|
+
? `import { makeSupervisor } from "../src/agents/supervisor";\nimport { MemorySaver } from "@langchain/langgraph";\n`
|
|
567
|
+
: "";
|
|
568
|
+
return `import { describe, expect, it } from "vitest";
|
|
569
|
+
import { AIMessage } from "@langchain/core/messages";
|
|
570
|
+
import { makeAgent } from "../src/agents/factory";
|
|
571
|
+
${supervisorImports}import { ScriptedToolCallingModel } from "./helpers/scripted-model";
|
|
572
|
+
|
|
573
|
+
describe("agents", () => {
|
|
574
|
+
it("runs a single agent to a final answer", async () => {
|
|
575
|
+
const llm = new ScriptedToolCallingModel([new AIMessage("Hello there.")]);
|
|
576
|
+
const agent = makeAgent({ name: "assistant", llm, tools: [], system: "Be brief." });
|
|
577
|
+
|
|
578
|
+
const result = await agent.invoke({ messages: [{ role: "user", content: "hi" }] });
|
|
579
|
+
|
|
580
|
+
expect(result.messages.at(-1)?.content).toBe("Hello there.");
|
|
581
|
+
});
|
|
582
|
+
${supervisorTest}});
|
|
583
|
+
`;
|
|
584
|
+
}
|
|
266
585
|
// Pattern-specific: only files for selected patterns are generated
|
|
267
586
|
function getPatternFiles(config) {
|
|
268
587
|
const files = [];
|
|
@@ -303,8 +622,11 @@ export async function createSupervisorApp() {
|
|
|
303
622
|
});
|
|
304
623
|
|
|
305
624
|
return makeSupervisor({
|
|
306
|
-
|
|
307
|
-
|
|
625
|
+
subagents: [
|
|
626
|
+
{ name: "math_expert", description: "Delegate calculations to the math expert.", agent: math },
|
|
627
|
+
{ name: "writer", description: "Delegate writing. Include all facts the text should contain.", agent: writer },
|
|
628
|
+
],
|
|
629
|
+
llm,
|
|
308
630
|
supervisorName: "supervisor",
|
|
309
631
|
});
|
|
310
632
|
}
|
|
@@ -322,56 +644,131 @@ export async function createSupervisorApp() {
|
|
|
322
644
|
if (config.patterns.includes("swarm")) {
|
|
323
645
|
files.push({
|
|
324
646
|
path: "src/agents/swarm.ts",
|
|
325
|
-
content: `import
|
|
326
|
-
import {
|
|
327
|
-
import {
|
|
647
|
+
content: `import { z } from "zod";
|
|
648
|
+
import { AIMessage } from "@langchain/core/messages";
|
|
649
|
+
import {
|
|
650
|
+
StateGraph,
|
|
651
|
+
StateSchema,
|
|
652
|
+
MessagesValue,
|
|
653
|
+
MemorySaver,
|
|
654
|
+
START,
|
|
655
|
+
END,
|
|
656
|
+
} from "@langchain/langgraph";
|
|
657
|
+
import type { BaseCheckpointSaver } from "@langchain/langgraph-checkpoint";
|
|
658
|
+
import type { AgentGraph } from "./factory";
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Swarm via the "handoffs" pattern: each agent is a graph node, and handoff
|
|
662
|
+
* tools (see ./handoff.ts) jump between nodes while flipping activeAgent.
|
|
663
|
+
* activeAgent is checkpointed, so the conversation resumes with whichever
|
|
664
|
+
* agent last held it.
|
|
665
|
+
*/
|
|
328
666
|
|
|
329
|
-
export const SwarmState =
|
|
330
|
-
|
|
331
|
-
activeAgent:
|
|
667
|
+
export const SwarmState = new StateSchema({
|
|
668
|
+
messages: MessagesValue,
|
|
669
|
+
activeAgent: z.string().optional(),
|
|
332
670
|
});
|
|
333
671
|
|
|
334
|
-
type
|
|
672
|
+
export type SwarmStateType = typeof SwarmState.State;
|
|
335
673
|
|
|
336
|
-
export
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
674
|
+
export interface SwarmAgentSpec {
|
|
675
|
+
/** Node name; handoff tools reference it as transfer_to_<name>. */
|
|
676
|
+
name: string;
|
|
677
|
+
agent: AgentGraph;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export interface MakeSwarmParams {
|
|
681
|
+
agents: SwarmAgentSpec[];
|
|
340
682
|
defaultActiveAgent: string;
|
|
341
683
|
checkpointer?: BaseCheckpointSaver;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export function makeSwarm({
|
|
687
|
+
agents,
|
|
688
|
+
defaultActiveAgent,
|
|
689
|
+
checkpointer,
|
|
690
|
+
}: MakeSwarmParams) {
|
|
691
|
+
const names = agents.map((a) => a.name);
|
|
692
|
+
|
|
693
|
+
const routeToActive = (state: SwarmStateType) =>
|
|
694
|
+
state.activeAgent ?? defaultActiveAgent;
|
|
695
|
+
|
|
696
|
+
// After an agent runs: done if it answered (no pending tool calls);
|
|
697
|
+
// otherwise a handoff moved activeAgent, so continue there.
|
|
698
|
+
const routeAfterAgent = (state: SwarmStateType) => {
|
|
699
|
+
const last = state.messages.at(-1);
|
|
700
|
+
if (last && AIMessage.isInstance(last) && !last.tool_calls?.length) {
|
|
701
|
+
return END;
|
|
702
|
+
}
|
|
703
|
+
return state.activeAgent ?? defaultActiveAgent;
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// Node names are only known at runtime, so the graph is built with
|
|
707
|
+
// string keys and the routers are cast to the node-name union.
|
|
708
|
+
let builder = new StateGraph(SwarmState) as StateGraph<
|
|
709
|
+
typeof SwarmState,
|
|
710
|
+
SwarmStateType
|
|
711
|
+
>;
|
|
712
|
+
for (const { name, agent } of agents) {
|
|
713
|
+
builder = builder.addNode(name, async (state: SwarmStateType) =>
|
|
714
|
+
agent.invoke(state)
|
|
715
|
+
) as typeof builder;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
builder.addConditionalEdges(START, routeToActive as never, names as never);
|
|
719
|
+
for (const name of names) {
|
|
720
|
+
builder.addConditionalEdges(
|
|
721
|
+
name as never,
|
|
722
|
+
routeAfterAgent as never,
|
|
723
|
+
[...names, END] as never
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return builder.compile({
|
|
728
|
+
checkpointer: checkpointer ?? new MemorySaver(),
|
|
729
|
+
});
|
|
346
730
|
}
|
|
347
731
|
`,
|
|
348
732
|
});
|
|
349
733
|
files.push({
|
|
350
734
|
path: "src/agents/handoff.ts",
|
|
351
735
|
content: `import { z } from "zod";
|
|
352
|
-
import {
|
|
353
|
-
import {
|
|
354
|
-
import { Command
|
|
736
|
+
import { tool, type ToolRuntime } from "@langchain/core/tools";
|
|
737
|
+
import { AIMessage, ToolMessage } from "@langchain/core/messages";
|
|
738
|
+
import { Command } from "@langchain/langgraph";
|
|
739
|
+
import type { SwarmStateType } from "./swarm";
|
|
355
740
|
|
|
741
|
+
/**
|
|
742
|
+
* Handoff tool for the handoffs pattern. Returns a Command targeting the
|
|
743
|
+
* PARENT graph: it flips activeAgent and jumps to the target agent's node.
|
|
744
|
+
* The calling agent's last AI message (the one containing this tool call)
|
|
745
|
+
* plus a ToolMessage are copied into parent state so the conversation
|
|
746
|
+
* history stays well-formed.
|
|
747
|
+
*/
|
|
356
748
|
export function createHandoffTool({ agentName, description }: { agentName: string; description?: string }) {
|
|
357
|
-
const toolName = \`transfer_to_\${agentName.replace(/\\s+/g, "_").toLowerCase()}\`;
|
|
358
|
-
|
|
359
749
|
return tool(
|
|
360
|
-
async (
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
750
|
+
async (_, runtime: ToolRuntime<SwarmStateType>) => {
|
|
751
|
+
const lastAiMessage = [...(runtime.state.messages ?? [])]
|
|
752
|
+
.reverse()
|
|
753
|
+
.find(AIMessage.isInstance);
|
|
754
|
+
const transferMessage = new ToolMessage({
|
|
364
755
|
content: \`Transferred to \${agentName}\`,
|
|
365
|
-
|
|
366
|
-
tool_call_id: cfg.toolCall.id,
|
|
756
|
+
tool_call_id: runtime.toolCallId ?? "",
|
|
367
757
|
});
|
|
368
758
|
return new Command({
|
|
369
759
|
goto: agentName,
|
|
760
|
+
update: {
|
|
761
|
+
activeAgent: agentName,
|
|
762
|
+
messages: [lastAiMessage, transferMessage].filter(Boolean),
|
|
763
|
+
},
|
|
370
764
|
graph: Command.PARENT,
|
|
371
|
-
update: { messages: messages.concat(tm), activeAgent: agentName },
|
|
372
765
|
});
|
|
373
766
|
},
|
|
374
|
-
{
|
|
767
|
+
{
|
|
768
|
+
name: \`transfer_to_\${agentName}\`,
|
|
769
|
+
description: description ?? \`Transfer the conversation to \${agentName}.\`,
|
|
770
|
+
schema: z.object({}),
|
|
771
|
+
}
|
|
375
772
|
);
|
|
376
773
|
}
|
|
377
774
|
`,
|
|
@@ -383,7 +780,7 @@ import { z } from "zod";
|
|
|
383
780
|
import { tool } from "@langchain/core/tools";
|
|
384
781
|
import { makeAgent } from "../agents/factory";
|
|
385
782
|
import { createHandoffTool } from "../agents/handoff";
|
|
386
|
-
import { makeSwarm
|
|
783
|
+
import { makeSwarm } from "../agents/swarm";
|
|
387
784
|
|
|
388
785
|
const add = tool(async ({ a, b }) => String(a + b), {
|
|
389
786
|
name: "add", description: "Add two numbers",
|
|
@@ -411,7 +808,10 @@ export async function createSwarmApp() {
|
|
|
411
808
|
});
|
|
412
809
|
|
|
413
810
|
return makeSwarm({
|
|
414
|
-
agents: [
|
|
811
|
+
agents: [
|
|
812
|
+
{ name: "alice", agent: alice },
|
|
813
|
+
{ name: "bob", agent: bob },
|
|
814
|
+
],
|
|
415
815
|
defaultActiveAgent: "alice",
|
|
416
816
|
});
|
|
417
817
|
}
|
|
@@ -431,10 +831,9 @@ export async function createSwarmApp() {
|
|
|
431
831
|
path: "src/apps/interrupt.ts",
|
|
432
832
|
content: `import { z } from "zod";
|
|
433
833
|
import { tool } from "@langchain/core/tools";
|
|
434
|
-
import { interrupt } from "@langchain/langgraph";
|
|
834
|
+
import { interrupt, MemorySaver } from "@langchain/langgraph";
|
|
435
835
|
import { getLlm } from "../config/llm";
|
|
436
836
|
import { makeAgent } from "../agents/factory";
|
|
437
|
-
import { makeSupervisor } from "../agents/supervisor";
|
|
438
837
|
|
|
439
838
|
const deleteRecord = tool(
|
|
440
839
|
async (args) => {
|
|
@@ -457,16 +856,13 @@ const deleteRecord = tool(
|
|
|
457
856
|
export async function createInterruptApp() {
|
|
458
857
|
const llm = await getLlm();
|
|
459
858
|
|
|
460
|
-
|
|
859
|
+
// A single agent with a checkpointer — interrupt() inside delete_record
|
|
860
|
+
// pauses the graph; resume with Command({ resume: "yes" }) on the thread.
|
|
861
|
+
return makeAgent({
|
|
461
862
|
name: "db_admin", llm,
|
|
462
863
|
tools: [deleteRecord],
|
|
463
864
|
system: "You are a database administrator.",
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
return makeSupervisor({
|
|
467
|
-
agents: [dbAdmin], llm,
|
|
468
|
-
outputMode: "last_message",
|
|
469
|
-
supervisorName: "interrupt_supervisor",
|
|
865
|
+
checkpointer: new MemorySaver(),
|
|
470
866
|
});
|
|
471
867
|
}
|
|
472
868
|
`,
|
|
@@ -491,9 +887,10 @@ export async function createInterruptApp() {
|
|
|
491
887
|
files.push({
|
|
492
888
|
path: "src/apps/analyst.ts",
|
|
493
889
|
content: `import { z } from "zod";
|
|
890
|
+
import { toolStrategy } from "langchain";
|
|
891
|
+
import { MemorySaver } from "@langchain/langgraph";
|
|
494
892
|
import { getLlm } from "../config/llm";
|
|
495
893
|
import { makeAgent } from "../agents/factory";
|
|
496
|
-
import { makeSupervisor } from "../agents/supervisor";
|
|
497
894
|
|
|
498
895
|
const SummarySchema = z.object({
|
|
499
896
|
title: z.string(),
|
|
@@ -504,16 +901,14 @@ const SummarySchema = z.object({
|
|
|
504
901
|
export async function createAnalystApp() {
|
|
505
902
|
const llm = await getLlm();
|
|
506
903
|
|
|
507
|
-
|
|
904
|
+
// The structured result is returned on the structuredResponse key of the
|
|
905
|
+
// final state. toolStrategy works with every provider; models with native
|
|
906
|
+
// structured output support could use providerStrategy instead.
|
|
907
|
+
return makeAgent({
|
|
508
908
|
name: "analyst", llm, tools: [],
|
|
509
909
|
system: "Analyze text and produce structured summaries.",
|
|
510
|
-
responseFormat: SummarySchema,
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
return makeSupervisor({
|
|
514
|
-
agents: [analyst], llm,
|
|
515
|
-
outputMode: "last_message",
|
|
516
|
-
supervisorName: "analyst_supervisor",
|
|
910
|
+
responseFormat: toolStrategy(SummarySchema),
|
|
911
|
+
checkpointer: new MemorySaver(),
|
|
517
912
|
});
|
|
518
913
|
}
|
|
519
914
|
`,
|
|
@@ -525,14 +920,15 @@ export async function createAnalystApp() {
|
|
|
525
920
|
{ messages: [{ role: "user", content: "Analyze: Revenue grew 25% but churn increased 8%." }] },
|
|
526
921
|
{ configurable: { thread_id: "analyst-demo" } }
|
|
527
922
|
);
|
|
528
|
-
|
|
923
|
+
const structured = (analysis as Record<string, unknown>).structuredResponse;
|
|
924
|
+
console.log("Result:", JSON.stringify(structured ?? analysis.messages.at(-1)?.content));`);
|
|
529
925
|
}
|
|
530
926
|
if (config.patterns.includes("rag")) {
|
|
531
927
|
files.push({
|
|
532
928
|
path: "src/apps/rag.ts",
|
|
533
|
-
content: `import {
|
|
929
|
+
content: `import { MemorySaver } from "@langchain/langgraph";
|
|
930
|
+
import { getLlm } from "../config/llm";
|
|
534
931
|
import { makeAgent } from "../agents/factory";
|
|
535
|
-
import { makeSupervisor } from "../agents/supervisor";
|
|
536
932
|
// TODO: Add your vector store, embeddings, and retrieval tool here.
|
|
537
933
|
// See the full starter kit for a complete RAG implementation:
|
|
538
934
|
// https://github.com/ac12644/langgraph-starter-kit
|
|
@@ -540,15 +936,10 @@ import { makeSupervisor } from "../agents/supervisor";
|
|
|
540
936
|
export async function createRagApp() {
|
|
541
937
|
const llm = await getLlm();
|
|
542
938
|
|
|
543
|
-
|
|
939
|
+
return makeAgent({
|
|
544
940
|
name: "rag_agent", llm, tools: [],
|
|
545
941
|
system: "You are a knowledgeable assistant. Answer questions based on your knowledge.",
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
return makeSupervisor({
|
|
549
|
-
agents: [ragAgent], llm,
|
|
550
|
-
outputMode: "last_message",
|
|
551
|
-
supervisorName: "rag_supervisor",
|
|
942
|
+
checkpointer: new MemorySaver(),
|
|
552
943
|
});
|
|
553
944
|
}
|
|
554
945
|
`,
|
|
@@ -562,6 +953,11 @@ export async function createRagApp() {
|
|
|
562
953
|
);
|
|
563
954
|
console.log("Result:", rag.messages.at(-1)?.content);`);
|
|
564
955
|
}
|
|
956
|
+
// HTTP server — routes only the apps that were generated
|
|
957
|
+
files.push({ path: "src/server.ts", content: generateServer(config) });
|
|
958
|
+
// Tests — offline, no API key needed
|
|
959
|
+
files.push({ path: "tests/helpers/scripted-model.ts", content: generateScriptedModel() });
|
|
960
|
+
files.push({ path: "tests/agents.test.ts", content: generateAgentTest(config) });
|
|
565
961
|
// Generate index.ts
|
|
566
962
|
files.push({
|
|
567
963
|
path: "src/index.ts",
|