create-langgraph-app 1.4.0 → 1.5.1

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/README.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  Interactive CLI to scaffold a new LangGraph multi-agent project in seconds.
4
4
 
5
- ## Usage
6
-
7
5
  ```bash
8
6
  npx create-langgraph-app
9
7
  ```
@@ -11,28 +9,68 @@ npx create-langgraph-app
11
9
  ## What it does
12
10
 
13
11
  1. Asks for your project name
14
- 2. Lets you pick an LLM provider (OpenAI, Anthropic, Google, Groq, DeepSeek, Ollama)
12
+ 2. Lets you pick an LLM provider — OpenAI, Anthropic, Google, Groq, DeepSeek, or Ollama
15
13
  3. Lets you select which agent patterns to include:
16
- - **Supervisor** — central coordinator + worker agents
14
+ - **Supervisor** — central coordinator delegating to worker agents
17
15
  - **Swarm** — peer-to-peer agent handoffs
18
16
  - **Human-in-the-Loop** — approval before dangerous actions
19
- - **Structured Output** — typed JSON responses
20
- - **RAG** — retrieval-augmented generation
21
- 4. Generates a ready-to-run project with only the patterns you selected
17
+ - **Structured Output** — typed JSON responses validated by Zod
18
+ - **RAG** — retrieval-augmented generation over an in-memory vector store
19
+ 4. Generates a ready-to-run project containing only the patterns you picked
22
20
  5. Installs dependencies
23
21
 
22
+ ## What you get
23
+
24
+ ```
25
+ src/
26
+ ├── config/ env validation, LLM factory, embeddings (with RAG)
27
+ ├── agents/ makeAgent() and the helpers your patterns need
28
+ ├── apps/ one file per selected pattern
29
+ ├── tools/ retrieval tool + vector store (with RAG)
30
+ ├── server.ts Fastify server — invoke, stream, resume, threads
31
+ └── index.ts CLI demo running each pattern
32
+ tests/ offline tests that need no API key
33
+ ```
34
+
35
+ Everything is built on LangChain's `createAgent` — the **subagents** pattern for
36
+ supervisors, **handoffs** for swarms. No deprecated packages.
37
+
38
+ ## RAG and embeddings
39
+
40
+ Selecting RAG generates a working implementation: chunking, embedding,
41
+ cosine-similarity search, and a retrieval tool the agent calls before answering.
42
+ Swap `SAMPLE_DOCS` in `src/tools/rag.ts` for your own documents.
43
+
44
+ Embeddings are a separate capability from chat. OpenAI, Google and Ollama have
45
+ their own; **Anthropic, Groq and DeepSeek do not**, so those projects use OpenAI
46
+ embeddings and need `OPENAI_API_KEY` in addition to the chat provider's key. The
47
+ generated `.env` says so, and `EMBEDDINGS_MODEL` overrides the model.
48
+
49
+ To run RAG with no API key at all, scaffold with Ollama.
50
+
51
+ If embeddings are unavailable, only RAG is affected — the other patterns and
52
+ routes keep working, and the server logs why `/rag` is disabled.
53
+
24
54
  ## Development
25
55
 
26
56
  ```bash
27
57
  cd create-langgraph-app
28
58
  npm install
29
- npm run dev # Run locally
59
+ npm run dev # run the CLI locally
60
+ npm run build # typecheck and emit dist/
30
61
  ```
31
62
 
32
- ## Publishing
63
+ Generated projects are compiled in CI, so a change that breaks the output fails
64
+ the build. See `tests/scaffolder/` in the [starter kit](https://github.com/ac12644/langgraph-starter-kit).
65
+
66
+ ## Releasing
67
+
68
+ Publishing is automated. Bump the version in `package.json`, then push a
69
+ matching `cli-v*` tag:
33
70
 
34
71
  ```bash
35
- cd create-langgraph-app
36
- npm run build
37
- npm publish
72
+ git tag -a cli-v1.5.0 -m "create-langgraph-app v1.5.0"
73
+ git push origin cli-v1.5.0
38
74
  ```
75
+
76
+ The `Publish CLI to npm` workflow builds and publishes with provenance.
package/dist/create.d.ts CHANGED
@@ -1,2 +1,13 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ export interface Config {
3
+ name: string;
4
+ provider: string;
5
+ patterns: string[];
6
+ }
7
+ export declare function generateEnv(config: Config): string;
8
+ export declare function generatePackageJson(config: Config): string;
9
+ export declare function generateTsConfig(): string;
10
+ export declare function getPatternFiles(config: Config): {
11
+ path: string;
12
+ content: string;
13
+ }[];
package/dist/create.js CHANGED
@@ -4,6 +4,7 @@ import { stdin, stdout } from "node:process";
4
4
  import * as fs from "node:fs";
5
5
  import * as path from "node:path";
6
6
  import { execSync } from "node:child_process";
7
+ import { pathToFileURL } from "node:url";
7
8
  // ── Helpers ──────────────────────────────────────────────────────────────────
8
9
  const BOLD = "\x1b[1m";
9
10
  const DIM = "\x1b[2m";
@@ -54,7 +55,25 @@ const PROVIDERS = {
54
55
  ollama: { envKey: "", defaultModel: "llama3.2" },
55
56
  deepseek: { envKey: "DEEPSEEK_API_KEY", defaultModel: "deepseek-v4-flash" },
56
57
  };
57
- function generateEnv(config) {
58
+ /**
59
+ * Extra .env lines when RAG borrows another provider's embeddings — that key is
60
+ * required too, which is not obvious from the chat provider alone.
61
+ */
62
+ function embeddingsEnvLines(config) {
63
+ if (!config.patterns.includes("rag"))
64
+ return [];
65
+ const e = embeddingsFor(config.provider);
66
+ if (!e.borrowed)
67
+ return [];
68
+ return [
69
+ `# RAG embeddings: ${config.provider} has no embeddings API, so RAG uses ${e.id}`,
70
+ `# and needs this key in addition to your chat provider's.`,
71
+ `${e.envKey}=`,
72
+ `# EMBEDDINGS_MODEL=${e.defaultModel}`,
73
+ ``,
74
+ ];
75
+ }
76
+ export function generateEnv(config) {
58
77
  const prov = PROVIDERS[config.provider];
59
78
  const lines = [
60
79
  `# LLM Provider`,
@@ -68,12 +87,12 @@ function generateEnv(config) {
68
87
  else {
69
88
  lines.push(`# No API key needed for ${config.provider}`);
70
89
  }
71
- lines.push(``, `# Model (optional — defaults to ${prov.defaultModel})`, `# LLM_MODEL=${prov.defaultModel}`, `LLM_TEMPERATURE=0`, ``, ...(config.provider === "deepseek"
90
+ lines.push(``, `# Model (optional — defaults to ${prov.defaultModel})`, `# LLM_MODEL=${prov.defaultModel}`, `LLM_TEMPERATURE=0`, ``, ...embeddingsEnvLines(config), ...(config.provider === "deepseek"
72
91
  ? [`# DeepSeek reasoning mode (optional — "disabled" | "enabled")`, `# DEEPSEEK_THINKING=disabled`, ``]
73
92
  : []), `PORT=3000`, ``, `# LangSmith tracing (optional)`, `# LANGCHAIN_TRACING_V2=true`, `# LANGSMITH_API_KEY=`, `# LANGSMITH_PROJECT=${config.name}`);
74
93
  return lines.join("\n") + "\n";
75
94
  }
76
- function generatePackageJson(config) {
95
+ export function generatePackageJson(config) {
77
96
  const deps = {
78
97
  "@langchain/core": "^1.2.3",
79
98
  "@langchain/langgraph": "^1.4.8",
@@ -96,6 +115,11 @@ function generatePackageJson(config) {
96
115
  deepseek: "@langchain/deepseek",
97
116
  };
98
117
  deps[provPkg[config.provider]] = "latest";
118
+ // RAG needs an embeddings SDK, which is a different package when the chat
119
+ // provider has no embeddings API of its own (anthropic, groq, deepseek).
120
+ if (config.patterns.includes("rag")) {
121
+ deps[embeddingsFor(config.provider).pkg] ??= "latest";
122
+ }
99
123
  const pkg = {
100
124
  name: config.name,
101
125
  version: "0.1.0",
@@ -118,7 +142,7 @@ function generatePackageJson(config) {
118
142
  };
119
143
  return JSON.stringify(pkg, null, 2) + "\n";
120
144
  }
121
- function generateTsConfig() {
145
+ export function generateTsConfig() {
122
146
  return JSON.stringify({
123
147
  compilerOptions: {
124
148
  target: "ES2022",
@@ -183,6 +207,63 @@ const PROVIDER_LLM = {
183
207
  ollama: { pkg: "@langchain/ollama", className: "ChatOllama", modelArg: "model", defaultModel: "llama3.2" },
184
208
  deepseek: { pkg: "@langchain/deepseek", className: "ChatDeepSeek", modelArg: "model", defaultModel: "deepseek-v4-flash", extraArgs: 'modelKwargs: { thinking: { type: DEEPSEEK_THINKING } }' },
185
209
  };
210
+ // Only these three have a native embeddings API. Anthropic, Groq and DeepSeek
211
+ // have none, so RAG on those providers borrows OpenAI's.
212
+ const PROVIDER_EMBEDDINGS = {
213
+ openai: { pkg: "@langchain/openai", className: "OpenAIEmbeddings", defaultModel: "text-embedding-3-small", envKey: "OPENAI_API_KEY" },
214
+ google: { pkg: "@langchain/google-genai", className: "GoogleGenerativeAIEmbeddings", defaultModel: "text-embedding-004", envKey: "GOOGLE_API_KEY" },
215
+ ollama: { pkg: "@langchain/ollama", className: "OllamaEmbeddings", defaultModel: "nomic-embed-text", envKey: "" },
216
+ };
217
+ /** Resolves which provider supplies embeddings for a given chat provider. */
218
+ function embeddingsFor(chatProvider) {
219
+ const id = chatProvider in PROVIDER_EMBEDDINGS ? chatProvider : "openai";
220
+ return { id, borrowed: id !== chatProvider, ...PROVIDER_EMBEDDINGS[id] };
221
+ }
222
+ /**
223
+ * The scaffold installs one embeddings SDK — the one for the provider resolved
224
+ * at generation time — so the generated config imports only that SDK. Switching
225
+ * EMBEDDINGS_PROVIDER later means installing the matching package and adding a
226
+ * branch here.
227
+ */
228
+ function generateEmbeddingsConfig(config) {
229
+ const e = embeddingsFor(config.provider);
230
+ const borrowedNote = e.borrowed
231
+ ? `// ${config.provider} has no embeddings API, so RAG uses ${e.id} — it needs\n` +
232
+ `// ${e.envKey} in addition to your chat provider's key.\n`
233
+ : "";
234
+ return `import type { Embeddings } from "@langchain/core/embeddings";
235
+ import { ${e.className} } from "${e.pkg}";
236
+
237
+ ${borrowedNote}// Set EMBEDDINGS_MODEL in .env to override the default model.
238
+ const PROVIDER = "${e.id}";
239
+ const DEFAULT_MODEL = "${e.defaultModel}";
240
+ const REQUIRED_KEY = "${e.envKey}";
241
+
242
+ /**
243
+ * Builds the embeddings client for RAG.
244
+ *
245
+ * Validation happens here rather than at import time: a project that never uses
246
+ * RAG should not need an embeddings key, and an eager throw would take down
247
+ * every route rather than just the RAG one.
248
+ */
249
+ export async function createEmbeddings(modelOverride?: string): Promise<Embeddings> {
250
+ const requested = process.env.EMBEDDINGS_PROVIDER?.trim().toLowerCase();
251
+ if (requested && requested !== PROVIDER) {
252
+ throw new Error(
253
+ \`EMBEDDINGS_PROVIDER="\${requested}" but this project was scaffolded with "\${PROVIDER}" embeddings. \` +
254
+ \`Install the matching SDK and add a branch in src/config/embeddings.ts.\`
255
+ );
256
+ }
257
+
258
+ if (REQUIRED_KEY && !process.env[REQUIRED_KEY]) {
259
+ throw new Error(\`\${REQUIRED_KEY} is required for "\${PROVIDER}" embeddings but is not set in .env\`);
260
+ }
261
+
262
+ const model = modelOverride ?? process.env.EMBEDDINGS_MODEL?.trim() ?? DEFAULT_MODEL;
263
+ return new ${e.className}({ model });
264
+ }
265
+ `;
266
+ }
186
267
  function generateLlmConfig(config) {
187
268
  const p = PROVIDER_LLM[config.provider] ?? PROVIDER_LLM.openai;
188
269
  const envImports = config.provider === "deepseek"
@@ -363,7 +444,27 @@ function selectedApps(config) {
363
444
  function generateServer(config) {
364
445
  const apps = selectedApps(config);
365
446
  const imports = apps.map((a) => `import { ${a.fn} } from "${a.path}";`).join("\n");
366
- const entries = apps.map((a) => ` "${a.route}": asApp(await ${a.fn}()),`).join("\n");
447
+ // RAG is the only app that can fail at startup — it needs an embeddings
448
+ // provider, and providers without one borrow another provider's key. Build it
449
+ // in a try/catch so a missing key costs /rag rather than every route.
450
+ const hasRag = apps.some((a) => a.route === "rag");
451
+ const entries = apps
452
+ .filter((a) => a.route !== "rag")
453
+ .map((a) => ` "${a.route}": asApp(await ${a.fn}()),`)
454
+ .join("\n");
455
+ const ragEntry = hasRag ? `\n ...(ragApp ? { rag: ragApp } : {}),` : "";
456
+ const ragInit = hasRag
457
+ ? ` let ragApp: AppGraph | undefined;
458
+ try {
459
+ ragApp = asApp(await createRagApp());
460
+ } catch (err) {
461
+ const msg = err instanceof Error ? err.message : String(err);
462
+ console.warn(\`RAG app unavailable (embeddings failed): \${msg}\`);
463
+ console.warn(" /rag routes are disabled; every other app still works.");
464
+ }
465
+
466
+ `
467
+ : "";
367
468
  return `import "./config/env";
368
469
  import { fastify, type FastifyError, type FastifyReply, type FastifyRequest } from "fastify";
369
470
  import { Command } from "@langchain/langgraph";
@@ -384,19 +485,35 @@ function httpError(status: number, message: string) {
384
485
  return Object.assign(new Error(message), { statusCode: status });
385
486
  }
386
487
 
488
+ // \`system\` is excluded: the app owns its system prompt, and accepting one
489
+ // from the request lets any caller override it.
490
+ const CLIENT_ROLES = ["user", "assistant"];
491
+
387
492
  /** Reject malformed bodies before they reach the agent. */
388
493
  function validateMessages(raw: unknown): { role: string; content: string }[] {
389
- if (raw === undefined) return [];
390
494
  if (!Array.isArray(raw)) throw httpError(400, '"messages" must be an array');
495
+ if (raw.length === 0) throw httpError(400, '"messages" must not be empty');
391
496
  return raw.map((m, i) => {
392
497
  const { role, content } = (m ?? {}) as Record<string, unknown>;
393
498
  if (typeof role !== "string" || typeof content !== "string") {
394
499
  throw httpError(400, \`messages[\${i}] needs string "role" and "content"\`);
395
500
  }
501
+ if (!CLIENT_ROLES.includes(role)) {
502
+ throw httpError(400, \`messages[\${i}].role must be one of: \${CLIENT_ROLES.join(", ")}\`);
503
+ }
396
504
  return { role, content };
397
505
  });
398
506
  }
399
507
 
508
+ /** Defaults to "default"; anything but a non-empty string is a 400, not a 500. */
509
+ function validateThreadId(raw: unknown): string {
510
+ if (raw === undefined) return "default";
511
+ if (typeof raw !== "string" || raw.trim() === "") {
512
+ throw httpError(400, '"thread_id" must be a non-empty string');
513
+ }
514
+ return raw;
515
+ }
516
+
400
517
  const server = fastify({ logger: false });
401
518
 
402
519
  server.setErrorHandler(async (error: FastifyError, _req: FastifyRequest, reply: FastifyReply) => {
@@ -405,8 +522,8 @@ server.setErrorHandler(async (error: FastifyError, _req: FastifyRequest, reply:
405
522
  });
406
523
 
407
524
  async function start() {
408
- const apps: Record<string, AppGraph> = {
409
- ${entries}
525
+ ${ragInit} const apps: Record<string, AppGraph> = {
526
+ ${entries}${ragEntry}
410
527
  };
411
528
 
412
529
  const getApp = (name: string) => {
@@ -417,9 +534,9 @@ ${entries}
417
534
 
418
535
  server.post<{ Params: { app: string } }>("/:app/invoke", async (req, reply) => {
419
536
  const app = getApp(req.params.app);
420
- const body = (req.body ?? {}) as { messages?: unknown; thread_id?: string };
537
+ const body = (req.body ?? {}) as { messages?: unknown; thread_id?: unknown };
421
538
  const messages = validateMessages(body.messages);
422
- const thread_id = body.thread_id ?? "default";
539
+ const thread_id = validateThreadId(body.thread_id);
423
540
 
424
541
  const result = await app.invoke({ messages }, { configurable: { thread_id } });
425
542
  const last = result.messages.at(-1);
@@ -432,9 +549,9 @@ ${entries}
432
549
 
433
550
  server.post<{ Params: { app: string } }>("/:app/stream", async (req, reply) => {
434
551
  const app = getApp(req.params.app);
435
- const body = (req.body ?? {}) as { messages?: unknown; thread_id?: string };
552
+ const body = (req.body ?? {}) as { messages?: unknown; thread_id?: unknown };
436
553
  const messages = validateMessages(body.messages);
437
- const thread_id = body.thread_id ?? "default";
554
+ const thread_id = validateThreadId(body.thread_id);
438
555
 
439
556
  reply.raw.writeHead(200, {
440
557
  "Content-Type": "text/event-stream",
@@ -460,12 +577,13 @@ ${entries}
460
577
 
461
578
  server.post<{ Params: { app: string } }>("/:app/resume", async (req, reply) => {
462
579
  const app = getApp(req.params.app);
463
- const body = (req.body ?? {}) as { thread_id?: string; decision?: string };
580
+ const body = (req.body ?? {}) as { thread_id?: unknown; decision?: string };
581
+ const thread_id = validateThreadId(body.thread_id);
464
582
  if (body.decision === undefined) {
465
583
  return reply.status(400).send({ error: '"decision" field is required' });
466
584
  }
467
585
  const result = await app.invoke(new Command({ resume: body.decision }), {
468
- configurable: { thread_id: body.thread_id ?? "default" },
586
+ configurable: { thread_id },
469
587
  });
470
588
  const last = result.messages.at(-1);
471
589
  return reply.send({
@@ -595,7 +713,7 @@ ${supervisorTest}});
595
713
  `;
596
714
  }
597
715
  // Pattern-specific: only files for selected patterns are generated
598
- function getPatternFiles(config) {
716
+ export function getPatternFiles(config) {
599
717
  const files = [];
600
718
  // Always include core files
601
719
  files.push({ path: "src/config/env.ts", content: generateEnvConfig(config) });
@@ -936,21 +1054,129 @@ export async function createAnalystApp() {
936
1054
  console.log("Result:", JSON.stringify(structured ?? analysis.messages.at(-1)?.content));`);
937
1055
  }
938
1056
  if (config.patterns.includes("rag")) {
1057
+ files.push({ path: "src/config/embeddings.ts", content: generateEmbeddingsConfig(config) });
1058
+ files.push({
1059
+ path: "src/tools/rag.ts",
1060
+ content: `import { z } from "zod";
1061
+ import { tool } from "@langchain/core/tools";
1062
+ import type { Embeddings } from "@langchain/core/embeddings";
1063
+ import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
1064
+
1065
+ /** In-memory vector store — no external database required. */
1066
+ export interface InMemoryVectorStore {
1067
+ size: number;
1068
+ search: (query: string, k?: number) => Promise<string[]>;
1069
+ }
1070
+
1071
+ interface Chunk {
1072
+ content: string;
1073
+ embedding: number[];
1074
+ }
1075
+
1076
+ function cosineSimilarity(a: number[], b: number[]): number {
1077
+ let dot = 0, normA = 0, normB = 0;
1078
+ for (let i = 0; i < a.length; i++) {
1079
+ dot += a[i] * b[i];
1080
+ normA += a[i] * a[i];
1081
+ normB += b[i] * b[i];
1082
+ }
1083
+ // Guard the zero vector: without this an empty embedding yields NaN and
1084
+ // silently corrupts the ranking.
1085
+ return normA === 0 || normB === 0 ? 0 : dot / (Math.sqrt(normA) * Math.sqrt(normB));
1086
+ }
1087
+
1088
+ export async function buildVectorStore(
1089
+ embeddings: Embeddings,
1090
+ documents: string[],
1091
+ chunkSize = 500,
1092
+ chunkOverlap = 100,
1093
+ ): Promise<InMemoryVectorStore> {
1094
+ const splitter = new RecursiveCharacterTextSplitter({ chunkSize, chunkOverlap });
1095
+ const docs = await splitter.createDocuments(documents);
1096
+ const texts = docs.map((d) => d.pageContent);
1097
+ const vectors = await embeddings.embedDocuments(texts);
1098
+ const chunks: Chunk[] = texts.map((content, i) => ({ content, embedding: vectors[i] }));
1099
+
1100
+ return {
1101
+ size: chunks.length,
1102
+ search: async (query: string, k = 4) => {
1103
+ if (chunks.length === 0) return [];
1104
+ const queryVector = await embeddings.embedQuery(query);
1105
+ return chunks
1106
+ .map((c) => ({ content: c.content, score: cosineSimilarity(queryVector, c.embedding) }))
1107
+ .sort((a, b) => b.score - a.score)
1108
+ .slice(0, k)
1109
+ .map((c) => c.content);
1110
+ },
1111
+ };
1112
+ }
1113
+
1114
+ export function createRetrievalTool(vectorStore: InMemoryVectorStore) {
1115
+ return tool(
1116
+ async ({ query, k }) => {
1117
+ const results = await vectorStore.search(query, k);
1118
+ if (results.length === 0) return "No relevant documents found. Try rephrasing.";
1119
+ return results.map((r, i) => "[" + (i + 1) + "] " + r).join("\\n\\n");
1120
+ },
1121
+ {
1122
+ name: "search_knowledge_base",
1123
+ description:
1124
+ "Search the knowledge base for relevant information. Use this to find context before answering.",
1125
+ schema: z.object({
1126
+ query: z.string().describe("The search query"),
1127
+ k: z.number().optional().default(4).describe("Number of results to return"),
1128
+ }),
1129
+ }
1130
+ );
1131
+ }
1132
+
1133
+ /** Replace these with your own documents. */
1134
+ export const SAMPLE_DOCS = [
1135
+ "LangGraph is a framework for building stateful, multi-actor applications with LLMs. It uses a graph architecture where nodes are computational steps and edges define the flow between them. Key features include persistence via checkpointing, streaming, and human-in-the-loop workflows.",
1136
+ "The supervisor pattern uses a central coordinator that routes tasks to specialized worker agents and aggregates their results. Workers are stateless and run in isolated context windows, so the supervisor only sees each worker's final answer.",
1137
+ "The swarm pattern lets peer agents hand off control to each other with transfer tools. Each agent is a graph node, and the active agent persists across turns, so a conversation resumes with whichever agent last held it.",
1138
+ ];
1139
+ `,
1140
+ });
939
1141
  files.push({
940
1142
  path: "src/apps/rag.ts",
941
1143
  content: `import { MemorySaver } from "@langchain/langgraph";
942
1144
  import { getLlm } from "../config/llm";
1145
+ import { createEmbeddings } from "../config/embeddings";
943
1146
  import { makeAgent } from "../agents/factory";
944
- // TODO: Add your vector store, embeddings, and retrieval tool here.
945
- // See the full starter kit for a complete RAG implementation:
946
- // https://github.com/ac12644/langgraph-starter-kit
1147
+ import {
1148
+ buildVectorStore,
1149
+ createRetrievalTool,
1150
+ SAMPLE_DOCS,
1151
+ type InMemoryVectorStore,
1152
+ } from "../tools/rag";
1153
+
1154
+ let _vectorStore: InMemoryVectorStore | null = null;
1155
+
1156
+ /** Indexes the documents once; later calls reuse the same store. */
1157
+ export async function initRagStore(documents: string[] = SAMPLE_DOCS): Promise<InMemoryVectorStore> {
1158
+ if (!_vectorStore) {
1159
+ const embeddings = await createEmbeddings();
1160
+ _vectorStore = await buildVectorStore(embeddings, documents);
1161
+ console.log("RAG: indexed " + _vectorStore.size + " chunks");
1162
+ }
1163
+ return _vectorStore;
1164
+ }
947
1165
 
948
- export async function createRagApp() {
1166
+ export async function createRagApp(vectorStore?: InMemoryVectorStore) {
949
1167
  const llm = await getLlm();
1168
+ const retrievalTool = createRetrievalTool(vectorStore ?? (await initRagStore()));
950
1169
 
951
1170
  return makeAgent({
952
- name: "rag_agent", llm, tools: [],
953
- system: "You are a knowledgeable assistant. Answer questions based on your knowledge.",
1171
+ name: "rag_agent",
1172
+ llm,
1173
+ tools: [retrievalTool],
1174
+ system: [
1175
+ "You are a knowledgeable assistant with access to a knowledge base.",
1176
+ "ALWAYS search the knowledge base before answering questions.",
1177
+ "Base your answers on the retrieved documents. If they do not contain",
1178
+ "the answer, say so clearly rather than guessing.",
1179
+ ].join("\\n"),
954
1180
  checkpointer: new MemorySaver(),
955
1181
  });
956
1182
  }
@@ -960,7 +1186,7 @@ export async function createRagApp() {
960
1186
  demos.push(` console.log("\\n=== RAG Demo ===");
961
1187
  const ragApp = await createRagApp();
962
1188
  const rag = await ragApp.invoke(
963
- { messages: [{ role: "user", content: "What is RAG and how does it work?" }] },
1189
+ { messages: [{ role: "user", content: "What is the supervisor pattern?" }] },
964
1190
  { configurable: { thread_id: "rag-demo" } }
965
1191
  );
966
1192
  console.log("Result:", rag.messages.at(-1)?.content);`);
@@ -1048,7 +1274,11 @@ Provider: ${provider}${RESET}
1048
1274
  rl.close();
1049
1275
  }
1050
1276
  }
1051
- main().catch((err) => {
1052
- console.error("Error:", err);
1053
- process.exit(1);
1054
- });
1277
+ // Only run the interactive CLI when executed directly. Importing this module
1278
+ // (e.g. from the scaffolder tests) must not prompt.
1279
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
1280
+ main().catch((err) => {
1281
+ console.error("Error:", err);
1282
+ process.exit(1);
1283
+ });
1284
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-langgraph-app",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Scaffold a new LangGraph multi-agent project in seconds",
5
5
  "type": "module",
6
6
  "bin": {