@devilsdev/rag-pipeline-utils 2.2.1 → 2.3.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.
Files changed (40) hide show
  1. package/README.md +260 -19
  2. package/contracts/contract-schema.json +189 -0
  3. package/dist/index.cjs +19 -0
  4. package/dist/index.mjs +15 -1
  5. package/package.json +3 -2
  6. package/src/cli/commands/wizard.js +627 -0
  7. package/src/cli/input-sanitization-middleware.js +356 -0
  8. package/src/cli/templates/development.template.json +64 -0
  9. package/src/cli/templates/minimal.template.json +33 -0
  10. package/src/cli/templates/production.template.json +104 -0
  11. package/src/cli/templates/testing.template.json +45 -0
  12. package/src/config/connection-pool-config.json +259 -0
  13. package/src/config/memory-optimization.js +414 -0
  14. package/src/config/redaction-patterns.json +150 -0
  15. package/src/config/sanitization-rules.json +323 -0
  16. package/src/core/plugin-registry.js +266 -20
  17. package/src/dag/core/error-context.js +391 -0
  18. package/src/dag/core/execution-scheduler.js +350 -0
  19. package/src/dag/core/topology-validation.js +415 -0
  20. package/src/dag/dag-engine.js +26 -424
  21. package/src/dev/README.md +126 -0
  22. package/src/dev/dev-server.js +356 -0
  23. package/src/dev/examples/hot-reload-usage.js +355 -0
  24. package/src/dev/hot-reload.js +807 -0
  25. package/src/enterprise/sso-integration.js +67 -25
  26. package/src/index.js +19 -0
  27. package/src/observability/event-logger.js +62 -2
  28. package/src/plugins/optimized-openai-embedder.js +169 -0
  29. package/src/security/jwt-config.js +431 -0
  30. package/src/security/jwt-validator.js +701 -0
  31. package/src/utils/api-client-pool-integration.js +436 -0
  32. package/src/utils/batch-processor.js +710 -0
  33. package/src/utils/connection-pool.js +605 -0
  34. package/src/utils/contract-validator.js +460 -0
  35. package/src/utils/error-formatter.js +603 -0
  36. package/src/utils/examples/error-formatter-usage.js +351 -0
  37. package/src/utils/input-sanitizer.js +967 -0
  38. package/src/utils/memory-monitor.js +800 -0
  39. package/src/utils/secure-logger-config.js +352 -0
  40. package/src/utils/secure-logger.js +576 -0
@@ -6,10 +6,23 @@
6
6
  */
7
7
 
8
8
  const DAGNode = require('./dag-node.js');
9
+ const {
10
+ buildAdjacency,
11
+ getSinkIds,
12
+ topologicalSort,
13
+ validateDAG,
14
+ validateTopology,
15
+ } = require('./core/topology-validation.js');
16
+ const ExecutionScheduler = require('./core/execution-scheduler.js');
17
+ const ErrorContext = require('./core/error-context.js');
9
18
 
10
19
  class DAG {
11
20
  constructor() {
12
21
  this.nodes = new Map();
22
+ this.scheduler = new ExecutionScheduler({
23
+ getSinkIds: (fwd) => getSinkIds(fwd),
24
+ });
25
+ this.errorContext = new ErrorContext();
13
26
  }
14
27
 
15
28
  addNode(id, _fn, options = {}) {
@@ -87,89 +100,7 @@ class DAG {
87
100
  * @returns {Promise<void>}
88
101
  */
89
102
  async _runAllNodes(order, context) {
90
- const {
91
- concurrency,
92
- results,
93
- errors,
94
- fwd,
95
- continueOnError,
96
- requiredIds,
97
- seed,
98
- gracefulDegradation,
99
- requiredNodes,
100
- retryFailedNodes,
101
- maxRetries,
102
- } = context;
103
-
104
- const semaphore = concurrency ? new Array(concurrency).fill(null) : null;
105
- let semaphoreIndex = 0;
106
-
107
- const executeWithSemaphore = async (node) => {
108
- if (semaphore) {
109
- // Wait for available slot
110
- while (semaphore[semaphoreIndex % semaphore.length] !== null) {
111
- await new Promise((resolve) => setTimeout(resolve, 10));
112
- }
113
- const slotIndex = semaphoreIndex % semaphore.length;
114
- semaphoreIndex++;
115
-
116
- try {
117
- const result = await this.executeNode(node, {
118
- results,
119
- errors,
120
- fwd,
121
- continueOnError,
122
- requiredIds,
123
- seed,
124
- gracefulDegradation,
125
- requiredNodes,
126
- retryFailedNodes,
127
- maxRetries,
128
- });
129
- // Only set result if it's not undefined (failed optional nodes return undefined)
130
- if (result !== undefined) {
131
- results.set(node.id, result);
132
- }
133
- } finally {
134
- semaphore[slotIndex] = null;
135
- }
136
- } else {
137
- const result = await this.executeNode(node, {
138
- results,
139
- errors,
140
- fwd,
141
- continueOnError,
142
- requiredIds,
143
- seed,
144
- gracefulDegradation,
145
- requiredNodes,
146
- retryFailedNodes,
147
- maxRetries,
148
- });
149
- // Only set result if it's not the special failed-optional-node symbol
150
- if (
151
- typeof result !== 'symbol' ||
152
- result.description !== 'failed-optional-node'
153
- ) {
154
- results.set(node.id, result);
155
- }
156
- }
157
- };
158
-
159
- // Execute nodes respecting dependencies
160
- for (const node of order) {
161
- // Check if all dependencies are satisfied
162
- const canExecute = this._canExecuteNode(node, {
163
- results,
164
- errors,
165
- gracefulDegradation,
166
- requiredNodes,
167
- });
168
-
169
- if (canExecute) {
170
- await executeWithSemaphore(node);
171
- }
172
- }
103
+ return this.scheduler.scheduleExecution(order, context);
173
104
  }
174
105
 
175
106
  /**
@@ -179,23 +110,7 @@ class DAG {
179
110
  * @returns {boolean}
180
111
  */
181
112
  _canExecuteNode(node, context) {
182
- const { results, errors, gracefulDegradation, requiredNodes } = context;
183
-
184
- return node.inputs.every((dep) => {
185
- // If dependency has result or error, it's satisfied
186
- if (results.has(dep.id) || errors.has(dep.id)) {
187
- return true;
188
- }
189
- // If graceful degradation is enabled and dependency is not required, it's satisfied
190
- if (
191
- gracefulDegradation &&
192
- requiredNodes &&
193
- !requiredNodes.includes(dep.id)
194
- ) {
195
- return true;
196
- }
197
- return false;
198
- });
113
+ return this.scheduler.canExecuteNode(node, context);
199
114
  }
200
115
 
201
116
  /**
@@ -221,41 +136,7 @@ class DAG {
221
136
  * @throws {Error} - Appropriately wrapped error
222
137
  */
223
138
  _handleExecutionError(error) {
224
- // Error policy: wrap everything in try/catch
225
- // If caught error is a node error (message starts with "Node " OR error.nodeId) → rethrow unchanged
226
- if (error.message.startsWith('Node ') || error.nodeId) {
227
- throw error;
228
- }
229
-
230
- // If caught error is validation or aggregate → wrap once
231
- if (
232
- error.cycle ||
233
- /^DAG validation failed/.test(error.message) ||
234
- error.errors
235
- ) {
236
- const wrappedError = new Error(`DAG execution failed: ${error.message}`);
237
- if (error.cycle) wrappedError.cycle = error.cycle;
238
- if (error.errors) wrappedError.errors = error.errors;
239
- if (error.nodeId) wrappedError.nodeId = error.nodeId;
240
- if (error.timestamp) wrappedError.timestamp = error.timestamp;
241
- throw wrappedError;
242
- }
243
-
244
- // Do NOT wrap timeout errors or 'no sink nodes' errors
245
- if (
246
- error.message === 'Execution timeout' ||
247
- error.message === 'DAG has no sink nodes - no final output available'
248
- ) {
249
- throw error;
250
- }
251
-
252
- // Otherwise wrap with 'DAG execution failed: ...'
253
- const wrappedError = new Error(`DAG execution failed: ${error.message}`);
254
- if (error.errors) wrappedError.errors = error.errors;
255
- if (error.nodeId) wrappedError.nodeId = error.nodeId;
256
- if (error.timestamp) wrappedError.timestamp = error.timestamp;
257
- if (error.cycle) wrappedError.cycle = error.cycle;
258
- throw wrappedError;
139
+ throw this.errorContext.wrapExecutionError(error);
259
140
  }
260
141
 
261
142
  /**
@@ -265,16 +146,7 @@ class DAG {
265
146
  * @returns {Promise<void>}
266
147
  */
267
148
  async _executeWithTimeout(runFn, timeout) {
268
- if (timeout && timeout > 0) {
269
- await Promise.race([
270
- runFn(),
271
- new Promise((_, reject) => {
272
- setTimeout(() => reject(new Error('Execution timeout')), timeout);
273
- }),
274
- ]);
275
- } else {
276
- await runFn();
277
- }
149
+ return this.scheduler.executeWithTimeout(runFn, timeout);
278
150
  }
279
151
 
280
152
  /**
@@ -294,10 +166,9 @@ class DAG {
294
166
  } = options;
295
167
 
296
168
  // Check for multiple errors first
297
- if (errors.size >= 2) {
298
- const agg = new Error('Multiple execution errors');
299
- agg.errors = [...errors.values()].map((x) => x.cause || x);
300
- throw agg;
169
+ const aggregatedError = this.errorContext.aggregateErrors(errors);
170
+ if (aggregatedError && errors.size >= 2) {
171
+ throw aggregatedError;
301
172
  }
302
173
 
303
174
  // Determine sink nodes and successful sinks
@@ -435,27 +306,7 @@ class DAG {
435
306
  * @returns {Object} - Object with fwd and rev adjacency maps
436
307
  */
437
308
  _buildAdjacency() {
438
- const fwd = new Map(); // node -> Set of children
439
- const rev = new Map(); // node -> Set of parents
440
-
441
- // Initialize adjacency lists for all nodes
442
- for (const node of this.nodes.values()) {
443
- fwd.set(node.id, new Set());
444
- rev.set(node.id, new Set());
445
- }
446
-
447
- // Populate adjacency lists based on node connections
448
- for (const node of this.nodes.values()) {
449
- for (const output of node.outputs) {
450
- fwd.get(node.id).add(output.id);
451
- // Only add reverse edge if the output node exists in the DAG
452
- if (rev.has(output.id)) {
453
- rev.get(output.id).add(node.id);
454
- }
455
- }
456
- }
457
-
458
- return { fwd, rev };
309
+ return buildAdjacency(this.nodes);
459
310
  }
460
311
 
461
312
  /**
@@ -464,13 +315,7 @@ class DAG {
464
315
  * @returns {Array} - Array of sink node IDs
465
316
  */
466
317
  _getSinkIds(fwd) {
467
- const sinkIds = [];
468
- for (const [nodeId, children] of fwd) {
469
- if (children.size === 0) {
470
- sinkIds.push(nodeId);
471
- }
472
- }
473
- return sinkIds;
318
+ return getSinkIds(fwd);
474
319
  }
475
320
 
476
321
  /**
@@ -523,46 +368,7 @@ class DAG {
523
368
  * @throws {Error} - If validation fails
524
369
  */
525
370
  validate() {
526
- if (this.nodes.size === 0) {
527
- throw new Error('DAG is empty - no nodes to execute');
528
- }
529
-
530
- // Check for cycles using topological sort
531
- try {
532
- this.topoSort();
533
- } catch (error) {
534
- if (error.message.includes('Cycle detected')) {
535
- // Extract cycle information from error message or use error.cycle if available
536
- let nodes = error.cycle;
537
- if (!nodes && error.message.includes('involving node:')) {
538
- // Parse cycle from message if cycle array not available
539
- const match = error.message.match(/involving node: (.+)/);
540
- if (match) {
541
- nodes = match[1].split(' -> ');
542
- }
543
- }
544
- if (!nodes) {
545
- nodes = ['unknown'];
546
- }
547
-
548
- const pretty = nodes.join(' -> ');
549
- const err = new Error(
550
- `DAG validation failed: DAG topological sort failed: Cycle detected involving node: ${pretty}`,
551
- );
552
- err.cycle = nodes;
553
- throw err;
554
- }
555
- throw new Error(`DAG validation failed: ${error.message}`);
556
- }
557
-
558
- // Check for orphaned nodes (nodes with no path to any sink)
559
- const { fwd } = this._buildAdjacency();
560
- const sinkIds = this._getSinkIds(fwd);
561
- if (sinkIds.length === 0) {
562
- throw new Error('DAG has no sink nodes - no final output available');
563
- }
564
-
565
- return true;
371
+ return validateDAG(this.nodes);
566
372
  }
567
373
 
568
374
  /**
@@ -572,104 +378,7 @@ class DAG {
572
378
  * @returns {Promise<any>} Node execution result
573
379
  */
574
380
  async executeNode(node, context) {
575
- const {
576
- results,
577
- errors,
578
- fwd,
579
- continueOnError,
580
- requiredIds,
581
- seed,
582
- gracefulDegradation,
583
- requiredNodes,
584
- } = context;
585
-
586
- // Handle undefined node.run function
587
- if (typeof node.run !== 'function') {
588
- throw new Error(`Node ${node.id} has no run function`);
589
- }
590
-
591
- // Determine criticality - default is critical unless explicitly marked optional
592
- // OR if gracefulDegradation is enabled and node is not in requiredNodes
593
- // OR if there are multiple sink nodes (individual sink failures should be non-critical)
594
- const sinkIds = this._getSinkIds(fwd);
595
- const isMultipleSinks = sinkIds.length > 1;
596
- const isSinkNode = sinkIds.includes(node.id);
597
-
598
- const isNonCritical =
599
- node?.optional === true ||
600
- node?.isOptional === true ||
601
- node?.critical === false ||
602
- (gracefulDegradation &&
603
- requiredNodes &&
604
- !requiredNodes.includes(node.id)) ||
605
- (isMultipleSinks && isSinkNode); // Sink nodes are non-critical when there are multiple sinks
606
-
607
- // Prepare input based on dependencies
608
- let input;
609
- if (node.inputs.length === 0) {
610
- input = seed;
611
- } else if (node.inputs.length === 1) {
612
- input = results.get(node.inputs[0].id);
613
- } else {
614
- input = node.inputs.map((inputNode) => {
615
- // For graceful degradation, return undefined for failed optional dependencies
616
- if (
617
- !results.has(inputNode.id) &&
618
- gracefulDegradation &&
619
- requiredNodes &&
620
- !requiredNodes.includes(inputNode.id)
621
- ) {
622
- return undefined;
623
- }
624
- return results.get(inputNode.id);
625
- });
626
- }
627
-
628
- // Retry logic - use global options if available, otherwise node-specific
629
- const { retryFailedNodes, maxRetries: globalMaxRetries } = context;
630
- const nodeMaxRetries = retryFailedNodes
631
- ? Math.max(0, globalMaxRetries ?? 3)
632
- : Math.max(0, node?.retry?.retries ?? 0);
633
- const delayMs = Math.max(0, node?.retry?.delayMs ?? 0);
634
- let attempt = 0;
635
- let keepRetrying = true;
636
-
637
- while (keepRetrying) {
638
- try {
639
- // Execute the node
640
- const output = await node.run(input);
641
- if (errors.has(node.id)) errors.delete(node.id);
642
- return output;
643
- } catch (e) {
644
- attempt += 1;
645
- if (attempt <= nodeMaxRetries) {
646
- if (delayMs) await new Promise((r) => setTimeout(r, delayMs));
647
- continue; // no error recorded yet
648
- }
649
- keepRetrying = false;
650
- // FINAL failure path
651
- const deps = [...(fwd.get(node.id) || [])];
652
- const suffix = deps.length
653
- ? `. This affects downstream nodes: ${deps.join(', ')}`
654
- : '';
655
- const nodeErr = new Error(
656
- `Node ${node.id} execution failed: ${e.message}${suffix}`,
657
- );
658
- nodeErr.nodeId = node.id;
659
- nodeErr.timestamp = Date.now();
660
- nodeErr.cause = e;
661
-
662
- // Handle non-critical failures with warnings and no result nullification
663
- if (isNonCritical || (continueOnError && !requiredIds.has(node.id))) {
664
- console.warn(`Non-critical node failure: ${nodeErr.message}`);
665
- errors.set(node.id, nodeErr);
666
- // Don't set result for failed optional nodes - return special symbol so they don't appear in results
667
- return Symbol('failed-optional-node');
668
- }
669
- errors.set(node.id, nodeErr);
670
- throw nodeErr;
671
- }
672
- }
381
+ return this.scheduler.executeNode(node, context);
673
382
  }
674
383
 
675
384
  /**
@@ -678,59 +387,7 @@ class DAG {
678
387
  * @returns {Array} - Array of warnings (empty if no issues)
679
388
  */
680
389
  validateTopology(_options = {}) {
681
- const { strict = true } = _options;
682
- const warnings = [];
683
-
684
- // Check for empty DAG
685
- if (this.nodes.size === 0) {
686
- throw new Error('DAG cannot be empty');
687
- }
688
-
689
- // Check for self-loops
690
- for (const node of this.nodes.values()) {
691
- if (node.outputs.includes(node)) {
692
- throw new Error('Self-loop detected');
693
- }
694
- }
695
-
696
- // Check for cycles by attempting topological sort
697
- try {
698
- this.topoSort();
699
- } catch (error) {
700
- if (error.message.includes('Cycle detected')) {
701
- // Extract cycle information from error message or use error.cycle if available
702
- let nodes = error.cycle;
703
- if (!nodes && error.message.includes('involving node:')) {
704
- // Parse cycle from message if cycle array not available
705
- const match = error.message.match(/involving node: (.+)/);
706
- if (match) {
707
- nodes = match[1].split(' -> ');
708
- }
709
- }
710
- if (!nodes) {
711
- nodes = ['unknown'];
712
- }
713
-
714
- const err = new Error('Cycle detected in DAG');
715
- err.cycle = nodes;
716
- throw err;
717
- }
718
- throw error;
719
- }
720
-
721
- // Check for isolated nodes
722
- for (const node of this.nodes.values()) {
723
- if (node.inputs.length === 0 && node.outputs.length === 0) {
724
- const message = `Orphaned node detected: ${node.id}`;
725
- if (strict) {
726
- throw new Error(message);
727
- } else {
728
- warnings.push(message);
729
- }
730
- }
731
- }
732
-
733
- return warnings;
390
+ return validateTopology(this.nodes, _options);
734
391
  }
735
392
 
736
393
  /**
@@ -739,62 +396,7 @@ class DAG {
739
396
  * @throws {Error} If cycles are detected
740
397
  */
741
398
  topoSort() {
742
- const order = [];
743
- const visited = new Set();
744
- const visiting = new Set(); // Track nodes currently being visited
745
-
746
- const visit = (node, path = []) => {
747
- if (visiting.has(node.id)) {
748
- const cycleStart = path.indexOf(node.id);
749
- let cyclePath =
750
- cycleStart >= 0
751
- ? path.slice(cycleStart).concat([node.id])
752
- : [node.id];
753
- // Reverse interior to show forward traversal order (A->B->C->A)
754
- if (cyclePath.length > 2) {
755
- const startNode = cyclePath[0];
756
- const restNodes = cyclePath.slice(1, -1).reverse();
757
- cyclePath = [startNode, ...restNodes, startNode];
758
- }
759
- const cyclePathString = cyclePath.join(' -> ');
760
- const error = new Error(
761
- `Cycle detected involving node: ${cyclePathString}`,
762
- );
763
- error.cycle = cyclePath; // Array format for test assertions
764
- throw error;
765
- }
766
-
767
- if (visited.has(node.id)) return;
768
-
769
- visiting.add(node.id);
770
- const newPath = [...path, node.id];
771
-
772
- try {
773
- node.inputs.forEach((input) => visit(input, newPath));
774
- } catch (error) {
775
- if (error.message.includes('Cycle detected')) {
776
- throw error; // Preserve original message and cycle property
777
- }
778
- throw error;
779
- }
780
-
781
- visiting.delete(node.id);
782
- visited.add(node.id);
783
- order.push(node);
784
- };
785
-
786
- try {
787
- for (const node of this.nodes.values()) {
788
- visit(node);
789
- }
790
- } catch (error) {
791
- if (error.message.includes('Cycle detected')) {
792
- throw error;
793
- }
794
- throw new Error(`DAG topological sort failed: ${error.message}`);
795
- }
796
-
797
- return order;
399
+ return topologicalSort(this.nodes);
798
400
  }
799
401
 
800
402
  /**
@@ -0,0 +1,126 @@
1
+ # Development Tools
2
+
3
+ This directory contains development tools for the RAG Pipeline Utils library.
4
+
5
+ ## Available Tools
6
+
7
+ ### Hot Reload Manager (`hot-reload.js`)
8
+
9
+ Provides automatic reloading of plugins and configuration files during development.
10
+
11
+ **Features:**
12
+
13
+ - File watching with chokidar
14
+ - Plugin hot-reloading with module cache clearing
15
+ - Configuration hot-reloading with validation
16
+ - State preservation and restoration
17
+ - Lifecycle hooks (beforeReload, afterReload, onError, etc.)
18
+ - Statistics tracking and history
19
+ - Error handling with notifications
20
+
21
+ **Usage:**
22
+
23
+ ```javascript
24
+ const { createHotReloadManager } = require("./hot-reload");
25
+
26
+ const hotReload = createHotReloadManager({
27
+ watchPaths: ["./src/plugins"],
28
+ configPath: ".ragrc.json",
29
+ verbose: true,
30
+ });
31
+
32
+ await hotReload.start();
33
+ ```
34
+
35
+ ### Development Server (`dev-server.js`)
36
+
37
+ Integrated development server with hot reload support.
38
+
39
+ **Features:**
40
+
41
+ - Hot reload integration
42
+ - Real-time notifications
43
+ - Statistics dashboard
44
+ - Event-driven architecture
45
+ - Graceful shutdown
46
+ - Pipeline integration
47
+
48
+ **Usage:**
49
+
50
+ ```javascript
51
+ const { createDevServer } = require("./dev-server");
52
+
53
+ const devServer = createDevServer({
54
+ port: 3000,
55
+ watchPaths: ["./src/plugins"],
56
+ autoReload: true,
57
+ });
58
+
59
+ await devServer.start();
60
+ ```
61
+
62
+ ## Examples
63
+
64
+ See `examples/hot-reload-usage.js` for comprehensive examples including:
65
+
66
+ 1. Basic hot reload setup
67
+ 2. Integration with plugin registry
68
+ 3. Development server usage
69
+ 4. Pipeline integration
70
+ 5. Custom state preservation
71
+ 6. Manual reload triggering
72
+ 7. Configuration validation
73
+ 8. Complete development workflows
74
+
75
+ ## Documentation
76
+
77
+ Full documentation is available at `docs/hot-reload.md`.
78
+
79
+ ## Testing
80
+
81
+ Tests are located in `__tests__/dev/hot-reload.test.js`.
82
+
83
+ Run tests:
84
+
85
+ ```bash
86
+ npm test -- __tests__/dev/hot-reload.test.js
87
+ ```
88
+
89
+ ## API Reference
90
+
91
+ ### Hot Reload Manager
92
+
93
+ - `start()` - Start watching for changes
94
+ - `stop()` - Stop watching
95
+ - `triggerReload(filePath, type)` - Manually trigger reload
96
+ - `registerHook(hookName, callback)` - Register lifecycle hook
97
+ - `getStats()` - Get reload statistics
98
+ - `getHistory()` - Get reload history
99
+
100
+ ### Development Server
101
+
102
+ - `start()` - Start dev server
103
+ - `stop()` - Stop dev server
104
+ - `getStatus()` - Get server status
105
+ - `getStatistics()` - Get formatted statistics
106
+ - `triggerReload(filePath, type)` - Manually trigger reload
107
+ - `registerHook(hookName, callback)` - Register lifecycle hook
108
+
109
+ ## Events
110
+
111
+ Both tools emit various events:
112
+
113
+ - `started` / `stopped`
114
+ - `plugin:reloaded` / `plugin:reload-failed`
115
+ - `config:reloaded` / `config:reload-failed`
116
+ - `plugin:added` / `plugin:removed`
117
+ - `error`
118
+ - `notification` (DevServer only)
119
+
120
+ ## Dependencies
121
+
122
+ - `chokidar` - File system watching
123
+
124
+ ## License
125
+
126
+ GPL-3.0