@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,60 @@
1
+ /**
2
+ * Serve command handler
3
+ * Runs DevServer: a local HTTP API (GET /health, POST /query) over the
4
+ * pipeline in .ragrc.json, reloading custom plugin modules on change.
5
+ */
6
+
7
+ const path = require("path");
8
+ const { loadCliConfig } = require("../shared/config-context.js");
9
+
10
+ /**
11
+ * @param {object} globalOptions - Global CLI options from program.opts()
12
+ * @param {object} options - { port, host }
13
+ * @returns {Promise<object|undefined>} The started server (for tests)
14
+ */
15
+ async function handleServe(globalOptions, options = {}) {
16
+ try {
17
+ // Validate the config up front so a broken file fails at startup.
18
+ const { config, configPath, configDir } = loadCliConfig(globalOptions);
19
+ const pluginFiles = Object.values(config.plugins || {})
20
+ .flatMap((group) => Object.values(group || {}))
21
+ .filter((p) => typeof p === "string")
22
+ .map((p) => path.resolve(configDir, p));
23
+
24
+ const { DevServer } = require("../../dev/dev-server.js");
25
+ const server = new DevServer({
26
+ port: options.port,
27
+ host: options.host,
28
+ configPath,
29
+ watchPaths: pluginFiles,
30
+ verbose: false,
31
+ });
32
+ await server.start();
33
+
34
+ console.log(`🚀 Serving ${path.basename(configPath)} at ${server.url}`);
35
+ console.log(` GET ${server.url}/health`);
36
+ console.log(` POST ${server.url}/query {"question": "...", "topK": 3}`);
37
+ if (
38
+ !["127.0.0.1", "localhost", "::1"].includes(options.host || "127.0.0.1")
39
+ ) {
40
+ console.warn(
41
+ `⚠️ Listening on ${options.host}: the API has no authentication and anyone who can reach this address can query your index.`,
42
+ );
43
+ }
44
+ console.log("Press Ctrl+C to stop.");
45
+
46
+ const shutdown = async () => {
47
+ await server.stop();
48
+ process.exit(0);
49
+ };
50
+ process.once("SIGINT", shutdown);
51
+ process.once("SIGTERM", shutdown);
52
+ return server;
53
+ } catch (error) {
54
+ console.error(`❌ Server failed to start: ${error.message}`);
55
+ process.exitCode = 1;
56
+ return undefined;
57
+ }
58
+ }
59
+
60
+ module.exports = { handleServe };
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Summary statistics for latency samples measured by `rag-pipeline ai
5
+ * benchmark`. Pure functions so the math can be unit-tested.
6
+ */
7
+
8
+ /**
9
+ * Nearest-rank percentile: the smallest sample such that at least p% of the
10
+ * samples are less than or equal to it. Always returns an observed value.
11
+ *
12
+ * @param {number[]} samples - Unsorted samples (not modified)
13
+ * @param {number} p - Percentile in (0, 100]
14
+ * @returns {number}
15
+ */
16
+ function percentile(samples, p) {
17
+ if (!Array.isArray(samples) || samples.length === 0) {
18
+ throw new Error("percentile() needs at least one sample");
19
+ }
20
+ if (!(p > 0 && p <= 100)) {
21
+ throw new Error(`Percentile must be in (0, 100], got ${p}`);
22
+ }
23
+ const sorted = [...samples].sort((a, b) => a - b);
24
+ const rank = Math.ceil((p / 100) * sorted.length);
25
+ return sorted[rank - 1];
26
+ }
27
+
28
+ /**
29
+ * @param {number[]} samples
30
+ * @returns {number} Arithmetic mean
31
+ */
32
+ function mean(samples) {
33
+ if (!Array.isArray(samples) || samples.length === 0) {
34
+ throw new Error("mean() needs at least one sample");
35
+ }
36
+ return samples.reduce((sum, x) => sum + x, 0) / samples.length;
37
+ }
38
+
39
+ /**
40
+ * @param {number[]} samples - Latencies in milliseconds
41
+ * @returns {{count: number, mean: number, p50: number, p95: number, min: number, max: number}}
42
+ */
43
+ function summarize(samples) {
44
+ return {
45
+ count: samples.length,
46
+ mean: mean(samples),
47
+ p50: percentile(samples, 50),
48
+ p95: percentile(samples, 95),
49
+ min: Math.min(...samples),
50
+ max: Math.max(...samples),
51
+ };
52
+ }
53
+
54
+ module.exports = { percentile, mean, summarize };
@@ -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,83 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Wraps pipeline components so the CLI can report per-stage timings
5
+ * (`query --trace`) and see the retrieved passages when streaming (the
6
+ * stream's final chunk carries citations, not results).
7
+ *
8
+ * Each wrapper forwards to the real component with `this` bound to it; only
9
+ * the stage methods are timed.
10
+ */
11
+
12
+ const { performance } = require("perf_hooks");
13
+
14
+ const STAGE_METHODS = {
15
+ embedder: { embedQuery: "embed", embed: "embed" },
16
+ retriever: { retrieve: "retrieve" },
17
+ reranker: { rerank: "rerank" },
18
+ llm: { generate: "generate", generateStream: "generate" },
19
+ };
20
+
21
+ async function* timedStream(source, record) {
22
+ const start = performance.now();
23
+ try {
24
+ yield* await source;
25
+ } finally {
26
+ record(performance.now() - start);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * @param {Object} components - { embedder, retriever, reranker, llm }
32
+ * @returns {{ components: Object, timings: Array<{stage: string, ms: number}>, captured: { results: Array|null } }}
33
+ */
34
+ function instrumentComponents(components) {
35
+ const timings = [];
36
+ const captured = { results: null };
37
+
38
+ const wrap = (kind, component) => {
39
+ if (!component) return component;
40
+ const methods = STAGE_METHODS[kind];
41
+ return new Proxy(component, {
42
+ get(target, prop, receiver) {
43
+ const value = Reflect.get(target, prop, receiver);
44
+ const stage = methods[prop];
45
+ if (typeof value !== "function" || !stage) return value;
46
+
47
+ if (prop === "generateStream") {
48
+ return (...args) =>
49
+ timedStream(value.apply(target, args), (ms) =>
50
+ timings.push({ stage, ms }),
51
+ );
52
+ }
53
+ return async (...args) => {
54
+ const start = performance.now();
55
+ try {
56
+ const out = await value.apply(target, args);
57
+ if (kind === "retriever" || kind === "reranker") {
58
+ // The reranker runs after the retriever, so its order wins.
59
+ captured.results = out;
60
+ }
61
+ return out;
62
+ } finally {
63
+ timings.push({ stage, ms: performance.now() - start });
64
+ }
65
+ };
66
+ },
67
+ });
68
+ };
69
+
70
+ return {
71
+ components: {
72
+ ...components,
73
+ embedder: wrap("embedder", components.embedder),
74
+ retriever: wrap("retriever", components.retriever),
75
+ reranker: wrap("reranker", components.reranker),
76
+ llm: wrap("llm", components.llm),
77
+ },
78
+ timings,
79
+ captured,
80
+ };
81
+ }
82
+
83
+ module.exports = { instrumentComponents };