@devilsdev/rag-pipeline-utils 2.4.4 → 2.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.
Files changed (225) hide show
  1. package/.ragrc.schema.json +83 -192
  2. package/CHANGELOG.md +345 -0
  3. package/README.md +418 -117
  4. package/SECURITY.md +5 -5
  5. package/dist/experimental.cjs +2 -0
  6. package/dist/experimental.d.ts +1 -0
  7. package/dist/experimental.mjs +5 -0
  8. package/dist/index.cjs +4992 -1242
  9. package/dist/index.d.ts +80 -2
  10. package/dist/index.mjs +1 -1
  11. package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
  12. package/dist/types/agentic/iterative-retriever.d.ts +85 -0
  13. package/dist/types/agentic/query-planner.d.ts +145 -0
  14. package/dist/types/agentic/self-critique.d.ts +45 -0
  15. package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
  16. package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
  17. package/dist/types/ai/multimodal-processing.d.ts +32 -0
  18. package/dist/types/ai/multimodal.d.ts +39 -0
  19. package/dist/types/ai/retrieval-engine.d.ts +37 -0
  20. package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
  21. package/dist/types/chunking/chunking-engine.d.ts +70 -0
  22. package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
  23. package/dist/types/chunking/strategies/recursive.d.ts +20 -0
  24. package/dist/types/chunking/strategies/semantic.d.ts +19 -0
  25. package/dist/types/chunking/strategies/sentence.d.ts +13 -0
  26. package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
  27. package/dist/types/citation/citation-tracker.d.ts +64 -0
  28. package/dist/types/citation/hallucination-detector.d.ts +40 -0
  29. package/dist/types/citation/source-mapper.d.ts +37 -0
  30. package/dist/types/connectors/anthropic-connector.d.ts +92 -0
  31. package/dist/types/connectors/base-connector.d.ts +87 -0
  32. package/dist/types/connectors/cohere-connector.d.ts +89 -0
  33. package/dist/types/connectors/connector-utils.d.ts +110 -0
  34. package/dist/types/connectors/local-embedder.d.ts +86 -0
  35. package/dist/types/connectors/memory-retriever.d.ts +91 -0
  36. package/dist/types/connectors/ollama-connector.d.ts +110 -0
  37. package/dist/types/connectors/openai-connector.d.ts +114 -0
  38. package/dist/types/connectors/rag-prompt.d.ts +40 -0
  39. package/dist/types/cost/cost-calculator.d.ts +152 -0
  40. package/dist/types/cost/cost-tracker.d.ts +126 -0
  41. package/dist/types/cost/token-budget.d.ts +110 -0
  42. package/dist/types/dag/core/error-context.d.ts +239 -0
  43. package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
  44. package/dist/types/dag/core/topology-validation.d.ts +226 -0
  45. package/dist/types/dag/dag-engine.d.ts +156 -0
  46. package/dist/types/dag/dag-node.d.ts +37 -0
  47. package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
  48. package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
  49. package/dist/types/debug/execution-tracer.d.ts +57 -0
  50. package/dist/types/debug/trace-inspector.d.ts +31 -0
  51. package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
  52. package/dist/types/enterprise/data-governance.d.ts +203 -0
  53. package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
  54. package/dist/types/enterprise/sso-integration.d.ts +420 -0
  55. package/dist/types/evaluate/context-metrics.d.ts +55 -0
  56. package/dist/types/evaluate/faithfulness.d.ts +41 -0
  57. package/dist/types/evaluate/groundedness.d.ts +60 -0
  58. package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
  59. package/dist/types/evaluate/relevance.d.ts +21 -0
  60. package/dist/types/evaluate/scoring.d.ts +123 -0
  61. package/dist/types/experimental.d.ts +15 -0
  62. package/dist/types/graph/entity-extractor.d.ts +84 -0
  63. package/dist/types/graph/graph-index.d.ts +49 -0
  64. package/dist/types/graph/graph-retriever.d.ts +81 -0
  65. package/dist/types/graph/knowledge-graph.d.ts +129 -0
  66. package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
  67. package/dist/types/guardrails/injection-detector.d.ts +47 -0
  68. package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
  69. package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
  70. package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
  71. package/dist/types/mcp/mcp-server.d.ts +85 -0
  72. package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
  73. package/dist/types/reranker/cascade-reranker.d.ts +40 -0
  74. package/dist/types/reranker/embedding-reranker.d.ts +77 -0
  75. package/dist/types/reranker/scoring-reranker.d.ts +41 -0
  76. package/dist/types/retrieval/bm25-search.d.ts +43 -0
  77. package/dist/types/retrieval/file-vector-store.d.ts +87 -0
  78. package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
  79. package/dist/types/retrieval/rank-fusion.d.ts +10 -0
  80. package/dist/types/security/jwt-config.d.ts +80 -0
  81. package/dist/types/security/jwt-validator.d.ts +195 -0
  82. package/dist/types/security/plugin-sandbox.d.ts +158 -0
  83. package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
  84. package/dist/types/security/validator.d.ts +5 -0
  85. package/dist/types/streaming/embedding-stream.d.ts +65 -0
  86. package/dist/types/streaming/sse-adapter.d.ts +59 -0
  87. package/dist/types/streaming/stream-router.d.ts +88 -0
  88. package/dist/types/streaming/streaming-embedder.d.ts +99 -0
  89. package/dist/types/streaming/websocket-adapter.d.ts +71 -0
  90. package/dist/types/templates/scaffold.d.ts +55 -0
  91. package/dist/types/templates/template-registry.d.ts +63 -0
  92. package/dist/types/utils/embed-query.d.ts +20 -0
  93. package/dist/types/utils/logger.d.ts +50 -0
  94. package/dist/types/utils/rate-limiter.d.ts +66 -0
  95. package/dist/types/utils/retry.d.ts +27 -0
  96. package/dist/types/utils/run-abort.d.ts +34 -0
  97. package/dist/types/utils/secure-logging.d.ts +51 -0
  98. package/dist/types/utils/structured-logger.d.ts +126 -0
  99. package/package.json +53 -29
  100. package/src/agentic/agentic-pipeline.js +75 -7
  101. package/src/agentic/iterative-retriever.js +47 -10
  102. package/src/agentic/query-planner.js +329 -78
  103. package/src/citation/citation-tracker.js +6 -4
  104. package/src/citation/hallucination-detector.js +2 -2
  105. package/src/citation/source-mapper.js +15 -28
  106. package/src/cli/commands/ai.js +101 -0
  107. package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
  108. package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
  109. package/src/cli/commands/plugin-hub/local-commands.js +143 -0
  110. package/src/cli/commands/plugin-hub.js +96 -36
  111. package/src/cli/diagnostics/config-checker.js +76 -63
  112. package/src/cli/diagnostics/environment-checker.js +62 -31
  113. package/src/cli/diagnostics/plugin-checker.js +36 -89
  114. package/src/cli/doctor-command.js +39 -5
  115. package/src/cli/enhanced-cli-commands.js +72 -217
  116. package/src/cli/handlers/ai-handler.js +364 -0
  117. package/src/cli/handlers/diagnostics-handler.js +78 -31
  118. package/src/cli/handlers/ingest-handler.js +28 -22
  119. package/src/cli/handlers/init-handler.js +148 -25
  120. package/src/cli/handlers/query-handler.js +41 -26
  121. package/src/cli/handlers/run-handler.js +16 -167
  122. package/src/cli/handlers/serve-handler.js +60 -0
  123. package/src/cli/services/bench-stats.js +54 -0
  124. package/src/cli/services/benchmark.js +161 -0
  125. package/src/cli/services/feedback.js +155 -0
  126. package/src/cli/services/rag-service.js +322 -0
  127. package/src/cli/shared/cli-utils.js +24 -7
  128. package/src/cli/shared/config-context.js +21 -0
  129. package/src/cli/wizard/config-builder.js +81 -0
  130. package/src/cli/wizard/init-wizard.js +617 -0
  131. package/src/config/component-factory.js +125 -0
  132. package/src/config/load-config.js +29 -10
  133. package/src/config/load-plugin-config.js +46 -2
  134. package/src/connectors/anthropic-connector.js +93 -55
  135. package/src/connectors/base-connector.js +74 -19
  136. package/src/connectors/cohere-connector.js +88 -32
  137. package/src/connectors/connector-utils.js +302 -0
  138. package/src/connectors/local-embedder.js +43 -5
  139. package/src/connectors/memory-retriever.js +108 -15
  140. package/src/connectors/ollama-connector.js +83 -49
  141. package/src/connectors/openai-connector.js +108 -44
  142. package/src/connectors/rag-prompt.js +88 -0
  143. package/src/core/create-pipeline.js +209 -60
  144. package/src/core/plugin-registry.js +87 -11
  145. package/src/cost/cost-calculator.js +205 -32
  146. package/src/dev/dev-server.js +166 -11
  147. package/src/dev/hot-reload.js +40 -1
  148. package/src/ecosystem/plugin-checks.js +181 -0
  149. package/src/ecosystem/plugin-hub.js +50 -125
  150. package/src/ecosystem/plugin-installer.js +298 -0
  151. package/src/ecosystem/plugin-probe.js +127 -0
  152. package/src/ecosystem/plugin-scanner.js +185 -0
  153. package/src/ecosystem/tar-extract.js +114 -0
  154. package/src/enterprise/audit-logging.js +158 -29
  155. package/src/enterprise/sso-integration.js +715 -228
  156. package/src/evaluate/context-metrics.js +28 -16
  157. package/src/evaluate/evaluator.js +3 -3
  158. package/src/evaluate/faithfulness.js +18 -14
  159. package/src/evaluate/groundedness.js +43 -12
  160. package/src/evaluate/pipeline-evaluator.js +35 -8
  161. package/src/evaluate/scoring.js +251 -3
  162. package/src/experimental.js +71 -0
  163. package/src/guardrails/guardrails-pipeline.js +69 -7
  164. package/src/guardrails/injection-detector.js +283 -0
  165. package/src/guardrails/post-generation-guard.js +19 -0
  166. package/src/guardrails/pre-retrieval-guard.js +36 -36
  167. package/src/guardrails/retrieval-guard.js +58 -8
  168. package/src/index.js +36 -1
  169. package/src/loader/document-reader.js +91 -0
  170. package/src/reranker/cascade-reranker.js +3 -0
  171. package/src/reranker/embedding-reranker.js +136 -21
  172. package/src/reranker/llm-reranker.js +79 -12
  173. package/src/reranker/scoring-reranker.js +3 -0
  174. package/src/retrieval/bm25-search.js +12 -6
  175. package/src/retrieval/file-vector-store.js +212 -0
  176. package/src/retrieval/hybrid-retriever.js +22 -19
  177. package/src/security/jwt-config.js +13 -2
  178. package/src/security/jwt-validator.js +134 -30
  179. package/src/security/plugin-sandbox.js +18 -1
  180. package/src/security/plugin-signature-verifier.js +245 -70
  181. package/src/utils/deprecate.js +69 -0
  182. package/src/utils/embed-query.js +33 -0
  183. package/src/utils/input-sanitizer.js +19 -4
  184. package/src/utils/logger.js +34 -4
  185. package/src/utils/run-abort.js +102 -0
  186. package/src/ai/federated-learning.js +0 -211
  187. package/src/ai/federation.js +0 -71
  188. package/src/ai/index.js +0 -22
  189. package/src/ai/model-trainer.js +0 -217
  190. package/src/ai/model-training.js +0 -202
  191. package/src/ai/multimodal/multi-modal-processor.js +0 -345
  192. package/src/ai/orchestrator.js +0 -65
  193. package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
  194. package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
  195. package/src/cli/commands/ai-ml/federated-commands.js +0 -207
  196. package/src/cli/commands/ai-ml/index.js +0 -17
  197. package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
  198. package/src/cli/commands/ai-ml/shared.js +0 -51
  199. package/src/cli/commands/ai-ml/training-commands.js +0 -228
  200. package/src/cli/commands/ai-ml.js +0 -386
  201. package/src/cli/commands/docs.js +0 -228
  202. package/src/cli/commands/dx.js +0 -538
  203. package/src/cli/commands/wizard.js +0 -627
  204. package/src/cli/input-sanitization-middleware.js +0 -356
  205. package/src/cli/interactive-wizard.js +0 -373
  206. package/src/cli/templates/development.template.json +0 -64
  207. package/src/cli/templates/minimal.template.json +0 -33
  208. package/src/cli/templates/production.template.json +0 -104
  209. package/src/cli/templates/testing.template.json +0 -45
  210. package/src/cli/wizard/configuration.js +0 -148
  211. package/src/cli/wizard/observability.js +0 -79
  212. package/src/cli/wizard/performance.js +0 -76
  213. package/src/cli/wizard/plugin-selection.js +0 -297
  214. package/src/cli/wizard/preview-save.js +0 -172
  215. package/src/cli/wizard/project-setup.js +0 -66
  216. package/src/core/pipeline-factory.js +0 -59
  217. package/src/dag/enhanced-dag-engine.js +0 -894
  218. package/src/dx/index.js +0 -40
  219. package/src/dx/integration-templates.js +0 -662
  220. package/src/dx/performance-profiler.js +0 -968
  221. package/src/dx/realtime-debugger.js +0 -755
  222. package/src/dx/visual-pipeline-builder.js +0 -578
  223. package/src/ingest.js +0 -59
  224. package/src/pipeline/pipeline.js +0 -18
  225. package/src/query.js +0 -117
@@ -0,0 +1,617 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Interactive setup wizard for `rag-pipeline init`.
5
+ *
6
+ * Every choice maps to something buildComponents() can instantiate: the
7
+ * built-in components in config/component-factory, or a custom plugin
8
+ * module that is loaded and checked for the stage's methods before it is
9
+ * accepted. The prompt function is injected (inquirer.prompt in the CLI) and
10
+ * is always called with a single question, so tests can script answers.
11
+ */
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+ const {
16
+ BUILT_INS,
17
+ loadPluginModule,
18
+ buildComponents: defaultBuildComponents,
19
+ } = require("../../config/component-factory");
20
+ const { embedQuery } = require("../../utils/embed-query");
21
+ const { buildInitConfig, DEFAULT_INDEX_PATH } = require("./config-builder");
22
+
23
+ const CUSTOM = "custom";
24
+ const NONE = "none";
25
+ const DEFAULT_TIMEOUT_MS = 15000;
26
+
27
+ /** Environment variable each hosted provider reads its API key from. */
28
+ const API_KEY_ENV = {
29
+ openai: "OPENAI_API_KEY",
30
+ anthropic: "ANTHROPIC_API_KEY",
31
+ cohere: "COHERE_API_KEY",
32
+ };
33
+
34
+ const CONNECTOR_MODULES = {
35
+ openai: "../../connectors/openai-connector",
36
+ anthropic: "../../connectors/anthropic-connector",
37
+ ollama: "../../connectors/ollama-connector",
38
+ cohere: "../../connectors/cohere-connector",
39
+ };
40
+
41
+ /** Connector options the wizard asks for, per stage and provider. */
42
+ const PROVIDER_OPTIONS = {
43
+ embedder: {
44
+ openai: ["embeddingModel"],
45
+ ollama: ["baseURL", "embeddingModel"],
46
+ cohere: ["embeddingModel"],
47
+ },
48
+ llm: {
49
+ openai: ["model"],
50
+ anthropic: ["model"],
51
+ ollama: ["baseURL", "model"],
52
+ },
53
+ };
54
+
55
+ const OPTION_LABELS = {
56
+ model: "model",
57
+ embeddingModel: "embedding model",
58
+ baseURL: "server URL",
59
+ };
60
+
61
+ /**
62
+ * Methods a custom plugin must expose (`anyOf`), and methods the CLI needs
63
+ * for some commands (`recommended`, a warning when missing).
64
+ */
65
+ const STAGE_CONTRACTS = {
66
+ embedder: {
67
+ anyOf: ["embed", "embedQuery"],
68
+ recommended: { embed: "`ingest` calls embed() to embed documents" },
69
+ },
70
+ llm: { anyOf: ["generate"], recommended: {} },
71
+ retriever: {
72
+ anyOf: ["retrieve"],
73
+ recommended: { store: "`ingest` calls store() to save vectors" },
74
+ },
75
+ reranker: { anyOf: ["rerank"], recommended: {} },
76
+ };
77
+
78
+ const CUSTOM_CHOICE = {
79
+ name: "custom plugin (path to a .js module)",
80
+ value: CUSTOM,
81
+ };
82
+
83
+ const STAGE_CHOICES = {
84
+ embedder: [
85
+ { name: "local (TF-IDF, offline, no API key)", value: "local" },
86
+ { name: "openai (OPENAI_API_KEY)", value: "openai" },
87
+ { name: "ollama (local Ollama server)", value: "ollama" },
88
+ { name: "cohere (COHERE_API_KEY)", value: "cohere" },
89
+ CUSTOM_CHOICE,
90
+ ],
91
+ llm: [
92
+ { name: "none (query prints the retrieved passages)", value: NONE },
93
+ { name: "openai (OPENAI_API_KEY)", value: "openai" },
94
+ { name: "anthropic (ANTHROPIC_API_KEY)", value: "anthropic" },
95
+ { name: "ollama (local Ollama server)", value: "ollama" },
96
+ CUSTOM_CHOICE,
97
+ ],
98
+ retriever: [
99
+ {
100
+ name: "file-store (JSON index on disk, persists between commands)",
101
+ value: "file-store",
102
+ },
103
+ {
104
+ name: "memory (in-process only, lost when the command exits)",
105
+ value: "memory",
106
+ },
107
+ CUSTOM_CHOICE,
108
+ ],
109
+ reranker: [
110
+ { name: "none", value: NONE },
111
+ { name: "scoring (BM25 re-scoring, offline)", value: "scoring" },
112
+ CUSTOM_CHOICE,
113
+ ],
114
+ };
115
+
116
+ const STAGE_TITLES = {
117
+ embedder: "Embedder",
118
+ llm: "Language model",
119
+ retriever: "Retriever",
120
+ reranker: "Reranker (optional)",
121
+ };
122
+
123
+ /** Ask one question and return its answer. */
124
+ async function ask(prompt, question) {
125
+ const answers = await prompt([question]);
126
+ return answers ? answers[question.name] : undefined;
127
+ }
128
+
129
+ /** Defaults for the options the wizard asks for (never the API key). */
130
+ function connectorDefaults(provider) {
131
+ const { DEFAULT_CONFIG } = require(CONNECTOR_MODULES[provider]);
132
+ return {
133
+ model: DEFAULT_CONFIG.model,
134
+ embeddingModel: DEFAULT_CONFIG.embeddingModel,
135
+ baseURL: DEFAULT_CONFIG.baseURL,
136
+ };
137
+ }
138
+
139
+ /** Path written to the config: relative to its directory, forward slashes. */
140
+ function toConfigPath(absPath, configDir) {
141
+ // Segment-wise instead of path.relative(), which returns wrong results on
142
+ // some Windows Node releases (e.g. "..C:/..." for a file inside `from`).
143
+ const win = process.platform === "win32";
144
+ const same = (a, b) => (win ? a.toLowerCase() === b.toLowerCase() : a === b);
145
+ const from = path
146
+ .resolve(configDir)
147
+ .split(/[\\/]+/)
148
+ .filter(Boolean);
149
+ const to = path
150
+ .resolve(absPath)
151
+ .split(/[\\/]+/)
152
+ .filter(Boolean);
153
+ if (win && !same(from[0] || "", to[0] || "")) {
154
+ return path.resolve(absPath).split(path.sep).join("/"); // other drive
155
+ }
156
+ let common = 0;
157
+ while (
158
+ common < from.length &&
159
+ common < to.length &&
160
+ same(from[common], to[common])
161
+ ) {
162
+ common++;
163
+ }
164
+ const rel = [
165
+ ...Array(from.length - common).fill(".."),
166
+ ...to.slice(common),
167
+ ].join("/");
168
+ return rel.startsWith("../") ? rel : `./${rel}`;
169
+ }
170
+
171
+ /** Plugin name from the file name, never shadowed by a built-in. */
172
+ function pluginNameFor(stage, absPath) {
173
+ const base =
174
+ path
175
+ .basename(absPath, path.extname(absPath))
176
+ .replace(/[^A-Za-z0-9_-]+/g, "-")
177
+ .replace(/^-+|-+$/g, "") || `custom-${stage}`;
178
+ const builtIns = BUILT_INS[stage] || {};
179
+ let name = base;
180
+ for (let n = 2; Object.prototype.hasOwnProperty.call(builtIns, name); n++) {
181
+ name = n === 2 ? `${base}-custom` : `${base}-custom-${n}`;
182
+ }
183
+ return name;
184
+ }
185
+
186
+ /**
187
+ * Load a custom plugin the way buildComponents() will and check that it
188
+ * exposes the stage's methods.
189
+ *
190
+ * @param {string} stage - embedder | llm | retriever | reranker
191
+ * @param {string} input - Path as typed by the user (relative to cwd)
192
+ * @param {object} options
193
+ * @param {string} options.cwd - Directory user input is resolved against
194
+ * @param {string} options.configDir - Directory of the output config file
195
+ * @returns {{ok: true, selection: object, warnings: string[]} | {ok: false, error: string}}
196
+ */
197
+ function inspectPlugin(stage, input, { cwd, configDir }) {
198
+ const absPath = path.resolve(cwd, input);
199
+ let stat;
200
+ try {
201
+ stat = fs.statSync(absPath);
202
+ } catch {
203
+ return { ok: false, error: `File not found: ${absPath}` };
204
+ }
205
+ if (!stat.isFile()) {
206
+ return { ok: false, error: `Not a file: ${absPath}` };
207
+ }
208
+
209
+ // Pick up edits when the user fixes the file and tries again.
210
+ try {
211
+ delete require.cache[require.resolve(absPath)];
212
+ } catch {
213
+ // not resolvable yet; loadPluginModule reports the real error
214
+ }
215
+
216
+ let instance;
217
+ try {
218
+ instance = loadPluginModule(absPath, {}, configDir);
219
+ } catch (error) {
220
+ return { ok: false, error: `Could not load ${absPath}: ${error.message}` };
221
+ }
222
+
223
+ const contract = STAGE_CONTRACTS[stage];
224
+ const has = (method) => instance && typeof instance[method] === "function";
225
+ if (!contract.anyOf.some(has)) {
226
+ return {
227
+ ok: false,
228
+ error: `${absPath} does not look like a ${stage}: it must export a class or object with ${contract.anyOf.map((m) => `${m}()`).join(" or ")}`,
229
+ };
230
+ }
231
+
232
+ const warnings = Object.entries(contract.recommended)
233
+ .filter(([method]) => !has(method))
234
+ .map(([method, why]) => `No ${method}() method: ${why}, so it will fail.`);
235
+
236
+ return {
237
+ ok: true,
238
+ selection: {
239
+ name: pluginNameFor(stage, absPath),
240
+ module: toConfigPath(absPath, configDir),
241
+ },
242
+ warnings,
243
+ };
244
+ }
245
+
246
+ /** Ask for a custom plugin path until it validates; empty input cancels. */
247
+ async function askCustomPlugin(ctx, stage) {
248
+ for (;;) {
249
+ const answer = await ask(ctx.prompt, {
250
+ type: "input",
251
+ name: `${stage}Plugin`,
252
+ message: `Path to your ${stage} module (empty to go back):`,
253
+ });
254
+ const input = String(answer || "").trim();
255
+ if (!input) return null;
256
+
257
+ const result = inspectPlugin(stage, input, ctx);
258
+ if (!result.ok) {
259
+ ctx.log(` ✖ ${result.error}`);
260
+ continue;
261
+ }
262
+ for (const warning of result.warnings) ctx.log(` ⚠ ${warning}`);
263
+ ctx.log(
264
+ ` ✔ ${stage} plugin "${result.selection.name}" -> ${result.selection.module}`,
265
+ );
266
+ return result.selection;
267
+ }
268
+ }
269
+
270
+ /** Ask for the connector options of a built-in provider. */
271
+ async function askProviderOptions(ctx, stage, provider) {
272
+ const keys = (PROVIDER_OPTIONS[stage] || {})[provider] || [];
273
+ if (keys.length === 0) return undefined;
274
+
275
+ const defaults = connectorDefaults(provider);
276
+ const config = {};
277
+ for (const key of keys) {
278
+ // Reuse the Ollama URL typed for the embedder as the LLM's default.
279
+ const fallback =
280
+ key === "baseURL" && ctx.ollamaBaseURL
281
+ ? ctx.ollamaBaseURL
282
+ : defaults[key];
283
+ const answer = await ask(ctx.prompt, {
284
+ type: "input",
285
+ name: `${stage}_${key}`,
286
+ message: `${provider} ${OPTION_LABELS[key]}:`,
287
+ default: fallback,
288
+ });
289
+ config[key] = String(answer ?? "").trim() || fallback;
290
+ if (key === "baseURL") ctx.ollamaBaseURL = config[key];
291
+ }
292
+ return config;
293
+ }
294
+
295
+ /** One pipeline stage: pick a provider, then its options or plugin path. */
296
+ async function askStage(ctx, stage, defaultName) {
297
+ for (;;) {
298
+ const choice = await ask(ctx.prompt, {
299
+ type: "list",
300
+ name: stage,
301
+ message: `${STAGE_TITLES[stage]}:`,
302
+ choices: STAGE_CHOICES[stage],
303
+ default: defaultName,
304
+ });
305
+
306
+ if (choice === NONE || choice === null || choice === undefined) {
307
+ if (stage === "embedder" || stage === "retriever") {
308
+ ctx.log(` ✖ A ${stage} is required.`);
309
+ continue;
310
+ }
311
+ return null;
312
+ }
313
+
314
+ if (choice === CUSTOM) {
315
+ const selection = await askCustomPlugin(ctx, stage);
316
+ if (selection) return selection;
317
+ continue;
318
+ }
319
+
320
+ if (!BUILT_INS[stage] || !BUILT_INS[stage][choice]) {
321
+ ctx.log(` ✖ Unknown ${stage} "${choice}".`);
322
+ continue;
323
+ }
324
+
325
+ if (stage === "retriever") return askRetrieverOptions(ctx, choice);
326
+
327
+ const config = await askProviderOptions(ctx, stage, choice);
328
+ return config ? { name: choice, config } : { name: choice };
329
+ }
330
+ }
331
+
332
+ async function askRetrieverOptions(ctx, name) {
333
+ if (name === "memory") {
334
+ ctx.log(
335
+ " ⚠ The memory retriever does not persist: `ingest` and `query` run as " +
336
+ "separate processes, so documents ingested by one command are gone " +
337
+ "for the next. Use it from code or with `run --ingest ... --query ...`.",
338
+ );
339
+ return { name };
340
+ }
341
+ const answer = await ask(ctx.prompt, {
342
+ type: "input",
343
+ name: "indexPath",
344
+ message: "Index file (relative to the config file):",
345
+ default: ctx.defaults.indexPath || DEFAULT_INDEX_PATH,
346
+ });
347
+ const indexPath =
348
+ String(answer ?? "").trim() || ctx.defaults.indexPath || DEFAULT_INDEX_PATH;
349
+ return { name, config: { path: indexPath } };
350
+ }
351
+
352
+ /**
353
+ * Which API keys / services the chosen built-in providers need.
354
+ *
355
+ * @param {object} selection - Stage selections from the wizard
356
+ * @param {object} env - Environment to check (process.env in the CLI)
357
+ * @returns {Array<{provider: string, stages: string[], envVar?: string, set?: boolean, baseURL?: string}>}
358
+ */
359
+ function checkEnvironment(selection, env) {
360
+ const byProvider = new Map();
361
+ for (const stage of ["embedder", "llm", "reranker"]) {
362
+ const sel = selection[stage];
363
+ if (!sel || sel.module) continue;
364
+ const envVar = API_KEY_ENV[sel.name];
365
+ if (envVar) {
366
+ const item = byProvider.get(sel.name) || {
367
+ provider: sel.name,
368
+ stages: [],
369
+ envVar,
370
+ set: Boolean(env[envVar]),
371
+ };
372
+ item.stages.push(stage);
373
+ byProvider.set(sel.name, item);
374
+ } else if (sel.name === "ollama") {
375
+ const baseURL =
376
+ (sel.config && sel.config.baseURL) || "http://localhost:11434";
377
+ const key = `ollama ${baseURL}`;
378
+ const item = byProvider.get(key) || {
379
+ provider: "ollama",
380
+ stages: [],
381
+ baseURL,
382
+ };
383
+ item.stages.push(stage);
384
+ byProvider.set(key, item);
385
+ }
386
+ }
387
+ return [...byProvider.values()];
388
+ }
389
+
390
+ function reportEnvironment(checks, log) {
391
+ log("\nEnvironment check:");
392
+ if (checks.length === 0) {
393
+ log(" ✔ No API keys needed for the chosen components.");
394
+ return;
395
+ }
396
+ for (const check of checks) {
397
+ const used = `${check.provider} ${check.stages.join(" + ")}`;
398
+ if (check.envVar && check.set) {
399
+ log(` ✔ ${check.envVar} is set (${used})`);
400
+ } else if (check.envVar) {
401
+ log(
402
+ ` ⚠ ${check.envVar} is not set (${used}). Set it before ingest/query:`,
403
+ );
404
+ log(` export ${check.envVar}=<your key> # bash/zsh`);
405
+ log(` $env:${check.envVar} = "<your key>" # PowerShell`);
406
+ } else {
407
+ log(
408
+ ` • ${used} needs an Ollama server at ${check.baseURL} (no API key)`,
409
+ );
410
+ }
411
+ }
412
+ log(
413
+ " API keys are read from the environment and never written to the config file.",
414
+ );
415
+ }
416
+
417
+ /**
418
+ * Run fn(signal) with a time limit. On timeout the AbortSignal handed to fn
419
+ * is aborted (with the timeout error as its reason) so a connector that
420
+ * honours `options.signal` cancels its request instead of leaving a socket
421
+ * open; the timer is unref'd and always cleared.
422
+ */
423
+ async function withTimeout(fn, ms, label) {
424
+ const controller = new AbortController();
425
+ let timer;
426
+ const timeout = new Promise((_, reject) => {
427
+ timer = setTimeout(() => {
428
+ const error = new Error(`${label} timed out after ${ms} ms`);
429
+ controller.abort(error);
430
+ reject(error);
431
+ }, ms);
432
+ if (timer && typeof timer.unref === "function") timer.unref();
433
+ });
434
+ try {
435
+ return await Promise.race([
436
+ Promise.resolve().then(() => fn(controller.signal)),
437
+ timeout,
438
+ ]);
439
+ } finally {
440
+ clearTimeout(timer);
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Instantiate the embedder and llm from the config and make one real call
446
+ * to each. Failures are reported, never thrown.
447
+ *
448
+ * @returns {Promise<Array<{stage: string, ok: boolean, skipped?: boolean, message: string}>>}
449
+ */
450
+ async function testConnections(config, options) {
451
+ const { buildComponents, configDir, timeoutMs } = options;
452
+ const results = [];
453
+
454
+ for (const stage of ["embedder", "llm"]) {
455
+ const entry = config.pipeline.find((e) => e.stage === stage);
456
+ if (!entry) continue;
457
+ if (stage === "embedder" && entry.name === "local") {
458
+ results.push({
459
+ stage,
460
+ ok: true,
461
+ skipped: true,
462
+ message: "local embedder runs offline; nothing to test",
463
+ });
464
+ continue;
465
+ }
466
+
467
+ try {
468
+ const components = buildComponents(
469
+ { ...config, pipeline: [entry] },
470
+ { configDir },
471
+ );
472
+ const component = components[stage];
473
+ if (stage === "embedder") {
474
+ const vector = await withTimeout(
475
+ (signal) => embedQuery(component, "test", { signal }),
476
+ timeoutMs,
477
+ 'embedQuery("test")',
478
+ );
479
+ if (!Array.isArray(vector) || vector.length === 0) {
480
+ throw new Error('embedQuery("test") returned no vector');
481
+ }
482
+ results.push({
483
+ stage,
484
+ ok: true,
485
+ message: `${entry.name} returned a ${vector.length}-dimension vector`,
486
+ });
487
+ } else {
488
+ const reply = await withTimeout(
489
+ (signal) => component.generate("Say OK", [], { signal }),
490
+ timeoutMs,
491
+ 'generate("Say OK")',
492
+ );
493
+ const text =
494
+ typeof reply === "string"
495
+ ? reply
496
+ : reply && (reply.text || reply.content || reply.answer);
497
+ results.push({
498
+ stage,
499
+ ok: true,
500
+ message: `${entry.name} replied${text ? `: ${String(text).trim().slice(0, 60)}` : ""}`,
501
+ });
502
+ }
503
+ } catch (error) {
504
+ results.push({
505
+ stage,
506
+ ok: false,
507
+ message: `${entry.name}: ${error && error.message ? error.message : error}`,
508
+ });
509
+ }
510
+ }
511
+ return results;
512
+ }
513
+
514
+ /**
515
+ * Run the wizard.
516
+ *
517
+ * @param {object} options
518
+ * @param {Function} options.prompt - inquirer-style prompt([question]) => answers
519
+ * @param {object} [options.defaults] - {embedder, llm, indexPath} from --template
520
+ * @param {string} [options.outputPath='.ragrc.json'] - Config file to write
521
+ * @param {boolean} [options.overwrite=false] - The output file exists and will be replaced
522
+ * @param {object} [options.env=process.env] - Environment for the API key check
523
+ * @param {Function} [options.buildComponents] - Used by the connection test
524
+ * @param {Function} [options.log=console.log]
525
+ * @param {string} [options.cwd=process.cwd()] - Base for plugin paths typed by the user
526
+ * @param {number} [options.timeoutMs=15000] - Connection test timeout per call
527
+ * @returns {Promise<null | {config: object, selection: object, environment: Array, connection: Array|null}>}
528
+ * null when the user declines to write the file
529
+ */
530
+ async function runInitWizard(options) {
531
+ const {
532
+ prompt,
533
+ defaults = {},
534
+ outputPath = ".ragrc.json",
535
+ overwrite = false,
536
+ env = process.env,
537
+ buildComponents = defaultBuildComponents,
538
+ log = console.log,
539
+ cwd = process.cwd(),
540
+ timeoutMs = DEFAULT_TIMEOUT_MS,
541
+ } = options;
542
+ if (typeof prompt !== "function") {
543
+ throw new Error("runInitWizard requires a prompt function");
544
+ }
545
+
546
+ const configDir = path.dirname(path.resolve(cwd, outputPath));
547
+ const ctx = { prompt, log, cwd, configDir, defaults };
548
+
549
+ log("rag-pipeline setup: choose the components for your pipeline.");
550
+ log(
551
+ "Custom plugin paths are checked now and stored relative to the config file.\n",
552
+ );
553
+
554
+ const selection = {};
555
+ selection.embedder = await askStage(
556
+ ctx,
557
+ "embedder",
558
+ defaults.embedder || "local",
559
+ );
560
+ selection.llm = await askStage(ctx, "llm", defaults.llm || NONE);
561
+ selection.retriever = await askStage(ctx, "retriever", "file-store");
562
+ selection.reranker = await askStage(ctx, "reranker", NONE);
563
+
564
+ const config = buildInitConfig(selection);
565
+
566
+ const environment = checkEnvironment(selection, env);
567
+ reportEnvironment(environment, log);
568
+
569
+ let connection = null;
570
+ const wantsTest = await ask(prompt, {
571
+ type: "confirm",
572
+ name: "testConnection",
573
+ message: "Test the embedder/LLM now? (makes real API calls)",
574
+ default: false,
575
+ });
576
+ if (wantsTest) {
577
+ log("\nConnection test:");
578
+ connection = await testConnections(config, {
579
+ buildComponents,
580
+ configDir,
581
+ timeoutMs,
582
+ });
583
+ for (const result of connection) {
584
+ const mark = result.skipped ? "•" : result.ok ? "✔" : "✖";
585
+ log(` ${mark} ${result.stage}: ${result.message}`);
586
+ }
587
+ if (connection.some((r) => !r.ok)) {
588
+ log(
589
+ " The configuration can still be saved; fix the issue before running queries.",
590
+ );
591
+ }
592
+ }
593
+
594
+ log(`\nConfiguration preview (${outputPath}):`);
595
+ log(JSON.stringify(config, null, 2));
596
+ const confirmed = await ask(prompt, {
597
+ type: "confirm",
598
+ name: "confirmWrite",
599
+ message: overwrite
600
+ ? `Overwrite ${outputPath} with this configuration?`
601
+ : `Write this configuration to ${outputPath}?`,
602
+ default: true,
603
+ });
604
+ if (!confirmed) return null;
605
+
606
+ return { config, selection, environment, connection };
607
+ }
608
+
609
+ module.exports = {
610
+ runInitWizard,
611
+ inspectPlugin,
612
+ checkEnvironment,
613
+ testConnections,
614
+ ask,
615
+ API_KEY_ENV,
616
+ STAGE_CONTRACTS,
617
+ };