@devilsdev/rag-pipeline-utils 2.3.1 → 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.
Files changed (160) hide show
  1. package/README.md +293 -369
  2. package/contracts/loader-contract.json +11 -6
  3. package/contracts/reranker-contract.json +18 -26
  4. package/dist/index.cjs +21190 -47
  5. package/dist/index.d.ts +298 -68
  6. package/dist/index.mjs +9 -76
  7. package/package.json +20 -8
  8. package/src/agentic/agentic-pipeline.js +235 -0
  9. package/src/agentic/iterative-retriever.js +213 -0
  10. package/src/agentic/query-planner.js +208 -0
  11. package/src/agentic/self-critique.js +102 -0
  12. package/src/ai/adaptive-retrieval.js +0 -3
  13. package/src/ai/federated-learning.js +0 -3
  14. package/src/ai/federation.js +1 -6
  15. package/src/ai/index.js +9 -20
  16. package/src/ai/model-training.js +0 -3
  17. package/src/ai/multimodal-processing.js +0 -3
  18. package/src/ai/multimodal.js +1 -6
  19. package/src/ai/orchestrator.js +1 -6
  20. package/src/ai/retrieval-engine.js +1 -6
  21. package/src/chunking/chunking-engine.js +142 -0
  22. package/src/chunking/strategies/fixed-size.js +54 -0
  23. package/src/chunking/strategies/recursive.js +127 -0
  24. package/src/chunking/strategies/semantic.js +128 -0
  25. package/src/chunking/strategies/sentence.js +32 -0
  26. package/src/chunking/strategies/structure-aware.js +138 -0
  27. package/src/citation/citation-tracker.js +161 -0
  28. package/src/citation/hallucination-detector.js +74 -0
  29. package/src/citation/source-mapper.js +95 -0
  30. package/src/cli/commands/ai-ml/adaptive-commands.js +178 -0
  31. package/src/cli/commands/ai-ml/federated-commands.js +207 -0
  32. package/src/cli/commands/ai-ml/index.js +17 -0
  33. package/src/cli/commands/ai-ml/multimodal-commands.js +187 -0
  34. package/src/cli/commands/ai-ml/shared.js +51 -0
  35. package/src/cli/commands/ai-ml/training-commands.js +228 -0
  36. package/src/cli/commands/ai-ml.js +176 -757
  37. package/src/cli/commands/plugin-hub/certification-commands.js +221 -0
  38. package/src/cli/commands/plugin-hub/event-handlers.js +64 -0
  39. package/src/cli/commands/plugin-hub/hub-commands.js +290 -0
  40. package/src/cli/commands/plugin-hub/review-commands.js +78 -0
  41. package/src/cli/commands/plugin-hub.js +106 -782
  42. package/src/cli/diagnostics/config-checker.js +81 -0
  43. package/src/cli/diagnostics/dependency-checker.js +69 -0
  44. package/src/cli/diagnostics/environment-checker.js +45 -0
  45. package/src/cli/diagnostics/performance-checker.js +51 -0
  46. package/src/cli/diagnostics/plugin-checker.js +106 -0
  47. package/src/cli/diagnostics/security-checker.js +61 -0
  48. package/src/cli/doctor-command.js +70 -369
  49. package/src/cli/enhanced-cli-commands.js +162 -706
  50. package/src/cli/handlers/config-handler.js +126 -0
  51. package/src/cli/handlers/diagnostics-handler.js +113 -0
  52. package/src/cli/handlers/ingest-handler.js +42 -0
  53. package/src/cli/handlers/init-handler.js +51 -0
  54. package/src/cli/handlers/query-handler.js +45 -0
  55. package/src/cli/handlers/run-handler.js +181 -0
  56. package/src/cli/interactive-wizard.js +99 -723
  57. package/src/cli/shared/cli-utils.js +89 -0
  58. package/src/cli/wizard/configuration.js +148 -0
  59. package/src/cli/wizard/observability.js +79 -0
  60. package/src/cli/wizard/performance.js +76 -0
  61. package/src/cli/wizard/plugin-selection.js +297 -0
  62. package/src/cli/wizard/preview-save.js +172 -0
  63. package/src/cli/wizard/project-setup.js +66 -0
  64. package/src/config/memory-optimization.js +3 -3
  65. package/src/connectors/anthropic-connector.js +179 -0
  66. package/src/connectors/base-connector.js +94 -0
  67. package/src/connectors/cohere-connector.js +143 -0
  68. package/src/connectors/local-embedder.js +171 -0
  69. package/src/connectors/memory-retriever.js +150 -0
  70. package/src/connectors/ollama-connector.js +205 -0
  71. package/src/connectors/openai-connector.js +173 -0
  72. package/src/core/create-pipeline.js +21 -1
  73. package/src/core/observability/tracing.js +2 -1
  74. package/src/core/pipeline-factory.js +0 -1
  75. package/src/core/plugin-registry.js +14 -14
  76. package/src/cost/cost-calculator.js +146 -0
  77. package/src/cost/cost-tracker.js +260 -0
  78. package/src/cost/token-budget.js +245 -0
  79. package/src/dag/core/execution-scheduler.js +2 -1
  80. package/src/dag/core/topology-validation.js +0 -3
  81. package/src/dag/dag-engine.js +3 -5
  82. package/src/dag/dag-node.js +0 -1
  83. package/src/dag/enhanced-dag-engine.js +171 -99
  84. package/src/dashboard/dashboard-generator.js +642 -0
  85. package/src/dashboard/metrics-aggregator.js +409 -0
  86. package/src/debug/execution-tracer.js +210 -0
  87. package/src/debug/trace-inspector.js +281 -0
  88. package/src/dev/dev-server.js +5 -5
  89. package/src/dev/hot-reload.js +3 -2
  90. package/src/dx/integration-templates.js +0 -3
  91. package/src/ecosystem/plugin-analytics-dashboard.js +2 -2
  92. package/src/ecosystem/plugin-hub.js +2 -2
  93. package/src/enterprise/audit-logging.js +2 -2
  94. package/src/enterprise/sso-integration.js +52 -32
  95. package/src/evaluate/context-metrics.js +127 -0
  96. package/src/evaluate/evaluator.js +4 -2
  97. package/src/evaluate/faithfulness.js +117 -0
  98. package/src/evaluate/groundedness.js +60 -0
  99. package/src/evaluate/pipeline-evaluator.js +156 -0
  100. package/src/evaluate/relevance.js +129 -0
  101. package/src/evaluate/scoring.js +27 -0
  102. package/src/graph/entity-extractor.js +183 -0
  103. package/src/graph/graph-index.js +113 -0
  104. package/src/graph/graph-retriever.js +244 -0
  105. package/src/graph/knowledge-graph.js +256 -0
  106. package/src/guardrails/guardrails-pipeline.js +136 -0
  107. package/src/guardrails/post-generation-guard.js +170 -0
  108. package/src/guardrails/pre-retrieval-guard.js +161 -0
  109. package/src/guardrails/retrieval-guard.js +126 -0
  110. package/src/index.js +255 -9
  111. package/src/loader/html-loader.js +4 -15
  112. package/src/loader/markdown-loader.js +4 -15
  113. package/src/mcp/mcp-server.js +199 -0
  114. package/src/mcp/mcp-tool-builder.js +121 -0
  115. package/src/observability/audit-logger.js +0 -2
  116. package/src/observability/event-logger.js +2 -1
  117. package/src/observability/security-monitor.js +0 -2
  118. package/src/observability/telemetry.js +0 -2
  119. package/src/reranker/cascade-reranker.js +79 -0
  120. package/src/reranker/embedding-reranker.js +124 -0
  121. package/src/reranker/llm-reranker.js +6 -3
  122. package/src/reranker/scoring-reranker.js +123 -0
  123. package/src/retrieval/bm25-search.js +157 -0
  124. package/src/retrieval/hybrid-retriever.js +111 -0
  125. package/src/retrieval/rank-fusion.js +62 -0
  126. package/src/security/jwt-config.js +4 -3
  127. package/src/security/jwt-validator.js +2 -1
  128. package/src/security/plugin-sandbox.js +0 -3
  129. package/src/security/plugin-signature-verifier.js +4 -6
  130. package/src/security/validator.js +0 -2
  131. package/src/streaming/embedding-stream.js +197 -0
  132. package/src/streaming/sse-adapter.js +168 -0
  133. package/src/streaming/stream-router.js +210 -0
  134. package/src/streaming/streaming-embedder.js +231 -0
  135. package/src/streaming/websocket-adapter.js +171 -0
  136. package/src/templates/scaffold.js +147 -0
  137. package/src/templates/template-registry.js +272 -0
  138. package/src/utils/contract-validator.js +2 -1
  139. package/src/utils/input-sanitizer.js +80 -99
  140. package/src/utils/performance-monitor.js +0 -3
  141. package/src/utils/plugin-scaffolder.js +9 -16
  142. package/src/utils/rate-limiter.js +0 -1
  143. package/src/utils/retry-policy.js +0 -3
  144. package/src/utils/safe-expression-evaluator.js +0 -3
  145. package/src/utils/secure-logger-config.js +2 -4
  146. package/src/utils/secure-logging.js +0 -3
  147. package/src/utils/structured-logger.js +0 -3
  148. package/src/ai/index-fixed.js +0 -509
  149. package/src/ai/module-template.js +0 -37
  150. package/src/ci/workflow-analyzer.js +0 -71
  151. package/src/cli/plugin-marketplace-commands.js +0 -869
  152. package/src/dev/README.md +0 -126
  153. package/src/dev/examples/hot-reload-usage.js +0 -355
  154. package/src/mocks/openai-embedder.js +0 -119
  155. package/src/mocks/openai-llm.js +0 -77
  156. package/src/mocks/pdf-loader.js +0 -21
  157. package/src/mocks/pinecone-retriever.js +0 -106
  158. package/src/scripts/script-utilities.js +0 -36
  159. package/src/utils/ci/diagnostic-reporter.js +0 -57
  160. 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 };