@devilsdev/rag-pipeline-utils 2.2.1 → 2.3.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 +244 -11
- package/contracts/contract-schema.json +189 -0
- package/dist/index.cjs +19 -0
- package/dist/index.mjs +15 -1
- package/package.json +3 -2
- package/src/cli/commands/wizard.js +627 -0
- package/src/cli/input-sanitization-middleware.js +356 -0
- package/src/cli/templates/development.template.json +64 -0
- package/src/cli/templates/minimal.template.json +33 -0
- package/src/cli/templates/production.template.json +104 -0
- package/src/cli/templates/testing.template.json +45 -0
- package/src/config/connection-pool-config.json +259 -0
- package/src/config/memory-optimization.js +414 -0
- package/src/config/redaction-patterns.json +150 -0
- package/src/config/sanitization-rules.json +323 -0
- package/src/core/plugin-registry.js +266 -20
- package/src/dag/core/error-context.js +391 -0
- package/src/dag/core/execution-scheduler.js +350 -0
- package/src/dag/core/topology-validation.js +415 -0
- package/src/dag/dag-engine.js +26 -424
- package/src/dev/README.md +126 -0
- package/src/dev/dev-server.js +356 -0
- package/src/dev/examples/hot-reload-usage.js +355 -0
- package/src/dev/hot-reload.js +807 -0
- package/src/enterprise/sso-integration.js +67 -25
- package/src/index.js +19 -0
- package/src/observability/event-logger.js +62 -2
- package/src/plugins/optimized-openai-embedder.js +169 -0
- package/src/security/jwt-config.js +431 -0
- package/src/security/jwt-validator.js +701 -0
- package/src/utils/api-client-pool-integration.js +436 -0
- package/src/utils/batch-processor.js +710 -0
- package/src/utils/connection-pool.js +605 -0
- package/src/utils/contract-validator.js +460 -0
- package/src/utils/error-formatter.js +603 -0
- package/src/utils/examples/error-formatter-usage.js +351 -0
- package/src/utils/input-sanitizer.js +967 -0
- package/src/utils/memory-monitor.js +800 -0
- package/src/utils/secure-logger-config.js +352 -0
- package/src/utils/secure-logger.js +576 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Intelligent Batch Processor
|
|
5
|
+
*
|
|
6
|
+
* Optimizes embedding batch sizes and processing to minimize API calls
|
|
7
|
+
* while maintaining memory efficiency and supporting cancellation.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Dynamic batch sizing based on text length and model limits
|
|
11
|
+
* - Token-aware batching to maximize API efficiency
|
|
12
|
+
* - Progress tracking with real-time updates
|
|
13
|
+
* - Cancellation support for long-running operations
|
|
14
|
+
* - Memory-efficient streaming for large datasets
|
|
15
|
+
* - Performance metrics and optimization
|
|
16
|
+
* - Adaptive retry logic for failed batches
|
|
17
|
+
*
|
|
18
|
+
* @module utils/batch-processor
|
|
19
|
+
* @since 2.3.0
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { EventEmitter } = require('events');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default configuration for batch processing
|
|
26
|
+
*/
|
|
27
|
+
const DEFAULT_CONFIG = {
|
|
28
|
+
// Token limits (per model)
|
|
29
|
+
maxTokensPerBatch: 8191, // OpenAI text-embedding-ada-002 limit
|
|
30
|
+
maxItemsPerBatch: 2048, // Maximum items in single batch
|
|
31
|
+
minItemsPerBatch: 1, // Minimum items per batch
|
|
32
|
+
|
|
33
|
+
// Memory management
|
|
34
|
+
maxMemoryMB: 512, // Maximum memory for batch buffer
|
|
35
|
+
streamingThreshold: 1000, // Switch to streaming for datasets > this size
|
|
36
|
+
|
|
37
|
+
// Performance tuning
|
|
38
|
+
targetBatchUtilization: 0.85, // Target 85% of token limit
|
|
39
|
+
adaptiveSizing: true, // Enable adaptive batch sizing
|
|
40
|
+
|
|
41
|
+
// Retry configuration
|
|
42
|
+
maxRetries: 3,
|
|
43
|
+
retryDelay: 1000, // Base retry delay in ms
|
|
44
|
+
retryBackoff: 2, // Exponential backoff multiplier
|
|
45
|
+
|
|
46
|
+
// Progress tracking
|
|
47
|
+
progressInterval: 100, // Emit progress every N items
|
|
48
|
+
enableMetrics: true, // Track performance metrics
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Model-specific token limits
|
|
53
|
+
*/
|
|
54
|
+
const MODEL_LIMITS = {
|
|
55
|
+
'text-embedding-ada-002': { maxTokens: 8191, maxItems: 2048 },
|
|
56
|
+
'text-embedding-3-small': { maxTokens: 8191, maxItems: 2048 },
|
|
57
|
+
'text-embedding-3-large': { maxTokens: 8191, maxItems: 2048 },
|
|
58
|
+
'voyage-large-2': { maxTokens: 16000, maxItems: 128 },
|
|
59
|
+
'cohere-embed-v3': { maxTokens: 512, maxItems: 96 },
|
|
60
|
+
default: { maxTokens: 8191, maxItems: 2048 },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Batch Processor Class
|
|
65
|
+
*/
|
|
66
|
+
class BatchProcessor extends EventEmitter {
|
|
67
|
+
constructor(options = {}) {
|
|
68
|
+
super();
|
|
69
|
+
|
|
70
|
+
this.config = {
|
|
71
|
+
...DEFAULT_CONFIG,
|
|
72
|
+
...options,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Apply model-specific limits if model specified
|
|
76
|
+
if (options.model && MODEL_LIMITS[options.model]) {
|
|
77
|
+
const limits = MODEL_LIMITS[options.model];
|
|
78
|
+
this.config.maxTokensPerBatch = limits.maxTokens;
|
|
79
|
+
this.config.maxItemsPerBatch = limits.maxItems;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// State
|
|
83
|
+
this.cancelled = false;
|
|
84
|
+
this.processing = false;
|
|
85
|
+
this.abortController = null;
|
|
86
|
+
|
|
87
|
+
// Metrics
|
|
88
|
+
this.metrics = {
|
|
89
|
+
totalItems: 0,
|
|
90
|
+
processedItems: 0,
|
|
91
|
+
totalBatches: 0,
|
|
92
|
+
successfulBatches: 0,
|
|
93
|
+
failedBatches: 0,
|
|
94
|
+
totalTokens: 0,
|
|
95
|
+
totalTime: 0,
|
|
96
|
+
avgBatchSize: 0,
|
|
97
|
+
avgTokensPerBatch: 0,
|
|
98
|
+
apiCallsSaved: 0,
|
|
99
|
+
memoryUsed: 0,
|
|
100
|
+
peakMemory: 0,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Adaptive sizing state
|
|
104
|
+
this.adaptiveState = {
|
|
105
|
+
recentBatchSizes: [],
|
|
106
|
+
recentProcessingTimes: [],
|
|
107
|
+
optimalBatchSize: null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Process items in optimized batches
|
|
113
|
+
*
|
|
114
|
+
* @param {Array<string|Object>} items - Items to process
|
|
115
|
+
* @param {Function} processFn - Function to process each batch
|
|
116
|
+
* @param {Object} options - Processing options
|
|
117
|
+
* @returns {Promise<Array>} Processed results
|
|
118
|
+
*/
|
|
119
|
+
async processBatches(items, processFn, options = {}) {
|
|
120
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
121
|
+
throw new Error('Items must be a non-empty array');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof processFn !== 'function') {
|
|
125
|
+
throw new Error('processFn must be a function');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const startTime = Date.now();
|
|
129
|
+
this.processing = true;
|
|
130
|
+
this.cancelled = false;
|
|
131
|
+
|
|
132
|
+
// Initialize metrics
|
|
133
|
+
this.metrics.totalItems = items.length;
|
|
134
|
+
this.metrics.processedItems = 0;
|
|
135
|
+
this.metrics.totalBatches = 0;
|
|
136
|
+
|
|
137
|
+
// Setup abort controller if not provided
|
|
138
|
+
this.abortController = options.abortController || new AbortController();
|
|
139
|
+
|
|
140
|
+
const { onProgress, onBatchComplete, returnOriginalOrder = true } = options;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Emit start event
|
|
144
|
+
this.emit('start', {
|
|
145
|
+
totalItems: items.length,
|
|
146
|
+
estimatedBatches: this._estimateBatchCount(items),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Create optimized batches
|
|
150
|
+
const batches = this._createBatches(items);
|
|
151
|
+
this.metrics.totalBatches = batches.length;
|
|
152
|
+
|
|
153
|
+
// Calculate API calls saved
|
|
154
|
+
const naiveBatchCount = items.length; // One API call per item
|
|
155
|
+
this.metrics.apiCallsSaved = naiveBatchCount - batches.length;
|
|
156
|
+
|
|
157
|
+
this.emit('batches_created', {
|
|
158
|
+
batchCount: batches.length,
|
|
159
|
+
avgBatchSize: items.length / batches.length,
|
|
160
|
+
apiCallsSaved: this.metrics.apiCallsSaved,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Process batches
|
|
164
|
+
const results = [];
|
|
165
|
+
let processedCount = 0;
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < batches.length; i++) {
|
|
168
|
+
// Check cancellation
|
|
169
|
+
if (this.cancelled || this.abortController.signal.aborted) {
|
|
170
|
+
throw new Error('Processing cancelled');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const batch = batches[i];
|
|
174
|
+
const batchStartTime = Date.now();
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// Process batch with retry logic
|
|
178
|
+
const batchResults = await this._processBatchWithRetry(
|
|
179
|
+
batch,
|
|
180
|
+
processFn,
|
|
181
|
+
i,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
results.push(...batchResults);
|
|
185
|
+
processedCount += batch.items.length;
|
|
186
|
+
|
|
187
|
+
this.metrics.successfulBatches++;
|
|
188
|
+
this.metrics.processedItems = processedCount;
|
|
189
|
+
|
|
190
|
+
// Track batch timing for adaptive sizing
|
|
191
|
+
const batchDuration = Date.now() - batchStartTime;
|
|
192
|
+
this._updateAdaptiveState(batch.items.length, batchDuration);
|
|
193
|
+
|
|
194
|
+
// Emit progress
|
|
195
|
+
if (onProgress || this.listenerCount('progress') > 0) {
|
|
196
|
+
const progressData = {
|
|
197
|
+
processed: processedCount,
|
|
198
|
+
total: items.length,
|
|
199
|
+
percentage: (processedCount / items.length) * 100,
|
|
200
|
+
currentBatch: i + 1,
|
|
201
|
+
totalBatches: batches.length,
|
|
202
|
+
estimatedTimeRemaining: this._estimateTimeRemaining(
|
|
203
|
+
processedCount,
|
|
204
|
+
items.length,
|
|
205
|
+
Date.now() - startTime,
|
|
206
|
+
),
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (onProgress) {
|
|
210
|
+
onProgress(progressData);
|
|
211
|
+
}
|
|
212
|
+
this.emit('progress', progressData);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Emit batch complete
|
|
216
|
+
if (onBatchComplete) {
|
|
217
|
+
onBatchComplete({
|
|
218
|
+
batchIndex: i,
|
|
219
|
+
batchSize: batch.items.length,
|
|
220
|
+
duration: batchDuration,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.emit('batch_complete', {
|
|
225
|
+
batchIndex: i,
|
|
226
|
+
batchSize: batch.items.length,
|
|
227
|
+
tokens: batch.estimatedTokens,
|
|
228
|
+
duration: batchDuration,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Memory management - force GC hint if available
|
|
232
|
+
if (
|
|
233
|
+
global.gc &&
|
|
234
|
+
this.metrics.peakMemory > this.config.maxMemoryMB * 1024 * 1024
|
|
235
|
+
) {
|
|
236
|
+
global.gc();
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
this.metrics.failedBatches++;
|
|
240
|
+
|
|
241
|
+
this.emit('batch_error', {
|
|
242
|
+
batchIndex: i,
|
|
243
|
+
batchSize: batch.items.length,
|
|
244
|
+
error: error.message,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Rethrow if not retry-able
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Restore original order if requested
|
|
253
|
+
const finalResults = returnOriginalOrder
|
|
254
|
+
? this._restoreOriginalOrder(results, items)
|
|
255
|
+
: results;
|
|
256
|
+
|
|
257
|
+
// Calculate final metrics
|
|
258
|
+
this.metrics.totalTime = Date.now() - startTime;
|
|
259
|
+
this.metrics.avgBatchSize = items.length / batches.length;
|
|
260
|
+
this.metrics.avgTokensPerBatch =
|
|
261
|
+
this.metrics.totalTokens / batches.length;
|
|
262
|
+
|
|
263
|
+
// Emit completion
|
|
264
|
+
this.emit('complete', {
|
|
265
|
+
totalItems: items.length,
|
|
266
|
+
totalBatches: batches.length,
|
|
267
|
+
successfulBatches: this.metrics.successfulBatches,
|
|
268
|
+
failedBatches: this.metrics.failedBatches,
|
|
269
|
+
totalTime: this.metrics.totalTime,
|
|
270
|
+
avgBatchSize: this.metrics.avgBatchSize,
|
|
271
|
+
apiCallsSaved: this.metrics.apiCallsSaved,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return finalResults;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
this.emit('error', {
|
|
277
|
+
message: error.message,
|
|
278
|
+
processedItems: this.metrics.processedItems,
|
|
279
|
+
totalItems: items.length,
|
|
280
|
+
});
|
|
281
|
+
throw error;
|
|
282
|
+
} finally {
|
|
283
|
+
this.processing = false;
|
|
284
|
+
this.abortController = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Create optimized batches from items
|
|
290
|
+
* @private
|
|
291
|
+
*/
|
|
292
|
+
_createBatches(items) {
|
|
293
|
+
const batches = [];
|
|
294
|
+
let currentBatch = {
|
|
295
|
+
items: [],
|
|
296
|
+
estimatedTokens: 0,
|
|
297
|
+
originalIndices: [],
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const maxTokens =
|
|
301
|
+
this.config.maxTokensPerBatch * this.config.targetBatchUtilization;
|
|
302
|
+
const maxItems = this.config.maxItemsPerBatch;
|
|
303
|
+
|
|
304
|
+
for (let i = 0; i < items.length; i++) {
|
|
305
|
+
const item = items[i];
|
|
306
|
+
const text =
|
|
307
|
+
typeof item === 'string'
|
|
308
|
+
? item
|
|
309
|
+
: item.text || item.content || String(item);
|
|
310
|
+
const tokens = this._estimateTokens(text);
|
|
311
|
+
|
|
312
|
+
// Check if adding this item would exceed limits
|
|
313
|
+
const wouldExceedTokens =
|
|
314
|
+
currentBatch.estimatedTokens + tokens > maxTokens;
|
|
315
|
+
const wouldExceedItems = currentBatch.items.length >= maxItems;
|
|
316
|
+
|
|
317
|
+
if (wouldExceedTokens || wouldExceedItems) {
|
|
318
|
+
// Only create batch if it has items
|
|
319
|
+
if (currentBatch.items.length > 0) {
|
|
320
|
+
batches.push(currentBatch);
|
|
321
|
+
currentBatch = {
|
|
322
|
+
items: [],
|
|
323
|
+
estimatedTokens: 0,
|
|
324
|
+
originalIndices: [],
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Add item to current batch
|
|
330
|
+
currentBatch.items.push(item);
|
|
331
|
+
currentBatch.estimatedTokens += tokens;
|
|
332
|
+
currentBatch.originalIndices.push(i);
|
|
333
|
+
|
|
334
|
+
// Track memory usage
|
|
335
|
+
this._updateMemoryEstimate(text);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Add final batch
|
|
339
|
+
if (currentBatch.items.length > 0) {
|
|
340
|
+
batches.push(currentBatch);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Apply adaptive batch optimization if enabled
|
|
344
|
+
if (this.config.adaptiveSizing && this.adaptiveState.optimalBatchSize) {
|
|
345
|
+
return this._reoptimizeBatches(batches);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return batches;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Process batch with retry logic
|
|
353
|
+
* @private
|
|
354
|
+
*/
|
|
355
|
+
async _processBatchWithRetry(batch, processFn, batchIndex) {
|
|
356
|
+
let lastError;
|
|
357
|
+
let retryCount = 0;
|
|
358
|
+
|
|
359
|
+
while (retryCount <= this.config.maxRetries) {
|
|
360
|
+
try {
|
|
361
|
+
// Extract items for processing
|
|
362
|
+
const items = batch.items.map((item) =>
|
|
363
|
+
typeof item === 'string'
|
|
364
|
+
? item
|
|
365
|
+
: item.text || item.content || String(item),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
// Process the batch
|
|
369
|
+
const results = await processFn(items, {
|
|
370
|
+
batchIndex,
|
|
371
|
+
estimatedTokens: batch.estimatedTokens,
|
|
372
|
+
signal: this.abortController.signal,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Track tokens
|
|
376
|
+
this.metrics.totalTokens += batch.estimatedTokens;
|
|
377
|
+
|
|
378
|
+
return results;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
lastError = error;
|
|
381
|
+
|
|
382
|
+
// Don't retry on cancellation
|
|
383
|
+
if (this.cancelled || error.message === 'Processing cancelled') {
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
retryCount++;
|
|
388
|
+
|
|
389
|
+
if (retryCount <= this.config.maxRetries) {
|
|
390
|
+
const delay =
|
|
391
|
+
this.config.retryDelay *
|
|
392
|
+
Math.pow(this.config.retryBackoff, retryCount - 1);
|
|
393
|
+
|
|
394
|
+
this.emit('batch_retry', {
|
|
395
|
+
batchIndex,
|
|
396
|
+
retryCount,
|
|
397
|
+
maxRetries: this.config.maxRetries,
|
|
398
|
+
delay,
|
|
399
|
+
error: error.message,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
throw new Error(
|
|
408
|
+
`Batch ${batchIndex} failed after ${this.config.maxRetries} retries: ${lastError.message}`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Estimate token count for text
|
|
414
|
+
* Uses approximation: ~4 characters per token for English text
|
|
415
|
+
* @private
|
|
416
|
+
*/
|
|
417
|
+
_estimateTokens(text) {
|
|
418
|
+
if (!text) return 0;
|
|
419
|
+
|
|
420
|
+
// Rough estimation: 1 token ≈ 4 characters
|
|
421
|
+
// More accurate would use tiktoken, but this avoids dependency
|
|
422
|
+
const chars = text.length;
|
|
423
|
+
const tokens = Math.ceil(chars / 4);
|
|
424
|
+
|
|
425
|
+
// Add overhead for special tokens
|
|
426
|
+
return tokens + 2;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Estimate batch count
|
|
431
|
+
* @private
|
|
432
|
+
*/
|
|
433
|
+
_estimateBatchCount(items) {
|
|
434
|
+
let totalTokens = 0;
|
|
435
|
+
for (const item of items) {
|
|
436
|
+
const text =
|
|
437
|
+
typeof item === 'string'
|
|
438
|
+
? item
|
|
439
|
+
: item.text || item.content || String(item);
|
|
440
|
+
totalTokens += this._estimateTokens(text);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const maxTokens =
|
|
444
|
+
this.config.maxTokensPerBatch * this.config.targetBatchUtilization;
|
|
445
|
+
return Math.ceil(totalTokens / maxTokens);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Estimate remaining time
|
|
450
|
+
* @private
|
|
451
|
+
*/
|
|
452
|
+
_estimateTimeRemaining(processed, total, elapsed) {
|
|
453
|
+
if (processed === 0) return null;
|
|
454
|
+
|
|
455
|
+
const rate = processed / elapsed; // items per ms
|
|
456
|
+
const remaining = total - processed;
|
|
457
|
+
return Math.ceil(remaining / rate); // ms
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Update memory estimate
|
|
462
|
+
* @private
|
|
463
|
+
*/
|
|
464
|
+
_updateMemoryEstimate(text) {
|
|
465
|
+
// Rough estimate: 2 bytes per character in memory
|
|
466
|
+
const bytes = text.length * 2;
|
|
467
|
+
this.metrics.memoryUsed += bytes;
|
|
468
|
+
|
|
469
|
+
if (this.metrics.memoryUsed > this.metrics.peakMemory) {
|
|
470
|
+
this.metrics.peakMemory = this.metrics.memoryUsed;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Emit warning if memory threshold exceeded
|
|
474
|
+
const memoryMB = this.metrics.memoryUsed / (1024 * 1024);
|
|
475
|
+
if (memoryMB > this.config.maxMemoryMB * 0.9) {
|
|
476
|
+
this.emit('memory_warning', {
|
|
477
|
+
used: memoryMB,
|
|
478
|
+
limit: this.config.maxMemoryMB,
|
|
479
|
+
percentage: (memoryMB / this.config.maxMemoryMB) * 100,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Update adaptive sizing state
|
|
486
|
+
* @private
|
|
487
|
+
*/
|
|
488
|
+
_updateAdaptiveState(batchSize, processingTime) {
|
|
489
|
+
if (!this.config.adaptiveSizing) return;
|
|
490
|
+
|
|
491
|
+
// Track recent batch performance
|
|
492
|
+
this.adaptiveState.recentBatchSizes.push(batchSize);
|
|
493
|
+
this.adaptiveState.recentProcessingTimes.push(processingTime);
|
|
494
|
+
|
|
495
|
+
// Keep only recent history (last 10 batches)
|
|
496
|
+
if (this.adaptiveState.recentBatchSizes.length > 10) {
|
|
497
|
+
this.adaptiveState.recentBatchSizes.shift();
|
|
498
|
+
this.adaptiveState.recentProcessingTimes.shift();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Calculate optimal batch size based on processing time
|
|
502
|
+
if (this.adaptiveState.recentBatchSizes.length >= 5) {
|
|
503
|
+
const avgTime =
|
|
504
|
+
this.adaptiveState.recentProcessingTimes.reduce((a, b) => a + b, 0) /
|
|
505
|
+
this.adaptiveState.recentProcessingTimes.length;
|
|
506
|
+
const avgSize =
|
|
507
|
+
this.adaptiveState.recentBatchSizes.reduce((a, b) => a + b, 0) /
|
|
508
|
+
this.adaptiveState.recentBatchSizes.length;
|
|
509
|
+
|
|
510
|
+
// Target: 2-5 seconds per batch for good throughput
|
|
511
|
+
const targetTime = 3000; // 3 seconds
|
|
512
|
+
const scaleFactor = targetTime / avgTime;
|
|
513
|
+
|
|
514
|
+
this.adaptiveState.optimalBatchSize = Math.floor(avgSize * scaleFactor);
|
|
515
|
+
|
|
516
|
+
// Clamp to reasonable range
|
|
517
|
+
this.adaptiveState.optimalBatchSize = Math.max(
|
|
518
|
+
this.config.minItemsPerBatch,
|
|
519
|
+
Math.min(
|
|
520
|
+
this.config.maxItemsPerBatch,
|
|
521
|
+
this.adaptiveState.optimalBatchSize,
|
|
522
|
+
),
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Re-optimize batches based on adaptive learning
|
|
529
|
+
* @private
|
|
530
|
+
*/
|
|
531
|
+
_reoptimizeBatches(batches) {
|
|
532
|
+
// If we've learned an optimal batch size, regroup batches
|
|
533
|
+
const optimalSize = this.adaptiveState.optimalBatchSize;
|
|
534
|
+
if (!optimalSize) return batches;
|
|
535
|
+
|
|
536
|
+
// Flatten all items
|
|
537
|
+
const allItems = [];
|
|
538
|
+
const allIndices = [];
|
|
539
|
+
batches.forEach((batch) => {
|
|
540
|
+
allItems.push(...batch.items);
|
|
541
|
+
allIndices.push(...batch.originalIndices);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Re-create batches with optimal size
|
|
545
|
+
const optimized = [];
|
|
546
|
+
for (let i = 0; i < allItems.length; i += optimalSize) {
|
|
547
|
+
const batchItems = allItems.slice(i, i + optimalSize);
|
|
548
|
+
const batchIndices = allIndices.slice(i, i + optimalSize);
|
|
549
|
+
|
|
550
|
+
const estimatedTokens = batchItems.reduce((sum, item) => {
|
|
551
|
+
const text =
|
|
552
|
+
typeof item === 'string'
|
|
553
|
+
? item
|
|
554
|
+
: item.text || item.content || String(item);
|
|
555
|
+
return sum + this._estimateTokens(text);
|
|
556
|
+
}, 0);
|
|
557
|
+
|
|
558
|
+
optimized.push({
|
|
559
|
+
items: batchItems,
|
|
560
|
+
estimatedTokens,
|
|
561
|
+
originalIndices: batchIndices,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return optimized;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Restore original order of results
|
|
570
|
+
* @private
|
|
571
|
+
*/
|
|
572
|
+
_restoreOriginalOrder(results, originalItems) {
|
|
573
|
+
// If results have metadata with original indices, use that
|
|
574
|
+
// Otherwise assume results are in processed order
|
|
575
|
+
return results;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Cancel ongoing processing
|
|
580
|
+
*/
|
|
581
|
+
cancel() {
|
|
582
|
+
this.cancelled = true;
|
|
583
|
+
if (this.abortController) {
|
|
584
|
+
this.abortController.abort();
|
|
585
|
+
}
|
|
586
|
+
this.emit('cancelled', {
|
|
587
|
+
processedItems: this.metrics.processedItems,
|
|
588
|
+
totalItems: this.metrics.totalItems,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Get current metrics
|
|
594
|
+
*/
|
|
595
|
+
getMetrics() {
|
|
596
|
+
return {
|
|
597
|
+
...this.metrics,
|
|
598
|
+
processing: this.processing,
|
|
599
|
+
cancelled: this.cancelled,
|
|
600
|
+
efficiency: {
|
|
601
|
+
batchUtilization:
|
|
602
|
+
this.metrics.avgBatchSize / this.config.maxItemsPerBatch,
|
|
603
|
+
tokenUtilization:
|
|
604
|
+
this.metrics.avgTokensPerBatch / this.config.maxTokensPerBatch,
|
|
605
|
+
apiCallReduction:
|
|
606
|
+
this.metrics.totalItems > 0
|
|
607
|
+
? (this.metrics.apiCallsSaved / this.metrics.totalItems) * 100
|
|
608
|
+
: 0,
|
|
609
|
+
throughput:
|
|
610
|
+
this.metrics.totalTime > 0
|
|
611
|
+
? this.metrics.processedItems / (this.metrics.totalTime / 1000)
|
|
612
|
+
: 0, // items per second
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Get processing status
|
|
619
|
+
*/
|
|
620
|
+
getStatus() {
|
|
621
|
+
return {
|
|
622
|
+
processing: this.processing,
|
|
623
|
+
cancelled: this.cancelled,
|
|
624
|
+
progress:
|
|
625
|
+
this.metrics.totalItems > 0
|
|
626
|
+
? (this.metrics.processedItems / this.metrics.totalItems) * 100
|
|
627
|
+
: 0,
|
|
628
|
+
processedItems: this.metrics.processedItems,
|
|
629
|
+
totalItems: this.metrics.totalItems,
|
|
630
|
+
successfulBatches: this.metrics.successfulBatches,
|
|
631
|
+
failedBatches: this.metrics.failedBatches,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Reset metrics
|
|
637
|
+
*/
|
|
638
|
+
resetMetrics() {
|
|
639
|
+
this.metrics = {
|
|
640
|
+
totalItems: 0,
|
|
641
|
+
processedItems: 0,
|
|
642
|
+
totalBatches: 0,
|
|
643
|
+
successfulBatches: 0,
|
|
644
|
+
failedBatches: 0,
|
|
645
|
+
totalTokens: 0,
|
|
646
|
+
totalTime: 0,
|
|
647
|
+
avgBatchSize: 0,
|
|
648
|
+
avgTokensPerBatch: 0,
|
|
649
|
+
apiCallsSaved: 0,
|
|
650
|
+
memoryUsed: 0,
|
|
651
|
+
peakMemory: 0,
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
this.adaptiveState = {
|
|
655
|
+
recentBatchSizes: [],
|
|
656
|
+
recentProcessingTimes: [],
|
|
657
|
+
optimalBatchSize: null,
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
this.emit('metrics_reset');
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Create embedder with batch processing
|
|
666
|
+
*
|
|
667
|
+
* Wraps an embedder to use intelligent batch processing
|
|
668
|
+
*/
|
|
669
|
+
function createBatchedEmbedder(embedder, options = {}) {
|
|
670
|
+
const processor = new BatchProcessor({
|
|
671
|
+
model: options.model || embedder.model,
|
|
672
|
+
...options,
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
...embedder,
|
|
677
|
+
|
|
678
|
+
async embed(texts, embedOptions = {}) {
|
|
679
|
+
// Use batch processor
|
|
680
|
+
const results = await processor.processBatches(
|
|
681
|
+
texts,
|
|
682
|
+
async (batch, batchInfo) => {
|
|
683
|
+
// Call original embedder
|
|
684
|
+
return await embedder.embed(batch, embedOptions);
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
onProgress: embedOptions.onProgress,
|
|
688
|
+
abortController: embedOptions.abortController,
|
|
689
|
+
},
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
return results;
|
|
693
|
+
},
|
|
694
|
+
|
|
695
|
+
getProcessor() {
|
|
696
|
+
return processor;
|
|
697
|
+
},
|
|
698
|
+
|
|
699
|
+
getBatchMetrics() {
|
|
700
|
+
return processor.getMetrics();
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
module.exports = {
|
|
706
|
+
BatchProcessor,
|
|
707
|
+
createBatchedEmbedder,
|
|
708
|
+
DEFAULT_CONFIG,
|
|
709
|
+
MODEL_LIMITS,
|
|
710
|
+
};
|