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 +12 -1
- package/dist/create.js +232 -19
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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",
|
|
953
|
-
|
|
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
|
|
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
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
+
}
|