create-langgraph-app 1.4.0 → 1.5.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.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";
@@ -405,8 +506,8 @@ server.setErrorHandler(async (error: FastifyError, _req: FastifyRequest, reply:
405
506
  });
406
507
 
407
508
  async function start() {
408
- const apps: Record<string, AppGraph> = {
409
- ${entries}
509
+ ${ragInit} const apps: Record<string, AppGraph> = {
510
+ ${entries}${ragEntry}
410
511
  };
411
512
 
412
513
  const getApp = (name: string) => {
@@ -595,7 +696,7 @@ ${supervisorTest}});
595
696
  `;
596
697
  }
597
698
  // Pattern-specific: only files for selected patterns are generated
598
- function getPatternFiles(config) {
699
+ export function getPatternFiles(config) {
599
700
  const files = [];
600
701
  // Always include core files
601
702
  files.push({ path: "src/config/env.ts", content: generateEnvConfig(config) });
@@ -936,21 +1037,129 @@ export async function createAnalystApp() {
936
1037
  console.log("Result:", JSON.stringify(structured ?? analysis.messages.at(-1)?.content));`);
937
1038
  }
938
1039
  if (config.patterns.includes("rag")) {
1040
+ files.push({ path: "src/config/embeddings.ts", content: generateEmbeddingsConfig(config) });
1041
+ files.push({
1042
+ path: "src/tools/rag.ts",
1043
+ content: `import { z } from "zod";
1044
+ import { tool } from "@langchain/core/tools";
1045
+ import type { Embeddings } from "@langchain/core/embeddings";
1046
+ import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
1047
+
1048
+ /** In-memory vector store — no external database required. */
1049
+ export interface InMemoryVectorStore {
1050
+ size: number;
1051
+ search: (query: string, k?: number) => Promise<string[]>;
1052
+ }
1053
+
1054
+ interface Chunk {
1055
+ content: string;
1056
+ embedding: number[];
1057
+ }
1058
+
1059
+ function cosineSimilarity(a: number[], b: number[]): number {
1060
+ let dot = 0, normA = 0, normB = 0;
1061
+ for (let i = 0; i < a.length; i++) {
1062
+ dot += a[i] * b[i];
1063
+ normA += a[i] * a[i];
1064
+ normB += b[i] * b[i];
1065
+ }
1066
+ // Guard the zero vector: without this an empty embedding yields NaN and
1067
+ // silently corrupts the ranking.
1068
+ return normA === 0 || normB === 0 ? 0 : dot / (Math.sqrt(normA) * Math.sqrt(normB));
1069
+ }
1070
+
1071
+ export async function buildVectorStore(
1072
+ embeddings: Embeddings,
1073
+ documents: string[],
1074
+ chunkSize = 500,
1075
+ chunkOverlap = 100,
1076
+ ): Promise<InMemoryVectorStore> {
1077
+ const splitter = new RecursiveCharacterTextSplitter({ chunkSize, chunkOverlap });
1078
+ const docs = await splitter.createDocuments(documents);
1079
+ const texts = docs.map((d) => d.pageContent);
1080
+ const vectors = await embeddings.embedDocuments(texts);
1081
+ const chunks: Chunk[] = texts.map((content, i) => ({ content, embedding: vectors[i] }));
1082
+
1083
+ return {
1084
+ size: chunks.length,
1085
+ search: async (query: string, k = 4) => {
1086
+ if (chunks.length === 0) return [];
1087
+ const queryVector = await embeddings.embedQuery(query);
1088
+ return chunks
1089
+ .map((c) => ({ content: c.content, score: cosineSimilarity(queryVector, c.embedding) }))
1090
+ .sort((a, b) => b.score - a.score)
1091
+ .slice(0, k)
1092
+ .map((c) => c.content);
1093
+ },
1094
+ };
1095
+ }
1096
+
1097
+ export function createRetrievalTool(vectorStore: InMemoryVectorStore) {
1098
+ return tool(
1099
+ async ({ query, k }) => {
1100
+ const results = await vectorStore.search(query, k);
1101
+ if (results.length === 0) return "No relevant documents found. Try rephrasing.";
1102
+ return results.map((r, i) => "[" + (i + 1) + "] " + r).join("\\n\\n");
1103
+ },
1104
+ {
1105
+ name: "search_knowledge_base",
1106
+ description:
1107
+ "Search the knowledge base for relevant information. Use this to find context before answering.",
1108
+ schema: z.object({
1109
+ query: z.string().describe("The search query"),
1110
+ k: z.number().optional().default(4).describe("Number of results to return"),
1111
+ }),
1112
+ }
1113
+ );
1114
+ }
1115
+
1116
+ /** Replace these with your own documents. */
1117
+ export const SAMPLE_DOCS = [
1118
+ "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.",
1119
+ "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.",
1120
+ "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.",
1121
+ ];
1122
+ `,
1123
+ });
939
1124
  files.push({
940
1125
  path: "src/apps/rag.ts",
941
1126
  content: `import { MemorySaver } from "@langchain/langgraph";
942
1127
  import { getLlm } from "../config/llm";
1128
+ import { createEmbeddings } from "../config/embeddings";
943
1129
  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
1130
+ import {
1131
+ buildVectorStore,
1132
+ createRetrievalTool,
1133
+ SAMPLE_DOCS,
1134
+ type InMemoryVectorStore,
1135
+ } from "../tools/rag";
1136
+
1137
+ let _vectorStore: InMemoryVectorStore | null = null;
1138
+
1139
+ /** Indexes the documents once; later calls reuse the same store. */
1140
+ export async function initRagStore(documents: string[] = SAMPLE_DOCS): Promise<InMemoryVectorStore> {
1141
+ if (!_vectorStore) {
1142
+ const embeddings = await createEmbeddings();
1143
+ _vectorStore = await buildVectorStore(embeddings, documents);
1144
+ console.log("RAG: indexed " + _vectorStore.size + " chunks");
1145
+ }
1146
+ return _vectorStore;
1147
+ }
947
1148
 
948
- export async function createRagApp() {
1149
+ export async function createRagApp(vectorStore?: InMemoryVectorStore) {
949
1150
  const llm = await getLlm();
1151
+ const retrievalTool = createRetrievalTool(vectorStore ?? (await initRagStore()));
950
1152
 
951
1153
  return makeAgent({
952
- name: "rag_agent", llm, tools: [],
953
- system: "You are a knowledgeable assistant. Answer questions based on your knowledge.",
1154
+ name: "rag_agent",
1155
+ llm,
1156
+ tools: [retrievalTool],
1157
+ system: [
1158
+ "You are a knowledgeable assistant with access to a knowledge base.",
1159
+ "ALWAYS search the knowledge base before answering questions.",
1160
+ "Base your answers on the retrieved documents. If they do not contain",
1161
+ "the answer, say so clearly rather than guessing.",
1162
+ ].join("\\n"),
954
1163
  checkpointer: new MemorySaver(),
955
1164
  });
956
1165
  }
@@ -960,7 +1169,7 @@ export async function createRagApp() {
960
1169
  demos.push(` console.log("\\n=== RAG Demo ===");
961
1170
  const ragApp = await createRagApp();
962
1171
  const rag = await ragApp.invoke(
963
- { messages: [{ role: "user", content: "What is RAG and how does it work?" }] },
1172
+ { messages: [{ role: "user", content: "What is the supervisor pattern?" }] },
964
1173
  { configurable: { thread_id: "rag-demo" } }
965
1174
  );
966
1175
  console.log("Result:", rag.messages.at(-1)?.content);`);
@@ -1048,7 +1257,11 @@ Provider: ${provider}${RESET}
1048
1257
  rl.close();
1049
1258
  }
1050
1259
  }
1051
- main().catch((err) => {
1052
- console.error("Error:", err);
1053
- process.exit(1);
1054
- });
1260
+ // Only run the interactive CLI when executed directly. Importing this module
1261
+ // (e.g. from the scaffolder tests) must not prompt.
1262
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
1263
+ main().catch((err) => {
1264
+ console.error("Error:", err);
1265
+ process.exit(1);
1266
+ });
1267
+ }
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.0",
4
4
  "description": "Scaffold a new LangGraph multi-agent project in seconds",
5
5
  "type": "module",
6
6
  "bin": {