@devilsdev/rag-pipeline-utils 2.3.1 → 2.4.1
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 +94 -480
- 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 +37 -11
- 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,210 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stream Router
|
|
5
|
+
*
|
|
6
|
+
* Wraps a RAG pipeline and routes streaming output to the appropriate
|
|
7
|
+
* transport adapter (SSE or WebSocket) based on the incoming request.
|
|
8
|
+
*
|
|
9
|
+
* @module streaming/stream-router
|
|
10
|
+
* @since 2.4.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { EventEmitter } = require('events');
|
|
14
|
+
const { SSEAdapter } = require('./sse-adapter');
|
|
15
|
+
const { WebSocketAdapter } = require('./websocket-adapter');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Default configuration for the stream router
|
|
19
|
+
* @type {Object}
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_CONFIG = {
|
|
22
|
+
/** @type {string} Transport to use when auto-detection is inconclusive */
|
|
23
|
+
defaultTransport: 'sse',
|
|
24
|
+
/** @type {boolean} Whether to set CORS headers on responses */
|
|
25
|
+
enableCORS: true,
|
|
26
|
+
/** @type {string} Value for the Access-Control-Allow-Origin header */
|
|
27
|
+
corsOrigin: '*',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Routes pipeline streaming output to SSE or WebSocket transports.
|
|
32
|
+
*
|
|
33
|
+
* Provides both an Express/Node-HTTP-compatible request handler via
|
|
34
|
+
* {@link StreamRouter#createHandler} and lower-level convenience methods
|
|
35
|
+
* ({@link StreamRouter#streamSSE}, {@link StreamRouter#streamWS}) for
|
|
36
|
+
* manual integration.
|
|
37
|
+
*
|
|
38
|
+
* @extends EventEmitter
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* const router = new StreamRouter({ enableCORS: true });
|
|
42
|
+
* const handler = router.createHandler(pipeline);
|
|
43
|
+
* app.post('/api/query', handler);
|
|
44
|
+
*/
|
|
45
|
+
class StreamRouter extends EventEmitter {
|
|
46
|
+
/**
|
|
47
|
+
* @param {Object} [options] - Configuration overrides
|
|
48
|
+
* @param {string} [options.defaultTransport='sse'] - Fallback transport type
|
|
49
|
+
* @param {boolean} [options.enableCORS=true] - Enable CORS headers
|
|
50
|
+
* @param {string} [options.corsOrigin='*'] - CORS origin value
|
|
51
|
+
* @param {Object} [options.sseOptions] - Options forwarded to SSEAdapter
|
|
52
|
+
* @param {Object} [options.wsOptions] - Options forwarded to WebSocketAdapter
|
|
53
|
+
*/
|
|
54
|
+
constructor(options = {}) {
|
|
55
|
+
super();
|
|
56
|
+
this.config = { ...DEFAULT_CONFIG, ...options };
|
|
57
|
+
this.sseAdapter = new SSEAdapter(options.sseOptions);
|
|
58
|
+
this.wsAdapter = new WebSocketAdapter(options.wsOptions);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create an HTTP request handler that runs the pipeline in streaming mode
|
|
63
|
+
* and routes output to the appropriate transport.
|
|
64
|
+
*
|
|
65
|
+
* The returned function is compatible with Node's `http.createServer`,
|
|
66
|
+
* Express, and similar frameworks.
|
|
67
|
+
*
|
|
68
|
+
* Transport detection:
|
|
69
|
+
* - `Accept: text/event-stream` header selects SSE
|
|
70
|
+
* - `Upgrade: websocket` header selects WebSocket (note: actual upgrade
|
|
71
|
+
* negotiation is typically handled at a higher level)
|
|
72
|
+
* - Otherwise falls back to {@link DEFAULT_CONFIG.defaultTransport}
|
|
73
|
+
*
|
|
74
|
+
* @param {Object} pipeline - A pipeline object with a `.run()` method
|
|
75
|
+
* @returns {Function} Async request handler `(req, res) => Promise<void>`
|
|
76
|
+
*/
|
|
77
|
+
createHandler(pipeline) {
|
|
78
|
+
/**
|
|
79
|
+
* @param {import('http').IncomingMessage} req
|
|
80
|
+
* @param {import('http').ServerResponse} res
|
|
81
|
+
*/
|
|
82
|
+
const handler = async (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
// --- 1. Parse query -----------------------------------------------
|
|
85
|
+
const query =
|
|
86
|
+
(req.body && req.body.query) || (req.query && req.query.q) || '';
|
|
87
|
+
|
|
88
|
+
if (!query) {
|
|
89
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
90
|
+
res.end(JSON.stringify({ error: 'Missing query parameter' }));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- 2. CORS headers ----------------------------------------------
|
|
95
|
+
if (this.config.enableCORS) {
|
|
96
|
+
res.setHeader('Access-Control-Allow-Origin', this.config.corsOrigin);
|
|
97
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
98
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Handle CORS preflight
|
|
102
|
+
if (req.method === 'OPTIONS') {
|
|
103
|
+
res.writeHead(204);
|
|
104
|
+
res.end();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- 3. Detect transport ------------------------------------------
|
|
109
|
+
const accept = (req.headers && req.headers.accept) || '';
|
|
110
|
+
const upgrade = (req.headers && req.headers.upgrade) || '';
|
|
111
|
+
|
|
112
|
+
let transport = this.config.defaultTransport;
|
|
113
|
+
if (accept.includes('text/event-stream')) {
|
|
114
|
+
transport = 'sse';
|
|
115
|
+
} else if (upgrade.toLowerCase() === 'websocket') {
|
|
116
|
+
transport = 'ws';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --- 4. Route to adapter ------------------------------------------
|
|
120
|
+
if (transport === 'sse') {
|
|
121
|
+
await this.streamSSE(pipeline, query, res);
|
|
122
|
+
} else if (transport === 'ws') {
|
|
123
|
+
// WebSocket upgrade is normally handled at the server level;
|
|
124
|
+
// send a hint response when received through a regular handler.
|
|
125
|
+
res.writeHead(426, { 'Content-Type': 'application/json' });
|
|
126
|
+
res.end(
|
|
127
|
+
JSON.stringify({
|
|
128
|
+
error: 'WebSocket upgrade required',
|
|
129
|
+
hint: 'Use the streamWS() method with an upgraded WebSocket connection',
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
await this.streamSSE(pipeline, query, res);
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
this.emit('error', err);
|
|
137
|
+
|
|
138
|
+
if (!res.headersSent) {
|
|
139
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
140
|
+
}
|
|
141
|
+
if (!res.writableEnded) {
|
|
142
|
+
res.end(
|
|
143
|
+
JSON.stringify({ error: err.message || 'Internal server error' }),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return handler;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Run the pipeline in streaming mode and pipe tokens to an HTTP response
|
|
154
|
+
* via Server-Sent Events.
|
|
155
|
+
*
|
|
156
|
+
* If the pipeline returns a non-iterable result, it is sent as a single
|
|
157
|
+
* SSE data frame followed by the done sentinel.
|
|
158
|
+
*
|
|
159
|
+
* @param {Object} pipeline - Pipeline with a `.run()` method
|
|
160
|
+
* @param {string} query - User query string
|
|
161
|
+
* @param {import('http').ServerResponse} httpResponse - Writable HTTP response
|
|
162
|
+
* @param {Object} [options={}] - Additional pipeline run options
|
|
163
|
+
* @returns {Promise<void>}
|
|
164
|
+
*/
|
|
165
|
+
async streamSSE(pipeline, query, httpResponse, options = {}) {
|
|
166
|
+
const result = await pipeline.run({
|
|
167
|
+
query,
|
|
168
|
+
options: { ...options, stream: true },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (result && Symbol.asyncIterator in Object(result)) {
|
|
172
|
+
await this.sseAdapter.stream(result, httpResponse);
|
|
173
|
+
} else {
|
|
174
|
+
// Non-streaming result: send as a single SSE frame then DONE
|
|
175
|
+
httpResponse.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
|
176
|
+
httpResponse.write(`data: ${JSON.stringify(result)}\n\n`);
|
|
177
|
+
httpResponse.write(`data: [DONE]\n\n`);
|
|
178
|
+
httpResponse.end();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Run the pipeline in streaming mode and pipe tokens over a WebSocket
|
|
184
|
+
* connection.
|
|
185
|
+
*
|
|
186
|
+
* If the pipeline returns a non-iterable result, it is sent as a single
|
|
187
|
+
* `result` message followed by a `done` message.
|
|
188
|
+
*
|
|
189
|
+
* @param {Object} pipeline - Pipeline with a `.run()` method
|
|
190
|
+
* @param {string} query - User query string
|
|
191
|
+
* @param {Object} wsConnection - WebSocket-like connection with `.send()`
|
|
192
|
+
* @param {Object} [options={}] - Additional pipeline run options
|
|
193
|
+
* @returns {Promise<void>}
|
|
194
|
+
*/
|
|
195
|
+
async streamWS(pipeline, query, wsConnection, options = {}) {
|
|
196
|
+
const result = await pipeline.run({
|
|
197
|
+
query,
|
|
198
|
+
options: { ...options, stream: true },
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (result && Symbol.asyncIterator in Object(result)) {
|
|
202
|
+
await this.wsAdapter.stream(result, wsConnection);
|
|
203
|
+
} else {
|
|
204
|
+
wsConnection.send(JSON.stringify({ type: 'result', data: result }));
|
|
205
|
+
wsConnection.send(JSON.stringify({ type: 'done' }));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = { StreamRouter, DEFAULT_CONFIG };
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Streaming Embedder
|
|
5
|
+
*
|
|
6
|
+
* Class wrapper that provides an EventEmitter interface around the
|
|
7
|
+
* {@link module:streaming/embedding-stream createEmbeddingStream} async
|
|
8
|
+
* generator. Emits lifecycle events (`start`, `embedding`, `batch`,
|
|
9
|
+
* `backpressure`, `done`, `error`) so that callers can monitor progress
|
|
10
|
+
* without manually iterating the generator.
|
|
11
|
+
*
|
|
12
|
+
* Also exposes a convenience `embedAll()` method that collects every
|
|
13
|
+
* embedding into an array, and tracks cumulative statistics across calls.
|
|
14
|
+
*
|
|
15
|
+
* @module streaming/streaming-embedder
|
|
16
|
+
* @since 2.5.0
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { EventEmitter } = require('events');
|
|
20
|
+
const { createEmbeddingStream } = require('./embedding-stream');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Default configuration for the StreamingEmbedder
|
|
24
|
+
* @type {Object}
|
|
25
|
+
*/
|
|
26
|
+
const DEFAULT_CONFIG = {
|
|
27
|
+
/** @type {number} Number of documents per embedding batch */
|
|
28
|
+
batchSize: 50,
|
|
29
|
+
/** @type {number} Maximum concurrent batch operations */
|
|
30
|
+
maxConcurrency: 3,
|
|
31
|
+
/** @type {number} Maximum heap memory budget in megabytes */
|
|
32
|
+
maxMemoryMB: 512,
|
|
33
|
+
/** @type {number} Fraction of maxMemoryMB that triggers backpressure (0-1) */
|
|
34
|
+
backpressureThreshold: 0.8,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* EventEmitter-based wrapper around {@link createEmbeddingStream}.
|
|
39
|
+
*
|
|
40
|
+
* @extends EventEmitter
|
|
41
|
+
*
|
|
42
|
+
* @fires StreamingEmbedder#start
|
|
43
|
+
* @fires StreamingEmbedder#embedding
|
|
44
|
+
* @fires StreamingEmbedder#batch
|
|
45
|
+
* @fires StreamingEmbedder#backpressure
|
|
46
|
+
* @fires StreamingEmbedder#done
|
|
47
|
+
* @fires StreamingEmbedder#error
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* const se = new StreamingEmbedder(myEmbedder, { batchSize: 25 });
|
|
51
|
+
* se.on('embedding', (e) => console.log(e.id, e.progress));
|
|
52
|
+
* se.on('done', (stats) => console.log('finished', stats));
|
|
53
|
+
* const all = await se.embedAll(documents);
|
|
54
|
+
*/
|
|
55
|
+
class StreamingEmbedder extends EventEmitter {
|
|
56
|
+
/**
|
|
57
|
+
* @param {Object} embedder - Embedder instance with `.embed()` or `.embedQuery()`
|
|
58
|
+
* @param {Object} [options] - Override any key from {@link DEFAULT_CONFIG}
|
|
59
|
+
*/
|
|
60
|
+
constructor(embedder, options = {}) {
|
|
61
|
+
super();
|
|
62
|
+
|
|
63
|
+
if (
|
|
64
|
+
!embedder ||
|
|
65
|
+
(typeof embedder.embed !== 'function' &&
|
|
66
|
+
typeof embedder.embedQuery !== 'function')
|
|
67
|
+
) {
|
|
68
|
+
throw new TypeError(
|
|
69
|
+
'Embedder must expose an .embed() or .embedQuery() method',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @type {Object} */
|
|
74
|
+
this.embedder = embedder;
|
|
75
|
+
|
|
76
|
+
/** @type {Object} */
|
|
77
|
+
this.config = { ...DEFAULT_CONFIG, ...options };
|
|
78
|
+
|
|
79
|
+
/** @type {{ processed: number, failed: number, totalTime: number }} */
|
|
80
|
+
this.stats = { processed: 0, failed: 0, totalTime: 0 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Async generator that yields embeddings while emitting lifecycle events.
|
|
85
|
+
*
|
|
86
|
+
* @param {Array<{id: string, content: string}>} documents - Documents to embed
|
|
87
|
+
* @yields {{ id: string, vector: number[], index: number, progress: number }}
|
|
88
|
+
*
|
|
89
|
+
* @fires StreamingEmbedder#start
|
|
90
|
+
* @fires StreamingEmbedder#embedding
|
|
91
|
+
* @fires StreamingEmbedder#batch
|
|
92
|
+
* @fires StreamingEmbedder#backpressure
|
|
93
|
+
* @fires StreamingEmbedder#done
|
|
94
|
+
* @fires StreamingEmbedder#error
|
|
95
|
+
*/
|
|
96
|
+
async *embedStream(documents) {
|
|
97
|
+
if (!Array.isArray(documents)) {
|
|
98
|
+
throw new TypeError('documents must be an array');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const startTime = Date.now();
|
|
102
|
+
const documentCount = documents.length;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @event StreamingEmbedder#start
|
|
106
|
+
* @type {{ documentCount: number }}
|
|
107
|
+
*/
|
|
108
|
+
this.emit('start', { documentCount });
|
|
109
|
+
|
|
110
|
+
let batchCount = 0;
|
|
111
|
+
let itemsInCurrentBatch = 0;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const stream = createEmbeddingStream(
|
|
115
|
+
documents,
|
|
116
|
+
this.embedder,
|
|
117
|
+
this.config,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
for await (const embedding of stream) {
|
|
121
|
+
// Track backpressure events
|
|
122
|
+
if (embedding.backpressure) {
|
|
123
|
+
/**
|
|
124
|
+
* @event StreamingEmbedder#backpressure
|
|
125
|
+
* @type {{ index: number, progress: number }}
|
|
126
|
+
*/
|
|
127
|
+
this.emit('backpressure', {
|
|
128
|
+
index: embedding.index,
|
|
129
|
+
progress: embedding.progress,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Track errors
|
|
134
|
+
if (embedding.error) {
|
|
135
|
+
this.stats.failed += 1;
|
|
136
|
+
/**
|
|
137
|
+
* @event StreamingEmbedder#error
|
|
138
|
+
* @type {{ id: string, error: Error, index: number }}
|
|
139
|
+
*/
|
|
140
|
+
this.emit('error', {
|
|
141
|
+
id: embedding.id,
|
|
142
|
+
error: embedding.error,
|
|
143
|
+
index: embedding.index,
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
this.stats.processed += 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @event StreamingEmbedder#embedding
|
|
151
|
+
* @type {{ id: string, vector?: number[], index: number, progress: number }}
|
|
152
|
+
*/
|
|
153
|
+
this.emit('embedding', embedding);
|
|
154
|
+
|
|
155
|
+
// Emit batch event every batchSize items
|
|
156
|
+
itemsInCurrentBatch += 1;
|
|
157
|
+
if (itemsInCurrentBatch >= this.config.batchSize) {
|
|
158
|
+
batchCount += 1;
|
|
159
|
+
/**
|
|
160
|
+
* @event StreamingEmbedder#batch
|
|
161
|
+
* @type {{ batchNumber: number, processedSoFar: number, progress: number }}
|
|
162
|
+
*/
|
|
163
|
+
this.emit('batch', {
|
|
164
|
+
batchNumber: batchCount,
|
|
165
|
+
processedSoFar: embedding.index + 1,
|
|
166
|
+
progress: embedding.progress,
|
|
167
|
+
});
|
|
168
|
+
itemsInCurrentBatch = 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
yield embedding;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Emit a final batch event for any remaining items
|
|
175
|
+
if (itemsInCurrentBatch > 0) {
|
|
176
|
+
batchCount += 1;
|
|
177
|
+
this.emit('batch', {
|
|
178
|
+
batchNumber: batchCount,
|
|
179
|
+
processedSoFar: documentCount,
|
|
180
|
+
progress: 1,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
} finally {
|
|
184
|
+
const elapsed = Date.now() - startTime;
|
|
185
|
+
this.stats.totalTime += elapsed;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @event StreamingEmbedder#done
|
|
189
|
+
* @type {{ processed: number, failed: number, totalTime: number, documentCount: number }}
|
|
190
|
+
*/
|
|
191
|
+
this.emit('done', {
|
|
192
|
+
processed: this.stats.processed,
|
|
193
|
+
failed: this.stats.failed,
|
|
194
|
+
totalTime: this.stats.totalTime,
|
|
195
|
+
documentCount,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Convenience method that collects all embeddings into an array.
|
|
202
|
+
*
|
|
203
|
+
* @param {Array<{id: string, content: string}>} documents - Documents to embed
|
|
204
|
+
* @returns {Promise<Array<{ id: string, vector: number[], index: number, progress: number }>>}
|
|
205
|
+
*/
|
|
206
|
+
async embedAll(documents) {
|
|
207
|
+
const results = [];
|
|
208
|
+
for await (const embedding of this.embedStream(documents)) {
|
|
209
|
+
results.push(embedding);
|
|
210
|
+
}
|
|
211
|
+
return results;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Returns a snapshot of the cumulative statistics.
|
|
216
|
+
*
|
|
217
|
+
* @returns {{ processed: number, failed: number, totalTime: number }}
|
|
218
|
+
*/
|
|
219
|
+
getStats() {
|
|
220
|
+
return { ...this.stats };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Resets the cumulative statistics to zero.
|
|
225
|
+
*/
|
|
226
|
+
resetStats() {
|
|
227
|
+
this.stats = { processed: 0, failed: 0, totalTime: 0 };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = { StreamingEmbedder, DEFAULT_CONFIG };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WebSocket Adapter
|
|
5
|
+
*
|
|
6
|
+
* Sends AsyncGenerator tokens over a WebSocket connection
|
|
7
|
+
* for real-time bidirectional streaming of RAG pipeline responses.
|
|
8
|
+
*
|
|
9
|
+
* @module streaming/websocket-adapter
|
|
10
|
+
* @since 2.4.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { EventEmitter } = require('events');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default configuration for WebSocket streaming
|
|
17
|
+
* @type {Object}
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_CONFIG = {
|
|
20
|
+
/** @type {string} Message type identifier for token payloads */
|
|
21
|
+
messageType: 'token',
|
|
22
|
+
/** @type {string} Message type identifier for stream completion */
|
|
23
|
+
doneType: 'done',
|
|
24
|
+
/** @type {string} Message type identifier for error payloads */
|
|
25
|
+
errorType: 'error',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* WebSocket ready-state constants (mirrors the W3C WebSocket API).
|
|
30
|
+
* @enum {number}
|
|
31
|
+
* @private
|
|
32
|
+
*/
|
|
33
|
+
const WS_READY_STATE = {
|
|
34
|
+
OPEN: 1,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Adapts an AsyncGenerator to a WebSocket connection.
|
|
39
|
+
*
|
|
40
|
+
* Sends JSON-framed messages for each token, a completion message when
|
|
41
|
+
* the generator is exhausted, and an error message on failure. Handles
|
|
42
|
+
* premature connection closure gracefully.
|
|
43
|
+
*
|
|
44
|
+
* @extends EventEmitter
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* const adapter = new WebSocketAdapter();
|
|
48
|
+
* adapter.on('done', () => console.log('stream complete'));
|
|
49
|
+
* await adapter.stream(asyncTokenGenerator, ws);
|
|
50
|
+
*/
|
|
51
|
+
class WebSocketAdapter extends EventEmitter {
|
|
52
|
+
/**
|
|
53
|
+
* @param {Object} [options] - Configuration overrides
|
|
54
|
+
* @param {string} [options.messageType='token'] - Type field for token messages
|
|
55
|
+
* @param {string} [options.doneType='done'] - Type field for completion message
|
|
56
|
+
* @param {string} [options.errorType='error'] - Type field for error messages
|
|
57
|
+
*/
|
|
58
|
+
constructor(options = {}) {
|
|
59
|
+
super();
|
|
60
|
+
this.config = { ...DEFAULT_CONFIG, ...options };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Stream tokens from an AsyncGenerator over a WebSocket connection.
|
|
65
|
+
*
|
|
66
|
+
* Each yielded value should be an object with at least a `token` property
|
|
67
|
+
* and an optional `done` boolean. Messages are sent as JSON strings via
|
|
68
|
+
* `wsConnection.send()`.
|
|
69
|
+
*
|
|
70
|
+
* @param {AsyncGenerator<{token: string, done?: boolean}>} asyncGenerator - Token source
|
|
71
|
+
* @param {Object} wsConnection - WebSocket-like object with a `send(data)` method
|
|
72
|
+
* @param {Function} wsConnection.send - Sends a string message over the socket
|
|
73
|
+
* @param {number} [wsConnection.readyState] - Optional W3C readyState property
|
|
74
|
+
* @returns {Promise<void>} Resolves when the stream ends or the connection closes
|
|
75
|
+
* @throws {TypeError} If wsConnection lacks a send method
|
|
76
|
+
*/
|
|
77
|
+
async stream(asyncGenerator, wsConnection) {
|
|
78
|
+
// --- 1. Validate connection -----------------------------------------
|
|
79
|
+
if (!wsConnection || typeof wsConnection.send !== 'function') {
|
|
80
|
+
throw new TypeError('wsConnection must have a send() method');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let connectionClosed = false;
|
|
84
|
+
|
|
85
|
+
// --- 2. Connection lifecycle handlers --------------------------------
|
|
86
|
+
const onClose = () => {
|
|
87
|
+
connectionClosed = true;
|
|
88
|
+
};
|
|
89
|
+
const onError = (err) => {
|
|
90
|
+
connectionClosed = true;
|
|
91
|
+
this.emit('error', err);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (typeof wsConnection.on === 'function') {
|
|
95
|
+
wsConnection.on('close', onClose);
|
|
96
|
+
wsConnection.on('error', onError);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- 3. Iterate and send --------------------------------------------
|
|
100
|
+
try {
|
|
101
|
+
this.emit('started');
|
|
102
|
+
|
|
103
|
+
for await (const value of asyncGenerator) {
|
|
104
|
+
if (connectionClosed || !this._isOpen(wsConnection)) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (value && value.done) {
|
|
109
|
+
this._send(wsConnection, { type: this.config.doneType });
|
|
110
|
+
this.emit('done');
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const token = value && value.token !== undefined ? value.token : value;
|
|
115
|
+
this._send(wsConnection, {
|
|
116
|
+
type: this.config.messageType,
|
|
117
|
+
data: { token },
|
|
118
|
+
});
|
|
119
|
+
this.emit('token', token);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Generator exhausted without explicit done flag
|
|
123
|
+
if (!connectionClosed && this._isOpen(wsConnection)) {
|
|
124
|
+
this._send(wsConnection, { type: this.config.doneType });
|
|
125
|
+
this.emit('done');
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
this.emit('error', err);
|
|
129
|
+
|
|
130
|
+
if (!connectionClosed && this._isOpen(wsConnection)) {
|
|
131
|
+
this._send(wsConnection, {
|
|
132
|
+
type: this.config.errorType,
|
|
133
|
+
data: { message: err.message || 'Internal streaming error' },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
} finally {
|
|
137
|
+
if (typeof wsConnection.removeListener === 'function') {
|
|
138
|
+
wsConnection.removeListener('close', onClose);
|
|
139
|
+
wsConnection.removeListener('error', onError);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check whether the WebSocket connection is still open.
|
|
146
|
+
*
|
|
147
|
+
* @param {Object} wsConnection
|
|
148
|
+
* @returns {boolean}
|
|
149
|
+
* @private
|
|
150
|
+
*/
|
|
151
|
+
_isOpen(wsConnection) {
|
|
152
|
+
if (typeof wsConnection.readyState === 'number') {
|
|
153
|
+
return wsConnection.readyState === WS_READY_STATE.OPEN;
|
|
154
|
+
}
|
|
155
|
+
// If readyState is not available, assume open
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Send a JSON-serialised message over the WebSocket.
|
|
161
|
+
*
|
|
162
|
+
* @param {Object} wsConnection
|
|
163
|
+
* @param {Object} payload
|
|
164
|
+
* @private
|
|
165
|
+
*/
|
|
166
|
+
_send(wsConnection, payload) {
|
|
167
|
+
wsConnection.send(JSON.stringify(payload));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { WebSocketAdapter, DEFAULT_CONFIG };
|