@devilsdev/rag-pipeline-utils 2.4.5 → 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,161 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * `rag-pipeline ai benchmark`: time real queries against the configured
5
+ * pipeline and index. Every number reported is measured here.
6
+ */
7
+
8
+ const fs = require("fs");
9
+ const { embedQuery } = require("../../utils/embed-query");
10
+ const { loadComponents, query } = require("./rag-service");
11
+ const { summarize } = require("./bench-stats");
12
+
13
+ const SAMPLE_QUERIES = 5;
14
+ const SAMPLE_WORDS = 8;
15
+
16
+ /**
17
+ * Read queries from a file: a JSON array of strings, or one query per line.
18
+ * @param {string} file
19
+ * @returns {string[]}
20
+ */
21
+ function readQueryFile(file) {
22
+ if (!fs.existsSync(file)) throw new Error(`Queries file not found: ${file}`);
23
+ const text = fs.readFileSync(file, "utf8");
24
+ const trimmed = text.trim();
25
+ let queries;
26
+ if (trimmed.startsWith("[")) {
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(trimmed);
30
+ } catch (error) {
31
+ throw new Error(
32
+ `Queries file ${file} is not valid JSON: ${error.message}`,
33
+ );
34
+ }
35
+ if (!parsed.every((q) => typeof q === "string")) {
36
+ throw new Error(`Queries file ${file} must be a JSON array of strings`);
37
+ }
38
+ queries = parsed;
39
+ } else {
40
+ queries = text.split(/\r?\n/);
41
+ }
42
+ queries = queries.map((q) => q.trim()).filter(Boolean);
43
+ if (queries.length === 0) throw new Error(`No queries found in ${file}`);
44
+ return queries;
45
+ }
46
+
47
+ /**
48
+ * Deterministic sample: the first words of the first few indexed chunks.
49
+ * @param {Array<{content: string}>} chunks
50
+ * @returns {string[]}
51
+ */
52
+ function sampleQueries(chunks, count = SAMPLE_QUERIES) {
53
+ return chunks
54
+ .map((c) => String(c.content).split(/\s+/).filter(Boolean))
55
+ .filter((words) => words.length > 0)
56
+ .slice(0, count)
57
+ .map((words) => words.slice(0, SAMPLE_WORDS).join(" "));
58
+ }
59
+
60
+ const elapsedMs = (start) => Number(process.hrtime.bigint() - start) / 1e6;
61
+
62
+ async function timed(fn) {
63
+ const start = process.hrtime.bigint();
64
+ const value = await fn();
65
+ return { value, ms: elapsedMs(start) };
66
+ }
67
+
68
+ /**
69
+ * @param {object} params
70
+ * @param {object} params.config
71
+ * @param {string} params.configDir
72
+ * @param {string} [params.queriesFile]
73
+ * @param {number} [params.runs=3] - Passes over the query set
74
+ * @param {number} [params.topK=3]
75
+ * @returns {Promise<object>} Measured report
76
+ */
77
+ async function runBenchmark({
78
+ config,
79
+ configDir,
80
+ queriesFile,
81
+ runs = 3,
82
+ topK = 3,
83
+ }) {
84
+ const setup = await timed(() => loadComponents({ config, configDir }));
85
+ const components = setup.value;
86
+ const { embedder, retriever, reranker, llm } = components;
87
+
88
+ const queries = queriesFile
89
+ ? readQueryFile(queriesFile)
90
+ : sampleQueries(retriever.list ? retriever.list() : []);
91
+ if (queries.length === 0) {
92
+ throw new Error(
93
+ "No queries: pass --queries <file> (the index has no text to sample from).",
94
+ );
95
+ }
96
+
97
+ // First query pays one-time costs (lazy init, JIT); reported, not counted.
98
+ const warmup = await timed(() =>
99
+ query({ components, question: queries[0], topK }),
100
+ );
101
+
102
+ const samples = { embed: [], retrieve: [], rerank: [], endToEnd: [] };
103
+ const resultCounts = [];
104
+ for (let run = 0; run < runs; run++) {
105
+ for (const question of queries) {
106
+ const embedded = await timed(() => embedQuery(embedder, question));
107
+ samples.embed.push(embedded.ms);
108
+ const retrieved = await timed(() =>
109
+ retriever.retrieve({
110
+ query: question,
111
+ queryVector: embedded.value,
112
+ topK,
113
+ }),
114
+ );
115
+ samples.retrieve.push(retrieved.ms);
116
+ if (reranker) {
117
+ const reranked = await timed(() =>
118
+ reranker.rerank(question, retrieved.value, { topK }),
119
+ );
120
+ samples.rerank.push(reranked.ms);
121
+ }
122
+ const e2e = await timed(() => query({ components, question, topK }));
123
+ samples.endToEnd.push(e2e.ms);
124
+ resultCounts.push(e2e.value.results.length);
125
+ }
126
+ }
127
+
128
+ const stages = {
129
+ embed: summarize(samples.embed),
130
+ retrieve: summarize(samples.retrieve),
131
+ ...(reranker && { rerank: summarize(samples.rerank) }),
132
+ endToEnd: summarize(samples.endToEnd),
133
+ };
134
+
135
+ return {
136
+ queries,
137
+ queriesSource: queriesFile || "sampled from indexed chunks",
138
+ runs,
139
+ topK,
140
+ setupMs: setup.ms,
141
+ warmupMs: warmup.ms,
142
+ stages,
143
+ includesGeneration: Boolean(llm),
144
+ results: {
145
+ min: Math.min(...resultCounts),
146
+ max: Math.max(...resultCounts),
147
+ },
148
+ environment: {
149
+ node: process.version,
150
+ platform: `${process.platform} ${process.arch}`,
151
+ chunks: retriever.size ? retriever.size() : null,
152
+ embedder: components.names.embedder,
153
+ dimensions: retriever.getEmbedderInfo?.()?.dimensions ?? null,
154
+ retriever: components.names.retriever,
155
+ reranker: components.names.reranker || null,
156
+ llm: components.names.llm || null,
157
+ },
158
+ };
159
+ }
160
+
161
+ module.exports = { runBenchmark, readQueryFile, sampleQueries };
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Relevance feedback for `rag-pipeline ai feedback` / `ai search`.
5
+ *
6
+ * Feedback is stored as JSON next to the index. Scoring is deterministic and
7
+ * explainable: every stored vote for a result's docId contributes
8
+ *
9
+ * sign(vote) * VOTE_WEIGHT * jaccard(terms(question), terms(votedQuestion))
10
+ *
11
+ * and the summed adjustment is capped to +/- MAX_ADJUSTMENT before being added
12
+ * to the retrieval score. Votes on unrelated questions (no shared terms)
13
+ * contribute nothing.
14
+ */
15
+
16
+ const fs = require("fs");
17
+ const path = require("path");
18
+ const { tokenize } = require("../../evaluate/scoring");
19
+
20
+ const FORMAT_VERSION = 1;
21
+ const VOTE_WEIGHT = 0.1;
22
+ const MAX_ADJUSTMENT = 0.3;
23
+ const RATINGS = ["up", "down"];
24
+
25
+ /**
26
+ * Where feedback lives: next to a file-store index, else <configDir>/.rag.
27
+ *
28
+ * @param {object} retriever - Built retriever (FileVectorStore has `path`)
29
+ * @param {string} configDir
30
+ * @returns {string}
31
+ */
32
+ function feedbackPath(retriever, configDir) {
33
+ const dir =
34
+ retriever && typeof retriever.path === "string"
35
+ ? path.dirname(retriever.path)
36
+ : path.join(configDir, ".rag");
37
+ return path.join(dir, "feedback.json");
38
+ }
39
+
40
+ /**
41
+ * @param {string} file
42
+ * @returns {{version: number, entries: Array<{question: string, docId: string, rating: string, at: string}>}}
43
+ */
44
+ function loadFeedback(file) {
45
+ if (!fs.existsSync(file)) return { version: FORMAT_VERSION, entries: [] };
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
49
+ } catch (error) {
50
+ throw new Error(
51
+ `Feedback file ${file} is not valid JSON: ${error.message}`,
52
+ );
53
+ }
54
+ if (parsed.version !== FORMAT_VERSION || !Array.isArray(parsed.entries)) {
55
+ throw new Error(`Unsupported feedback file format in ${file}`);
56
+ }
57
+ return parsed;
58
+ }
59
+
60
+ /**
61
+ * Append one vote and write the file atomically (temp file, then rename).
62
+ *
63
+ * @param {string} file
64
+ * @param {{question: string, docId: string, rating: string}} vote
65
+ * @returns {{entry: object, total: number}}
66
+ */
67
+ function addFeedback(file, { question, docId, rating }) {
68
+ if (!RATINGS.includes(rating)) {
69
+ throw new Error(`Rating must be one of: ${RATINGS.join(", ")}`);
70
+ }
71
+ if (!String(question || "").trim()) throw new Error("Question is empty");
72
+ const data = loadFeedback(file);
73
+ const entry = { question, docId, rating, at: new Date().toISOString() };
74
+ data.entries.push(entry);
75
+ fs.mkdirSync(path.dirname(file), { recursive: true });
76
+ const tmp = `${file}.${process.pid}.tmp`;
77
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
78
+ fs.renameSync(tmp, file);
79
+ return { entry, total: data.entries.length };
80
+ }
81
+
82
+ /**
83
+ * Jaccard similarity of the two texts' token sets (0..1).
84
+ * @param {string} a
85
+ * @param {string} b
86
+ * @returns {number}
87
+ */
88
+ function termSimilarity(a, b) {
89
+ const sa = new Set(tokenize(a));
90
+ const sb = new Set(tokenize(b));
91
+ if (sa.size === 0 || sb.size === 0) return 0;
92
+ let shared = 0;
93
+ for (const t of sa) if (sb.has(t)) shared++;
94
+ return shared / (sa.size + sb.size - shared);
95
+ }
96
+
97
+ /**
98
+ * Re-score retrieval results with stored feedback.
99
+ *
100
+ * @param {Array<{id: string, score: number}>} results - Retrieval results
101
+ * @param {Array<{question: string, docId: string, rating: string}>} entries - Stored votes
102
+ * @param {string} question - Current question
103
+ * @param {object} [options]
104
+ * @param {number} [options.voteWeight=VOTE_WEIGHT]
105
+ * @param {number} [options.maxAdjustment=MAX_ADJUSTMENT]
106
+ * @returns {Array<object>} Results sorted by finalScore (ties keep retrieval
107
+ * order), each with baseScore, adjustment, finalScore and `reasons`.
108
+ */
109
+ function applyFeedback(results, entries, question, options = {}) {
110
+ const voteWeight = options.voteWeight ?? VOTE_WEIGHT;
111
+ const maxAdjustment = options.maxAdjustment ?? MAX_ADJUSTMENT;
112
+
113
+ const scored = results.map((result, rank) => {
114
+ const reasons = [];
115
+ let raw = 0;
116
+ for (const vote of entries) {
117
+ if (vote.docId !== result.id) continue;
118
+ const similarity = termSimilarity(question, vote.question);
119
+ if (similarity === 0) continue;
120
+ const contribution =
121
+ (vote.rating === "up" ? 1 : -1) * voteWeight * similarity;
122
+ raw += contribution;
123
+ reasons.push({
124
+ rating: vote.rating,
125
+ question: vote.question,
126
+ similarity,
127
+ contribution,
128
+ });
129
+ }
130
+ const adjustment = Math.max(-maxAdjustment, Math.min(maxAdjustment, raw));
131
+ const baseScore = typeof result.score === "number" ? result.score : 0;
132
+ return {
133
+ ...result,
134
+ baseScore,
135
+ adjustment,
136
+ finalScore: baseScore + adjustment,
137
+ reasons,
138
+ rank,
139
+ };
140
+ });
141
+
142
+ scored.sort((a, b) => b.finalScore - a.finalScore || a.rank - b.rank);
143
+ return scored.map(({ rank, ...rest }) => rest);
144
+ }
145
+
146
+ module.exports = {
147
+ feedbackPath,
148
+ loadFeedback,
149
+ addFeedback,
150
+ termSimilarity,
151
+ applyFeedback,
152
+ RATINGS,
153
+ VOTE_WEIGHT,
154
+ MAX_ADJUSTMENT,
155
+ };
@@ -0,0 +1,322 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Ingest and query operations shared by the `ingest`, `query` and `run` CLI
5
+ * commands (and the dev server). Components come from the .ragrc.json
6
+ * pipeline via buildComponents(); the retriever must persist between
7
+ * processes for ingest and query to be separate commands (file-store does).
8
+ */
9
+
10
+ const { buildComponents } = require("../../config/component-factory");
11
+ const { readDocuments } = require("../../loader/document-reader");
12
+ const { embedQuery } = require("../../utils/embed-query");
13
+ const { createRagPipeline } = require("../../core/create-pipeline");
14
+
15
+ function requireStage(components, stage) {
16
+ if (!components[stage]) {
17
+ throw new Error(
18
+ `No ${stage} configured. Add a { "stage": "${stage}", ... } entry to the pipeline in .ragrc.json.`,
19
+ );
20
+ }
21
+ return components[stage];
22
+ }
23
+
24
+ /** Embed document text with embed() (not embedQuery), normalizing the shape. */
25
+ function embedDocument(embedder, text) {
26
+ return embedQuery({ embed: (t) => embedder.embed(t) }, text);
27
+ }
28
+
29
+ /**
30
+ * Restore embedder state saved with the index (e.g. LocalEmbedder's
31
+ * vocabulary), so queries embed into the same vector space as ingestion.
32
+ */
33
+ function restoreEmbedder(embedder, retriever) {
34
+ const info = retriever.getEmbedderInfo?.();
35
+ if (info?.state && typeof embedder.setState === "function") {
36
+ embedder.setState(info.state);
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Embed entries with the (already trained/restored) embedder, store them and
42
+ * record the embedder metadata and state with the index.
43
+ *
44
+ * @returns {Promise<Array<{id: string, vector: number[]}>>} The stored vectors
45
+ */
46
+ async function embedAndStore({
47
+ embedder,
48
+ embedderName,
49
+ store,
50
+ entries,
51
+ previous,
52
+ }) {
53
+ const vectors = [];
54
+ for (const entry of entries) {
55
+ vectors.push({
56
+ ...entry,
57
+ vector: await embedDocument(embedder, entry.content),
58
+ });
59
+ }
60
+ const dimensions = vectors[0]?.vector?.length ?? previous?.dimensions ?? 0;
61
+ if (vectors.some((v) => v.vector.length !== dimensions)) {
62
+ throw new Error("Embedder returned vectors of inconsistent dimensions");
63
+ }
64
+
65
+ await store.store(vectors);
66
+ store.setEmbedderInfo?.({
67
+ name: embedderName,
68
+ dimensions,
69
+ ...(typeof embedder.getState === "function" && {
70
+ state: embedder.getState(),
71
+ }),
72
+ });
73
+ store.save?.();
74
+ return vectors;
75
+ }
76
+
77
+ /**
78
+ * Load documents from a file or directory, embed their chunks and store them.
79
+ * Re-ingesting a source replaces its previous chunks.
80
+ *
81
+ * @param {object} params
82
+ * @param {object} params.config - Normalized config
83
+ * @param {string} params.configDir - Directory of the config file
84
+ * @param {string} params.inputPath - File or directory to ingest
85
+ * @param {boolean} [params.rebuild=false] - Discard the existing index first
86
+ * @returns {Promise<{files: number, chunks: number, totalChunks: number, sources: string[]}>}
87
+ */
88
+ async function ingest({ config, configDir, inputPath, rebuild = false }) {
89
+ const components = buildComponents(config, { configDir });
90
+ const embedder = requireStage(components, "embedder");
91
+ const store = requireStage(components, "retriever");
92
+ if (typeof store.store !== "function") {
93
+ throw new Error(
94
+ `Retriever "${components.names.retriever}" cannot store documents. Use "file-store".`,
95
+ );
96
+ }
97
+
98
+ const embedderName = components.names.embedder;
99
+ if (rebuild) store.clear?.();
100
+ const previous = store.getEmbedderInfo?.();
101
+ if (previous && previous.name !== embedderName && store.size() > 0) {
102
+ throw new Error(
103
+ `The index was built with embedder "${previous.name}" but the config now uses ` +
104
+ `"${embedderName}". Re-run with --rebuild to re-index everything.`,
105
+ );
106
+ }
107
+
108
+ const documents = await readDocuments(inputPath, { baseDir: configDir });
109
+ for (const { source } of documents) store.removeBySource?.(source);
110
+
111
+ let entries = documents.flatMap(({ source, chunks }) =>
112
+ chunks.map((content, i) => ({
113
+ id: `${source}#${i}`,
114
+ content,
115
+ metadata: { source, chunk: i },
116
+ })),
117
+ );
118
+
119
+ // Corpus-fitted embedders (LocalEmbedder) must be retrained on everything
120
+ // stored, and every stored chunk re-embedded into the new vector space.
121
+ if (typeof embedder.train === "function") {
122
+ entries = [...(store.list?.() || []), ...entries];
123
+ embedder.train(entries.map((e) => e.content));
124
+ } else {
125
+ restoreEmbedder(embedder, store);
126
+ }
127
+
128
+ const vectors = await embedAndStore({
129
+ embedder,
130
+ embedderName,
131
+ store,
132
+ entries,
133
+ previous,
134
+ });
135
+
136
+ return {
137
+ files: documents.length,
138
+ chunks: documents.reduce((n, d) => n + d.chunks.length, 0),
139
+ totalChunks: store.size?.() ?? vectors.length,
140
+ sources: documents.map((d) => d.source),
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Refit a corpus-trained embedder (only the built-in `local` TF-IDF embedder)
146
+ * and re-embed every stored chunk with the new model.
147
+ *
148
+ * Without `inputPath` the embedder is fitted on the indexed chunks. With it,
149
+ * the embedder is fitted on the documents read from that path only (they are
150
+ * not added to the index); indexed chunks that share no term with the new
151
+ * vocabulary embed to all-zero vectors and are counted in `zeroVectors`.
152
+ *
153
+ * @param {object} params
154
+ * @param {object} params.config
155
+ * @param {string} params.configDir
156
+ * @param {string} [params.inputPath] - Optional training corpus
157
+ * @returns {Promise<{trainedOn: string, trainingDocuments: number, vocabularySize: number,
158
+ * previousVocabularySize: number|null, reembedded: number, zeroVectors: number}>}
159
+ */
160
+ async function retrain({ config, configDir, inputPath }) {
161
+ // Check the configured name before building components, so a hosted
162
+ // embedder without credentials still gets this explanation.
163
+ const entry = (config.pipeline || []).find((e) => e.stage === "embedder");
164
+ if (!entry) {
165
+ throw new Error(
166
+ 'No embedder configured. Add { "stage": "embedder", "name": "local" } to the pipeline in .ragrc.json.',
167
+ );
168
+ }
169
+ if (entry.name !== "local") {
170
+ throw new Error(
171
+ `Training is only supported for the built-in "local" (TF-IDF) embedder; ` +
172
+ `this pipeline uses "${entry.name}". Hosted embedders (openai, cohere, ollama) ` +
173
+ "use fixed pre-trained models and cannot be fitted to your corpus.",
174
+ );
175
+ }
176
+
177
+ const components = buildComponents(config, { configDir });
178
+ const embedderName = components.names.embedder;
179
+ const embedder = requireStage(components, "embedder");
180
+ const store = requireStage(components, "retriever");
181
+ if (typeof store.list !== "function" || typeof store.save !== "function") {
182
+ throw new Error(
183
+ `Retriever "${components.names.retriever}" does not persist an index. Use "file-store".`,
184
+ );
185
+ }
186
+
187
+ const stored = store.list();
188
+ if (stored.length === 0) {
189
+ throw new Error(
190
+ "The index is empty. Run `rag-pipeline ingest <path>` first.",
191
+ );
192
+ }
193
+ const previous = store.getEmbedderInfo?.();
194
+ if (previous && previous.name !== embedderName) {
195
+ throw new Error(
196
+ `The index was built with embedder "${previous.name}". ` +
197
+ "Re-run `rag-pipeline ingest <path> --rebuild` with the local embedder first.",
198
+ );
199
+ }
200
+
201
+ let corpus = stored.map((e) => e.content);
202
+ let trainedOn = "index";
203
+ if (inputPath) {
204
+ const documents = await readDocuments(inputPath, { baseDir: configDir });
205
+ corpus = documents.flatMap((d) => d.chunks);
206
+ if (corpus.length === 0) {
207
+ throw new Error(`No text found to train on in ${inputPath}`);
208
+ }
209
+ trainedOn = inputPath;
210
+ }
211
+
212
+ embedder.train(corpus);
213
+ const vectors = await embedAndStore({
214
+ embedder,
215
+ embedderName,
216
+ store,
217
+ entries: stored,
218
+ previous,
219
+ });
220
+
221
+ return {
222
+ trainedOn,
223
+ trainingDocuments: corpus.length,
224
+ vocabularySize: embedder.getVocabularySize?.() ?? embedder.dimensions,
225
+ previousVocabularySize: previous?.dimensions ?? null,
226
+ reembedded: vectors.length,
227
+ zeroVectors: vectors.filter((v) => v.vector.every((x) => x === 0)).length,
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Build the configured components once and restore the embedder state saved
233
+ * with the index, so they can be reused across several retrieve()/query()
234
+ * calls (e.g. by `ai benchmark`).
235
+ *
236
+ * @returns {object} Components as returned by buildComponents()
237
+ */
238
+ function loadComponents({ config, configDir }) {
239
+ const components = buildComponents(config, { configDir });
240
+ const embedder = requireStage(components, "embedder");
241
+ const retriever = requireStage(components, "retriever");
242
+ if (retriever.size && retriever.size() === 0) {
243
+ throw new Error(
244
+ "The index is empty. Run `rag-pipeline ingest <path>` first.",
245
+ );
246
+ }
247
+ restoreEmbedder(embedder, retriever);
248
+ return components;
249
+ }
250
+
251
+ /**
252
+ * Retrieval only (no llm): embed the question, retrieve and, if configured,
253
+ * rerank.
254
+ *
255
+ * @param {object} params
256
+ * @param {object} [params.components] - Prebuilt components (loadComponents)
257
+ * @param {object} [params.config]
258
+ * @param {string} [params.configDir]
259
+ * @param {string} params.question
260
+ * @param {number} [params.topK=3]
261
+ * @returns {Promise<Array<{id: string, score: number, content: string, metadata: object}>>}
262
+ */
263
+ async function retrieve({ components, config, configDir, question, topK = 3 }) {
264
+ const c = components || loadComponents({ config, configDir });
265
+ const queryVector = await embedQuery(c.embedder, question);
266
+ let results = await c.retriever.retrieve({
267
+ query: question,
268
+ queryVector,
269
+ topK,
270
+ });
271
+ if (c.reranker) {
272
+ results = await c.reranker.rerank(question, results, { topK });
273
+ }
274
+ return results;
275
+ }
276
+
277
+ /**
278
+ * Answer a question from the index. Without an llm stage this returns the
279
+ * retrieved passages only (answer: null) instead of a placeholder answer.
280
+ *
281
+ * @param {object} params
282
+ * @param {object} params.config
283
+ * @param {string} params.configDir
284
+ * @param {string} params.question
285
+ * @param {number} [params.topK=3]
286
+ * @param {object} [params.components] - Prebuilt components (loadComponents)
287
+ * @returns {Promise<{answer: string|null, results: Array, citations?: object}>}
288
+ */
289
+ async function query({ config, configDir, question, topK = 3, components }) {
290
+ const c = components || loadComponents({ config, configDir });
291
+
292
+ if (!c.llm) {
293
+ const results = await retrieve({ components: c, question, topK });
294
+ return { answer: null, results };
295
+ }
296
+
297
+ const pipeline = createRagPipeline({
298
+ embedder: c.embedder,
299
+ retriever: c.retriever,
300
+ llm: c.llm,
301
+ reranker: c.reranker,
302
+ });
303
+ const result = await pipeline.run({
304
+ query: question,
305
+ options: { topK, citations: true },
306
+ });
307
+ if (!result.success) throw new Error(result.error);
308
+ return {
309
+ answer: result.answer,
310
+ results: result.results,
311
+ citations: result.citations,
312
+ };
313
+ }
314
+
315
+ module.exports = {
316
+ ingest,
317
+ retrain,
318
+ query,
319
+ retrieve,
320
+ loadComponents,
321
+ restoreEmbedder,
322
+ };
@@ -28,6 +28,23 @@ function parsePositiveInteger(value) {
28
28
  return parsed;
29
29
  }
30
30
 
31
+ // Top-level commands offered by shell completion
32
+ const COMMANDS = [
33
+ "init",
34
+ "ingest",
35
+ "query",
36
+ "run",
37
+ "serve",
38
+ "doctor",
39
+ "validate",
40
+ "hub",
41
+ "ai",
42
+ "plugin",
43
+ "config",
44
+ "info",
45
+ "completion",
46
+ ].join(" ");
47
+
31
48
  /**
32
49
  * Generate shell completion script
33
50
  */
@@ -36,7 +53,7 @@ function generateCompletionScript(shell) {
36
53
  case "bash":
37
54
  return `# RAG Pipeline bash completion
38
55
  _rag_pipeline_completions() {
39
- COMPREPLY=($(compgen -W "init doctor ingest query plugin config info validate completion" -- "\${COMP_WORDS[COMP_CWORD]}"))
56
+ COMPREPLY=($(compgen -W "${COMMANDS}" -- "\${COMP_WORDS[COMP_CWORD]}"))
40
57
  }
41
58
  complete -F _rag_pipeline_completions rag-pipeline`;
42
59
 
@@ -45,13 +62,13 @@ complete -F _rag_pipeline_completions rag-pipeline`;
45
62
  #compdef rag-pipeline
46
63
  _rag_pipeline() {
47
64
  _arguments \\
48
- '1:command:(init doctor ingest query plugin config info validate completion)'
65
+ '1:command:(${COMMANDS})'
49
66
  }
50
67
  _rag_pipeline "$@"`;
51
68
 
52
69
  case "fish":
53
70
  return `# RAG Pipeline fish completion
54
- complete -c rag-pipeline -n '__fish_use_subcommand' -a 'init doctor ingest query plugin config info validate completion'`;
71
+ complete -c rag-pipeline -n '__fish_use_subcommand' -a '${COMMANDS}'`;
55
72
 
56
73
  default:
57
74
  return `# Completion not available for ${shell}`;
@@ -71,10 +88,10 @@ function getVersion() {
71
88
  function getExtendedHelp() {
72
89
  return `
73
90
  Examples:
74
- rag-pipeline init --interactive Initialize with wizard
75
- rag-pipeline doctor --auto-fix Diagnose and fix issues
76
- rag-pipeline plugin search openai Search for plugins
77
- rag-pipeline config show plugins Show plugin configuration
91
+ rag-pipeline init Create .ragrc.json (prompts in a terminal)
92
+ rag-pipeline ingest ./docs Index a file or directory
93
+ rag-pipeline query "What is X?" Ask a question about the indexed documents
94
+ rag-pipeline config show Show the current configuration
78
95
 
79
96
  For more information, visit: https://github.com/DevilsDev/rag-pipeline-utils
80
97
  `;