create-langgraph-app 1.3.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/README.md CHANGED
@@ -11,7 +11,7 @@ npx create-langgraph-app
11
11
  ## What it does
12
12
 
13
13
  1. Asks for your project name
14
- 2. Lets you pick an LLM provider (OpenAI, Anthropic, Google, Groq, Ollama)
14
+ 2. Lets you pick an LLM provider (OpenAI, Anthropic, Google, Groq, DeepSeek, Ollama)
15
15
  3. Lets you select which agent patterns to include:
16
16
  - **Supervisor** — central coordinator + worker agents
17
17
  - **Swarm** — peer-to-peer agent handoffs
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";
@@ -52,8 +53,27 @@ const PROVIDERS = {
52
53
  google: { envKey: "GOOGLE_API_KEY", defaultModel: "gemini-2.0-flash" },
53
54
  groq: { envKey: "GROQ_API_KEY", defaultModel: "llama-3.3-70b-versatile" },
54
55
  ollama: { envKey: "", defaultModel: "llama3.2" },
56
+ deepseek: { envKey: "DEEPSEEK_API_KEY", defaultModel: "deepseek-v4-flash" },
55
57
  };
56
- 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) {
57
77
  const prov = PROVIDERS[config.provider];
58
78
  const lines = [
59
79
  `# LLM Provider`,
@@ -67,10 +87,12 @@ function generateEnv(config) {
67
87
  else {
68
88
  lines.push(`# No API key needed for ${config.provider}`);
69
89
  }
70
- lines.push(``, `# Model (optional — defaults to ${prov.defaultModel})`, `# LLM_MODEL=${prov.defaultModel}`, `LLM_TEMPERATURE=0`, ``, `PORT=3000`, ``, `# LangSmith tracing (optional)`, `# LANGCHAIN_TRACING_V2=true`, `# LANGSMITH_API_KEY=`, `# LANGSMITH_PROJECT=${config.name}`);
90
+ lines.push(``, `# Model (optional — defaults to ${prov.defaultModel})`, `# LLM_MODEL=${prov.defaultModel}`, `LLM_TEMPERATURE=0`, ``, ...embeddingsEnvLines(config), ...(config.provider === "deepseek"
91
+ ? [`# DeepSeek reasoning mode (optional — "disabled" | "enabled")`, `# DEEPSEEK_THINKING=disabled`, ``]
92
+ : []), `PORT=3000`, ``, `# LangSmith tracing (optional)`, `# LANGCHAIN_TRACING_V2=true`, `# LANGSMITH_API_KEY=`, `# LANGSMITH_PROJECT=${config.name}`);
71
93
  return lines.join("\n") + "\n";
72
94
  }
73
- function generatePackageJson(config) {
95
+ export function generatePackageJson(config) {
74
96
  const deps = {
75
97
  "@langchain/core": "^1.2.3",
76
98
  "@langchain/langgraph": "^1.4.8",
@@ -90,8 +112,14 @@ function generatePackageJson(config) {
90
112
  google: "@langchain/google-genai",
91
113
  groq: "@langchain/groq",
92
114
  ollama: "@langchain/ollama",
115
+ deepseek: "@langchain/deepseek",
93
116
  };
94
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
+ }
95
123
  const pkg = {
96
124
  name: config.name,
97
125
  version: "0.1.0",
@@ -114,7 +142,7 @@ function generatePackageJson(config) {
114
142
  };
115
143
  return JSON.stringify(pkg, null, 2) + "\n";
116
144
  }
117
- function generateTsConfig() {
145
+ export function generateTsConfig() {
118
146
  return JSON.stringify({
119
147
  compilerOptions: {
120
148
  target: "ES2022",
@@ -133,7 +161,7 @@ function generateTsConfig() {
133
161
  function generateEnvConfig(config) {
134
162
  return `import "dotenv/config";
135
163
 
136
- const VALID_PROVIDERS = ["openai", "anthropic", "google", "groq", "ollama"] as const;
164
+ const VALID_PROVIDERS = ["openai", "anthropic", "google", "groq", "ollama", "deepseek"] as const;
137
165
  export type LlmProvider = (typeof VALID_PROVIDERS)[number];
138
166
 
139
167
  function resolveProvider(): LlmProvider {
@@ -150,6 +178,7 @@ const API_KEY_MAP: Record<LlmProvider, string> = {
150
178
  google: "GOOGLE_API_KEY",
151
179
  groq: "GROQ_API_KEY",
152
180
  ollama: "",
181
+ deepseek: "DEEPSEEK_API_KEY",
153
182
  };
154
183
 
155
184
  export const LLM_PROVIDER = resolveProvider();
@@ -162,6 +191,9 @@ if (requiredKey && !process.env[requiredKey]) {
162
191
 
163
192
  export const LLM_MODEL = process.env.LLM_MODEL || undefined;
164
193
  export const LLM_TEMPERATURE = Number(process.env.LLM_TEMPERATURE ?? 0);
194
+ // Uses || rather than ?? so an empty DEEPSEEK_THINKING= in .env falls back
195
+ // to "disabled" instead of sending an invalid thinking type to the API.
196
+ export const DEEPSEEK_THINKING = process.env.DEEPSEEK_THINKING || "disabled";
165
197
  export const PORT = Number(process.env.PORT ?? 3000);
166
198
  `;
167
199
  }
@@ -173,11 +205,72 @@ const PROVIDER_LLM = {
173
205
  google: { pkg: "@langchain/google-genai", className: "ChatGoogleGenerativeAI", modelArg: "model", defaultModel: "gemini-2.0-flash" },
174
206
  groq: { pkg: "@langchain/groq", className: "ChatGroq", modelArg: "model", defaultModel: "llama-3.3-70b-versatile" },
175
207
  ollama: { pkg: "@langchain/ollama", className: "ChatOllama", modelArg: "model", defaultModel: "llama3.2" },
208
+ deepseek: { pkg: "@langchain/deepseek", className: "ChatDeepSeek", modelArg: "model", defaultModel: "deepseek-v4-flash", extraArgs: 'modelKwargs: { thinking: { type: DEEPSEEK_THINKING } }' },
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: "" },
176
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
+ }
177
267
  function generateLlmConfig(config) {
178
268
  const p = PROVIDER_LLM[config.provider] ?? PROVIDER_LLM.openai;
269
+ const envImports = config.provider === "deepseek"
270
+ ? "LLM_MODEL, LLM_TEMPERATURE, DEEPSEEK_THINKING"
271
+ : "LLM_MODEL, LLM_TEMPERATURE";
179
272
  return `import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
180
- import { LLM_MODEL, LLM_TEMPERATURE } from "./env";
273
+ import { ${envImports} } from "./env";
181
274
 
182
275
  const DEFAULT_MODEL = "${p.defaultModel}";
183
276
 
@@ -191,7 +284,7 @@ async function createLlm(opts: LlmOptions = {}): Promise<BaseChatModel> {
191
284
  const temperature = opts.temperature ?? LLM_TEMPERATURE;
192
285
 
193
286
  const { ${p.className} } = await import("${p.pkg}");
194
- return new ${p.className}({ ${p.modelArg}: model, temperature });
287
+ return new ${p.className}({ ${p.modelArg}: model, temperature${p.extraArgs ? `, ${p.extraArgs}` : ""} });
195
288
  }
196
289
 
197
290
  let _default: Promise<BaseChatModel> | undefined;
@@ -351,7 +444,27 @@ function selectedApps(config) {
351
444
  function generateServer(config) {
352
445
  const apps = selectedApps(config);
353
446
  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");
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
+ : "";
355
468
  return `import "./config/env";
356
469
  import { fastify, type FastifyError, type FastifyReply, type FastifyRequest } from "fastify";
357
470
  import { Command } from "@langchain/langgraph";
@@ -393,8 +506,8 @@ server.setErrorHandler(async (error: FastifyError, _req: FastifyRequest, reply:
393
506
  });
394
507
 
395
508
  async function start() {
396
- const apps: Record<string, AppGraph> = {
397
- ${entries}
509
+ ${ragInit} const apps: Record<string, AppGraph> = {
510
+ ${entries}${ragEntry}
398
511
  };
399
512
 
400
513
  const getApp = (name: string) => {
@@ -583,7 +696,7 @@ ${supervisorTest}});
583
696
  `;
584
697
  }
585
698
  // Pattern-specific: only files for selected patterns are generated
586
- function getPatternFiles(config) {
699
+ export function getPatternFiles(config) {
587
700
  const files = [];
588
701
  // Always include core files
589
702
  files.push({ path: "src/config/env.ts", content: generateEnvConfig(config) });
@@ -924,21 +1037,129 @@ export async function createAnalystApp() {
924
1037
  console.log("Result:", JSON.stringify(structured ?? analysis.messages.at(-1)?.content));`);
925
1038
  }
926
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
+ });
927
1124
  files.push({
928
1125
  path: "src/apps/rag.ts",
929
1126
  content: `import { MemorySaver } from "@langchain/langgraph";
930
1127
  import { getLlm } from "../config/llm";
1128
+ import { createEmbeddings } from "../config/embeddings";
931
1129
  import { makeAgent } from "../agents/factory";
932
- // TODO: Add your vector store, embeddings, and retrieval tool here.
933
- // See the full starter kit for a complete RAG implementation:
934
- // 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
+ }
935
1148
 
936
- export async function createRagApp() {
1149
+ export async function createRagApp(vectorStore?: InMemoryVectorStore) {
937
1150
  const llm = await getLlm();
1151
+ const retrievalTool = createRetrievalTool(vectorStore ?? (await initRagStore()));
938
1152
 
939
1153
  return makeAgent({
940
- name: "rag_agent", llm, tools: [],
941
- 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"),
942
1163
  checkpointer: new MemorySaver(),
943
1164
  });
944
1165
  }
@@ -948,7 +1169,7 @@ export async function createRagApp() {
948
1169
  demos.push(` console.log("\\n=== RAG Demo ===");
949
1170
  const ragApp = await createRagApp();
950
1171
  const rag = await ragApp.invoke(
951
- { messages: [{ role: "user", content: "What is RAG and how does it work?" }] },
1172
+ { messages: [{ role: "user", content: "What is the supervisor pattern?" }] },
952
1173
  { configurable: { thread_id: "rag-demo" } }
953
1174
  );
954
1175
  console.log("Result:", rag.messages.at(-1)?.content);`);
@@ -984,6 +1205,7 @@ async function main() {
984
1205
  { value: "anthropic", label: "Anthropic (Claude Sonnet)" },
985
1206
  { value: "google", label: "Google (Gemini 2.0 Flash)" },
986
1207
  { value: "groq", label: "Groq (Llama 3.3 70B)" },
1208
+ { value: "deepseek", label: "DeepSeek (deepseek-v4-flash)" },
987
1209
  { value: "ollama", label: "Ollama (local, no API key)" },
988
1210
  ]);
989
1211
  const patterns = await choose(rl, "Which patterns? (select multiple)", [
@@ -1035,7 +1257,11 @@ Provider: ${provider}${RESET}
1035
1257
  rl.close();
1036
1258
  }
1037
1259
  }
1038
- main().catch((err) => {
1039
- console.error("Error:", err);
1040
- process.exit(1);
1041
- });
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.3.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": {