@ai.ntellect/core 0.6.17 → 0.6.20

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 (79) hide show
  1. package/.mocharc.json +1 -2
  2. package/README.md +123 -178
  3. package/dist/graph/controller.js +29 -6
  4. package/dist/graph/index.js +302 -62
  5. package/dist/index.js +21 -6
  6. package/dist/interfaces/index.js +15 -0
  7. package/dist/modules/agenda/adapters/node-cron/index.js +29 -0
  8. package/dist/modules/agenda/index.js +140 -0
  9. package/dist/{services/embedding.js → modules/embedding/adapters/ai/index.js} +24 -7
  10. package/dist/modules/embedding/index.js +59 -0
  11. package/dist/modules/memory/adapters/in-memory/index.js +210 -0
  12. package/dist/{memory → modules/memory}/adapters/meilisearch/index.js +97 -2
  13. package/dist/{memory → modules/memory}/adapters/redis/index.js +77 -15
  14. package/dist/modules/memory/index.js +103 -0
  15. package/dist/utils/{stringifiy-zod-schema.js → generate-action-schema.js} +5 -5
  16. package/graph/controller.ts +46 -35
  17. package/graph/index.ts +534 -102
  18. package/graph.ts +74 -0
  19. package/index.ts +25 -7
  20. package/interfaces/index.ts +353 -27
  21. package/modules/agenda/adapters/node-cron/index.ts +25 -0
  22. package/modules/agenda/index.ts +159 -0
  23. package/modules/embedding/adapters/ai/index.ts +42 -0
  24. package/modules/embedding/index.ts +45 -0
  25. package/modules/memory/adapters/in-memory/index.ts +203 -0
  26. package/{memory → modules/memory}/adapters/meilisearch/index.ts +114 -12
  27. package/modules/memory/adapters/redis/index.ts +164 -0
  28. package/modules/memory/index.ts +93 -0
  29. package/package.json +3 -1
  30. package/test/graph/index.test.ts +578 -0
  31. package/test/modules/agenda/node-cron.test.ts +286 -0
  32. package/test/modules/embedding/ai.test.ts +78 -0
  33. package/test/modules/memory/adapters/in-memory.test.ts +153 -0
  34. package/test/{memory → modules/memory}/adapters/meilisearch.test.ts +79 -75
  35. package/test/modules/memory/adapters/redis.test.ts +169 -0
  36. package/test/modules/memory/base.test.ts +230 -0
  37. package/test/services/agenda.test.ts +279 -280
  38. package/types/index.ts +93 -202
  39. package/utils/{stringifiy-zod-schema.ts → generate-action-schema.ts} +3 -3
  40. package/app/README.md +0 -36
  41. package/app/app/favicon.ico +0 -0
  42. package/app/app/globals.css +0 -21
  43. package/app/app/gun.ts +0 -0
  44. package/app/app/layout.tsx +0 -18
  45. package/app/app/page.tsx +0 -321
  46. package/app/eslint.config.mjs +0 -16
  47. package/app/next.config.ts +0 -7
  48. package/app/package-lock.json +0 -5912
  49. package/app/package.json +0 -31
  50. package/app/pnpm-lock.yaml +0 -4031
  51. package/app/postcss.config.mjs +0 -8
  52. package/app/public/file.svg +0 -1
  53. package/app/public/globe.svg +0 -1
  54. package/app/public/next.svg +0 -1
  55. package/app/public/vercel.svg +0 -1
  56. package/app/public/window.svg +0 -1
  57. package/app/tailwind.config.ts +0 -18
  58. package/app/tsconfig.json +0 -27
  59. package/dist/memory/index.js +0 -9
  60. package/dist/services/agenda.js +0 -115
  61. package/dist/services/queue.js +0 -142
  62. package/dist/utils/experimental-graph-rag.js +0 -152
  63. package/dist/utils/generate-object.js +0 -111
  64. package/dist/utils/inject-actions.js +0 -16
  65. package/dist/utils/queue-item-transformer.js +0 -24
  66. package/dist/utils/sanitize-results.js +0 -60
  67. package/memory/adapters/redis/index.ts +0 -103
  68. package/memory/index.ts +0 -22
  69. package/services/agenda.ts +0 -118
  70. package/services/embedding.ts +0 -26
  71. package/services/queue.ts +0 -145
  72. package/test/memory/adapters/redis.test.ts +0 -159
  73. package/test/memory/base.test.ts +0 -225
  74. package/test/services/queue.test.ts +0 -286
  75. package/utils/experimental-graph-rag.ts +0 -170
  76. package/utils/generate-object.ts +0 -117
  77. package/utils/inject-actions.ts +0 -19
  78. package/utils/queue-item-transformer.ts +0 -38
  79. package/utils/sanitize-results.ts +0 -66
package/graph/index.ts CHANGED
@@ -1,193 +1,625 @@
1
- import { GraphConfig, GraphContext, GraphDefinition, Node } from "../types";
2
- import EventEmitter from "events";
1
+ import { EventEmitter } from "events";
2
+ import { IEventEmitter } from "interfaces";
3
3
  import { ZodSchema } from "zod";
4
+ import { GraphContext, GraphDefinition, Node } from "../types";
4
5
 
5
- export class Graph<T extends ZodSchema> {
6
- private nodes: Map<string, Node<T>>;
6
+ /**
7
+ * @module GraphFlow
8
+ * @description A flexible workflow engine that manages the execution of nodes in a graph-like structure.
9
+ *
10
+ * Key features:
11
+ * - Multiple branches support
12
+ * - Conditional branching (runs first matching condition, or all if none have conditions)
13
+ * - Event-driven nodes
14
+ * - Zod validation of context/inputs/outputs
15
+ * - Automatic retry on node failures
16
+ *
17
+ * @template T - Extends ZodSchema for type validation
18
+ */
19
+ export class GraphFlow<T extends ZodSchema> {
20
+ private nodes: Map<string, Node<T, any>>;
7
21
  private context: GraphContext<T>;
8
22
  public validator?: T;
9
- private eventEmitter: EventEmitter;
23
+ private eventEmitter: IEventEmitter;
10
24
  private globalErrorHandler?: (error: Error, context: GraphContext<T>) => void;
25
+ private graphEvents?: string[];
26
+ private entryNode?: string;
27
+ private logs: string[] = [];
28
+ private verbose: boolean = false;
29
+
30
+ /**
31
+ * Creates a new instance of GraphFlow
32
+ * @param {string} name - The name of the graph flow
33
+ * @param {GraphDefinition<T>} config - Configuration object containing nodes, schema, context, and error handlers
34
+ * @param {Object} options - Optional options for the graph flow
35
+ */
36
+ constructor(
37
+ public name: string,
38
+ config: GraphDefinition<T>,
39
+ options: { verbose?: boolean } = {}
40
+ ) {
41
+ this.nodes = new Map(
42
+ config.nodes.map((node: Node<T, any>) => [node.name, node])
43
+ );
44
+ this.validator = config.schema;
45
+ this.context = config.schema.parse(config.context) as GraphContext<T>;
46
+ this.globalErrorHandler = config.onError;
47
+ this.eventEmitter =
48
+ config.eventEmitter || (new EventEmitter() as IEventEmitter);
49
+ this.graphEvents = config.events;
50
+ this.verbose = options.verbose ?? false;
11
51
 
12
- constructor(public name: string, config: GraphConfig<T>) {
13
- this.nodes = new Map(config.nodes.map((node) => [node.name, node]));
14
- this.context = config.initialContext || ({} as GraphContext<T>);
15
- this.validator = config.validator;
16
- this.globalErrorHandler = config.globalErrorHandler;
17
- this.eventEmitter = new EventEmitter();
18
52
  this.setupEventListeners();
53
+ this.setupGraphEventListeners();
19
54
  }
20
55
 
56
+ /**
57
+ * Creates a new context for execution
58
+ * @private
59
+ * @returns {GraphContext<T>} A cloned context to prevent pollution during parallel execution
60
+ */
21
61
  private createNewContext(): GraphContext<T> {
22
62
  return structuredClone(this.context);
23
63
  }
24
64
 
65
+ /**
66
+ * Sets up event listeners for node-based events
67
+ * @private
68
+ * @description Attaches all node-based event triggers while preserving external listeners
69
+ */
25
70
  private setupEventListeners(): void {
71
+ // First remove only the existing node-based listeners that we might have created previously
72
+ // We do NOT remove, for example, "nodeStarted" or "nodeCompleted" listeners that test code added.
73
+ for (const [eventName, listener] of this.eventEmitter
74
+ .rawListeners("*")
75
+ .entries()) {
76
+ // This can be tricky—EventEmitter doesn't directly let you remove by "type" of listener.
77
+ // Alternatively, we can store references in a separate structure.
78
+ // For simplicity, let's do a full removeAllListeners() on node-specified events (only),
79
+ // then re-add them below, but keep the test-based events like "nodeStarted" or "nodeCompleted".
80
+ }
81
+
82
+ // The simplest approach: removeAllListeners for each event that is declared as a node event
83
+ // so we don't stack up duplicates:
84
+ const allEvents = new Set<string>();
26
85
  for (const node of this.nodes.values()) {
27
- node.events?.forEach((event) => {
28
- this.eventEmitter.on(event, async (data?: Partial<GraphContext<T>>) => {
29
- const context = this.createNewContext();
30
- if (data) Object.assign(context, data);
31
- await this.executeNode(node.name, context);
86
+ if (node.events) {
87
+ node.events.forEach((evt) => allEvents.add(evt));
88
+ }
89
+ }
90
+ for (const evt of allEvents) {
91
+ // remove only those events that are used by nodes
92
+ this.eventEmitter.removeAllListeners(evt);
93
+ }
94
+
95
+ // Now re-add the node-based event triggers
96
+ for (const node of this.nodes.values()) {
97
+ if (node.events && node.events.length > 0) {
98
+ node.events.forEach((event) => {
99
+ this.eventEmitter.on(
100
+ event,
101
+ async (data?: Partial<GraphContext<T>>) => {
102
+ const freshContext = this.createNewContext();
103
+ if (data) Object.assign(freshContext, data);
104
+
105
+ // If triggered by an event, we pass "true" so event-driven node will skip `next`.
106
+ await this.executeNode(
107
+ node.name,
108
+ freshContext,
109
+ undefined,
110
+ /* triggeredByEvent= */ true
111
+ );
112
+ }
113
+ );
32
114
  });
33
- });
115
+ }
116
+ }
117
+ }
118
+
119
+ private addLog(message: string): void {
120
+ const logMessage = `[${new Date().toISOString()}] ${message}`;
121
+ this.logs.push(logMessage);
122
+ if (this.verbose) {
123
+ console.log(`[${this.name}] ${message}`);
34
124
  }
35
125
  }
126
+
127
+ /**
128
+ * Enable or disable verbose logging
129
+ * @param {boolean} enabled - Whether to enable verbose logging
130
+ */
131
+ public setVerbose(enabled: boolean): void {
132
+ this.verbose = enabled;
133
+ }
134
+
135
+ /**
136
+ * Get current verbose setting
137
+ * @returns {boolean} Current verbose setting
138
+ */
139
+ public isVerbose(): boolean {
140
+ return this.verbose;
141
+ }
142
+
143
+ /**
144
+ * Executes a specific node in the graph
145
+ * @private
146
+ * @param {string} nodeName - Name of the node to execute
147
+ * @param {GraphContext<T>} context - Current execution context
148
+ * @param {any} inputs - Input parameters for the node
149
+ * @param {boolean} triggeredByEvent - Whether the execution was triggered by an event
150
+ * @returns {Promise<void>}
151
+ */
36
152
  private async executeNode(
37
153
  nodeName: string,
38
154
  context: GraphContext<T>,
39
- params?: any
155
+ inputs: any,
156
+ triggeredByEvent: boolean = false
40
157
  ): Promise<void> {
41
158
  const node = this.nodes.get(nodeName);
42
- if (!node) throw new Error(`❌ Node ${nodeName} not found`);
159
+ if (!node) throw new Error(`Node "${nodeName}" not found.`);
43
160
 
44
- if (node.condition && !node.condition(context)) return;
161
+ this.addLog(`🚀 Starting node "${nodeName}"`);
162
+ this.eventEmitter.emit("nodeStarted", { name: nodeName });
45
163
 
46
- let attempts = 0;
47
- const maxAttempts = node.retry?.maxAttempts || 1;
48
- const delay = node.retry?.delay || 0;
164
+ try {
165
+ const localContext = structuredClone(context);
49
166
 
50
- while (attempts < maxAttempts) {
51
- try {
52
- let validatedParams;
167
+ if (node.condition && !node.condition(localContext)) {
168
+ this.addLog(`⏭️ Skipping node "${nodeName}" - condition not met`);
169
+ return;
170
+ }
53
171
 
54
- // Si le nœud a un `parameters`, on valide `params` avant exécution
55
- if (node.parameters) {
56
- if (!params) {
57
- throw new Error(
58
- `❌ Paramètres requis pour le nœud "${nodeName}" mais reçus: ${params}`
59
- );
60
- }
61
- validatedParams = node.parameters.parse(params);
172
+ // Validate inputs
173
+ if (node.inputs) {
174
+ if (!inputs) {
175
+ this.addLog(`❌ Missing required inputs for node "${nodeName}"`);
176
+ throw new Error(`Inputs required for node "${nodeName}"`);
62
177
  }
178
+ this.addLog(`📥 Validating inputs for node "${nodeName}"`);
179
+ inputs = node.inputs.parse(inputs);
180
+ }
63
181
 
64
- this.eventEmitter.emit("nodeStarted", { name: nodeName, context });
65
- if (node.execute) {
66
- await node.execute(context);
67
- } else if (node.executeWithParams) {
68
- if (!validatedParams) {
69
- throw new Error(
70
- `❌ Paramètres invalides pour le nœud "${nodeName}"`
71
- );
182
+ // Handle retry logic
183
+ if (node.retry && node.retry.maxAttempts > 0) {
184
+ let attempts = 0;
185
+ let lastError: Error | null = null;
186
+
187
+ while (attempts < node.retry.maxAttempts) {
188
+ try {
189
+ this.addLog(`🔄 Attempt ${attempts + 1}/${node.retry.maxAttempts}`);
190
+ await node.execute(localContext, inputs);
191
+ lastError = null;
192
+ break;
193
+ } catch (error: any) {
194
+ lastError = error as Error;
195
+ attempts++;
196
+ this.addLog(`❌ Attempt ${attempts} failed: ${error.message}`);
197
+
198
+ if (attempts === node.retry.maxAttempts) {
199
+ // Si toutes les tentatives ont échoué et qu'il y a un gestionnaire d'échec
200
+ if (node.retry.onRetryFailed) {
201
+ this.addLog(
202
+ `🔄 Executing retry failure handler for node "${nodeName}"`
203
+ );
204
+ try {
205
+ await node.retry.onRetryFailed(lastError, localContext);
206
+ // Si le gestionnaire d'échec réussit, on continue l'exécution
207
+ // SEULEMENT si le gestionnaire a explicitement retourné true
208
+ if (node.retry.continueOnFailed) {
209
+ this.addLog(
210
+ `✅ Retry failure handler succeeded for node "${nodeName}" - continuing execution`
211
+ );
212
+ break;
213
+ } else {
214
+ this.addLog(
215
+ `⚠️ Retry failure handler executed but node "${nodeName}" will still fail`
216
+ );
217
+ throw lastError;
218
+ }
219
+ } catch (handlerError: any) {
220
+ this.addLog(
221
+ `❌ Retry failure handler failed for node "${nodeName}": ${handlerError.message}`
222
+ );
223
+ throw handlerError;
224
+ }
225
+ }
226
+ // Si pas de gestionnaire d'échec ou si le gestionnaire a échoué
227
+ throw lastError;
228
+ }
229
+
230
+ if (attempts < node.retry.maxAttempts) {
231
+ this.addLog(
232
+ `⏳ Waiting ${node.retry.delay}ms before next attempt`
233
+ );
234
+ await new Promise((resolve) =>
235
+ setTimeout(resolve, node.retry?.delay || 0)
236
+ );
237
+ }
72
238
  }
73
- await node.executeWithParams(context, validatedParams);
74
239
  }
240
+ } else {
241
+ await node.execute(localContext, inputs);
242
+ }
75
243
 
76
- this.validateContext(context);
244
+ // Validate outputs
245
+ if (node.outputs) {
246
+ this.addLog(`📤 Validating outputs for node "${nodeName}"`);
247
+ node.outputs.parse(localContext);
248
+ }
77
249
 
78
- this.eventEmitter.emit("nodeCompleted", { name: nodeName, context });
250
+ Object.assign(context, localContext);
79
251
 
80
- if (node.next) {
81
- await Promise.all(
82
- node.next.map((nextNode) => this.executeNode(nextNode, context))
83
- );
84
- }
85
- return;
86
- } catch (error) {
87
- attempts++;
88
-
89
- if (attempts >= maxAttempts) {
90
- this.eventEmitter.emit("nodeError", { nodeName, error });
91
- node.onError?.(error as Error);
92
- this.globalErrorHandler?.(error as Error, context);
93
- throw error;
94
- }
252
+ this.addLog(
253
+ `✅ Node "${nodeName}" executed successfully ${JSON.stringify(context)}`
254
+ );
255
+ this.eventEmitter.emit("nodeCompleted", { name: nodeName });
95
256
 
96
- console.warn(
97
- `[Graph ${this.name}] Retry attempt ${attempts} for node ${nodeName}`,
98
- { error }
257
+ // Handle waitForEvent
258
+ if (node.waitForEvent && !triggeredByEvent) {
259
+ this.addLog(
260
+ `⏳ Node "${nodeName}" waiting for events: ${node.events?.join(", ")}`
99
261
  );
100
262
 
101
- await new Promise((resolve) => setTimeout(resolve, delay));
263
+ await new Promise<void>((resolve) => {
264
+ const eventHandler = () => {
265
+ this.addLog(`🚀 Event received for node "${nodeName}"`);
266
+ resolve();
267
+ };
268
+
269
+ node.events?.forEach((event) => {
270
+ this.eventEmitter.once(event, eventHandler);
271
+ });
272
+ });
273
+
274
+ const nextNodes =
275
+ typeof node.next === "function"
276
+ ? node.next(context)
277
+ : node.next || [];
278
+
279
+ if (nextNodes.length > 0) {
280
+ this.addLog(`➡️ Executing next nodes: ${nextNodes.join(", ")}`);
281
+
282
+ // Créer un contexte unique pour toutes les branches
283
+ const branchContext = structuredClone(context);
284
+
285
+ // Exécuter les branches séquentiellement avec le même contexte
286
+ for (const nextNodeName of nextNodes) {
287
+ this.addLog(`🔄 Starting branch for node "${nextNodeName}"`);
288
+ const nextNode = this.nodes.get(nextNodeName);
289
+ if (nextNode) {
290
+ // Utiliser le même contexte pour toutes les branches
291
+ await this.executeNode(
292
+ nextNodeName,
293
+ branchContext,
294
+ undefined,
295
+ nextNode.waitForEvent
296
+ );
297
+ }
298
+ this.addLog(`✅ Branch "${nextNodeName}" completed`);
299
+ }
300
+
301
+ // Mettre à jour le contexte global avec le résultat final des branches
302
+ Object.assign(context, branchContext);
303
+ this.context = structuredClone(context);
304
+
305
+ this.eventEmitter.emit("graphCompleted", {
306
+ name: this.name,
307
+ context: this.context,
308
+ });
309
+
310
+ return;
311
+ }
312
+ }
313
+
314
+ // Execute next nodes
315
+ const nextNodes =
316
+ typeof node.next === "function"
317
+ ? node.next(localContext)
318
+ : node.next || [];
319
+
320
+ if (nextNodes.length > 0) {
321
+ this.addLog(`➡️ Executing next nodes: ${nextNodes.join(", ")}`);
322
+
323
+ // Créer un contexte unique pour toutes les branches
324
+ const branchContext = structuredClone(context);
325
+
326
+ // Exécuter les branches séquentiellement avec le même contexte
327
+ for (const nextNodeName of nextNodes) {
328
+ this.addLog(`🔄 Starting branch for node "${nextNodeName}"`);
329
+ const nextNode = this.nodes.get(nextNodeName);
330
+ if (nextNode) {
331
+ // Utiliser le même contexte pour toutes les branches
332
+ await this.executeNode(
333
+ nextNodeName,
334
+ branchContext,
335
+ undefined,
336
+ nextNode.waitForEvent
337
+ );
338
+ }
339
+ this.addLog(`✅ Branch "${nextNodeName}" completed`);
340
+ }
341
+
342
+ // Mettre à jour le contexte global avec le résultat final des branches
343
+ Object.assign(context, branchContext);
344
+ this.context = structuredClone(context);
102
345
  }
346
+ } catch (error: any) {
347
+ this.addLog(`❌ Error in node "${nodeName}": ${error.message}`);
348
+ this.eventEmitter.emit("nodeError", { name: nodeName, error });
349
+ throw error;
103
350
  }
104
351
  }
105
352
 
353
+ /**
354
+ * Validates the current context against the schema
355
+ * @private
356
+ * @param {GraphContext<T>} context - Context to validate
357
+ * @throws {Error} If validation fails
358
+ */
106
359
  private validateContext(context: GraphContext<T>): void {
107
360
  if (this.validator) {
108
361
  this.validator.parse(context);
109
362
  }
110
363
  }
111
364
 
365
+ /**
366
+ * Executes the graph flow starting from a specific node
367
+ * @param {string} startNode - Name of the node to start execution from
368
+ * @param {Partial<GraphContext<T>>} inputContext - Optional partial context to merge with current context
369
+ * @param {any} inputParams - Optional input parameters for the start node
370
+ * @returns {Promise<GraphContext<T>>} Final context after execution
371
+ */
112
372
  async execute(
113
373
  startNode: string,
114
- inputContext?: Partial<GraphContext<T>>,
115
- inputParams?: any
374
+ inputParams?: any,
375
+ inputContext?: Partial<GraphContext<T>>
116
376
  ): Promise<GraphContext<T>> {
117
- const context = this.createNewContext();
118
- if (inputContext) Object.assign(context, inputContext);
377
+ if (inputParams) {
378
+ // Merge inputParams into context
379
+ Object.assign(this.context, inputParams);
380
+ }
381
+
382
+ if (inputContext) {
383
+ Object.assign(this.context, inputContext);
384
+ }
119
385
 
120
386
  this.eventEmitter.emit("graphStarted", { name: this.name });
387
+
121
388
  try {
122
- await this.executeNode(startNode, context, inputParams);
123
- this.eventEmitter.emit("graphCompleted", { name: this.name, context });
124
- return context;
389
+ await this.executeNode(startNode, this.context, inputParams, false);
390
+
391
+ this.eventEmitter.emit("graphCompleted", {
392
+ name: this.name,
393
+ context: this.context,
394
+ });
395
+
396
+ return this.getContext();
125
397
  } catch (error) {
126
398
  this.eventEmitter.emit("graphError", { name: this.name, error });
127
- this.globalErrorHandler?.(error as Error, context); // Gestionnaire d'erreurs global
399
+ this.globalErrorHandler?.(error as Error, this.context);
128
400
  throw error;
129
401
  }
130
402
  }
131
403
 
132
- emit(
404
+ /**
405
+ * Emits an event to trigger event-based nodes
406
+ * @param {string} eventName - Name of the event to emit
407
+ * @param {Partial<GraphContext<T>>} data - Optional data to merge with context
408
+ * @returns {Promise<GraphContext<T>>} Updated context after event handling
409
+ */
410
+ public async emit(
133
411
  eventName: string,
134
412
  data?: Partial<GraphContext<T>>
135
413
  ): Promise<GraphContext<T>> {
136
- return new Promise((resolve, reject) => {
137
- if (data) Object.assign(this.context, data); // ✅ Met à jour le contexte global
414
+ const workingContext = structuredClone(this.context);
138
415
 
139
- this.eventEmitter.emit(eventName, this.context); // Utilise le contexte global
416
+ if (data) {
417
+ Object.assign(workingContext, data);
418
+ }
140
419
 
141
- const eventNodes = Array.from(this.nodes.values()).filter((node) =>
142
- node.events?.includes(eventName)
143
- );
144
- if (eventNodes.length === 0) return resolve(this.context);
145
-
146
- Promise.all(
147
- eventNodes.map(
148
- (node) =>
149
- new Promise<void>((resolve) => {
150
- this.eventEmitter.once("nodeCompleted", ({ nodeName }) => {
151
- if (nodeName === node.name) resolve();
152
- });
153
- })
154
- )
155
- )
156
- .then(() => resolve(this.context))
157
- .catch(reject);
420
+ const eventNodes = Array.from(this.nodes.values()).filter((node) =>
421
+ node.events?.includes(eventName)
422
+ );
423
+
424
+ // Execute event nodes sequentially with shared context
425
+ for (const node of eventNodes) {
426
+ await this.executeNode(node.name, workingContext, undefined, true);
427
+ }
428
+
429
+ // Update global context after all event nodes are executed
430
+ this.context = structuredClone(workingContext);
431
+
432
+ this.eventEmitter.emit("graphCompleted", {
433
+ name: this.name,
434
+ context: this.context,
158
435
  });
436
+
437
+ return this.getContext();
159
438
  }
160
439
 
440
+ /**
441
+ * Registers an event handler
442
+ * @param {string} eventName - Name of the event to listen for
443
+ * @param {Function} handler - Handler function to execute when event is emitted
444
+ */
161
445
  on(eventName: string, handler: (...args: any[]) => void): void {
162
446
  this.eventEmitter.on(eventName, handler);
163
447
  }
164
448
 
165
- loadDefinition(definition: GraphDefinition<T>): void {
449
+ /**
450
+ * Updates the graph definition with new configuration
451
+ * @param {GraphDefinition<T>} definition - New graph definition
452
+ */
453
+ load(definition: GraphDefinition<T>): void {
454
+ // Clear all existing nodes
166
455
  this.nodes.clear();
167
- Object.values(definition.nodes).forEach((node) =>
168
- this.nodes.set(node.name, node)
169
- );
170
- this.setupEventListeners();
456
+ // Wipe out old node-based event listeners
457
+ // (We keep external test listeners like "nodeStarted" or "nodeCompleted".)
458
+ if (definition.nodes?.length) {
459
+ const allEvents = new Set<string>();
460
+ definition.nodes.forEach((n) =>
461
+ n.events?.forEach((evt) => allEvents.add(evt))
462
+ );
463
+ for (const evt of allEvents) {
464
+ this.eventEmitter.removeAllListeners(evt);
465
+ }
466
+ }
467
+
468
+ // Add in new nodes
469
+ definition.nodes.forEach((node) => this.nodes.set(node.name, node));
470
+
471
+ // Parse the new context
472
+ this.context = definition.schema.parse(
473
+ definition.context
474
+ ) as GraphContext<T>;
475
+ this.validator = definition.schema;
476
+
477
+ // Store entry node
478
+ this.entryNode = definition.entryNode;
479
+ // Store graph events
480
+ this.graphEvents = definition.events;
481
+
482
+ // Re-setup only node-based event triggers
483
+ for (const node of this.nodes.values()) {
484
+ if (node.events && node.events.length > 0) {
485
+ node.events.forEach((event) => {
486
+ this.eventEmitter.on(
487
+ event,
488
+ async (data?: Partial<GraphContext<T>>) => {
489
+ const freshContext = structuredClone(this.context);
490
+ if (data) Object.assign(freshContext, data);
491
+ await this.executeNode(node.name, freshContext, undefined, true);
492
+ }
493
+ );
494
+ });
495
+ }
496
+ }
497
+
498
+ // Re-setup graph event listeners
499
+ this.setupGraphEventListeners();
171
500
  }
172
501
 
173
- getContext(): GraphContext<T> {
502
+ /**
503
+ * Gets a copy of the current context
504
+ * @returns {GraphContext<T>} A deep copy of the current context
505
+ */
506
+ public getContext(): GraphContext<T> {
174
507
  return structuredClone(this.context);
175
508
  }
176
509
 
510
+ /**
511
+ * Logs a message with optional data
512
+ * @param {string} message - Message to log
513
+ * @param {any} data - Optional data to log
514
+ */
177
515
  log(message: string, data?: any): void {
178
516
  console.log(`[Graph ${this.name}] ${message}`, data);
179
517
  }
180
518
 
181
- addNode(node: Node<T>): void {
519
+ /**
520
+ * Adds a new node to the graph
521
+ * @param {Node<T>} node - Node to add
522
+ * @throws {Error} If node with same name already exists
523
+ */
524
+ addNode(node: Node<T, any>): void {
182
525
  this.nodes.set(node.name, node);
183
- this.setupEventListeners();
526
+ if (node.events && node.events.length > 0) {
527
+ for (const evt of node.events) {
528
+ this.eventEmitter.on(evt, async (data?: Partial<GraphContext<T>>) => {
529
+ const freshContext = this.createNewContext();
530
+ if (data) Object.assign(freshContext, data);
531
+ await this.executeNode(node.name, freshContext, undefined, true);
532
+ });
533
+ }
534
+ }
184
535
  }
185
536
 
537
+ /**
538
+ * Removes a node from the graph
539
+ * @param {string} nodeName - Name of the node to remove
540
+ */
186
541
  removeNode(nodeName: string): void {
542
+ const node = this.nodes.get(nodeName);
543
+ if (!node) return;
544
+
545
+ // remove the node from the map
187
546
  this.nodes.delete(nodeName);
547
+
548
+ // remove any of its event-based listeners
549
+ if (node.events && node.events.length > 0) {
550
+ for (const evt of node.events) {
551
+ // removeAllListeners(evt) would also remove other node listeners,
552
+ // so we need a more fine-grained approach. Ideally, we should keep a reference
553
+ // to the exact listener function we attached. For brevity, let's remove all for that event:
554
+ this.eventEmitter.removeAllListeners(evt);
555
+ }
556
+ // Then reattach the others that remain in the graph
557
+ for (const n of this.nodes.values()) {
558
+ if (n.events && n.events.length > 0) {
559
+ n.events.forEach((e) => {
560
+ this.eventEmitter.on(e, async (data?: Partial<GraphContext<T>>) => {
561
+ const freshContext = this.createNewContext();
562
+ if (data) Object.assign(freshContext, data);
563
+ await this.executeNode(n.name, freshContext, undefined, true);
564
+ });
565
+ });
566
+ }
567
+ }
568
+ }
188
569
  }
189
570
 
190
- getNodes(): Node<T>[] {
571
+ /**
572
+ * Returns all nodes in the graph
573
+ * @returns {Node<T>[]} Array of all nodes
574
+ */
575
+ getNodes(): Node<T, any>[] {
191
576
  return Array.from(this.nodes.values());
192
577
  }
578
+
579
+ private setupGraphEventListeners(): void {
580
+ if (this.graphEvents && this.graphEvents.length > 0) {
581
+ this.graphEvents.forEach((event) => {
582
+ this.eventEmitter.on(event, async (data?: Partial<GraphContext<T>>) => {
583
+ const freshContext = this.createNewContext();
584
+ if (data) Object.assign(freshContext, data);
585
+
586
+ // Emit "graphStarted"
587
+ this.eventEmitter.emit("graphStarted", { name: this.name });
588
+
589
+ try {
590
+ // Execute the graph starting from the entry node
591
+ if (!this.entryNode) {
592
+ throw new Error("No entry node defined for graph event handling");
593
+ }
594
+
595
+ await this.executeNode(
596
+ this.entryNode,
597
+ freshContext,
598
+ undefined,
599
+ false
600
+ );
601
+
602
+ // Emit "graphCompleted"
603
+ this.eventEmitter.emit("graphCompleted", {
604
+ name: this.name,
605
+ context: this.context,
606
+ });
607
+ } catch (error) {
608
+ // Emit "graphError"
609
+ this.eventEmitter.emit("graphError", { name: this.name, error });
610
+ this.globalErrorHandler?.(error as Error, freshContext);
611
+ throw error;
612
+ }
613
+ });
614
+ });
615
+ }
616
+ }
617
+
618
+ getLogs(): string[] {
619
+ return [...this.logs];
620
+ }
621
+
622
+ clearLogs(): void {
623
+ this.logs = [];
624
+ }
193
625
  }