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 +50 -12
- package/dist/create.d.ts +12 -1
- package/dist/create.js +256 -26
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
59
|
+
npm run dev # run the CLI locally
|
|
60
|
+
npm run build # typecheck and emit dist/
|
|
30
61
|
```
|
|
31
62
|
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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";
|
|
@@ -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?:
|
|
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
|
|
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?:
|
|
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
|
|
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?:
|
|
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
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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",
|
|
953
|
-
|
|
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
|
|
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
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
+
}
|