create-leo 0.1.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +21 -0
  3. package/README.md +91 -0
  4. package/dist/index.js +316 -0
  5. package/package.json +30 -0
  6. package/templates/_shared/README.md +11 -0
  7. package/templates/_shared/_gitignore +8 -0
  8. package/templates/express-mongodb/.env.example +2 -0
  9. package/templates/express-mongodb/README.md +13 -0
  10. package/templates/express-mongodb/package.json +18 -0
  11. package/templates/express-mongodb/src/app.js +11 -0
  12. package/templates/express-mongodb/src/config/db.js +9 -0
  13. package/templates/express-mongodb/src/middleware/error.js +4 -0
  14. package/templates/express-mongodb/src/models/example.js +2 -0
  15. package/templates/express-mongodb/src/routes/examples.js +6 -0
  16. package/templates/express-mongodb/src/routes/health.js +5 -0
  17. package/templates/express-mongodb/src/server.js +5 -0
  18. package/templates/express-postgres/.env.example +2 -0
  19. package/templates/express-postgres/README.md +6 -0
  20. package/templates/express-postgres/package.json +18 -0
  21. package/templates/express-postgres/src/app.js +7 -0
  22. package/templates/express-postgres/src/config/db.js +4 -0
  23. package/templates/express-postgres/src/middleware/error.js +1 -0
  24. package/templates/express-postgres/src/routes/examples.js +5 -0
  25. package/templates/express-postgres/src/routes/health.js +4 -0
  26. package/templates/express-postgres/src/server.js +5 -0
  27. package/templates/fastapi-mongodb/.env.example +4 -0
  28. package/templates/fastapi-mongodb/README.md +12 -0
  29. package/templates/fastapi-mongodb/_gitignore +6 -0
  30. package/templates/fastapi-mongodb/app/api/routes/examples.py +5 -0
  31. package/templates/fastapi-mongodb/app/api/routes/health.py +4 -0
  32. package/templates/fastapi-mongodb/app/core/config.py +6 -0
  33. package/templates/fastapi-mongodb/app/database/mongo.py +4 -0
  34. package/templates/fastapi-mongodb/app/main.py +6 -0
  35. package/templates/fastapi-mongodb/requirements.txt +4 -0
  36. package/templates/fastapi-postgres/.env.example +3 -0
  37. package/templates/fastapi-postgres/README.md +12 -0
  38. package/templates/fastapi-postgres/_gitignore +6 -0
  39. package/templates/fastapi-postgres/app/api/routes/examples.py +7 -0
  40. package/templates/fastapi-postgres/app/api/routes/health.py +4 -0
  41. package/templates/fastapi-postgres/app/core/config.py +7 -0
  42. package/templates/fastapi-postgres/app/database/session.py +3 -0
  43. package/templates/fastapi-postgres/app/main.py +6 -0
  44. package/templates/fastapi-postgres/requirements.txt +5 -0
  45. package/templates/fastify-postgres/.env.example +2 -0
  46. package/templates/fastify-postgres/README.md +5 -0
  47. package/templates/fastify-postgres/package.json +18 -0
  48. package/templates/fastify-postgres/src/app.js +14 -0
  49. package/templates/fastify-postgres/src/plugins/db.js +3 -0
  50. package/templates/fastify-postgres/src/routes/health.js +1 -0
  51. package/templates/fastify-postgres/src/server.js +4 -0
  52. package/templates/hybrid/.env.example +8 -0
  53. package/templates/hybrid/README.md +27 -0
  54. package/templates/hybrid/_gitignore +6 -0
  55. package/templates/hybrid/app/api/routes/ingest.py +4 -0
  56. package/templates/hybrid/app/api/routes/query.py +4 -0
  57. package/templates/hybrid/app/core/config.py +4 -0
  58. package/templates/hybrid/app/main.py +4 -0
  59. package/templates/hybrid/app/retrieval/bm25.py +4 -0
  60. package/templates/hybrid/app/retrieval/fusion.py +4 -0
  61. package/templates/hybrid/app/retrieval/reranker.py +4 -0
  62. package/templates/hybrid/requirements.txt +9 -0
  63. package/templates/semantic/.env.example +7 -0
  64. package/templates/semantic/README.md +31 -0
  65. package/templates/semantic/_gitignore +6 -0
  66. package/templates/semantic/app/api/routes/health.py +10 -0
  67. package/templates/semantic/app/api/routes/ingest.py +4 -0
  68. package/templates/semantic/app/api/routes/query.py +4 -0
  69. package/templates/semantic/app/core/config.py +4 -0
  70. package/templates/semantic/app/main.py +8 -0
  71. package/templates/semantic/app/services/chunker.py +4 -0
  72. package/templates/semantic/app/services/llm.py +4 -0
  73. package/templates/semantic/app/vectorstore/qdrant.py +4 -0
  74. package/templates/semantic/requirements.txt +8 -0
  75. package/versions.json +24 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+ - Initial public release.
5
+ - Seven backend/RAG templates.
6
+ - Interactive and flag-based generation.
7
+ - Node 22+ native environment loading and watch mode.
8
+ - Python 3.11+ with uv or venv + pip installation.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Leo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # create-leo
2
+
3
+ `create-leo` is a small, production-oriented backend generator for Node.js, FastAPI, databases, and RAG.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 22+
8
+ - Python 3.11+ for Python templates
9
+ - `uv` is recommended for Python dependency installation, but Leo falls back to `venv` + `pip`
10
+
11
+ ## Use
12
+
13
+ ```bash
14
+ npx create-leo my-api
15
+ ```
16
+
17
+ Choose one of seven templates:
18
+
19
+ | ID | Stack |
20
+ |---|---|
21
+ | `express-mongodb` | Express 5 + Mongoose + MongoDB |
22
+ | `express-postgres` | Express 5 + pg + PostgreSQL |
23
+ | `fastify-postgres` | Fastify + pg + PostgreSQL |
24
+ | `fastapi-postgres` | FastAPI + SQLAlchemy + PostgreSQL |
25
+ | `fastapi-mongodb` | FastAPI + native async PyMongo |
26
+ | `semantic` | FastAPI + FastEmbed + local Qdrant + OpenAI-compatible LLM |
27
+ | `hybrid` | Semantic + BM25 + RRF + optional cross-encoder |
28
+
29
+ ### Non-interactive
30
+
31
+ ```bash
32
+ npx create-leo my-api --template express-mongodb
33
+ npx create-leo my-rag --template hybrid --no-install
34
+ npx create-leo --list
35
+ ```
36
+
37
+ `--no-install` is useful for CI, smoke tests, or when you want to install dependencies yourself.
38
+
39
+ ## Design choices
40
+
41
+ - Node 22 native `--watch` and `--env-file`; no nodemon or dotenv.
42
+ - ESM everywhere in Node templates.
43
+ - Express 5 async handlers; no custom async wrapper.
44
+ - FastAPI configuration uses `pydantic-settings`.
45
+ - MongoDB Python uses PyMongo's async client rather than Motor.
46
+ - RAG uses FastEmbed/ONNX rather than sentence-transformers + torch.
47
+ - Qdrant runs in local file mode; no Qdrant server is required for the starter.
48
+ - LLM calls use the OpenAI client with configurable `LLM_BASE_URL`.
49
+ - No LangChain dependency.
50
+ - Hybrid RAG overlays the semantic pipeline instead of duplicating ingestion.
51
+ - Every template has a DB-independent `/health` endpoint.
52
+
53
+ ## Free-tier development
54
+
55
+ The generated projects do not require a paid service. For hosted free tiers, use a provider appropriate to the database you choose, or run the database locally. RAG's local Qdrant storage is file-based. An LLM key is only needed when you want generated answers rather than retrieved context.
56
+
57
+ The first FastEmbed query downloads its embedding model. This is expected.
58
+
59
+ ## Publishing
60
+
61
+ Before publishing, verify the package name is available:
62
+
63
+ ```bash
64
+ npm view create-leo
65
+ node scripts/check-name.mjs
66
+ ```
67
+
68
+ If it is unavailable, use `create-leo-cli` or a scoped package.
69
+
70
+ Then:
71
+
72
+ ```bash
73
+ npm login
74
+ npm publish
75
+ ```
76
+
77
+ The `prepublishOnly` script resolves current Node dependency versions, builds `dist`, and runs tests.
78
+
79
+ ## Testing
80
+
81
+ ```bash
82
+ npm install
83
+ npm test
84
+ npm run build
85
+ ```
86
+
87
+ CI generates all templates with `--no-install` and checks their basic filesystem output.
88
+
89
+ ## License
90
+
91
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { parseArgs } from "util";
5
+ import * as p3 from "@clack/prompts";
6
+ import pc3 from "picocolors";
7
+
8
+ // src/registry.ts
9
+ var templates = [
10
+ { id: "express-mongodb", label: "Express + MongoDB", group: "Node.js", language: "node", description: "Express 5 API with Mongoose and MongoDB", postInstall: ["npm install"] },
11
+ { id: "express-postgres", label: "Express + PostgreSQL", group: "Node.js", language: "node", description: "Express 5 API with pg Pool and PostgreSQL", postInstall: ["npm install"] },
12
+ { id: "fastify-postgres", label: "Fastify + PostgreSQL", group: "Node.js", language: "node", description: "Fastify API with pg Pool and PostgreSQL", postInstall: ["npm install"] },
13
+ { id: "fastapi-postgres", label: "FastAPI + PostgreSQL", group: "Python", language: "python", description: "FastAPI with SQLAlchemy and PostgreSQL", postInstall: ["python -m pip install -r requirements.txt"] },
14
+ { id: "fastapi-mongodb", label: "FastAPI + MongoDB", group: "Python", language: "python", description: "FastAPI with native async PyMongo", postInstall: ["python -m pip install -r requirements.txt"] },
15
+ { id: "semantic", label: "Semantic RAG", group: "AI / RAG", language: "python", description: "FastAPI + FastEmbed + local Qdrant + configurable LLM", postInstall: ["python -m pip install -r requirements.txt"] },
16
+ { id: "hybrid", label: "Hybrid RAG", group: "AI / RAG", language: "python", description: "Semantic RAG plus BM25, RRF fusion and optional reranking", postInstall: ["python -m pip install -r requirements.txt"], overlay: "semantic" }
17
+ ];
18
+ function getTemplate(id) {
19
+ return templates.find((t) => t.id === id);
20
+ }
21
+
22
+ // src/prompts.ts
23
+ import * as p from "@clack/prompts";
24
+ import pc from "picocolors";
25
+ async function interactive(templateArg, nameArg) {
26
+ p.intro(pc.bold("\u25C7 Leo \u2014 backend project generator"));
27
+ let templateId = templateArg;
28
+ if (!templateId) {
29
+ const selected = await p.select({
30
+ message: "Choose a backend template",
31
+ options: templates.map((t) => ({ value: t.id, label: t.label, hint: `${t.group} \xB7 ${t.description}` }))
32
+ });
33
+ if (p.isCancel(selected)) return null;
34
+ templateId = selected;
35
+ }
36
+ let name = nameArg;
37
+ if (!name) {
38
+ const value = await p.text({ message: "Project name", placeholder: "my-api", validate: (v) => /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(v) ? void 0 : "Use letters, numbers, dots, dashes or underscores." });
39
+ if (p.isCancel(value)) return null;
40
+ name = value;
41
+ }
42
+ return { templateId, name };
43
+ }
44
+
45
+ // src/generate.ts
46
+ import path3 from "path";
47
+ import { mkdir as mkdir2, cp as cp2, readFile as readFile2, writeFile as writeFile2, rm } from "fs/promises";
48
+ import { fileURLToPath } from "url";
49
+ import * as p2 from "@clack/prompts";
50
+ import pc2 from "picocolors";
51
+
52
+ // src/utils/fs.ts
53
+ import { cp, mkdir, readFile, writeFile } from "fs/promises";
54
+ import { existsSync } from "fs";
55
+ import path from "path";
56
+ async function copyDir(src, dest) {
57
+ await mkdir(dest, { recursive: true });
58
+ await cp(src, dest, { recursive: true, force: true, errorOnExist: false });
59
+ }
60
+ async function replaceInTree(root, replacements) {
61
+ const entries = await walk(root);
62
+ for (const file of entries) {
63
+ if (!file.endsWith(".json") && !file.endsWith(".md") && !file.endsWith(".txt") && !file.endsWith(".env.example") && !file.endsWith(".py") && !file.endsWith(".js") && !file.endsWith(".ts")) continue;
64
+ const buf = await readFile(file);
65
+ if (buf.includes(0)) continue;
66
+ let text2 = buf.toString("utf8");
67
+ for (const [from, to] of Object.entries(replacements)) text2 = text2.split(from).join(to);
68
+ await writeFile(file, text2);
69
+ }
70
+ }
71
+ async function walk(dir) {
72
+ const { readdir, stat } = await import("fs/promises");
73
+ const out = [];
74
+ for (const entry of await readdir(dir)) {
75
+ const full = path.join(dir, entry);
76
+ const s = await stat(full);
77
+ if (s.isDirectory()) out.push(...await walk(full));
78
+ else out.push(full);
79
+ }
80
+ return out;
81
+ }
82
+ function pathExists(p4) {
83
+ return existsSync(p4);
84
+ }
85
+
86
+ // src/utils/process.ts
87
+ import { spawn } from "child_process";
88
+ function executable(command) {
89
+ if (process.platform !== "win32") return command;
90
+ if (command === "npm") return "npm.cmd";
91
+ if (command === "pnpm") return "pnpm.cmd";
92
+ if (command === "yarn") return "yarn.cmd";
93
+ if (command === "bun") return "bun.exe";
94
+ return command;
95
+ }
96
+ function run(command, args, cwd, env, shell = false) {
97
+ return new Promise((resolve, reject) => {
98
+ const child = spawn(executable(command), args, {
99
+ cwd,
100
+ env: { ...process.env, ...env },
101
+ stdio: "inherit",
102
+ // Windows cannot launch .cmd shims (npm.cmd, uv.cmd, etc.) directly.
103
+ // Callers opt into the shell only for trusted, fixed package-manager args.
104
+ shell
105
+ });
106
+ child.on("error", reject);
107
+ child.on("exit", (code, signal) => {
108
+ if (code === 0) resolve();
109
+ else reject(new Error(`${command} exited with ${code ?? signal}`));
110
+ });
111
+ });
112
+ }
113
+ async function commandExists(command) {
114
+ const checker = process.platform === "win32" ? "where" : "which";
115
+ return new Promise((resolve) => {
116
+ const child = spawn(checker, [command], { stdio: "ignore", shell: false });
117
+ child.on("exit", (code) => resolve(code === 0));
118
+ child.on("error", () => resolve(false));
119
+ });
120
+ }
121
+
122
+ // src/install/node.ts
123
+ function packageManager() {
124
+ const ua = process.env.npm_config_user_agent ?? "";
125
+ if (ua.startsWith("pnpm/")) return "pnpm";
126
+ if (ua.startsWith("yarn/")) return "yarn";
127
+ if (ua.startsWith("bun/")) return "bun";
128
+ return "npm";
129
+ }
130
+ async function installNode(projectDir) {
131
+ const manager = packageManager();
132
+ if (!await commandExists(manager)) throw new Error(`${manager} was not found. Install it or use npm.`);
133
+ const args = manager === "yarn" ? ["install"] : ["install"];
134
+ await run(manager, args, projectDir, void 0, process.platform === "win32");
135
+ }
136
+
137
+ // src/install/python.ts
138
+ import path2 from "path";
139
+ async function pythonCommand() {
140
+ if (process.platform === "win32" && await commandExists("py")) return "py";
141
+ if (await commandExists("python3")) return "python3";
142
+ if (await commandExists("python")) return "python";
143
+ throw new Error("Python 3.11+ was not found. Install Python or uv first.");
144
+ }
145
+ async function checkPython311(py) {
146
+ const { execFileSync } = await import("child_process");
147
+ try {
148
+ const args = process.platform === "win32" && py === "py" ? ["-3", "--version"] : ["--version"];
149
+ const output = execFileSync(py, args, { encoding: "utf8" }).trim();
150
+ const match = output.match(/Python (\d+)\.(\d+)/);
151
+ if (!match || Number(match[1]) < 3 || Number(match[1]) === 3 && Number(match[2]) < 11) {
152
+ throw new Error(`Python 3.11+ required; found ${output}`);
153
+ }
154
+ } catch (error) {
155
+ if (error instanceof Error && error.message.startsWith("Python 3.11+ required")) throw error;
156
+ throw new Error("Unable to verify Python 3.11+. Install a supported Python runtime.");
157
+ }
158
+ }
159
+ async function installPython(projectDir) {
160
+ const py = await pythonCommand();
161
+ await checkPython311(py);
162
+ if (await commandExists("uv")) {
163
+ await run("uv", ["venv", ".venv", "--python", "3.11"], projectDir);
164
+ const uvPython = path2.join(
165
+ projectDir,
166
+ ".venv",
167
+ process.platform === "win32" ? "Scripts" : "bin",
168
+ process.platform === "win32" ? "python.exe" : "python"
169
+ );
170
+ await run(uvPython, ["-m", "pip", "install", "-r", "requirements.txt"], projectDir);
171
+ return;
172
+ }
173
+ const pyArgs = process.platform === "win32" && py === "py" ? ["-3"] : [];
174
+ await run(py, [...pyArgs, "-m", "venv", ".venv"], projectDir);
175
+ const venvPython = path2.join(
176
+ projectDir,
177
+ process.platform === "win32" ? ".venv" : ".venv",
178
+ process.platform === "win32" ? "Scripts" : "bin",
179
+ process.platform === "win32" ? "python.exe" : "python"
180
+ );
181
+ await run(venvPython, ["-m", "pip", "install", "-r", "requirements.txt"], projectDir);
182
+ }
183
+
184
+ // src/generate.ts
185
+ var here = path3.dirname(fileURLToPath(import.meta.url));
186
+ var packageRoot = path3.resolve(here, "..");
187
+ var templateRoot = path3.join(packageRoot, "templates");
188
+ async function renameGitignore(root) {
189
+ const entries = await walk(root);
190
+ for (const file of entries) {
191
+ if (path3.basename(file) === "_gitignore") {
192
+ await cp2(file, path3.join(path3.dirname(file), ".gitignore"));
193
+ await rm(file);
194
+ }
195
+ }
196
+ }
197
+ async function patchPackageJson(root, name) {
198
+ const pkg = path3.join(root, "package.json");
199
+ if (!pathExists(pkg)) return;
200
+ const json = JSON.parse(await readFile2(pkg, "utf8"));
201
+ json.name = name;
202
+ await writeFile2(pkg, JSON.stringify(json, null, 2) + "\n");
203
+ }
204
+ async function generate(templateId, projectName, noInstall = false) {
205
+ const template = getTemplate(templateId);
206
+ if (!template) throw new Error(`Unknown template: ${templateId}`);
207
+ const target = path3.resolve(process.cwd(), projectName);
208
+ if (pathExists(target)) throw new Error(`Target folder already exists: ${target}`);
209
+ let committed = false;
210
+ const onInterrupt = async () => {
211
+ await rm(target, { recursive: true, force: true });
212
+ process.exit(130);
213
+ };
214
+ process.once("SIGINT", onInterrupt);
215
+ try {
216
+ await mkdir2(target, { recursive: true });
217
+ await copyDir(path3.join(templateRoot, "_shared"), target);
218
+ if (template.overlay) await copyDir(path3.join(templateRoot, template.overlay), target);
219
+ await copyDir(path3.join(templateRoot, template.id), target);
220
+ await replaceInTree(target, { "{{name}}": projectName });
221
+ await patchPackageJson(target, projectName);
222
+ const envExample = path3.join(target, ".env.example");
223
+ if (pathExists(envExample)) await cp2(envExample, path3.join(target, ".env"));
224
+ await renameGitignore(target);
225
+ committed = true;
226
+ if (!noInstall) {
227
+ const spin = p2.spinner();
228
+ spin.start(`Installing ${template.language} dependencies`);
229
+ try {
230
+ if (template.language === "node") await installNode(target);
231
+ else await installPython(target);
232
+ spin.stop("Dependencies installed");
233
+ } catch (error) {
234
+ spin.stop("Dependency installation failed");
235
+ throw error;
236
+ }
237
+ }
238
+ printNextSteps(target, projectName, template);
239
+ return target;
240
+ } catch (error) {
241
+ await rm(target, { recursive: true, force: true });
242
+ throw error;
243
+ } finally {
244
+ process.removeListener("SIGINT", onInterrupt);
245
+ }
246
+ }
247
+ function printNextSteps(target, name, template) {
248
+ const rel = path3.relative(process.cwd(), target) || name;
249
+ console.log(`
250
+ ${pc2.green("\u2714")} Created ${pc2.bold(name)} using ${template.label}`);
251
+ console.log(`
252
+ ${pc2.dim("Next steps")}`);
253
+ console.log(` ${pc2.cyan(`cd ${rel}`)}`);
254
+ if (template.language === "node") {
255
+ console.log(` ${pc2.cyan("cp .env.example .env")}`);
256
+ console.log(` ${pc2.cyan("npm run dev")}`);
257
+ } else {
258
+ console.log(` ${pc2.cyan("cp .env.example .env")}`);
259
+ console.log(` ${pc2.cyan("source .venv/bin/activate")} ${pc2.dim("(macOS/Linux)")}`);
260
+ console.log(` ${pc2.cyan("uvicorn app.main:app --reload")} ${pc2.dim("(or use the Windows venv equivalent)")}`);
261
+ }
262
+ console.log(`
263
+ ${pc2.dim("Health:")} ${pc2.cyan("GET /health")}`);
264
+ }
265
+
266
+ // src/index.ts
267
+ var { values, positionals } = parseArgs({
268
+ options: {
269
+ template: { type: "string", short: "t" },
270
+ "no-install": { type: "boolean", default: false },
271
+ help: { type: "boolean", short: "h" },
272
+ version: { type: "boolean", short: "v" },
273
+ list: { type: "boolean" }
274
+ },
275
+ allowPositionals: true,
276
+ strict: true
277
+ });
278
+ if (values.version) {
279
+ console.log("0.1.0");
280
+ process.exit(0);
281
+ }
282
+ if (values.help) {
283
+ console.log(`${pc3.bold("create-leo")} \u2014 lightweight backend project generator
284
+
285
+ Usage:
286
+ npx create-leo [project-name]
287
+ npx create-leo my-api --template express-mongodb
288
+ npx create-leo my-rag --template semantic --no-install
289
+
290
+ Options:
291
+ -t, --template <id> Generate directly from a template
292
+ --no-install Skip dependency installation
293
+ --list List templates
294
+ -h, --help Show help
295
+ -v, --version Show version
296
+
297
+ Templates:
298
+ ${templates.map((t) => ` ${t.id.padEnd(20)} ${t.description}`).join("\n")}`);
299
+ process.exit(0);
300
+ }
301
+ if (values.list) {
302
+ for (const t of templates) console.log(`${t.id} ${t.description}`);
303
+ process.exit(0);
304
+ }
305
+ try {
306
+ if (values.template && !getTemplate(values.template)) throw new Error(`Unknown template '${values.template}'. Run --list to see valid IDs.`);
307
+ const chosen = await interactive(values.template, positionals[0]);
308
+ if (!chosen) process.exit(0);
309
+ await generate(chosen.templateId, chosen.name, values["no-install"]);
310
+ } catch (error) {
311
+ if (error.code === "ERR_USE_AFTER_CLOSE") process.exit(130);
312
+ console.error(`
313
+ ${pc3.red("\u2716")} ${error instanceof Error ? error.message : String(error)}`);
314
+ p3.outro(pc3.dim("No partial project was left behind."));
315
+ process.exit(1);
316
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "create-leo",
3
+ "version": "0.1.0",
4
+ "description": "A lightweight project generator for production-oriented Node.js, FastAPI, database and RAG backends.",
5
+ "type": "module",
6
+ "bin": { "create-leo": "dist/index.js", "leo": "dist/index.js" },
7
+ "files": ["dist", "templates", "README.md", "LICENSE", "CHANGELOG.md", "versions.json"],
8
+ "scripts": {
9
+ "build": "tsup src/index.ts --format esm --target node22 --clean",
10
+ "dev": "tsx src/index.ts",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "resolve-versions": "node scripts/resolve-versions.mjs",
14
+ "prepublishOnly": "npm run resolve-versions && npm run build && npm test"
15
+ },
16
+ "engines": { "node": ">=22.0.0" },
17
+ "packageManager": "npm@11",
18
+ "publishConfig": { "access": "public" },
19
+ "dependencies": {
20
+ "@clack/prompts": "^0.11.0",
21
+ "picocolors": "^1.1.1"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^24.0.0",
25
+ "tsup": "^8.5.0",
26
+ "tsx": "^4.20.0",
27
+ "typescript": "^5.9.0",
28
+ "vitest": "^3.2.0"
29
+ }
30
+ }
@@ -0,0 +1,11 @@
1
+ # {{name}}
2
+
3
+ Generated by Leo.
4
+
5
+ ## Configuration
6
+
7
+ Copy `.env.example` to `.env` and fill in the values for your environment.
8
+
9
+ ## Health
10
+
11
+ `GET /health` is intentionally independent of the database so the service can boot before a database is available.
@@ -0,0 +1,8 @@
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ node_modules/
6
+ dist/
7
+ .qdrant/
8
+ .pytest_cache/
@@ -0,0 +1,2 @@
1
+ PORT=3000
2
+ MONGODB_URI=mongodb://127.0.0.1:27017/{{name}}
@@ -0,0 +1,13 @@
1
+ # {{name}}
2
+
3
+ Express 5 + MongoDB starter using Node 22 native `--watch` and `--env-file`.
4
+
5
+ ## Run
6
+
7
+ ```bash
8
+ cp .env.example .env
9
+ npm run dev
10
+ ```
11
+
12
+ MongoDB is connected lazily after startup. `GET /health` works without MongoDB.
13
+ For a free hosted database, use MongoDB Atlas and put its URI in `MONGODB_URI`.
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.0.0"
8
+ },
9
+ "scripts": {
10
+ "dev": "node --watch --env-file=.env src/server.js",
11
+ "start": "node --env-file=.env src/server.js"
12
+ },
13
+ "dependencies": {
14
+ "cors": "^2.8.5",
15
+ "express": "^5.1.0",
16
+ "mongoose": "^8.18.0"
17
+ }
18
+ }
@@ -0,0 +1,11 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import health from './routes/health.js';
4
+ import examples from './routes/examples.js';
5
+ import { errorHandler } from './middleware/error.js';
6
+ export const app = express();
7
+ app.use(cors());
8
+ app.use(express.json());
9
+ app.use(health);
10
+ app.use('/api/examples', examples);
11
+ app.use(errorHandler);
@@ -0,0 +1,9 @@
1
+ import mongoose from 'mongoose';
2
+
3
+ let connected = false;
4
+ export async function connectDb() {
5
+ if (connected) return;
6
+ await mongoose.connect(process.env.MONGODB_URI);
7
+ connected = true;
8
+ }
9
+ export function dbState() { return mongoose.connection.readyState; }
@@ -0,0 +1,4 @@
1
+ export function errorHandler(err, _req, res, _next) {
2
+ console.error(err);
3
+ res.status(err.statusCode ?? 500).json({ error: err.message ?? 'Internal server error' });
4
+ }
@@ -0,0 +1,2 @@
1
+ import mongoose from 'mongoose';
2
+ export const Example = mongoose.model('Example', new mongoose.Schema({ name: { type: String, required: true } }, { timestamps: true }));
@@ -0,0 +1,6 @@
1
+ import { Router } from 'express';
2
+ import { Example } from '../models/example.js';
3
+ const router = Router();
4
+ router.get('/', async (_req, res) => res.json(await Example.find().lean()));
5
+ router.post('/', async (req, res) => res.status(201).json(await Example.create(req.body)));
6
+ export default router;
@@ -0,0 +1,5 @@
1
+ import { Router } from 'express';
2
+ import { dbState } from '../config/db.js';
3
+ const router = Router();
4
+ router.get('/health', (_req, res) => res.json({ status: 'ok', database: dbState() === 1 ? 'connected' : 'not-connected' }));
5
+ export default router;
@@ -0,0 +1,5 @@
1
+ import { app } from './app.js';
2
+ import { connectDb } from './config/db.js';
3
+ const port = Number(process.env.PORT ?? 3000);
4
+ app.listen(port, () => console.log(`API listening on http://localhost:${port}`));
5
+ connectDb().catch((err) => console.warn('MongoDB unavailable; /health remains available:', err.message));
@@ -0,0 +1,2 @@
1
+ PORT=3000
2
+ DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/{{name}}
@@ -0,0 +1,6 @@
1
+ # {{name}}
2
+
3
+ Express 5 + PostgreSQL starter. The `pg` Pool is created once and shared by the app.
4
+
5
+ `GET /health` does not require PostgreSQL to be running.
6
+ For free hosted PostgreSQL, Neon is a simple option; place its connection string in `DATABASE_URL`.
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.0.0"
8
+ },
9
+ "scripts": {
10
+ "dev": "node --watch --env-file=.env src/server.js",
11
+ "start": "node --env-file=.env src/server.js"
12
+ },
13
+ "dependencies": {
14
+ "cors": "^2.8.5",
15
+ "express": "^5.1.0",
16
+ "pg": "^8.16.3"
17
+ }
18
+ }
@@ -0,0 +1,7 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import health from './routes/health.js';
4
+ import examples from './routes/examples.js';
5
+ import { errorHandler } from './middleware/error.js';
6
+ export const app = express();
7
+ app.use(cors()); app.use(express.json()); app.use(health); app.use('/api/examples', examples); app.use(errorHandler);
@@ -0,0 +1,4 @@
1
+ import pg from 'pg';
2
+ const { Pool } = pg;
3
+ export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
4
+ export async function checkDb() { await pool.query('SELECT 1'); }
@@ -0,0 +1 @@
1
+ export function errorHandler(err, _req, res, _next) { console.error(err); res.status(err.statusCode ?? 500).json({ error: err.message ?? 'Internal server error' }); }
@@ -0,0 +1,5 @@
1
+ import { Router } from 'express';
2
+ import { pool } from '../config/db.js';
3
+ const router = Router();
4
+ router.get('/', async (_req, res) => res.json((await pool.query('SELECT NOW() AS now')).rows));
5
+ export default router;
@@ -0,0 +1,4 @@
1
+ import { Router } from 'express';
2
+ const router = Router();
3
+ router.get('/health', (_req, res) => res.json({ status: 'ok' }));
4
+ export default router;
@@ -0,0 +1,5 @@
1
+ import { app } from './app.js';
2
+ import { checkDb } from './config/db.js';
3
+ const port = Number(process.env.PORT ?? 3000);
4
+ app.listen(port, () => console.log(`API listening on http://localhost:${port}`));
5
+ checkDb().catch((err) => console.warn('PostgreSQL unavailable; /health remains available:', err.message));
@@ -0,0 +1,4 @@
1
+ APP_ENV=development
2
+ PORT=8000
3
+ MONGODB_URI=mongodb://127.0.0.1:27017
4
+ MONGODB_DB={{name}}
@@ -0,0 +1,12 @@
1
+ # {{name}}
2
+
3
+ Generated by Leo.
4
+
5
+ ## Run
6
+
7
+ ```bash
8
+ cp .env.example .env
9
+ uvicorn app.main:app --reload --port 8000
10
+ ```
11
+
12
+ If Leo created `.venv`, activate it first. The `/health` endpoint is independent of external services.
@@ -0,0 +1,6 @@
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ .qdrant/
6
+ .pytest_cache/
@@ -0,0 +1,5 @@
1
+ from fastapi import APIRouter
2
+ from app.database.mongo import db
3
+ router = APIRouter(prefix='/api/examples', tags=['examples'])
4
+ @router.get('/')
5
+ async def examples(): return await db.example.find().to_list(100)
@@ -0,0 +1,4 @@
1
+ from fastapi import APIRouter
2
+ router = APIRouter()
3
+ @router.get('/health')
4
+ async def health(): return {'status': 'ok'}
@@ -0,0 +1,6 @@
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+ class Settings(BaseSettings):
3
+ app_env: str = 'development'; port: int = 8000
4
+ mongodb_uri: str = 'mongodb://127.0.0.1:27017'; mongodb_db: str = '{{name}}'
5
+ model_config = SettingsConfigDict(env_file='.env', extra='ignore')
6
+ settings = Settings()
@@ -0,0 +1,4 @@
1
+ from pymongo import AsyncMongoClient
2
+ from app.core.config import settings
3
+ client = AsyncMongoClient(settings.mongodb_uri)
4
+ db = client[settings.mongodb_db]
@@ -0,0 +1,6 @@
1
+ from fastapi import FastAPI
2
+ from app.api.routes.health import router as health_router
3
+ from app.api.routes.examples import router as examples_router
4
+ app = FastAPI(title='{{name}}')
5
+ app.include_router(health_router)
6
+ app.include_router(examples_router)
@@ -0,0 +1,4 @@
1
+ fastapi==0.142.2
2
+ uvicorn==0.54.0
3
+ pydantic-settings==2.15.0
4
+ pymongo==4.18.2
@@ -0,0 +1,3 @@
1
+ APP_ENV=development
2
+ PORT=8000
3
+ DATABASE_URL=postgresql+psycopg://postgres:postgres@127.0.0.1:5432/{{name}}
@@ -0,0 +1,12 @@
1
+ # {{name}}
2
+
3
+ Generated by Leo.
4
+
5
+ ## Run
6
+
7
+ ```bash
8
+ cp .env.example .env
9
+ uvicorn app.main:app --reload --port 8000
10
+ ```
11
+
12
+ If Leo created `.venv`, activate it first. The `/health` endpoint is independent of external services.
@@ -0,0 +1,6 @@
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ .qdrant/
6
+ .pytest_cache/
@@ -0,0 +1,7 @@
1
+ from fastapi import APIRouter
2
+ from sqlalchemy import text
3
+ from app.database.session import engine
4
+ router = APIRouter(prefix='/api/examples', tags=['examples'])
5
+ @router.get('/')
6
+ def examples():
7
+ with engine.connect() as conn: return [dict(row) for row in conn.execute(text('SELECT NOW() AS now')).mappings()]
@@ -0,0 +1,4 @@
1
+ from fastapi import APIRouter
2
+ router = APIRouter()
3
+ @router.get('/health')
4
+ def health(): return {'status': 'ok'}
@@ -0,0 +1,7 @@
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+ class Settings(BaseSettings):
3
+ app_env: str = 'development'
4
+ port: int = 8000
5
+ database_url: str = 'postgresql+psycopg://postgres:postgres@127.0.0.1:5432/{{name}}'
6
+ model_config = SettingsConfigDict(env_file='.env', extra='ignore')
7
+ settings = Settings()
@@ -0,0 +1,3 @@
1
+ from sqlalchemy import create_engine
2
+ from app.core.config import settings
3
+ engine = create_engine(settings.database_url, pool_pre_ping=True)
@@ -0,0 +1,6 @@
1
+ from fastapi import FastAPI
2
+ from app.api.routes.health import router as health_router
3
+ from app.api.routes.examples import router as examples_router
4
+ app = FastAPI(title='{{name}}')
5
+ app.include_router(health_router)
6
+ app.include_router(examples_router)
@@ -0,0 +1,5 @@
1
+ fastapi==0.142.2
2
+ uvicorn==0.54.0
3
+ pydantic-settings==2.15.0
4
+ sqlalchemy==2.1.2
5
+ psycopg==3.3.6
@@ -0,0 +1,2 @@
1
+ PORT=3000
2
+ DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/{{name}}
@@ -0,0 +1,5 @@
1
+ # {{name}}
2
+
3
+ Fastify + PostgreSQL starter. The DB plugin is registered once and exposes a shared `pg` Pool.
4
+
5
+ `GET /health` works before PostgreSQL is available.
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.0.0"
8
+ },
9
+ "scripts": {
10
+ "dev": "node --watch --env-file=.env src/server.js",
11
+ "start": "node --env-file=.env src/server.js"
12
+ },
13
+ "dependencies": {
14
+ "@fastify/cors": "^11.1.0",
15
+ "fastify": "^5.6.0",
16
+ "pg": "^8.16.3"
17
+ }
18
+ }
@@ -0,0 +1,14 @@
1
+ import Fastify from 'fastify';
2
+ import cors from '@fastify/cors';
3
+ import { createDb } from './plugins/db.js';
4
+ import health from './routes/health.js';
5
+ export function buildApp() {
6
+ const app = Fastify({ logger: true });
7
+ app.register(cors);
8
+ const db = createDb();
9
+ app.decorate('db', db);
10
+ app.addHook('onClose', async () => db.end());
11
+ app.register(health);
12
+ app.get('/api/examples', async () => (await app.db.query('SELECT NOW() AS now')).rows);
13
+ return app;
14
+ }
@@ -0,0 +1,3 @@
1
+ import pg from 'pg';
2
+ const { Pool } = pg;
3
+ export function createDb() { return new Pool({ connectionString: process.env.DATABASE_URL }); }
@@ -0,0 +1 @@
1
+ export default async function health(fastify) { fastify.get('/health', async () => ({ status: 'ok' })); }
@@ -0,0 +1,4 @@
1
+ import { buildApp } from './app.js';
2
+ const app = buildApp();
3
+ const port = Number(process.env.PORT ?? 3000);
4
+ try { await app.listen({ port, host: '0.0.0.0' }); } catch (err) { app.log.error(err); process.exit(1); }
@@ -0,0 +1,8 @@
1
+ APP_ENV=development
2
+ PORT=8000
3
+ LLM_BASE_URL=https://api.openai.com/v1
4
+ LLM_API_KEY=
5
+ LLM_MODEL=gpt-4o-mini
6
+ EMBEDDING_MODEL=BAAI/bge-small-en-v1.5
7
+ QDRANT_PATH=.qdrant
8
+ RERANK=false
@@ -0,0 +1,27 @@
1
+ # {{name}}
2
+
3
+ Hybrid RAG project skeleton.
4
+
5
+ Leo installs the dependencies and creates the folder structure; the retrieval/ingestion implementation is intentionally left for you to build.
6
+
7
+ Hybrid is designed as a thin retrieval overlay on top of the semantic architecture: semantic search + BM25 + RRF + optional cross-encoder reranking. Keep one ingestion pipeline.
8
+
9
+ ## Dependencies
10
+
11
+ Includes all Semantic RAG dependencies plus:
12
+
13
+ - `bm25s` — lexical/BM25 retrieval
14
+
15
+ The reranker uses the cross-encoder support provided by FastEmbed.
16
+
17
+ ## Structure
18
+
19
+ - `app/main.py` — application entry point
20
+ - `app/api/routes/ingest.py` — shared ingestion route
21
+ - `app/api/routes/query.py` — hybrid query route
22
+ - `app/core/config.py` — settings
23
+ - `app/retrieval/bm25.py` — lexical retrieval
24
+ - `app/retrieval/fusion.py` — RRF fusion
25
+ - `app/retrieval/reranker.py` — optional cross-encoder reranking
26
+
27
+ Each Python file contains only a short note describing its responsibility.
@@ -0,0 +1,6 @@
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ .qdrant/
6
+ .pytest_cache/
@@ -0,0 +1,4 @@
1
+ """Document ingestion route placeholder.
2
+
3
+ Significance: keep one ingestion pipeline shared with semantic retrieval; hybrid search should not duplicate ingestion.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Hybrid RAG query route placeholder.
2
+
3
+ Significance: combine semantic retrieval with lexical BM25 retrieval, fuse results with RRF, optionally rerank, then generate an answer.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Hybrid RAG configuration placeholder.
2
+
3
+ Significance: define LLM, embedding, Qdrant, and reranker settings from environment variables here.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Entry point for the Hybrid RAG application.
2
+
3
+ Significance: create the FastAPI application and connect the hybrid retrieval routes here.
4
+ """
@@ -0,0 +1,4 @@
1
+ """BM25 retrieval placeholder.
2
+
3
+ Significance: provide lexical/keyword retrieval to complement semantic vector search.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Rank-fusion placeholder.
2
+
3
+ Significance: combine semantic and BM25 result rankings with Reciprocal Rank Fusion (RRF).
4
+ """
@@ -0,0 +1,4 @@
1
+ """Reranker placeholder.
2
+
3
+ Significance: optionally apply a FastEmbed cross-encoder to reorder the fused candidates by relevance.
4
+ """
@@ -0,0 +1,9 @@
1
+ fastapi==0.142.2
2
+ uvicorn==0.54.0
3
+ pydantic-settings==2.15.0
4
+ python-multipart==0.0.32
5
+ pypdf==6.19.0
6
+ fastembed==0.8.1
7
+ qdrant-client==1.19.1
8
+ openai==3.24.0
9
+ bm25s==0.3.11
@@ -0,0 +1,7 @@
1
+ APP_ENV=development
2
+ PORT=8000
3
+ LLM_BASE_URL=https://api.openai.com/v1
4
+ LLM_API_KEY=
5
+ LLM_MODEL=gpt-4o-mini
6
+ EMBEDDING_MODEL=BAAI/bge-small-en-v1.5
7
+ QDRANT_PATH=.qdrant
@@ -0,0 +1,31 @@
1
+ # {{name}}
2
+
3
+ Semantic RAG project skeleton.
4
+
5
+ Leo installs the dependencies and creates the folder structure; the RAG implementation is intentionally left for you to build.
6
+
7
+ ## Dependencies
8
+
9
+ - `fastapi` / `uvicorn[standard]` — API server
10
+ - `pydantic-settings` — typed environment configuration
11
+ - `python-multipart` — file uploads
12
+ - `pypdf` — PDF text extraction
13
+ - `fastembed` — lightweight ONNX embeddings
14
+ - `qdrant-client` — local vector storage/client
15
+ - `openai` — OpenAI-compatible LLM client
16
+
17
+ ## Structure
18
+
19
+ - `app/main.py` — application entry point
20
+ - `app/api/routes/ingest.py` — document ingestion route
21
+ - `app/api/routes/query.py` — retrieval/query route
22
+ - `app/api/routes/health.py` — health endpoint
23
+ - `app/core/config.py` — settings
24
+ - `app/services/chunker.py` — chunking logic
25
+ - `app/services/llm.py` — LLM integration
26
+ - `app/vectorstore/qdrant.py` — vector-store integration
27
+
28
+ The API starts with `uvicorn app.main:app --reload` and exposes `GET /health`.
29
+ The ingestion and retrieval pipeline is a skeleton for you to implement.
30
+
31
+ The first real embedding operation will download the selected FastEmbed model.
@@ -0,0 +1,6 @@
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ .qdrant/
6
+ .pytest_cache/
@@ -0,0 +1,10 @@
1
+ """Lightweight health endpoint for the semantic RAG API."""
2
+
3
+ from fastapi import APIRouter
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ @router.get("/health")
9
+ def health() -> dict[str, str]:
10
+ return {"status": "ok"}
@@ -0,0 +1,4 @@
1
+ """Document ingestion route placeholder.
2
+
3
+ Significance: receive documents, extract text, chunk it, create embeddings, and store vectors here.
4
+ """
@@ -0,0 +1,4 @@
1
+ """RAG query route placeholder.
2
+
3
+ Significance: accept a user question, retrieve relevant chunks, and pass context to an LLM here.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Application configuration placeholder.
2
+
3
+ Significance: define typed settings and environment-variable loading here.
4
+ """
@@ -0,0 +1,8 @@
1
+ """FastAPI application entry point for the semantic RAG project."""
2
+
3
+ from fastapi import FastAPI
4
+
5
+ from app.api.routes.health import router as health_router
6
+
7
+ app = FastAPI(title="Semantic RAG API")
8
+ app.include_router(health_router)
@@ -0,0 +1,4 @@
1
+ """Text chunking placeholder.
2
+
3
+ Significance: split extracted documents into retrieval-friendly chunks while preserving useful context.
4
+ """
@@ -0,0 +1,4 @@
1
+ """LLM client placeholder.
2
+
3
+ Significance: configure the OpenAI-compatible client and generate an answer from retrieved context here.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Qdrant/vector-store placeholder.
2
+
3
+ Significance: initialize local Qdrant, create the collection, store embeddings, and perform vector search here.
4
+ """
@@ -0,0 +1,8 @@
1
+ fastapi==0.142.2
2
+ uvicorn==0.54.0
3
+ pydantic-settings==2.15.0
4
+ python-multipart==0.0.32
5
+ pypdf==6.19.0
6
+ fastembed==0.8.1
7
+ qdrant-client==1.19.1
8
+ openai==3.24.0
package/versions.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "node": {
3
+ "express": "^5.1.0",
4
+ "mongoose": "^8.18.0",
5
+ "cors": "^2.8.5",
6
+ "pg": "^8.16.3",
7
+ "fastify": "^5.6.0",
8
+ "@fastify/cors": "^11.1.0"
9
+ },
10
+ "python": {
11
+ "fastapi": "fastapi==0.142.2",
12
+ "uvicorn": "uvicorn==0.54.0",
13
+ "pydantic-settings": "pydantic-settings==2.15.0",
14
+ "sqlalchemy": "sqlalchemy==2.1.2",
15
+ "psycopg": "psycopg==3.3.6",
16
+ "pymongo": "pymongo==4.18.2",
17
+ "python-multipart": "python-multipart==0.0.32",
18
+ "pypdf": "pypdf==6.19.0",
19
+ "fastembed": "fastembed==0.8.1",
20
+ "qdrant-client": "qdrant-client==1.19.1",
21
+ "openai": "openai==3.24.0",
22
+ "bm25s": "bm25s==0.3.11"
23
+ }
24
+ }