@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.
- package/.ragrc.schema.json +83 -192
- package/CHANGELOG.md +345 -0
- package/README.md +418 -117
- package/SECURITY.md +5 -5
- package/dist/experimental.cjs +2 -0
- package/dist/experimental.d.ts +1 -0
- package/dist/experimental.mjs +5 -0
- package/dist/index.cjs +4992 -1242
- package/dist/index.d.ts +80 -2
- package/dist/index.mjs +1 -1
- package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
- package/dist/types/agentic/iterative-retriever.d.ts +85 -0
- package/dist/types/agentic/query-planner.d.ts +145 -0
- package/dist/types/agentic/self-critique.d.ts +45 -0
- package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
- package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
- package/dist/types/ai/multimodal-processing.d.ts +32 -0
- package/dist/types/ai/multimodal.d.ts +39 -0
- package/dist/types/ai/retrieval-engine.d.ts +37 -0
- package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
- package/dist/types/chunking/chunking-engine.d.ts +70 -0
- package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
- package/dist/types/chunking/strategies/recursive.d.ts +20 -0
- package/dist/types/chunking/strategies/semantic.d.ts +19 -0
- package/dist/types/chunking/strategies/sentence.d.ts +13 -0
- package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
- package/dist/types/citation/citation-tracker.d.ts +64 -0
- package/dist/types/citation/hallucination-detector.d.ts +40 -0
- package/dist/types/citation/source-mapper.d.ts +37 -0
- package/dist/types/connectors/anthropic-connector.d.ts +92 -0
- package/dist/types/connectors/base-connector.d.ts +87 -0
- package/dist/types/connectors/cohere-connector.d.ts +89 -0
- package/dist/types/connectors/connector-utils.d.ts +110 -0
- package/dist/types/connectors/local-embedder.d.ts +86 -0
- package/dist/types/connectors/memory-retriever.d.ts +91 -0
- package/dist/types/connectors/ollama-connector.d.ts +110 -0
- package/dist/types/connectors/openai-connector.d.ts +114 -0
- package/dist/types/connectors/rag-prompt.d.ts +40 -0
- package/dist/types/cost/cost-calculator.d.ts +152 -0
- package/dist/types/cost/cost-tracker.d.ts +126 -0
- package/dist/types/cost/token-budget.d.ts +110 -0
- package/dist/types/dag/core/error-context.d.ts +239 -0
- package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
- package/dist/types/dag/core/topology-validation.d.ts +226 -0
- package/dist/types/dag/dag-engine.d.ts +156 -0
- package/dist/types/dag/dag-node.d.ts +37 -0
- package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
- package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
- package/dist/types/debug/execution-tracer.d.ts +57 -0
- package/dist/types/debug/trace-inspector.d.ts +31 -0
- package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
- package/dist/types/enterprise/data-governance.d.ts +203 -0
- package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
- package/dist/types/enterprise/sso-integration.d.ts +420 -0
- package/dist/types/evaluate/context-metrics.d.ts +55 -0
- package/dist/types/evaluate/faithfulness.d.ts +41 -0
- package/dist/types/evaluate/groundedness.d.ts +60 -0
- package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
- package/dist/types/evaluate/relevance.d.ts +21 -0
- package/dist/types/evaluate/scoring.d.ts +123 -0
- package/dist/types/experimental.d.ts +15 -0
- package/dist/types/graph/entity-extractor.d.ts +84 -0
- package/dist/types/graph/graph-index.d.ts +49 -0
- package/dist/types/graph/graph-retriever.d.ts +81 -0
- package/dist/types/graph/knowledge-graph.d.ts +129 -0
- package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
- package/dist/types/guardrails/injection-detector.d.ts +47 -0
- package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
- package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
- package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
- package/dist/types/mcp/mcp-server.d.ts +85 -0
- package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
- package/dist/types/reranker/cascade-reranker.d.ts +40 -0
- package/dist/types/reranker/embedding-reranker.d.ts +77 -0
- package/dist/types/reranker/scoring-reranker.d.ts +41 -0
- package/dist/types/retrieval/bm25-search.d.ts +43 -0
- package/dist/types/retrieval/file-vector-store.d.ts +87 -0
- package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
- package/dist/types/retrieval/rank-fusion.d.ts +10 -0
- package/dist/types/security/jwt-config.d.ts +80 -0
- package/dist/types/security/jwt-validator.d.ts +195 -0
- package/dist/types/security/plugin-sandbox.d.ts +158 -0
- package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
- package/dist/types/security/validator.d.ts +5 -0
- package/dist/types/streaming/embedding-stream.d.ts +65 -0
- package/dist/types/streaming/sse-adapter.d.ts +59 -0
- package/dist/types/streaming/stream-router.d.ts +88 -0
- package/dist/types/streaming/streaming-embedder.d.ts +99 -0
- package/dist/types/streaming/websocket-adapter.d.ts +71 -0
- package/dist/types/templates/scaffold.d.ts +55 -0
- package/dist/types/templates/template-registry.d.ts +63 -0
- package/dist/types/utils/embed-query.d.ts +20 -0
- package/dist/types/utils/logger.d.ts +50 -0
- package/dist/types/utils/rate-limiter.d.ts +66 -0
- package/dist/types/utils/retry.d.ts +27 -0
- package/dist/types/utils/run-abort.d.ts +34 -0
- package/dist/types/utils/secure-logging.d.ts +51 -0
- package/dist/types/utils/structured-logger.d.ts +126 -0
- package/package.json +53 -29
- package/src/agentic/agentic-pipeline.js +75 -7
- package/src/agentic/iterative-retriever.js +47 -10
- package/src/agentic/query-planner.js +329 -78
- package/src/citation/citation-tracker.js +6 -4
- package/src/citation/hallucination-detector.js +2 -2
- package/src/citation/source-mapper.js +15 -28
- package/src/cli/commands/ai.js +101 -0
- package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
- package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
- package/src/cli/commands/plugin-hub/local-commands.js +143 -0
- package/src/cli/commands/plugin-hub.js +96 -36
- package/src/cli/diagnostics/config-checker.js +76 -63
- package/src/cli/diagnostics/environment-checker.js +62 -31
- package/src/cli/diagnostics/plugin-checker.js +36 -89
- package/src/cli/doctor-command.js +39 -5
- package/src/cli/enhanced-cli-commands.js +72 -217
- package/src/cli/handlers/ai-handler.js +364 -0
- package/src/cli/handlers/diagnostics-handler.js +78 -31
- package/src/cli/handlers/ingest-handler.js +28 -22
- package/src/cli/handlers/init-handler.js +148 -25
- package/src/cli/handlers/query-handler.js +41 -26
- package/src/cli/handlers/run-handler.js +16 -167
- package/src/cli/handlers/serve-handler.js +60 -0
- package/src/cli/services/bench-stats.js +54 -0
- package/src/cli/services/benchmark.js +161 -0
- package/src/cli/services/feedback.js +155 -0
- package/src/cli/services/rag-service.js +322 -0
- package/src/cli/shared/cli-utils.js +24 -7
- package/src/cli/shared/config-context.js +21 -0
- package/src/cli/wizard/config-builder.js +81 -0
- package/src/cli/wizard/init-wizard.js +617 -0
- package/src/config/component-factory.js +125 -0
- package/src/config/load-config.js +29 -10
- package/src/config/load-plugin-config.js +46 -2
- package/src/connectors/anthropic-connector.js +93 -55
- package/src/connectors/base-connector.js +74 -19
- package/src/connectors/cohere-connector.js +88 -32
- package/src/connectors/connector-utils.js +302 -0
- package/src/connectors/local-embedder.js +43 -5
- package/src/connectors/memory-retriever.js +108 -15
- package/src/connectors/ollama-connector.js +83 -49
- package/src/connectors/openai-connector.js +108 -44
- package/src/connectors/rag-prompt.js +88 -0
- package/src/core/create-pipeline.js +209 -60
- package/src/core/plugin-registry.js +87 -11
- package/src/cost/cost-calculator.js +205 -32
- package/src/dev/dev-server.js +166 -11
- package/src/dev/hot-reload.js +40 -1
- package/src/ecosystem/plugin-checks.js +181 -0
- package/src/ecosystem/plugin-hub.js +50 -125
- package/src/ecosystem/plugin-installer.js +298 -0
- package/src/ecosystem/plugin-probe.js +127 -0
- package/src/ecosystem/plugin-scanner.js +185 -0
- package/src/ecosystem/tar-extract.js +114 -0
- package/src/enterprise/audit-logging.js +158 -29
- package/src/enterprise/sso-integration.js +715 -228
- package/src/evaluate/context-metrics.js +28 -16
- package/src/evaluate/evaluator.js +3 -3
- package/src/evaluate/faithfulness.js +18 -14
- package/src/evaluate/groundedness.js +43 -12
- package/src/evaluate/pipeline-evaluator.js +35 -8
- package/src/evaluate/scoring.js +251 -3
- package/src/experimental.js +71 -0
- package/src/guardrails/guardrails-pipeline.js +69 -7
- package/src/guardrails/injection-detector.js +283 -0
- package/src/guardrails/post-generation-guard.js +19 -0
- package/src/guardrails/pre-retrieval-guard.js +36 -36
- package/src/guardrails/retrieval-guard.js +58 -8
- package/src/index.js +36 -1
- package/src/loader/document-reader.js +91 -0
- package/src/reranker/cascade-reranker.js +3 -0
- package/src/reranker/embedding-reranker.js +136 -21
- package/src/reranker/llm-reranker.js +79 -12
- package/src/reranker/scoring-reranker.js +3 -0
- package/src/retrieval/bm25-search.js +12 -6
- package/src/retrieval/file-vector-store.js +212 -0
- package/src/retrieval/hybrid-retriever.js +22 -19
- package/src/security/jwt-config.js +13 -2
- package/src/security/jwt-validator.js +134 -30
- package/src/security/plugin-sandbox.js +18 -1
- package/src/security/plugin-signature-verifier.js +245 -70
- package/src/utils/deprecate.js +69 -0
- package/src/utils/embed-query.js +33 -0
- package/src/utils/input-sanitizer.js +19 -4
- package/src/utils/logger.js +34 -4
- package/src/utils/run-abort.js +102 -0
- package/src/ai/federated-learning.js +0 -211
- package/src/ai/federation.js +0 -71
- package/src/ai/index.js +0 -22
- package/src/ai/model-trainer.js +0 -217
- package/src/ai/model-training.js +0 -202
- package/src/ai/multimodal/multi-modal-processor.js +0 -345
- package/src/ai/orchestrator.js +0 -65
- package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
- package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
- package/src/cli/commands/ai-ml/federated-commands.js +0 -207
- package/src/cli/commands/ai-ml/index.js +0 -17
- package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
- package/src/cli/commands/ai-ml/shared.js +0 -51
- package/src/cli/commands/ai-ml/training-commands.js +0 -228
- package/src/cli/commands/ai-ml.js +0 -386
- package/src/cli/commands/docs.js +0 -228
- package/src/cli/commands/dx.js +0 -538
- package/src/cli/commands/wizard.js +0 -627
- package/src/cli/input-sanitization-middleware.js +0 -356
- package/src/cli/interactive-wizard.js +0 -373
- package/src/cli/templates/development.template.json +0 -64
- package/src/cli/templates/minimal.template.json +0 -33
- package/src/cli/templates/production.template.json +0 -104
- package/src/cli/templates/testing.template.json +0 -45
- package/src/cli/wizard/configuration.js +0 -148
- package/src/cli/wizard/observability.js +0 -79
- package/src/cli/wizard/performance.js +0 -76
- package/src/cli/wizard/plugin-selection.js +0 -297
- package/src/cli/wizard/preview-save.js +0 -172
- package/src/cli/wizard/project-setup.js +0 -66
- package/src/core/pipeline-factory.js +0 -59
- package/src/dag/enhanced-dag-engine.js +0 -894
- package/src/dx/index.js +0 -40
- package/src/dx/integration-templates.js +0 -662
- package/src/dx/performance-profiler.js +0 -968
- package/src/dx/realtime-debugger.js +0 -755
- package/src/dx/visual-pipeline-builder.js +0 -578
- package/src/ingest.js +0 -59
- package/src/pipeline/pipeline.js +0 -18
- package/src/query.js +0 -117
package/src/index.js
CHANGED
|
@@ -49,6 +49,8 @@ const {
|
|
|
49
49
|
scoreAnswer,
|
|
50
50
|
computeBLEU,
|
|
51
51
|
computeROUGE,
|
|
52
|
+
checkNumericConsistency,
|
|
53
|
+
extractNumbers,
|
|
52
54
|
} = require('./evaluate/scoring');
|
|
53
55
|
const { computeFaithfulness } = require('./evaluate/faithfulness');
|
|
54
56
|
const { computeAnswerRelevance } = require('./evaluate/relevance');
|
|
@@ -74,9 +76,11 @@ const { detectHallucinations } = require('./citation/hallucination-detector');
|
|
|
74
76
|
const { BM25Search } = require('./retrieval/bm25-search');
|
|
75
77
|
const { reciprocalRankFusion } = require('./retrieval/rank-fusion');
|
|
76
78
|
const { HybridRetriever } = require('./retrieval/hybrid-retriever');
|
|
79
|
+
const { FileVectorStore } = require('./retrieval/file-vector-store');
|
|
77
80
|
|
|
78
81
|
// Guardrails
|
|
79
82
|
const { PreRetrievalGuard } = require('./guardrails/pre-retrieval-guard');
|
|
83
|
+
const { detectInjection } = require('./guardrails/injection-detector');
|
|
80
84
|
const { RetrievalGuard } = require('./guardrails/retrieval-guard');
|
|
81
85
|
const { PostGenerationGuard } = require('./guardrails/post-generation-guard');
|
|
82
86
|
const { GuardrailsPipeline } = require('./guardrails/guardrails-pipeline');
|
|
@@ -88,7 +92,7 @@ const { SelfCritiqueChecker } = require('./agentic/self-critique');
|
|
|
88
92
|
const { AgenticPipeline } = require('./agentic/agentic-pipeline');
|
|
89
93
|
|
|
90
94
|
// Cost Management
|
|
91
|
-
const { CostCalculator } = require('./cost/cost-calculator');
|
|
95
|
+
const { CostCalculator, PRICING_METADATA } = require('./cost/cost-calculator');
|
|
92
96
|
const { CostTracker } = require('./cost/cost-tracker');
|
|
93
97
|
const { TokenBudget } = require('./cost/token-budget');
|
|
94
98
|
|
|
@@ -218,6 +222,8 @@ module.exports = {
|
|
|
218
222
|
scoreAnswer,
|
|
219
223
|
computeBLEU,
|
|
220
224
|
computeROUGE,
|
|
225
|
+
checkNumericConsistency,
|
|
226
|
+
extractNumbers,
|
|
221
227
|
computeFaithfulness,
|
|
222
228
|
computeAnswerRelevance,
|
|
223
229
|
computeContextPrecision,
|
|
@@ -238,9 +244,11 @@ module.exports = {
|
|
|
238
244
|
BM25Search,
|
|
239
245
|
reciprocalRankFusion,
|
|
240
246
|
HybridRetriever,
|
|
247
|
+
FileVectorStore,
|
|
241
248
|
|
|
242
249
|
// Guardrails
|
|
243
250
|
PreRetrievalGuard,
|
|
251
|
+
detectInjection,
|
|
244
252
|
RetrievalGuard,
|
|
245
253
|
PostGenerationGuard,
|
|
246
254
|
GuardrailsPipeline,
|
|
@@ -253,6 +261,7 @@ module.exports = {
|
|
|
253
261
|
|
|
254
262
|
// Cost Management
|
|
255
263
|
CostCalculator,
|
|
264
|
+
PRICING_METADATA,
|
|
256
265
|
CostTracker,
|
|
257
266
|
TokenBudget,
|
|
258
267
|
|
|
@@ -330,3 +339,29 @@ module.exports = {
|
|
|
330
339
|
DevServer,
|
|
331
340
|
createDevServer,
|
|
332
341
|
};
|
|
342
|
+
|
|
343
|
+
// Simulated or incomplete modules now live in the ./experimental entry
|
|
344
|
+
// (src/experimental.js). Their main-entry exports keep working but emit a
|
|
345
|
+
// one-time DeprecationWarning when used, and will be removed from the main
|
|
346
|
+
// entry in the next major release (SEMVER.md, "Deprecation process").
|
|
347
|
+
const experimental = require('./experimental');
|
|
348
|
+
const { deprecateClass } = require('./utils/deprecate');
|
|
349
|
+
|
|
350
|
+
for (const name of Object.keys(experimental)) {
|
|
351
|
+
if (!(name in module.exports)) continue;
|
|
352
|
+
module.exports[name] = deprecateClass(experimental[name], {
|
|
353
|
+
name,
|
|
354
|
+
message:
|
|
355
|
+
`${name} is experimental (simulated or incomplete) and is deprecated in ` +
|
|
356
|
+
`the main entry. Import it from "@devilsdev/rag-pipeline-utils/experimental"; ` +
|
|
357
|
+
'it will be removed from the main entry in the next major release.',
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// The ./experimental build entry reads the unwrapped classes from here, so
|
|
362
|
+
// both entries share one copy of each class (non-enumerable: not a named export).
|
|
363
|
+
Object.defineProperty(
|
|
364
|
+
module.exports,
|
|
365
|
+
Symbol.for('@devilsdev/rag-pipeline-utils/experimental'),
|
|
366
|
+
{ value: experimental, enumerable: false },
|
|
367
|
+
);
|
|
@@ -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 };
|
|
@@ -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.
|
|
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
|
-
/**
|
|
13
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
152
|
+
const queryEmbedding = await raceAbort(
|
|
153
|
+
Promise.resolve().then(() => this._embed(query, signal)),
|
|
154
|
+
signal,
|
|
155
|
+
);
|
|
56
156
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
//
|
|
20
|
-
|
|
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 =
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
const docLen =
|
|
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
|
}
|