@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.
- package/.ragrc.schema.json +83 -192
- package/CHANGELOG.md +431 -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 +6442 -1355
- package/dist/index.d.ts +129 -2
- package/dist/index.mjs +1 -1
- package/dist/testing.cjs +2 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.mjs +5 -0
- package/dist/types/adapters/ai-sdk.d.ts +53 -0
- 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/cache/component-keys.d.ts +7 -0
- package/dist/types/cache/memory-cache.d.ts +54 -0
- package/dist/types/cache/pipeline-cache.d.ts +66 -0
- package/dist/types/cache/redis-cache.d.ts +46 -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 +97 -0
- package/dist/types/connectors/base-connector.d.ts +87 -0
- package/dist/types/connectors/cohere-connector.d.ts +94 -0
- package/dist/types/connectors/connector-utils.d.ts +110 -0
- package/dist/types/connectors/local-embedder.d.ts +95 -0
- package/dist/types/connectors/memory-retriever.d.ts +99 -0
- package/dist/types/connectors/ollama-connector.d.ts +115 -0
- package/dist/types/connectors/openai-connector.d.ts +129 -0
- package/dist/types/connectors/rag-prompt.d.ts +40 -0
- package/dist/types/core/create-pipeline.d.ts +8 -0
- package/dist/types/core/plugin-registry.d.ts +136 -0
- package/dist/types/core/stage-contracts.d.ts +36 -0
- package/dist/types/core/validate-plugin.d.ts +45 -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/evaluate/semantic-similarity.d.ts +14 -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/grounding/check-answer.d.ts +101 -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 +99 -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/testing.d.ts +168 -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 +72 -33
- package/src/adapters/ai-sdk.js +215 -0
- 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/cache/component-keys.js +38 -0
- package/src/cache/memory-cache.js +87 -0
- package/src/cache/pipeline-cache.js +260 -0
- package/src/cache/redis-cache.js +83 -0
- 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 +106 -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 +114 -49
- 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 +104 -233
- 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 +203 -25
- 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/instrument.js +83 -0
- package/src/cli/services/rag-service.js +361 -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 +602 -0
- package/src/config/component-factory.js +125 -0
- package/src/config/enhanced-ragrc-schema.js +0 -2
- package/src/config/load-config.js +29 -10
- package/src/config/load-plugin-config.js +46 -2
- package/src/config/validate-schema.js +4 -1
- package/src/connectors/anthropic-connector.js +115 -57
- package/src/connectors/base-connector.js +74 -19
- package/src/connectors/cohere-connector.js +108 -34
- package/src/connectors/connector-utils.js +302 -0
- package/src/connectors/local-embedder.js +63 -5
- package/src/connectors/memory-retriever.js +125 -14
- package/src/connectors/ollama-connector.js +112 -51
- package/src/connectors/openai-connector.js +153 -44
- package/src/connectors/rag-prompt.js +88 -0
- package/src/core/create-pipeline.js +307 -61
- package/src/core/performance/parallel-processor.js +62 -1
- package/src/core/plugin-registry.js +125 -26
- package/src/core/stage-contracts.js +36 -0
- package/src/core/validate-plugin.js +85 -0
- package/src/cost/cost-calculator.js +205 -32
- package/src/debug/execution-tracer.js +14 -4
- package/src/dev/dev-server.js +173 -11
- package/src/dev/hot-reload.js +44 -4
- 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 +716 -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/evaluate/semantic-similarity.js +79 -0
- package/src/experimental.js +71 -0
- package/src/grounding/check-answer.js +173 -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 +58 -1
- package/src/loader/csv-loader.js +3 -2
- package/src/loader/document-reader.js +91 -0
- package/src/loader/html-loader.js +3 -1
- package/src/loader/markdown-loader.js +3 -2
- 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 +241 -0
- package/src/retrieval/hybrid-retriever.js +22 -19
- package/src/security/jwt-config.js +13 -2
- package/src/security/jwt-validator.js +139 -34
- package/src/security/plugin-sandbox.js +18 -1
- package/src/security/plugin-signature-verifier.js +245 -70
- package/src/testing.js +254 -0
- 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/loader/csv-loader.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs/promises');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
}
|