@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
@@ -4,7 +4,8 @@
4
4
 
5
5
  const fs = require('fs/promises');
6
6
  const path = require('path');
7
- const { parse } = require('csv-parse/sync');
7
+ // csv-parse loads when a CSV file is read, not on import.
8
+ const loadCsvParse = () => require('csv-parse/sync'); // eslint-disable-line global-require
8
9
 
9
10
  class CSVLoader {
10
11
  /**
@@ -20,7 +21,7 @@ class CSVLoader {
20
21
  const src = raw.replace(/^\uFEFF/, ''); // Remove BOM
21
22
  const normalizedSrc = src.replace(/\r\n|\r/g, '\n'); // Normalize line endings
22
23
 
23
- const records = parse(normalizedSrc, { columns: true });
24
+ const records = loadCsvParse().parse(normalizedSrc, { columns: true });
24
25
 
25
26
  return [
26
27
  {
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const fg = require('fast-glob');
6
+ const { MarkdownLoader } = require('./markdown-loader.js');
7
+ const { HTMLLoader } = require('./html-loader.js');
8
+ const { CSVLoader } = require('./csv-loader.js');
9
+
10
+ const textLoader = {
11
+ async load(filePath) {
12
+ const text = await fs.promises.readFile(filePath, 'utf8');
13
+ return [
14
+ {
15
+ chunk: () => {
16
+ const { ChunkingEngine } = require('../chunking/chunking-engine');
17
+ return new ChunkingEngine({
18
+ strategy: 'sentence',
19
+ maxLen: 500,
20
+ }).chunk(text);
21
+ },
22
+ },
23
+ ];
24
+ },
25
+ };
26
+
27
+ const LOADERS_BY_EXTENSION = {
28
+ '.md': new MarkdownLoader(),
29
+ '.markdown': new MarkdownLoader(),
30
+ '.html': new HTMLLoader(),
31
+ '.htm': new HTMLLoader(),
32
+ '.csv': new CSVLoader(),
33
+ '.txt': textLoader,
34
+ };
35
+
36
+ const SUPPORTED_EXTENSIONS = Object.keys(LOADERS_BY_EXTENSION);
37
+
38
+ /**
39
+ * Read a file or every supported file under a directory and return its
40
+ * chunks together with the file they came from. Unlike DirectoryLoader this
41
+ * keeps the source path, which ingestion needs for stable chunk ids,
42
+ * re-ingestion and citations.
43
+ *
44
+ * @param {string} inputPath - File or directory
45
+ * @param {object} [options]
46
+ * @param {string} [options.baseDir=process.cwd()] - Sources are reported relative to this
47
+ * @returns {Promise<Array<{source: string, chunks: string[]}>>}
48
+ */
49
+ async function readDocuments(inputPath, options = {}) {
50
+ const baseDir = path.resolve(options.baseDir || process.cwd());
51
+ const absolute = path.resolve(inputPath);
52
+ if (!fs.existsSync(absolute)) {
53
+ throw new Error(`Path not found: ${inputPath}`);
54
+ }
55
+
56
+ let files;
57
+ if (fs.statSync(absolute).isDirectory()) {
58
+ const pattern = `**/*{${SUPPORTED_EXTENSIONS.join(',')}}`;
59
+ files = (
60
+ await fg(pattern, {
61
+ cwd: absolute,
62
+ absolute: true,
63
+ caseSensitiveMatch: false,
64
+ ignore: ['**/node_modules/**', '**/.git/**'],
65
+ })
66
+ ).sort();
67
+ } else {
68
+ const ext = path.extname(absolute).toLowerCase();
69
+ if (!LOADERS_BY_EXTENSION[ext]) {
70
+ throw new Error(
71
+ `Unsupported file type "${ext}". Supported: ${SUPPORTED_EXTENSIONS.join(', ')}`,
72
+ );
73
+ }
74
+ files = [absolute];
75
+ }
76
+
77
+ const documents = [];
78
+ for (const file of files) {
79
+ const loader = LOADERS_BY_EXTENSION[path.extname(file).toLowerCase()];
80
+ const loaded = await loader.load(file);
81
+ const chunks = loaded
82
+ .flatMap((doc) => doc.chunk())
83
+ .map((chunk) => String(chunk).trim())
84
+ .filter(Boolean);
85
+ const source = path.relative(baseDir, file).split(path.sep).join('/');
86
+ documents.push({ source, chunks });
87
+ }
88
+ return documents;
89
+ }
90
+
91
+ module.exports = { readDocuments, SUPPORTED_EXTENSIONS };
@@ -4,7 +4,8 @@
4
4
 
5
5
  const fs = require('fs/promises');
6
6
  const path = require('path');
7
- const { JSDOM } = require('jsdom');
7
+ // jsdom (about 100 packages) loads when an HTML file is read, not on import.
8
+ const loadJsdom = () => require('jsdom'); // eslint-disable-line global-require
8
9
 
9
10
  /**
10
11
  * HTMLLoader reads HTML files and extracts visible text.
@@ -19,6 +20,7 @@ class HTMLLoader {
19
20
  async load(filePath) {
20
21
  const absPath = path.resolve(filePath);
21
22
  const raw = await fs.readFile(absPath, 'utf-8');
23
+ const { JSDOM } = loadJsdom();
22
24
  const dom = new JSDOM(raw);
23
25
  let text = dom.window.document.body.textContent || '';
24
26
 
@@ -10,7 +10,8 @@ const fs = require('fs/promises');
10
10
  // eslint-disable-line global-require
11
11
  const path = require('path');
12
12
  // eslint-disable-line global-require
13
- const { marked } = require('marked');
13
+ // marked loads when a Markdown file is read, not on import.
14
+ const loadMarked = () => require('marked'); // eslint-disable-line global-require
14
15
  // eslint-disable-line global-require
15
16
 
16
17
  /**
@@ -26,7 +27,7 @@ class MarkdownLoader {
26
27
  async load(_filePath) {
27
28
  const absPath = path.resolve(_filePath);
28
29
  const raw = await fs.readFile(absPath, 'utf-8');
29
- const html = marked(raw);
30
+ const html = loadMarked().marked(raw);
30
31
  const text = html.replace(/<[^>]+>/g, ''); // strip tags
31
32
 
32
33
  return [
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  const { EventEmitter } = require('events');
11
+ const { throwIfAborted } = require('../connectors/connector-utils');
11
12
 
12
13
  /** @type {object} */
13
14
  const DEFAULT_CONFIG = {};
@@ -37,6 +38,7 @@ class CascadeReranker extends EventEmitter {
37
38
  * @param {Array<object|string>} documents - Documents to rerank
38
39
  * @param {object} [options]
39
40
  * @param {number} [options.topK] - Maximum number of results to return
41
+ * @param {AbortSignal} [options.signal] - Forwarded to every stage; checked between stages
40
42
  * @returns {Promise<object[]>} Reranked documents from the final stage
41
43
  * @throws {Error} If no rerankers are configured
42
44
  */
@@ -52,6 +54,7 @@ class CascadeReranker extends EventEmitter {
52
54
  let currentDocs = documents;
53
55
 
54
56
  for (let i = 0; i < this.rerankers.length; i++) {
57
+ throwIfAborted(options.signal);
55
58
  const reranker = this.rerankers[i];
56
59
  currentDocs = await reranker.rerank(query, currentDocs, options);
57
60
 
@@ -1,28 +1,53 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Version: 1.0.0
4
+ * Version: 1.1.0
5
5
  * Path: /src/reranker/embedding-reranker.js
6
6
  * Description: Cosine similarity reranker using any embedder
7
7
  * Author: Ali Kahwaji
8
8
  */
9
9
 
10
10
  const { EventEmitter } = require('events');
11
+ const { createHash } = require('crypto');
12
+ const {
13
+ isAbortError,
14
+ throwIfAborted,
15
+ raceAbort,
16
+ } = require('../connectors/connector-utils');
17
+
18
+ /** @type {{ cacheSize: number, concurrency: number }} */
19
+ const DEFAULT_CONFIG = {
20
+ // Maximum number of document embeddings kept in the LRU cache (0 disables).
21
+ cacheSize: 1000,
22
+ // Maximum number of document embedding calls in flight at once.
23
+ concurrency: 4,
24
+ };
11
25
 
12
- /** @type {object} */
13
- const DEFAULT_CONFIG = {};
26
+ /**
27
+ * Hash document text so cache keys stay small regardless of document size.
28
+ * @param {string} text
29
+ * @returns {string}
30
+ */
31
+ function contentHash(text) {
32
+ return createHash('sha256').update(text, 'utf8').digest('hex');
33
+ }
14
34
 
15
35
  /**
16
36
  * Reranks documents by computing cosine similarity between query and
17
37
  * document embeddings produced by an external embedder.
18
38
  * Implements the reranker contract: rerank(query, documents, options).
19
39
  *
40
+ * Document embeddings are cached in a bounded LRU keyed by a SHA-256 of the
41
+ * document text, and embedding calls run with bounded concurrency.
42
+ *
20
43
  * @extends EventEmitter
21
44
  */
22
45
  class EmbeddingReranker extends EventEmitter {
23
46
  /**
24
47
  * @param {object} embedder - Must expose .embedQuery(text) or .embed(text)
25
48
  * @param {object} [options]
49
+ * @param {number} [options.cacheSize=1000] - Max cached document embeddings (0 disables caching)
50
+ * @param {number} [options.concurrency=4] - Max concurrent embedding calls
26
51
  */
27
52
  constructor(embedder, options = {}) {
28
53
  super();
@@ -31,6 +56,79 @@ class EmbeddingReranker extends EventEmitter {
31
56
  }
32
57
  this.embedder = embedder;
33
58
  this.config = { ...DEFAULT_CONFIG, ...options };
59
+ if (
60
+ !(
61
+ Number.isInteger(this.config.concurrency) && this.config.concurrency > 0
62
+ )
63
+ ) {
64
+ throw new Error(
65
+ 'EmbeddingReranker: concurrency must be a positive integer',
66
+ );
67
+ }
68
+ /** @type {Map<string, Promise<number[]>>} LRU: oldest entry first */
69
+ this._cache = new Map();
70
+ this.cacheStats = { hits: 0, misses: 0 };
71
+ }
72
+
73
+ /**
74
+ * Call the embedder, forwarding the signal only when one is given.
75
+ * @private
76
+ */
77
+ _embed(text, signal) {
78
+ const fn = this.embedder.embedQuery ? 'embedQuery' : 'embed';
79
+ return signal
80
+ ? this.embedder[fn](text, { signal })
81
+ : this.embedder[fn](text);
82
+ }
83
+
84
+ /**
85
+ * Embed a document, using the LRU cache. In-flight promises are cached so
86
+ * duplicate documents in one batch are embedded once; failed embeddings
87
+ * are evicted so they can be retried.
88
+ * @private
89
+ */
90
+ _embedDocument(text, signal) {
91
+ const { cacheSize } = this.config;
92
+ if (!cacheSize || cacheSize <= 0) {
93
+ this.cacheStats.misses++;
94
+ return Promise.resolve().then(() => this._embed(text, signal));
95
+ }
96
+
97
+ const key = contentHash(text);
98
+ const cached = this._cache.get(key);
99
+ if (cached) {
100
+ this.cacheStats.hits++;
101
+ // Refresh recency.
102
+ this._cache.delete(key);
103
+ this._cache.set(key, cached);
104
+ // The shared promise may have been started by another request whose
105
+ // signal aborted; that abort must not fail this request.
106
+ return cached.catch((err) => {
107
+ if (isAbortError(err) && !(signal && signal.aborted)) {
108
+ return this._embedDocument(text, signal);
109
+ }
110
+ throw err;
111
+ });
112
+ }
113
+
114
+ this.cacheStats.misses++;
115
+ const promise = Promise.resolve().then(() => this._embed(text, signal));
116
+ this._cache.set(key, promise);
117
+ promise.catch(() => {
118
+ if (this._cache.get(key) === promise) this._cache.delete(key);
119
+ });
120
+ while (this._cache.size > cacheSize) {
121
+ this._cache.delete(this._cache.keys().next().value);
122
+ }
123
+ return promise;
124
+ }
125
+
126
+ /**
127
+ * Clear the document embedding cache.
128
+ */
129
+ clearCache() {
130
+ this._cache.clear();
131
+ this.cacheStats = { hits: 0, misses: 0 };
34
132
  }
35
133
 
36
134
  /**
@@ -40,6 +138,7 @@ class EmbeddingReranker extends EventEmitter {
40
138
  * @param {Array<object|string>} documents - Documents to rerank
41
139
  * @param {object} [options]
42
140
  * @param {number} [options.topK] - Maximum number of results to return
141
+ * @param {AbortSignal} [options.signal] - Cancels pending embedding calls
43
142
  * @returns {Promise<object[]>} Reranked documents with .relevanceScore
44
143
  */
45
144
  async rerank(query, documents, options = {}) {
@@ -47,27 +146,43 @@ class EmbeddingReranker extends EventEmitter {
47
146
  return [];
48
147
  }
49
148
 
50
- const { topK = documents.length } = options;
149
+ const { topK = documents.length, signal } = options;
150
+ throwIfAborted(signal);
51
151
 
52
- // Get query embedding
53
- const queryEmbedding = this.embedder.embedQuery
54
- ? await this.embedder.embedQuery(query)
55
- : await this.embedder.embed(query);
152
+ const queryEmbedding = await raceAbort(
153
+ Promise.resolve().then(() => this._embed(query, signal)),
154
+ signal,
155
+ );
56
156
 
57
- // Get document embeddings and compute similarities
58
- const scored = await Promise.all(
59
- documents.map(async (doc, originalIndex) => {
157
+ // Embed documents with at most `concurrency` calls in flight, keeping
158
+ // results in input order.
159
+ const embeddings = new Array(documents.length);
160
+ let next = 0;
161
+ const worker = async () => {
162
+ while (next < documents.length) {
163
+ throwIfAborted(signal);
164
+ const i = next++;
165
+ const doc = documents[i];
60
166
  const text = doc.content || doc.text || String(doc);
61
-
62
- const docEmbedding = this.embedder.embedQuery
63
- ? await this.embedder.embedQuery(text)
64
- : await this.embedder.embed(text);
65
-
66
- const similarity = this._cosineSimilarity(queryEmbedding, docEmbedding);
67
-
68
- return { doc, similarity, originalIndex };
69
- }),
70
- );
167
+ embeddings[i] = await raceAbort(
168
+ this._embedDocument(text, signal),
169
+ signal,
170
+ );
171
+ }
172
+ };
173
+ const workers = [];
174
+ const poolSize = Math.min(this.config.concurrency, documents.length);
175
+ for (let w = 0; w < poolSize; w++) workers.push(worker());
176
+ await Promise.all(workers);
177
+
178
+ const scored = documents.map((doc, originalIndex) => ({
179
+ doc,
180
+ similarity: this._cosineSimilarity(
181
+ queryEmbedding,
182
+ embeddings[originalIndex],
183
+ ),
184
+ originalIndex,
185
+ }));
71
186
 
72
187
  // Sort by similarity descending, then by original index for stability
73
188
  scored.sort((a, b) => {
@@ -1,28 +1,83 @@
1
1
  'use strict';
2
2
 
3
+ const {
4
+ createAbortError,
5
+ isAbortError,
6
+ raceAbort,
7
+ throwIfAborted,
8
+ } = require('../connectors/connector-utils');
9
+
10
+ // Characters of each document shown to the LLM; keeps the ranking prompt small.
11
+ const DEFAULT_MAX_DOC_CHARS = 500;
12
+
3
13
  class LLMReranker {
4
14
  constructor(options = {}) {
5
15
  this.llm = options.llm;
16
+ this.maxDocChars = options.maxDocChars || DEFAULT_MAX_DOC_CHARS;
17
+ }
18
+
19
+ /**
20
+ * Build the instruction sent to the LLM. Documents are numbered from 0 so
21
+ * the response can be validated as indices into `docs`.
22
+ */
23
+ _buildRankingPrompt(query, docs) {
24
+ const listing = docs
25
+ .map((doc, i) => {
26
+ const text = String(doc.text || doc.content || '').slice(
27
+ 0,
28
+ this.maxDocChars,
29
+ );
30
+ return `[${i}] ${text.replace(/\s+/g, ' ')}`;
31
+ })
32
+ .join('\n');
33
+ return [
34
+ 'Rank the documents below by how relevant they are to the query.',
35
+ 'Respond with only a JSON array of document numbers, most relevant',
36
+ 'first, for example [2, 0, 1]. Do not include any other text.',
37
+ '',
38
+ `Query: ${query}`,
39
+ '',
40
+ 'Documents:',
41
+ listing,
42
+ ].join('\n');
43
+ }
44
+
45
+ /**
46
+ * Parse the first JSON array in the response; models often wrap it in
47
+ * prose or a code fence.
48
+ */
49
+ _parseRanking(response) {
50
+ const text = typeof response === 'string' ? response : String(response);
51
+ const match = text.match(/\[[\s\d,]*\]/);
52
+ return JSON.parse(match ? match[0] : text);
6
53
  }
7
54
 
8
55
  async rerank(query, docs, opts = {}) {
9
56
  if (!Array.isArray(docs)) {
10
57
  return [];
11
58
  }
59
+ const signal = opts.signal;
60
+ throwIfAborted(signal);
12
61
 
13
62
  // If no LLM provided, fall back to simple overlap scoring
14
63
  if (!this.llm) {
15
- return this._fallbackRerank(query, docs);
64
+ return this._limit(this._fallbackRerank(query, docs), opts);
16
65
  }
17
66
 
18
67
  try {
19
- // Use LLM to get reranking order
20
- const response = await this.llm.generate(query, docs, opts);
68
+ // Ask the LLM for a ranking. Documents are inlined in the prompt, so no
69
+ // separate retrieval context is passed.
70
+ const response = await raceAbort(
71
+ Promise.resolve().then(() =>
72
+ this.llm.generate(this._buildRankingPrompt(query, docs), [], opts),
73
+ ),
74
+ signal,
75
+ );
21
76
 
22
77
  // Validate and sanitize LLM output before parsing (security fix)
23
78
  let rankingOrder;
24
79
  try {
25
- const parsed = JSON.parse(response);
80
+ const parsed = this._parseRanking(response);
26
81
 
27
82
  // Security validation: ensure parsed result is an array
28
83
  if (!Array.isArray(parsed)) {
@@ -49,22 +104,34 @@ class LLMReranker {
49
104
  }
50
105
  } catch (parseErr) {
51
106
  // Fallback: use simple overlap scoring if parsing fails
52
- return this._fallbackRerank(query, docs);
107
+ return this._limit(this._fallbackRerank(query, docs), opts);
53
108
  }
54
109
 
55
- // Apply the ranking order
56
- const reranked = [];
57
- for (const index of rankingOrder) {
58
- reranked.push(docs[index]);
59
- }
110
+ // Apply the ranking order; documents the LLM left out keep their
111
+ // original relative order after the ranked ones instead of vanishing.
112
+ const ranked = new Set(rankingOrder);
113
+ const reranked = [
114
+ ...rankingOrder.map((index) => docs[index]),
115
+ ...docs.filter((_, index) => !ranked.has(index)),
116
+ ];
60
117
 
61
- return reranked;
118
+ return this._limit(reranked, opts);
62
119
  } catch (error) {
120
+ // Cancellation is not an LLM failure: propagate it instead of falling back.
121
+ if (isAbortError(error) || (signal && signal.aborted)) {
122
+ throw createAbortError(signal);
123
+ }
63
124
  // Fall back to simple overlap scoring on LLM error
64
- return this._fallbackRerank(query, docs);
125
+ return this._limit(this._fallbackRerank(query, docs), opts);
65
126
  }
66
127
  }
67
128
 
129
+ _limit(docs, opts) {
130
+ return Number.isInteger(opts.topK) && opts.topK > 0
131
+ ? docs.slice(0, opts.topK)
132
+ : docs;
133
+ }
134
+
68
135
  _fallbackRerank(query, docs) {
69
136
  // Simple bag-of-words overlap scoring
70
137
  const queryTokens = query
@@ -9,6 +9,7 @@
9
9
 
10
10
  const { EventEmitter } = require('events');
11
11
  const { tokenize } = require('../evaluate/scoring');
12
+ const { throwIfAborted } = require('../connectors/connector-utils');
12
13
 
13
14
  /** @type {{ k1: number, b: number }} */
14
15
  const DEFAULT_CONFIG = { k1: 1.2, b: 0.75 };
@@ -37,9 +38,11 @@ class ScoringReranker extends EventEmitter {
37
38
  * @param {Array<object|string>} documents - Documents to rerank
38
39
  * @param {object} [options]
39
40
  * @param {number} [options.topK] - Maximum number of results to return
41
+ * @param {AbortSignal} [options.signal] - Rejects with AbortError when already aborted
40
42
  * @returns {Promise<object[]>} Reranked documents with .relevanceScore
41
43
  */
42
44
  async rerank(query, documents, options = {}) {
45
+ throwIfAborted(options.signal);
43
46
  if (!Array.isArray(documents) || documents.length === 0) {
44
47
  return [];
45
48
  }
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const { EventEmitter } = require('events');
4
- const { tokenize } = require('../evaluate/scoring');
4
+ // Called through the module object so tests can observe tokenization.
5
+ const scoring = require('../evaluate/scoring');
5
6
 
6
7
  const DEFAULT_CONFIG = { k1: 1.2, b: 0.75 };
7
8
 
@@ -11,6 +12,8 @@ class BM25Search extends EventEmitter {
11
12
  this.config = { ...DEFAULT_CONFIG, ...options };
12
13
  this.invertedIndex = new Map(); // token -> [{docIndex, tf}]
13
14
  this.documents = [];
15
+ /** @type {number[]} token count per document, computed at index time */
16
+ this.docLengths = [];
14
17
  this.avgDocLength = 0;
15
18
  this.docCount = 0;
16
19
  }
@@ -27,14 +30,16 @@ class BM25Search extends EventEmitter {
27
30
  this.documents = documents;
28
31
  this.docCount = documents.length;
29
32
  this.invertedIndex.clear();
33
+ this.docLengths = new Array(documents.length);
30
34
 
31
35
  let totalLength = 0;
32
36
 
33
37
  for (let docIndex = 0; docIndex < documents.length; docIndex++) {
34
38
  const doc = documents[docIndex];
35
39
  const text = doc.content || doc.text || '';
36
- const tokens = tokenize(text);
40
+ const tokens = scoring.tokenize(text);
37
41
 
42
+ this.docLengths[docIndex] = tokens.length;
38
43
  totalLength += tokens.length;
39
44
 
40
45
  // Count term frequencies for this document
@@ -73,7 +78,7 @@ class BM25Search extends EventEmitter {
73
78
  return [];
74
79
  }
75
80
 
76
- const queryTokens = tokenize(query);
81
+ const queryTokens = scoring.tokenize(query);
77
82
  if (queryTokens.length === 0) {
78
83
  return [];
79
84
  }
@@ -94,9 +99,9 @@ class BM25Search extends EventEmitter {
94
99
  const idf = Math.log((N - df + 0.5) / (df + 0.5) + 1);
95
100
 
96
101
  for (const { docIndex, tf } of postings) {
97
- const doc = this.documents[docIndex];
98
- const docText = doc.content || doc.text || '';
99
- const docLen = tokenize(docText).length;
102
+ // Document lengths were computed at index time; scoring never
103
+ // re-tokenizes documents.
104
+ const docLen = this.docLengths[docIndex];
100
105
 
101
106
  const numerator = tf * (k1 + 1);
102
107
  const denominator =
@@ -137,6 +142,7 @@ class BM25Search extends EventEmitter {
137
142
  clear() {
138
143
  this.invertedIndex.clear();
139
144
  this.documents = [];
145
+ this.docLengths = [];
140
146
  this.avgDocLength = 0;
141
147
  this.docCount = 0;
142
148
  }