@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,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_CONFIG = { k: 60 };
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reciprocal Rank Fusion - merges multiple ranked result lists
|
|
7
|
+
* @param {Array<Array>} resultSets - Array of result arrays, each result has {id, score, ...}
|
|
8
|
+
* @param {object} options - {k: 60, weights: number[] (per result set)}
|
|
9
|
+
* @returns {Array} Fused, deduplicated, sorted results
|
|
10
|
+
*/
|
|
11
|
+
function reciprocalRankFusion(resultSets, options = {}) {
|
|
12
|
+
const { k = DEFAULT_CONFIG.k, weights } = options;
|
|
13
|
+
|
|
14
|
+
if (!Array.isArray(resultSets) || resultSets.length === 0) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @type {Map<string, {score: number, fields: object}>} */
|
|
19
|
+
const scoreMap = new Map();
|
|
20
|
+
|
|
21
|
+
for (let setIndex = 0; setIndex < resultSets.length; setIndex++) {
|
|
22
|
+
const results = resultSets[setIndex];
|
|
23
|
+
if (!Array.isArray(results)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const weight =
|
|
28
|
+
Array.isArray(weights) && weights[setIndex] != null
|
|
29
|
+
? weights[setIndex]
|
|
30
|
+
: 1.0;
|
|
31
|
+
|
|
32
|
+
for (let rank = 0; rank < results.length; rank++) {
|
|
33
|
+
const doc = results[rank];
|
|
34
|
+
if (!doc || doc.id == null) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const id = String(doc.id);
|
|
39
|
+
const rrfScore = weight / (k + rank);
|
|
40
|
+
|
|
41
|
+
if (scoreMap.has(id)) {
|
|
42
|
+
const entry = scoreMap.get(id);
|
|
43
|
+
entry.score += rrfScore;
|
|
44
|
+
} else {
|
|
45
|
+
// Copy all original fields except score
|
|
46
|
+
const { score: _origScore, ...rest } = doc;
|
|
47
|
+
scoreMap.set(id, { score: rrfScore, fields: rest });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const fused = [];
|
|
53
|
+
for (const [id, entry] of scoreMap) {
|
|
54
|
+
fused.push({ ...entry.fields, id, score: entry.score });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fused.sort((a, b) => b.score - a.score);
|
|
58
|
+
|
|
59
|
+
return fused;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { reciprocalRankFusion, DEFAULT_CONFIG };
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const crypto = require('crypto');
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
const { logger } = require('../utils/logger');
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Load JWT configuration from environment or defaults
|
|
@@ -59,7 +60,7 @@ function loadJWTConfig() {
|
|
|
59
60
|
// Generate a random secret for development/test/undefined environments
|
|
60
61
|
secret = crypto.randomBytes(64).toString('hex');
|
|
61
62
|
if (!isUndefined) {
|
|
62
|
-
|
|
63
|
+
logger.warn(
|
|
63
64
|
'[JWT_CONFIG] Warning: Using generated JWT_SECRET for development. ' +
|
|
64
65
|
'Set JWT_SECRET environment variable for consistent tokens.',
|
|
65
66
|
);
|
|
@@ -97,7 +98,7 @@ function loadJWTConfig() {
|
|
|
97
98
|
'JWT_PUBLIC_KEY_PATH environment variable is required for asymmetric algorithms in production',
|
|
98
99
|
);
|
|
99
100
|
} else {
|
|
100
|
-
|
|
101
|
+
logger.warn(
|
|
101
102
|
'[JWT_CONFIG] Warning: No JWT_PUBLIC_KEY_PATH set. Token verification will not be available.',
|
|
102
103
|
);
|
|
103
104
|
}
|
|
@@ -112,7 +113,7 @@ function loadJWTConfig() {
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
if (!privateKeyPath) {
|
|
115
|
-
|
|
116
|
+
logger.warn(
|
|
116
117
|
'[JWT_CONFIG] Warning: No JWT_PRIVATE_KEY_PATH set. Token signing will not be available.',
|
|
117
118
|
);
|
|
118
119
|
} else {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
const jwt = require('jsonwebtoken');
|
|
19
19
|
const crypto = require('crypto');
|
|
20
20
|
const { EventEmitter } = require('events');
|
|
21
|
+
const { logger } = require('../utils/logger');
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Secure JWT signing and validation algorithms
|
|
@@ -198,7 +199,7 @@ class JWTValidator extends EventEmitter {
|
|
|
198
199
|
throw new Error('publicKey is required for asymmetric algorithms');
|
|
199
200
|
}
|
|
200
201
|
if (!privateKey) {
|
|
201
|
-
|
|
202
|
+
logger.warn(
|
|
202
203
|
'[JWT_VALIDATOR] Warning: privateKey not provided. Token signing will not be available.',
|
|
203
204
|
);
|
|
204
205
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
const crypto = require('crypto');
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
+
const { logger } = require('../utils/logger');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Plugin signature verification with Ed25519
|
|
@@ -285,9 +286,9 @@ class PluginSignatureVerifier {
|
|
|
285
286
|
|
|
286
287
|
// In production, this should write to secure audit log
|
|
287
288
|
if (process.env.NODE_ENV === 'production') {
|
|
288
|
-
|
|
289
|
+
logger.warn('[AUDIT]', JSON.stringify(auditEntry));
|
|
289
290
|
} else {
|
|
290
|
-
|
|
291
|
+
logger.info('[AUDIT]', auditEntry);
|
|
291
292
|
}
|
|
292
293
|
}
|
|
293
294
|
|
|
@@ -301,7 +302,7 @@ class PluginSignatureVerifier {
|
|
|
301
302
|
this.auditLogger(result, manifest);
|
|
302
303
|
} catch (error) {
|
|
303
304
|
// Audit logging failure should not break verification
|
|
304
|
-
|
|
305
|
+
logger.error('Audit logging failed:', error.message);
|
|
305
306
|
}
|
|
306
307
|
}
|
|
307
308
|
|
|
@@ -409,6 +410,3 @@ module.exports = {
|
|
|
409
410
|
PluginSignatureVerifier,
|
|
410
411
|
createPluginVerifier,
|
|
411
412
|
};
|
|
412
|
-
|
|
413
|
-
// CommonJS/ESM interop
|
|
414
|
-
module.exports.default = module.exports;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Streaming Embeddings Generator
|
|
5
|
+
*
|
|
6
|
+
* Async generator that yields embeddings chunk by chunk with backpressure
|
|
7
|
+
* support. Processes documents in configurable batches while monitoring
|
|
8
|
+
* memory usage to prevent OOM conditions in large-scale pipelines.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Batch-based document embedding with configurable size
|
|
12
|
+
* - Memory-aware backpressure (pauses when heap exceeds threshold)
|
|
13
|
+
* - Per-batch error isolation (failures don't abort the stream)
|
|
14
|
+
* - Progress tracking via yielded metadata
|
|
15
|
+
* - Support for both `.embed()` and `.embedQuery()` embedder APIs
|
|
16
|
+
*
|
|
17
|
+
* @module streaming/embedding-stream
|
|
18
|
+
* @since 2.5.0
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default configuration for the embedding stream
|
|
23
|
+
* @type {Object}
|
|
24
|
+
*/
|
|
25
|
+
const DEFAULT_CONFIG = {
|
|
26
|
+
/** @type {number} Number of documents per embedding batch */
|
|
27
|
+
batchSize: 50,
|
|
28
|
+
/** @type {number} Maximum concurrent batch operations */
|
|
29
|
+
maxConcurrency: 3,
|
|
30
|
+
/** @type {number} Maximum heap memory budget in megabytes */
|
|
31
|
+
maxMemoryMB: 512,
|
|
32
|
+
/** @type {number} Fraction of maxMemoryMB that triggers backpressure (0-1) */
|
|
33
|
+
backpressureThreshold: 0.8,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Delay helper that resolves after the given milliseconds.
|
|
38
|
+
*
|
|
39
|
+
* @param {number} ms - Milliseconds to wait
|
|
40
|
+
* @returns {Promise<void>}
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
function delay(ms) {
|
|
44
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns current heap usage in megabytes.
|
|
49
|
+
*
|
|
50
|
+
* @returns {number} Heap used in MB
|
|
51
|
+
* @private
|
|
52
|
+
*/
|
|
53
|
+
function heapUsedMB() {
|
|
54
|
+
return process.memoryUsage().heapUsed / (1024 * 1024);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validates that the provided embedder exposes a supported embedding method.
|
|
59
|
+
*
|
|
60
|
+
* @param {Object} embedder - The embedder instance to validate
|
|
61
|
+
* @throws {TypeError} If the embedder lacks both `.embed()` and `.embedQuery()`
|
|
62
|
+
* @private
|
|
63
|
+
*/
|
|
64
|
+
function validateEmbedder(embedder) {
|
|
65
|
+
if (
|
|
66
|
+
!embedder ||
|
|
67
|
+
(typeof embedder.embed !== 'function' &&
|
|
68
|
+
typeof embedder.embedQuery !== 'function')
|
|
69
|
+
) {
|
|
70
|
+
throw new TypeError(
|
|
71
|
+
'Embedder must expose an .embed() or .embedQuery() method',
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Embeds a single document using the best available method on the embedder.
|
|
78
|
+
*
|
|
79
|
+
* Prefers `.embed()` (which typically accepts an array and returns an array of
|
|
80
|
+
* vectors) but falls back to `.embedQuery()` for single-document embedders.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} embedder - Embedder instance
|
|
83
|
+
* @param {string} content - Text content to embed
|
|
84
|
+
* @returns {Promise<number[]>} The embedding vector
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
async function embedOne(embedder, content) {
|
|
88
|
+
if (typeof embedder.embed === 'function') {
|
|
89
|
+
const result = await embedder.embed([content]);
|
|
90
|
+
return Array.isArray(result) ? result[0] : result;
|
|
91
|
+
}
|
|
92
|
+
return embedder.embedQuery(content);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Async generator that streams embedding results for a collection of documents.
|
|
97
|
+
*
|
|
98
|
+
* Documents are processed in batches of `batchSize`. Before each batch the
|
|
99
|
+
* generator checks heap memory; when usage exceeds
|
|
100
|
+
* `backpressureThreshold * maxMemoryMB` it pauses briefly and retries,
|
|
101
|
+
* giving the GC time to reclaim memory.
|
|
102
|
+
*
|
|
103
|
+
* Each yielded value is an object containing the document id, the computed
|
|
104
|
+
* vector, the document index, and a progress fraction (0-1).
|
|
105
|
+
*
|
|
106
|
+
* Errors within a batch are caught and yielded as objects with an `error`
|
|
107
|
+
* property so that consumers can decide how to handle partial failures
|
|
108
|
+
* without losing the rest of the stream.
|
|
109
|
+
*
|
|
110
|
+
* @param {Array<{id: string, content: string}>} documents - Documents to embed
|
|
111
|
+
* @param {Object} embedder - Embedder with `.embed()` or `.embedQuery()`
|
|
112
|
+
* @param {Object} [options] - Override any key from {@link DEFAULT_CONFIG}
|
|
113
|
+
* @yields {{ id: string, vector: number[], index: number, progress: number }
|
|
114
|
+
* | { id: string, error: Error, index: number, progress: number }}
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* const docs = [{ id: '1', content: 'hello' }, { id: '2', content: 'world' }];
|
|
118
|
+
* for await (const emb of createEmbeddingStream(docs, myEmbedder)) {
|
|
119
|
+
* console.log(emb.id, emb.vector.length);
|
|
120
|
+
* }
|
|
121
|
+
*/
|
|
122
|
+
async function* createEmbeddingStream(documents, embedder, options = {}) {
|
|
123
|
+
// --- Validate inputs ---
|
|
124
|
+
if (!Array.isArray(documents)) {
|
|
125
|
+
throw new TypeError('documents must be an array');
|
|
126
|
+
}
|
|
127
|
+
validateEmbedder(embedder);
|
|
128
|
+
|
|
129
|
+
const config = { ...DEFAULT_CONFIG, ...options };
|
|
130
|
+
const total = documents.length;
|
|
131
|
+
|
|
132
|
+
if (total === 0) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const memoryLimit = config.backpressureThreshold * config.maxMemoryMB;
|
|
137
|
+
|
|
138
|
+
// --- Process batches ---
|
|
139
|
+
for (let batchStart = 0; batchStart < total; batchStart += config.batchSize) {
|
|
140
|
+
const batchEnd = Math.min(batchStart + config.batchSize, total);
|
|
141
|
+
const batch = documents.slice(batchStart, batchEnd);
|
|
142
|
+
|
|
143
|
+
// --- Backpressure: wait while memory is above threshold ---
|
|
144
|
+
let backpressureApplied = false;
|
|
145
|
+
while (heapUsedMB() > memoryLimit) {
|
|
146
|
+
backpressureApplied = true;
|
|
147
|
+
await delay(100);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- Embed the batch (isolate errors per batch) ---
|
|
151
|
+
try {
|
|
152
|
+
// Attempt bulk embed if the embedder supports arrays natively
|
|
153
|
+
if (typeof embedder.embed === 'function') {
|
|
154
|
+
const contents = batch.map((doc) => doc.content || '');
|
|
155
|
+
const vectors = await embedder.embed(contents);
|
|
156
|
+
const vectorArray = Array.isArray(vectors) ? vectors : [vectors];
|
|
157
|
+
|
|
158
|
+
for (let j = 0; j < batch.length; j++) {
|
|
159
|
+
const globalIndex = batchStart + j;
|
|
160
|
+
yield {
|
|
161
|
+
id: batch[j].id,
|
|
162
|
+
vector: vectorArray[j] || [],
|
|
163
|
+
index: globalIndex,
|
|
164
|
+
progress: (globalIndex + 1) / total,
|
|
165
|
+
...(backpressureApplied ? { backpressure: true } : {}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
// Fall back to embedQuery one at a time
|
|
170
|
+
for (let j = 0; j < batch.length; j++) {
|
|
171
|
+
const globalIndex = batchStart + j;
|
|
172
|
+
const vector = await embedder.embedQuery(batch[j].content || '');
|
|
173
|
+
yield {
|
|
174
|
+
id: batch[j].id,
|
|
175
|
+
vector,
|
|
176
|
+
index: globalIndex,
|
|
177
|
+
progress: (globalIndex + 1) / total,
|
|
178
|
+
...(backpressureApplied ? { backpressure: true } : {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
// Yield error entries for each document in the failed batch
|
|
184
|
+
for (let j = 0; j < batch.length; j++) {
|
|
185
|
+
const globalIndex = batchStart + j;
|
|
186
|
+
yield {
|
|
187
|
+
id: batch[j].id,
|
|
188
|
+
error: err,
|
|
189
|
+
index: globalIndex,
|
|
190
|
+
progress: (globalIndex + 1) / total,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { createEmbeddingStream, DEFAULT_CONFIG };
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SSE (Server-Sent Events) Adapter
|
|
5
|
+
*
|
|
6
|
+
* Converts AsyncGenerator tokens to Server-Sent Events format
|
|
7
|
+
* for real-time streaming of RAG pipeline responses over HTTP.
|
|
8
|
+
*
|
|
9
|
+
* @module streaming/sse-adapter
|
|
10
|
+
* @since 2.4.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { EventEmitter } = require('events');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default configuration for SSE streaming
|
|
17
|
+
* @type {Object}
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_CONFIG = {
|
|
20
|
+
/** @type {string} SSE event name for token events */
|
|
21
|
+
eventName: 'token',
|
|
22
|
+
/** @type {string} Sentinel value indicating stream completion */
|
|
23
|
+
doneEvent: '[DONE]',
|
|
24
|
+
/** @type {number} Interval in ms between heartbeat comments */
|
|
25
|
+
heartbeatIntervalMs: 15000,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Adapts an AsyncGenerator to the Server-Sent Events protocol.
|
|
30
|
+
*
|
|
31
|
+
* Writes well-formed SSE frames to an HTTP response, manages heartbeat
|
|
32
|
+
* keep-alive comments, and handles client disconnects gracefully.
|
|
33
|
+
*
|
|
34
|
+
* @extends EventEmitter
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* const adapter = new SSEAdapter({ heartbeatIntervalMs: 10000 });
|
|
38
|
+
* adapter.on('token', (t) => console.log('sent', t));
|
|
39
|
+
* await adapter.stream(asyncTokenGenerator, res);
|
|
40
|
+
*/
|
|
41
|
+
class SSEAdapter extends EventEmitter {
|
|
42
|
+
/**
|
|
43
|
+
* @param {Object} [options] - Configuration overrides
|
|
44
|
+
* @param {string} [options.eventName='token'] - SSE event name for token frames
|
|
45
|
+
* @param {string} [options.doneEvent='[DONE]'] - Sentinel value for the final data frame
|
|
46
|
+
* @param {number} [options.heartbeatIntervalMs=15000] - Heartbeat interval in milliseconds
|
|
47
|
+
*/
|
|
48
|
+
constructor(options = {}) {
|
|
49
|
+
super();
|
|
50
|
+
this.config = { ...DEFAULT_CONFIG, ...options };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Stream tokens from an AsyncGenerator to an HTTP response as SSE.
|
|
55
|
+
*
|
|
56
|
+
* Sets the required SSE headers, starts a heartbeat timer, and iterates
|
|
57
|
+
* over the generator. Each yielded value should be an object with at least
|
|
58
|
+
* a `token` property and an optional `done` boolean.
|
|
59
|
+
*
|
|
60
|
+
* @param {AsyncGenerator<{token: string, done?: boolean}>} asyncGenerator - Token source
|
|
61
|
+
* @param {import('http').ServerResponse} httpResponse - Writable HTTP response
|
|
62
|
+
* @returns {Promise<void>} Resolves when the stream ends or the client disconnects
|
|
63
|
+
*/
|
|
64
|
+
async stream(asyncGenerator, httpResponse) {
|
|
65
|
+
let heartbeatTimer = null;
|
|
66
|
+
let clientDisconnected = false;
|
|
67
|
+
|
|
68
|
+
// --- helpers --------------------------------------------------------
|
|
69
|
+
const cleanup = () => {
|
|
70
|
+
if (heartbeatTimer) {
|
|
71
|
+
clearInterval(heartbeatTimer);
|
|
72
|
+
heartbeatTimer = null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// --- 1. SSE headers -------------------------------------------------
|
|
77
|
+
httpResponse.writeHead(200, {
|
|
78
|
+
'Content-Type': 'text/event-stream',
|
|
79
|
+
'Cache-Control': 'no-cache',
|
|
80
|
+
Connection: 'keep-alive',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// --- 2. Heartbeat keep-alive ----------------------------------------
|
|
84
|
+
heartbeatTimer = setInterval(() => {
|
|
85
|
+
if (!clientDisconnected) {
|
|
86
|
+
httpResponse.write(': heartbeat\n\n');
|
|
87
|
+
}
|
|
88
|
+
}, this.config.heartbeatIntervalMs);
|
|
89
|
+
|
|
90
|
+
// --- 3. Client disconnect handler -----------------------------------
|
|
91
|
+
const onClose = () => {
|
|
92
|
+
clientDisconnected = true;
|
|
93
|
+
cleanup();
|
|
94
|
+
this.emit('disconnect');
|
|
95
|
+
};
|
|
96
|
+
httpResponse.on('close', onClose);
|
|
97
|
+
|
|
98
|
+
// --- 4. Iterate over the generator ----------------------------------
|
|
99
|
+
try {
|
|
100
|
+
this.emit('started');
|
|
101
|
+
|
|
102
|
+
for await (const value of asyncGenerator) {
|
|
103
|
+
if (clientDisconnected) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (value && value.done) {
|
|
108
|
+
// Final frame
|
|
109
|
+
httpResponse.write(`data: ${this.config.doneEvent}\n\n`);
|
|
110
|
+
this.emit('done');
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const token = value && value.token !== undefined ? value.token : value;
|
|
115
|
+
const frame = this.formatEvent(this.config.eventName, { token });
|
|
116
|
+
httpResponse.write(frame);
|
|
117
|
+
this.emit('token', token);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// If the generator is exhausted without an explicit done flag, send DONE
|
|
121
|
+
if (!clientDisconnected && !httpResponse.writableEnded) {
|
|
122
|
+
httpResponse.write(`data: ${this.config.doneEvent}\n\n`);
|
|
123
|
+
this.emit('done');
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
this.emit('error', err);
|
|
127
|
+
|
|
128
|
+
if (!clientDisconnected && !httpResponse.writableEnded) {
|
|
129
|
+
const errorFrame = this.formatEvent('error', {
|
|
130
|
+
message: err.message || 'Internal streaming error',
|
|
131
|
+
});
|
|
132
|
+
httpResponse.write(errorFrame);
|
|
133
|
+
}
|
|
134
|
+
} finally {
|
|
135
|
+
cleanup();
|
|
136
|
+
httpResponse.removeListener('close', onClose);
|
|
137
|
+
|
|
138
|
+
if (!httpResponse.writableEnded) {
|
|
139
|
+
httpResponse.end();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Format a single SSE event string.
|
|
146
|
+
*
|
|
147
|
+
* Multi-line data is split so each line is prefixed with `data: ` as
|
|
148
|
+
* required by the SSE specification.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} eventName - The SSE event type
|
|
151
|
+
* @param {*} data - Payload (will be JSON-stringified)
|
|
152
|
+
* @returns {string} Fully formatted SSE frame including trailing blank line
|
|
153
|
+
*/
|
|
154
|
+
formatEvent(eventName, data) {
|
|
155
|
+
const json = JSON.stringify(data);
|
|
156
|
+
const lines = json.split('\n');
|
|
157
|
+
|
|
158
|
+
let frame = `event: ${eventName}\n`;
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
frame += `data: ${line}\n`;
|
|
161
|
+
}
|
|
162
|
+
frame += '\n';
|
|
163
|
+
|
|
164
|
+
return frame;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = { SSEAdapter, DEFAULT_CONFIG };
|