@devilsdev/rag-pipeline-utils 2.3.0 → 2.4.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/README.md +293 -361
- package/contracts/loader-contract.json +11 -6
- package/contracts/reranker-contract.json +18 -26
- package/dist/index.cjs +21190 -47
- package/dist/index.d.ts +298 -68
- package/dist/index.mjs +9 -76
- package/package.json +20 -8
- package/src/agentic/agentic-pipeline.js +235 -0
- package/src/agentic/iterative-retriever.js +213 -0
- package/src/agentic/query-planner.js +208 -0
- package/src/agentic/self-critique.js +102 -0
- package/src/ai/adaptive-retrieval.js +0 -3
- package/src/ai/federated-learning.js +0 -3
- package/src/ai/federation.js +1 -6
- package/src/ai/index.js +9 -20
- package/src/ai/model-training.js +0 -3
- package/src/ai/multimodal-processing.js +0 -3
- package/src/ai/multimodal.js +1 -6
- package/src/ai/orchestrator.js +1 -6
- package/src/ai/retrieval-engine.js +1 -6
- package/src/chunking/chunking-engine.js +142 -0
- package/src/chunking/strategies/fixed-size.js +54 -0
- package/src/chunking/strategies/recursive.js +127 -0
- package/src/chunking/strategies/semantic.js +128 -0
- package/src/chunking/strategies/sentence.js +32 -0
- package/src/chunking/strategies/structure-aware.js +138 -0
- package/src/citation/citation-tracker.js +161 -0
- package/src/citation/hallucination-detector.js +74 -0
- package/src/citation/source-mapper.js +95 -0
- package/src/cli/commands/ai-ml/adaptive-commands.js +178 -0
- package/src/cli/commands/ai-ml/federated-commands.js +207 -0
- package/src/cli/commands/ai-ml/index.js +17 -0
- package/src/cli/commands/ai-ml/multimodal-commands.js +187 -0
- package/src/cli/commands/ai-ml/shared.js +51 -0
- package/src/cli/commands/ai-ml/training-commands.js +228 -0
- package/src/cli/commands/ai-ml.js +176 -757
- package/src/cli/commands/plugin-hub/certification-commands.js +221 -0
- package/src/cli/commands/plugin-hub/event-handlers.js +64 -0
- package/src/cli/commands/plugin-hub/hub-commands.js +290 -0
- package/src/cli/commands/plugin-hub/review-commands.js +78 -0
- package/src/cli/commands/plugin-hub.js +106 -782
- package/src/cli/diagnostics/config-checker.js +81 -0
- package/src/cli/diagnostics/dependency-checker.js +69 -0
- package/src/cli/diagnostics/environment-checker.js +45 -0
- package/src/cli/diagnostics/performance-checker.js +51 -0
- package/src/cli/diagnostics/plugin-checker.js +106 -0
- package/src/cli/diagnostics/security-checker.js +61 -0
- package/src/cli/doctor-command.js +70 -369
- package/src/cli/enhanced-cli-commands.js +162 -706
- package/src/cli/handlers/config-handler.js +126 -0
- package/src/cli/handlers/diagnostics-handler.js +113 -0
- package/src/cli/handlers/ingest-handler.js +42 -0
- package/src/cli/handlers/init-handler.js +51 -0
- package/src/cli/handlers/query-handler.js +45 -0
- package/src/cli/handlers/run-handler.js +181 -0
- package/src/cli/interactive-wizard.js +99 -723
- package/src/cli/shared/cli-utils.js +89 -0
- package/src/cli/wizard/configuration.js +148 -0
- package/src/cli/wizard/observability.js +79 -0
- package/src/cli/wizard/performance.js +76 -0
- package/src/cli/wizard/plugin-selection.js +297 -0
- package/src/cli/wizard/preview-save.js +172 -0
- package/src/cli/wizard/project-setup.js +66 -0
- package/src/config/memory-optimization.js +3 -3
- package/src/connectors/anthropic-connector.js +179 -0
- package/src/connectors/base-connector.js +94 -0
- package/src/connectors/cohere-connector.js +143 -0
- package/src/connectors/local-embedder.js +171 -0
- package/src/connectors/memory-retriever.js +150 -0
- package/src/connectors/ollama-connector.js +205 -0
- package/src/connectors/openai-connector.js +173 -0
- package/src/core/create-pipeline.js +21 -1
- package/src/core/observability/tracing.js +2 -1
- package/src/core/pipeline-factory.js +0 -1
- package/src/core/plugin-registry.js +14 -14
- package/src/cost/cost-calculator.js +146 -0
- package/src/cost/cost-tracker.js +260 -0
- package/src/cost/token-budget.js +245 -0
- package/src/dag/core/execution-scheduler.js +2 -1
- package/src/dag/core/topology-validation.js +0 -3
- package/src/dag/dag-engine.js +3 -5
- package/src/dag/dag-node.js +0 -1
- package/src/dag/enhanced-dag-engine.js +171 -99
- package/src/dashboard/dashboard-generator.js +642 -0
- package/src/dashboard/metrics-aggregator.js +409 -0
- package/src/debug/execution-tracer.js +210 -0
- package/src/debug/trace-inspector.js +281 -0
- package/src/dev/dev-server.js +5 -5
- package/src/dev/hot-reload.js +3 -2
- package/src/dx/integration-templates.js +0 -3
- package/src/ecosystem/plugin-analytics-dashboard.js +2 -2
- package/src/ecosystem/plugin-hub.js +2 -2
- package/src/enterprise/audit-logging.js +2 -2
- package/src/enterprise/sso-integration.js +52 -32
- package/src/evaluate/context-metrics.js +127 -0
- package/src/evaluate/evaluator.js +4 -2
- package/src/evaluate/faithfulness.js +117 -0
- package/src/evaluate/groundedness.js +60 -0
- package/src/evaluate/pipeline-evaluator.js +156 -0
- package/src/evaluate/relevance.js +129 -0
- package/src/evaluate/scoring.js +27 -0
- package/src/graph/entity-extractor.js +183 -0
- package/src/graph/graph-index.js +113 -0
- package/src/graph/graph-retriever.js +244 -0
- package/src/graph/knowledge-graph.js +256 -0
- package/src/guardrails/guardrails-pipeline.js +136 -0
- package/src/guardrails/post-generation-guard.js +170 -0
- package/src/guardrails/pre-retrieval-guard.js +161 -0
- package/src/guardrails/retrieval-guard.js +126 -0
- package/src/index.js +255 -9
- package/src/loader/html-loader.js +4 -15
- package/src/loader/markdown-loader.js +4 -15
- package/src/mcp/mcp-server.js +199 -0
- package/src/mcp/mcp-tool-builder.js +121 -0
- package/src/observability/audit-logger.js +0 -2
- package/src/observability/event-logger.js +2 -1
- package/src/observability/security-monitor.js +0 -2
- package/src/observability/telemetry.js +0 -2
- package/src/reranker/cascade-reranker.js +79 -0
- package/src/reranker/embedding-reranker.js +124 -0
- package/src/reranker/llm-reranker.js +6 -3
- package/src/reranker/scoring-reranker.js +123 -0
- package/src/retrieval/bm25-search.js +157 -0
- package/src/retrieval/hybrid-retriever.js +111 -0
- package/src/retrieval/rank-fusion.js +62 -0
- package/src/security/jwt-config.js +4 -3
- package/src/security/jwt-validator.js +2 -1
- package/src/security/plugin-sandbox.js +0 -3
- package/src/security/plugin-signature-verifier.js +4 -6
- package/src/security/validator.js +0 -2
- package/src/streaming/embedding-stream.js +197 -0
- package/src/streaming/sse-adapter.js +168 -0
- package/src/streaming/stream-router.js +210 -0
- package/src/streaming/streaming-embedder.js +231 -0
- package/src/streaming/websocket-adapter.js +171 -0
- package/src/templates/scaffold.js +147 -0
- package/src/templates/template-registry.js +272 -0
- package/src/utils/contract-validator.js +2 -1
- package/src/utils/input-sanitizer.js +80 -99
- package/src/utils/performance-monitor.js +0 -3
- package/src/utils/plugin-scaffolder.js +9 -16
- package/src/utils/rate-limiter.js +0 -1
- package/src/utils/retry-policy.js +0 -3
- package/src/utils/safe-expression-evaluator.js +0 -3
- package/src/utils/secure-logger-config.js +2 -4
- package/src/utils/secure-logging.js +0 -3
- package/src/utils/structured-logger.js +0 -3
- package/src/ai/index-fixed.js +0 -509
- package/src/ai/module-template.js +0 -37
- package/src/ci/workflow-analyzer.js +0 -71
- package/src/cli/plugin-marketplace-commands.js +0 -869
- package/src/dev/README.md +0 -126
- package/src/dev/examples/hot-reload-usage.js +0 -355
- package/src/mocks/openai-embedder.js +0 -119
- package/src/mocks/openai-llm.js +0 -77
- package/src/mocks/pdf-loader.js +0 -21
- package/src/mocks/pinecone-retriever.js +0 -106
- package/src/scripts/script-utilities.js +0 -36
- package/src/utils/ci/diagnostic-reporter.js +0 -57
- package/src/utils/examples/error-formatter-usage.js +0 -351
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('events');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default configuration for the SelfCritiqueChecker.
|
|
7
|
+
* @type {object}
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_CONFIG = {
|
|
10
|
+
approvalThreshold: 0.5,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Answer verification against retrieved source documents.
|
|
15
|
+
* Uses CitationTracker (lazy-loaded) to compute groundedness and detect hallucinations.
|
|
16
|
+
*
|
|
17
|
+
* @extends EventEmitter
|
|
18
|
+
*/
|
|
19
|
+
class SelfCritiqueChecker extends EventEmitter {
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} [options] - Override default configuration
|
|
22
|
+
* @param {number} [options.approvalThreshold=0.5] - Minimum groundedness score to approve an answer
|
|
23
|
+
*/
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
super();
|
|
26
|
+
this.config = { ...DEFAULT_CONFIG, ...options };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check an answer against retrieved documents for groundedness.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} answer - The LLM-generated answer to verify
|
|
33
|
+
* @param {Array<object>} retrievedDocs - Source documents to check against
|
|
34
|
+
* @returns {{
|
|
35
|
+
* approved: boolean,
|
|
36
|
+
* score: number,
|
|
37
|
+
* issues: Array<{sentence: string, classification: string}>,
|
|
38
|
+
* citationResult: object
|
|
39
|
+
* }}
|
|
40
|
+
* @throws {Error} If answer or retrievedDocs are invalid
|
|
41
|
+
* @fires SelfCritiqueChecker#checked
|
|
42
|
+
*/
|
|
43
|
+
check(answer, retrievedDocs) {
|
|
44
|
+
// 1. Validate inputs
|
|
45
|
+
if (!answer || typeof answer !== 'string' || !answer.trim()) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
'SelfCritiqueChecker.check(): answer must be a non-empty string',
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(retrievedDocs) || retrievedDocs.length === 0) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'SelfCritiqueChecker.check(): retrievedDocs must be a non-empty array',
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2. Lazy-require CitationTracker (not loaded at module level)
|
|
57
|
+
const { CitationTracker } = require('../citation/citation-tracker');
|
|
58
|
+
|
|
59
|
+
// 3. Create tracker instance
|
|
60
|
+
const tracker = new CitationTracker();
|
|
61
|
+
|
|
62
|
+
// 4. Track citations
|
|
63
|
+
const citationResult = tracker.track(answer, retrievedDocs);
|
|
64
|
+
|
|
65
|
+
// 5. Extract groundedness and hallucination data
|
|
66
|
+
const { groundednessScore, hallucinationReport } = citationResult;
|
|
67
|
+
|
|
68
|
+
// 6. Determine approval
|
|
69
|
+
const approved = groundednessScore >= this.config.approvalThreshold;
|
|
70
|
+
|
|
71
|
+
// 7. Build issues array from hallucination report
|
|
72
|
+
const issues = [];
|
|
73
|
+
if (hallucinationReport && Array.isArray(hallucinationReport.sentences)) {
|
|
74
|
+
for (const s of hallucinationReport.sentences) {
|
|
75
|
+
if (
|
|
76
|
+
s.classification === 'definite_hallucination' ||
|
|
77
|
+
s.classification === 'likely_hallucination'
|
|
78
|
+
) {
|
|
79
|
+
issues.push({
|
|
80
|
+
sentence: s.sentence || s.text || '',
|
|
81
|
+
classification: s.classification,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 8. Emit 'checked' event
|
|
88
|
+
const result = {
|
|
89
|
+
approved,
|
|
90
|
+
score: groundednessScore,
|
|
91
|
+
issues,
|
|
92
|
+
citationResult,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
this.emit('checked', result);
|
|
96
|
+
|
|
97
|
+
// 9. Return
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { SelfCritiqueChecker, DEFAULT_CONFIG };
|
package/src/ai/federation.js
CHANGED
|
@@ -68,9 +68,4 @@ class FederatedLearningCoordinator {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
const coordinator = new FederatedLearningCoordinator();
|
|
73
|
-
|
|
74
|
-
module.exports = coordinator;
|
|
75
|
-
module.exports.FederatedLearningCoordinator = FederatedLearningCoordinator;
|
|
76
|
-
module.exports.default = module.exports;
|
|
71
|
+
module.exports = { FederatedLearningCoordinator };
|
package/src/ai/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI/ML Module Index - Decomposed Architecture
|
|
3
|
-
* Provides unified interface to 4 separate AI modules
|
|
3
|
+
* Provides unified interface to 4 separate AI modules
|
|
4
4
|
*
|
|
5
5
|
* Decomposed modules:
|
|
6
6
|
* - orchestrator.js - ModelTrainingOrchestrator
|
|
@@ -9,25 +9,14 @@
|
|
|
9
9
|
* - federation.js - FederatedLearningCoordinator
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
12
|
+
const { ModelTrainingOrchestrator } = require("./orchestrator");
|
|
13
|
+
const { AdaptiveRetrievalEngine } = require("./retrieval-engine");
|
|
14
|
+
const { MultiModalProcessor } = require("./multimodal");
|
|
15
|
+
const { FederatedLearningCoordinator } = require("./federation");
|
|
16
16
|
|
|
17
|
-
// Export the decomposed modules
|
|
18
17
|
module.exports = {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
FederatedLearningCoordinator: federation,
|
|
24
|
-
|
|
25
|
-
// Legacy names for backward compatibility
|
|
26
|
-
modelTrainer: orchestrator,
|
|
27
|
-
adaptiveRetrieval: retrievalEngine,
|
|
28
|
-
multiModalProcessor: multimodal,
|
|
29
|
-
federatedLearning: federation,
|
|
18
|
+
ModelTrainingOrchestrator,
|
|
19
|
+
AdaptiveRetrievalEngine,
|
|
20
|
+
MultiModalProcessor,
|
|
21
|
+
FederatedLearningCoordinator,
|
|
30
22
|
};
|
|
31
|
-
|
|
32
|
-
// CJS+ESM interop pattern
|
|
33
|
-
module.exports.default = module.exports;
|
package/src/ai/model-training.js
CHANGED
package/src/ai/multimodal.js
CHANGED
|
@@ -63,9 +63,4 @@ class MultiModalProcessor {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
const processor = new MultiModalProcessor();
|
|
68
|
-
|
|
69
|
-
module.exports = processor;
|
|
70
|
-
module.exports.MultiModalProcessor = MultiModalProcessor;
|
|
71
|
-
module.exports.default = module.exports;
|
|
66
|
+
module.exports = { MultiModalProcessor };
|
package/src/ai/orchestrator.js
CHANGED
|
@@ -62,9 +62,4 @@ class ModelTrainingOrchestrator {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
const orchestrator = new ModelTrainingOrchestrator();
|
|
67
|
-
|
|
68
|
-
module.exports = orchestrator;
|
|
69
|
-
module.exports.ModelTrainingOrchestrator = ModelTrainingOrchestrator;
|
|
70
|
-
module.exports.default = module.exports;
|
|
65
|
+
module.exports = { ModelTrainingOrchestrator };
|
|
@@ -61,9 +61,4 @@ class AdaptiveRetrievalEngine {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
const engine = new AdaptiveRetrievalEngine();
|
|
66
|
-
|
|
67
|
-
module.exports = engine;
|
|
68
|
-
module.exports.AdaptiveRetrievalEngine = AdaptiveRetrievalEngine;
|
|
69
|
-
module.exports.default = module.exports;
|
|
64
|
+
module.exports = { AdaptiveRetrievalEngine };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('events');
|
|
4
|
+
const { sentenceChunk } = require('./strategies/sentence');
|
|
5
|
+
const { fixedSizeChunk } = require('./strategies/fixed-size');
|
|
6
|
+
const { recursiveChunk } = require('./strategies/recursive');
|
|
7
|
+
const { semanticChunk } = require('./strategies/semantic');
|
|
8
|
+
const { structureAwareChunk } = require('./strategies/structure-aware');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default configuration for the chunking engine.
|
|
12
|
+
* @type {object}
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_CONFIG = {
|
|
15
|
+
strategy: 'recursive',
|
|
16
|
+
chunkSize: 512,
|
|
17
|
+
chunkOverlap: 50,
|
|
18
|
+
minChunkSize: 50,
|
|
19
|
+
separators: ['\n\n', '\n', '. ', ' '],
|
|
20
|
+
similarityThreshold: 0.3,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* ChunkingEngine orchestrates text chunking using pluggable strategies.
|
|
25
|
+
* Extends EventEmitter to emit 'chunk' events after each chunking operation.
|
|
26
|
+
*
|
|
27
|
+
* @extends EventEmitter
|
|
28
|
+
*/
|
|
29
|
+
class ChunkingEngine extends EventEmitter {
|
|
30
|
+
/**
|
|
31
|
+
* Create a ChunkingEngine instance.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} [options={}] - Configuration options merged with DEFAULT_CONFIG.
|
|
34
|
+
* @param {string} [options.strategy='recursive'] - Default strategy name.
|
|
35
|
+
* @param {number} [options.chunkSize=512] - Default chunk size.
|
|
36
|
+
* @param {number} [options.chunkOverlap=50] - Default chunk overlap.
|
|
37
|
+
* @param {number} [options.minChunkSize=50] - Default minimum chunk size.
|
|
38
|
+
* @param {string[]} [options.separators] - Default separators for recursive strategy.
|
|
39
|
+
* @param {number} [options.similarityThreshold=0.3] - Default similarity threshold for semantic strategy.
|
|
40
|
+
*/
|
|
41
|
+
constructor(options = {}) {
|
|
42
|
+
super();
|
|
43
|
+
this._config = { ...DEFAULT_CONFIG, ...options };
|
|
44
|
+
this._strategies = new Map();
|
|
45
|
+
|
|
46
|
+
// Register built-in strategies
|
|
47
|
+
this._strategies.set('sentence', sentenceChunk);
|
|
48
|
+
this._strategies.set('fixed-size', fixedSizeChunk);
|
|
49
|
+
this._strategies.set('recursive', recursiveChunk);
|
|
50
|
+
this._strategies.set('semantic', semanticChunk);
|
|
51
|
+
this._strategies.set('structure-aware', structureAwareChunk);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Chunk text using the configured or specified strategy.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} text - The input text to chunk. Must be a non-empty string.
|
|
58
|
+
* @param {object} [options={}] - Per-call options that override the engine config.
|
|
59
|
+
* @param {string} [options.strategy] - Strategy name to use for this call.
|
|
60
|
+
* @returns {string[]} Array of text chunks.
|
|
61
|
+
* @throws {Error} If text is not a non-empty string.
|
|
62
|
+
* @throws {Error} If the specified strategy is not registered.
|
|
63
|
+
* @fires ChunkingEngine#chunk
|
|
64
|
+
*/
|
|
65
|
+
chunk(text, options = {}) {
|
|
66
|
+
if (!text || typeof text !== 'string' || text.trim().length === 0) {
|
|
67
|
+
throw new Error('text must be a non-empty string');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const mergedOptions = { ...this._config, ...options };
|
|
71
|
+
const strategyName = mergedOptions.strategy;
|
|
72
|
+
|
|
73
|
+
const strategyFn = this._strategies.get(strategyName);
|
|
74
|
+
if (!strategyFn) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Unknown chunking strategy "${strategyName}". Available strategies: ${this.listStrategies().join(', ')}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const results = strategyFn(text, mergedOptions);
|
|
81
|
+
|
|
82
|
+
// Filter out empty chunks
|
|
83
|
+
const chunks = results.filter((chunk) => chunk && chunk.trim().length > 0);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Chunk event emitted after a successful chunking operation.
|
|
87
|
+
*
|
|
88
|
+
* @event ChunkingEngine#chunk
|
|
89
|
+
* @type {object}
|
|
90
|
+
* @property {string} strategy - The strategy name used.
|
|
91
|
+
* @property {number} inputLength - Character length of the input text.
|
|
92
|
+
* @property {number} chunkCount - Number of resulting chunks.
|
|
93
|
+
*/
|
|
94
|
+
this.emit('chunk', {
|
|
95
|
+
strategy: strategyName,
|
|
96
|
+
inputLength: text.length,
|
|
97
|
+
chunkCount: chunks.length,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return chunks;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Register a custom chunking strategy.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} name - Strategy name (used to select it via options.strategy).
|
|
107
|
+
* @param {function} fn - Strategy function with signature (text, options) => string[].
|
|
108
|
+
* @throws {Error} If name is not a non-empty string.
|
|
109
|
+
* @throws {Error} If fn is not a function.
|
|
110
|
+
*/
|
|
111
|
+
registerStrategy(name, fn) {
|
|
112
|
+
if (!name || typeof name !== 'string') {
|
|
113
|
+
throw new Error('Strategy name must be a non-empty string');
|
|
114
|
+
}
|
|
115
|
+
if (typeof fn !== 'function') {
|
|
116
|
+
throw new Error(
|
|
117
|
+
'Strategy must be a function with signature (text, options) => string[]',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
this._strategies.set(name, fn);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* List all registered strategy names.
|
|
125
|
+
*
|
|
126
|
+
* @returns {string[]} Array of strategy names.
|
|
127
|
+
*/
|
|
128
|
+
listStrategies() {
|
|
129
|
+
return Array.from(this._strategies.keys());
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get the current engine configuration.
|
|
134
|
+
*
|
|
135
|
+
* @returns {object} A copy of the current configuration.
|
|
136
|
+
*/
|
|
137
|
+
getConfig() {
|
|
138
|
+
return { ...this._config };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { ChunkingEngine, DEFAULT_CONFIG };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fixed-size chunking strategy.
|
|
5
|
+
* Splits text by character count with configurable overlap.
|
|
6
|
+
* Attempts to break at word boundaries by looking back up to 20 characters.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} text - The input text to chunk.
|
|
9
|
+
* @param {object} [options={}] - Chunking options.
|
|
10
|
+
* @param {number} [options.chunkSize=512] - Target chunk size in characters.
|
|
11
|
+
* @param {number} [options.chunkOverlap=50] - Number of overlapping characters between chunks.
|
|
12
|
+
* @param {number} [options.minChunkSize=50] - Minimum chunk size; smaller chunks are filtered out.
|
|
13
|
+
* @returns {string[]} Array of text chunks.
|
|
14
|
+
*/
|
|
15
|
+
function fixedSizeChunk(text, options = {}) {
|
|
16
|
+
const { chunkSize = 512, chunkOverlap = 50, minChunkSize = 50 } = options;
|
|
17
|
+
|
|
18
|
+
if (!text || typeof text !== 'string') {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const step = chunkSize - chunkOverlap;
|
|
23
|
+
if (step <= 0) {
|
|
24
|
+
throw new Error('chunkSize must be greater than chunkOverlap');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const chunks = [];
|
|
28
|
+
let pos = 0;
|
|
29
|
+
|
|
30
|
+
while (pos < text.length) {
|
|
31
|
+
let end = Math.min(pos + chunkSize, text.length);
|
|
32
|
+
|
|
33
|
+
// Try to break at a word boundary by looking back up to 20 chars
|
|
34
|
+
if (end < text.length) {
|
|
35
|
+
const lookbackStart = Math.max(end - 20, pos);
|
|
36
|
+
const segment = text.slice(lookbackStart, end);
|
|
37
|
+
const lastSpace = segment.lastIndexOf(' ');
|
|
38
|
+
if (lastSpace !== -1) {
|
|
39
|
+
end = lookbackStart + lastSpace;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const chunk = text.slice(pos, end).trim();
|
|
44
|
+
if (chunk.length >= minChunkSize) {
|
|
45
|
+
chunks.push(chunk);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pos += step;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return chunks;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { fixedSizeChunk };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SEPARATORS = ['\n\n', '\n', '. ', ' '];
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Recursive text chunking strategy.
|
|
7
|
+
* Splits text using a hierarchy of separators, merging consecutive pieces
|
|
8
|
+
* into chunks up to chunkSize. Pieces exceeding chunkSize are recursively
|
|
9
|
+
* split with the next separator in the hierarchy.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} text - The input text to chunk.
|
|
12
|
+
* @param {object} [options={}] - Chunking options.
|
|
13
|
+
* @param {number} [options.chunkSize=512] - Maximum chunk size in characters.
|
|
14
|
+
* @param {number} [options.chunkOverlap=50] - Number of overlapping characters between chunks.
|
|
15
|
+
* @param {string[]} [options.separators] - Separator hierarchy for splitting.
|
|
16
|
+
* @param {number} [options.minChunkSize=50] - Minimum chunk size; smaller chunks are filtered out.
|
|
17
|
+
* @returns {string[]} Array of text chunks.
|
|
18
|
+
*/
|
|
19
|
+
function recursiveChunk(text, options = {}) {
|
|
20
|
+
const {
|
|
21
|
+
chunkSize = 512,
|
|
22
|
+
chunkOverlap = 50,
|
|
23
|
+
separators = DEFAULT_SEPARATORS,
|
|
24
|
+
minChunkSize = 50,
|
|
25
|
+
} = options;
|
|
26
|
+
|
|
27
|
+
if (!text || typeof text !== 'string') {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const results = _splitRecursive(text, separators, 0, chunkSize, chunkOverlap);
|
|
32
|
+
|
|
33
|
+
return results.filter((chunk) => chunk.length >= minChunkSize);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Internal recursive splitting function.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} text - Text to split.
|
|
40
|
+
* @param {string[]} separators - Separator hierarchy.
|
|
41
|
+
* @param {number} sepIndex - Current separator index.
|
|
42
|
+
* @param {number} chunkSize - Maximum chunk size.
|
|
43
|
+
* @param {number} chunkOverlap - Overlap size.
|
|
44
|
+
* @returns {string[]} Array of text chunks.
|
|
45
|
+
* @private
|
|
46
|
+
*/
|
|
47
|
+
function _splitRecursive(text, separators, sepIndex, chunkSize, chunkOverlap) {
|
|
48
|
+
// Base case: no more separators or text fits in one chunk
|
|
49
|
+
if (text.length <= chunkSize) {
|
|
50
|
+
return [text.trim()];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (sepIndex >= separators.length) {
|
|
54
|
+
// No more separators; hard-cut the text
|
|
55
|
+
return [text.slice(0, chunkSize).trim()];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const separator = separators[sepIndex];
|
|
59
|
+
const pieces = text.split(separator);
|
|
60
|
+
|
|
61
|
+
// If splitting produced only one piece, try next separator
|
|
62
|
+
if (pieces.length <= 1) {
|
|
63
|
+
return _splitRecursive(
|
|
64
|
+
text,
|
|
65
|
+
separators,
|
|
66
|
+
sepIndex + 1,
|
|
67
|
+
chunkSize,
|
|
68
|
+
chunkOverlap,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const chunks = [];
|
|
73
|
+
let buffer = '';
|
|
74
|
+
|
|
75
|
+
for (const piece of pieces) {
|
|
76
|
+
const candidate = buffer ? buffer + separator + piece : piece;
|
|
77
|
+
|
|
78
|
+
if (candidate.length <= chunkSize) {
|
|
79
|
+
buffer = candidate;
|
|
80
|
+
} else {
|
|
81
|
+
// Flush the current buffer
|
|
82
|
+
if (buffer) {
|
|
83
|
+
chunks.push(buffer.trim());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// If the piece itself exceeds chunkSize, recurse with next separator
|
|
87
|
+
if (piece.length > chunkSize) {
|
|
88
|
+
const subChunks = _splitRecursive(
|
|
89
|
+
piece,
|
|
90
|
+
separators,
|
|
91
|
+
sepIndex + 1,
|
|
92
|
+
chunkSize,
|
|
93
|
+
chunkOverlap,
|
|
94
|
+
);
|
|
95
|
+
chunks.push(...subChunks);
|
|
96
|
+
buffer = '';
|
|
97
|
+
} else {
|
|
98
|
+
buffer = piece;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (buffer.trim()) {
|
|
104
|
+
chunks.push(buffer.trim());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Apply overlap: prefix each chunk (except the first) with trailing text from previous chunk
|
|
108
|
+
if (chunkOverlap > 0 && chunks.length > 1) {
|
|
109
|
+
const overlapped = [chunks[0]];
|
|
110
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
111
|
+
const prev = chunks[i - 1];
|
|
112
|
+
const overlapText = prev.slice(-chunkOverlap);
|
|
113
|
+
const combined = overlapText + chunks[i];
|
|
114
|
+
// Only add overlap if it doesn't push us way over chunkSize
|
|
115
|
+
if (combined.length <= chunkSize + chunkOverlap) {
|
|
116
|
+
overlapped.push(combined.trim());
|
|
117
|
+
} else {
|
|
118
|
+
overlapped.push(chunks[i]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return overlapped;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return chunks;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { recursiveChunk };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { recursiveChunk } = require('./recursive');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tokenize text for Jaccard similarity computation.
|
|
7
|
+
* Lowercases, splits on whitespace, filters tokens with length > 2.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} text - Text to tokenize.
|
|
10
|
+
* @returns {Set<string>} Set of tokens.
|
|
11
|
+
* @private
|
|
12
|
+
*/
|
|
13
|
+
function _tokenize(text) {
|
|
14
|
+
const tokens = text
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.split(/\s+/)
|
|
17
|
+
.filter((t) => t.length > 2);
|
|
18
|
+
return new Set(tokens);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compute Jaccard similarity between two token sets.
|
|
23
|
+
*
|
|
24
|
+
* @param {Set<string>} setA - First token set.
|
|
25
|
+
* @param {Set<string>} setB - Second token set.
|
|
26
|
+
* @returns {number} Jaccard similarity coefficient (0-1).
|
|
27
|
+
* @private
|
|
28
|
+
*/
|
|
29
|
+
function _jaccardSimilarity(setA, setB) {
|
|
30
|
+
if (setA.size === 0 && setB.size === 0) {
|
|
31
|
+
return 1.0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let intersection = 0;
|
|
35
|
+
for (const token of setA) {
|
|
36
|
+
if (setB.has(token)) {
|
|
37
|
+
intersection++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const union = setA.size + setB.size - intersection;
|
|
42
|
+
return union === 0 ? 0 : intersection / union;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Semantic chunking strategy using topic-boundary detection.
|
|
47
|
+
* Splits text into sentences, computes Jaccard token overlap between
|
|
48
|
+
* consecutive sentence pairs, and inserts chunk boundaries where
|
|
49
|
+
* similarity drops below the threshold. Oversized segments fall back
|
|
50
|
+
* to recursive splitting.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} text - The input text to chunk.
|
|
53
|
+
* @param {object} [options={}] - Chunking options.
|
|
54
|
+
* @param {number} [options.chunkSize=512] - Maximum chunk size in characters.
|
|
55
|
+
* @param {number} [options.similarityThreshold=0.3] - Minimum similarity to keep sentences in the same chunk.
|
|
56
|
+
* @param {number} [options.minChunkSize=50] - Minimum chunk size; smaller chunks are filtered out.
|
|
57
|
+
* @returns {string[]} Array of text chunks.
|
|
58
|
+
*/
|
|
59
|
+
function semanticChunk(text, options = {}) {
|
|
60
|
+
const {
|
|
61
|
+
chunkSize = 512,
|
|
62
|
+
similarityThreshold = 0.3,
|
|
63
|
+
minChunkSize = 50,
|
|
64
|
+
} = options;
|
|
65
|
+
|
|
66
|
+
if (!text || typeof text !== 'string') {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const sentences = text
|
|
71
|
+
.split(/(?<=[.!?])\s+/)
|
|
72
|
+
.filter((s) => s.trim().length > 0);
|
|
73
|
+
|
|
74
|
+
if (sentences.length === 0) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (sentences.length === 1) {
|
|
79
|
+
const trimmed = sentences[0].trim();
|
|
80
|
+
if (trimmed.length > chunkSize) {
|
|
81
|
+
return recursiveChunk(trimmed, { chunkSize, minChunkSize });
|
|
82
|
+
}
|
|
83
|
+
return trimmed.length >= minChunkSize ? [trimmed] : [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Compute token sets for each sentence
|
|
87
|
+
const tokenSets = sentences.map(_tokenize);
|
|
88
|
+
|
|
89
|
+
// Group sentences into segments based on similarity boundaries
|
|
90
|
+
const segments = [];
|
|
91
|
+
let currentSegment = [sentences[0]];
|
|
92
|
+
|
|
93
|
+
for (let i = 1; i < sentences.length; i++) {
|
|
94
|
+
const similarity = _jaccardSimilarity(tokenSets[i - 1], tokenSets[i]);
|
|
95
|
+
|
|
96
|
+
if (similarity < similarityThreshold) {
|
|
97
|
+
// Topic boundary detected; flush current segment
|
|
98
|
+
segments.push(currentSegment.join(' '));
|
|
99
|
+
currentSegment = [sentences[i]];
|
|
100
|
+
} else {
|
|
101
|
+
currentSegment.push(sentences[i]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Flush the last segment
|
|
106
|
+
if (currentSegment.length > 0) {
|
|
107
|
+
segments.push(currentSegment.join(' '));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Apply chunkSize limit: oversized segments fall back to recursive splitting
|
|
111
|
+
const chunks = [];
|
|
112
|
+
for (const segment of segments) {
|
|
113
|
+
const trimmed = segment.trim();
|
|
114
|
+
if (trimmed.length === 0) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (trimmed.length > chunkSize) {
|
|
118
|
+
const subChunks = recursiveChunk(trimmed, { chunkSize, minChunkSize });
|
|
119
|
+
chunks.push(...subChunks);
|
|
120
|
+
} else if (trimmed.length >= minChunkSize) {
|
|
121
|
+
chunks.push(trimmed);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return chunks;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { semanticChunk };
|