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