@ai.ntellect/core 0.6.19 → 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.
@@ -15,17 +15,17 @@ export class GraphController {
15
15
  * @returns Map containing results of each graph execution, keyed by graph name and index
16
16
  * @template T - Zod schema type for graph context validation
17
17
  */
18
- static async executeSequential<T extends ZodSchema>(
19
- graphs: GraphFlow<T>[],
18
+ static async executeSequential<T extends ZodSchema[]>(
19
+ graphs: { [K in keyof T]: GraphFlow<T[K]> },
20
20
  startNodes: string[],
21
- inputContexts?: Partial<GraphContext<T>>[]
22
- ): Promise<Map<string, GraphContext<T>>> {
23
- const results = new Map<string, GraphContext<T>>();
21
+ inputs: any[]
22
+ ): Promise<any[]> {
23
+ const results = new Map<string, GraphContext<T[keyof T]>>();
24
24
  for (let i = 0; i < graphs.length; i++) {
25
- const result = await graphs[i].execute(startNodes[i], inputContexts?.[i]);
25
+ const result = await graphs[i].execute(startNodes[i], inputs[i]);
26
26
  results.set(`${graphs[i].name}-${i}`, result);
27
27
  }
28
- return results;
28
+ return Array.from(results.values());
29
29
  }
30
30
 
31
31
  /**
@@ -38,34 +38,25 @@ export class GraphController {
38
38
  * @returns Map containing results of each graph execution, keyed by graph name
39
39
  * @template T - Zod schema type for graph context validation
40
40
  */
41
- static async executeParallel<T extends ZodSchema>(
42
- graphs: GraphFlow<T>[],
41
+ static async executeParallel<T extends ZodSchema[]>(
42
+ graphs: { [K in keyof T]: GraphFlow<T[K]> },
43
43
  startNodes: string[],
44
- inputContexts?: Partial<GraphContext<T>>[],
45
- inputs?: any[],
46
- concurrencyLimit?: number
47
- ): Promise<Map<string, GraphContext<T>>> {
48
- const results = new Map<string, GraphContext<T>>();
49
-
50
- if (inputContexts) {
51
- inputContexts = inputContexts.map((ctx) => ctx || {});
52
- }
44
+ concurrency: number,
45
+ inputs: any[]
46
+ ): Promise<any[]> {
47
+ const results = new Map<string, GraphContext<T[keyof T]>>();
53
48
 
54
49
  if (inputs) {
55
50
  inputs = inputs.map((input) => input || {});
56
51
  }
57
52
 
58
- if (concurrencyLimit) {
59
- for (let i = 0; i < graphs.length; i += concurrencyLimit) {
53
+ if (concurrency) {
54
+ for (let i = 0; i < graphs.length; i += concurrency) {
60
55
  const batchResults = await Promise.all(
61
56
  graphs
62
- .slice(i, i + concurrencyLimit)
57
+ .slice(i, i + concurrency)
63
58
  .map((graph, index) =>
64
- graph.execute(
65
- startNodes[i + index],
66
- inputContexts?.[i + index] || {},
67
- inputs?.[i + index]
68
- )
59
+ graph.execute(startNodes[i + index], inputs?.[i + index])
69
60
  )
70
61
  );
71
62
  batchResults.forEach((result, index) => {
@@ -75,17 +66,13 @@ export class GraphController {
75
66
  } else {
76
67
  const allResults = await Promise.all(
77
68
  graphs.map((graph, index) =>
78
- graph.execute(
79
- startNodes[index],
80
- inputContexts?.[index] || {},
81
- inputs?.[index] || {}
82
- )
69
+ graph.execute(startNodes[index], inputs?.[index] || {})
83
70
  )
84
71
  );
85
72
  allResults.forEach((result, index) => {
86
73
  results.set(`${graphs[index].name}`, result);
87
74
  });
88
75
  }
89
- return results;
76
+ return Array.from(results.values());
90
77
  }
91
78
  }
package/graph/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from "events";
2
+ import { IEventEmitter } from "interfaces";
2
3
  import { ZodSchema } from "zod";
3
- import { IEventEmitter } from "../interfaces";
4
4
  import { GraphContext, GraphDefinition, Node } from "../types";
5
5
 
6
6
  /**
@@ -17,26 +17,37 @@ import { GraphContext, GraphDefinition, Node } from "../types";
17
17
  * @template T - Extends ZodSchema for type validation
18
18
  */
19
19
  export class GraphFlow<T extends ZodSchema> {
20
- private nodes: Map<string, Node<T>>;
20
+ private nodes: Map<string, Node<T, any>>;
21
21
  private context: GraphContext<T>;
22
22
  public validator?: T;
23
23
  private eventEmitter: IEventEmitter;
24
24
  private globalErrorHandler?: (error: Error, context: GraphContext<T>) => void;
25
25
  private graphEvents?: string[];
26
26
  private entryNode?: string;
27
+ private logs: string[] = [];
28
+ private verbose: boolean = false;
27
29
 
28
30
  /**
29
31
  * Creates a new instance of GraphFlow
30
32
  * @param {string} name - The name of the graph flow
31
33
  * @param {GraphDefinition<T>} config - Configuration object containing nodes, schema, context, and error handlers
34
+ * @param {Object} options - Optional options for the graph flow
32
35
  */
33
- constructor(public name: string, config: GraphDefinition<T>) {
34
- this.nodes = new Map(config.nodes.map((node) => [node.name, node]));
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
+ );
35
44
  this.validator = config.schema;
36
45
  this.context = config.schema.parse(config.context) as GraphContext<T>;
37
46
  this.globalErrorHandler = config.onError;
38
- this.eventEmitter = config.eventEmitter || new EventEmitter();
47
+ this.eventEmitter =
48
+ config.eventEmitter || (new EventEmitter() as IEventEmitter);
39
49
  this.graphEvents = config.events;
50
+ this.verbose = options.verbose ?? false;
40
51
 
41
52
  this.setupEventListeners();
42
53
  this.setupGraphEventListeners();
@@ -105,6 +116,30 @@ export class GraphFlow<T extends ZodSchema> {
105
116
  }
106
117
  }
107
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}`);
124
+ }
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
+
108
143
  /**
109
144
  * Executes a specific node in the graph
110
145
  * @private
@@ -117,96 +152,201 @@ export class GraphFlow<T extends ZodSchema> {
117
152
  private async executeNode(
118
153
  nodeName: string,
119
154
  context: GraphContext<T>,
120
- inputs?: any,
155
+ inputs: any,
121
156
  triggeredByEvent: boolean = false
122
157
  ): Promise<void> {
123
158
  const node = this.nodes.get(nodeName);
124
- if (!node) throw new Error(`❌ Node "${nodeName}" not found.`);
159
+ if (!node) throw new Error(`Node "${nodeName}" not found.`);
125
160
 
126
- if (node.condition && !node.condition(context)) {
127
- return;
128
- }
161
+ this.addLog(`🚀 Starting node "${nodeName}"`);
162
+ this.eventEmitter.emit("nodeStarted", { name: nodeName });
129
163
 
130
- let attempts = 0;
131
- const maxAttempts = node.retry?.maxAttempts || 1;
132
- const delay = node.retry?.delay || 0;
133
-
134
- while (attempts < maxAttempts) {
135
- try {
136
- let validatedInputs;
137
- if (node.inputs) {
138
- if (!inputs) {
139
- throw new Error(
140
- `❌ Inputs required for node "${nodeName}" but received: ${inputs}`
141
- );
142
- }
143
- validatedInputs = node.inputs.parse(inputs);
164
+ try {
165
+ const localContext = structuredClone(context);
166
+
167
+ if (node.condition && !node.condition(localContext)) {
168
+ this.addLog(`⏭️ Skipping node "${nodeName}" - condition not met`);
169
+ return;
170
+ }
171
+
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}"`);
144
177
  }
178
+ this.addLog(`📥 Validating inputs for node "${nodeName}"`);
179
+ inputs = node.inputs.parse(inputs);
180
+ }
145
181
 
146
- this.eventEmitter.emit("nodeStarted", { name: nodeName, context });
182
+ // Handle retry logic
183
+ if (node.retry && node.retry.maxAttempts > 0) {
184
+ let attempts = 0;
185
+ let lastError: Error | null = null;
147
186
 
148
- // Execute the node
149
- await node.execute(context, validatedInputs);
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
+ }
150
229
 
151
- if (node.outputs) {
152
- node.outputs.parse(context);
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
+ }
238
+ }
153
239
  }
240
+ } else {
241
+ await node.execute(localContext, inputs);
242
+ }
154
243
 
155
- this.validateContext(context);
156
- this.eventEmitter.emit("nodeCompleted", { name: nodeName, context });
244
+ // Validate outputs
245
+ if (node.outputs) {
246
+ this.addLog(`📤 Validating outputs for node "${nodeName}"`);
247
+ node.outputs.parse(localContext);
248
+ }
157
249
 
158
- // IMPORTANT: Si le nœud est déclenché par un événement et a des événements définis,
159
- // on arrête ici et on ne suit pas la chaîne next
160
- if (triggeredByEvent && node.events && node.events.length > 0) {
161
- this.context = structuredClone(context);
162
- return;
163
- }
250
+ Object.assign(context, localContext);
164
251
 
165
- // Gérer les nœuds suivants
166
- if (node.next && node.next.length > 0) {
167
- const branchContexts: GraphContext<T>[] = [];
252
+ this.addLog(
253
+ `✅ Node "${nodeName}" executed successfully ${JSON.stringify(context)}`
254
+ );
255
+ this.eventEmitter.emit("nodeCompleted", { name: nodeName });
168
256
 
169
- // Exécuter toutes les branches valides
170
- for (const nextNodeName of node.next) {
171
- const nextNode = this.nodes.get(nextNodeName);
172
- if (!nextNode) continue;
257
+ // Handle waitForEvent
258
+ if (node.waitForEvent && !triggeredByEvent) {
259
+ this.addLog(
260
+ `⏳ Node "${nodeName}" waiting for events: ${node.events?.join(", ")}`
261
+ );
173
262
 
174
- const branchContext = structuredClone(context);
263
+ await new Promise<void>((resolve) => {
264
+ const eventHandler = () => {
265
+ this.addLog(`🚀 Event received for node "${nodeName}"`);
266
+ resolve();
267
+ };
175
268
 
176
- // Si le nœud a une condition et qu'elle n'est pas remplie, passer au suivant
177
- if (nextNode.condition && !nextNode.condition(branchContext)) {
178
- continue;
179
- }
269
+ node.events?.forEach((event) => {
270
+ this.eventEmitter.once(event, eventHandler);
271
+ });
272
+ });
180
273
 
181
- await this.executeNode(nextNodeName, branchContext);
182
- branchContexts.push(branchContext);
183
- }
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(", ")}`);
184
281
 
185
- // Fusionner les résultats des branches dans l'ordre
186
- if (branchContexts.length > 0) {
187
- const finalContext = branchContexts[branchContexts.length - 1];
188
- Object.assign(context, finalContext);
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`);
189
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;
190
311
  }
312
+ }
191
313
 
192
- // Mettre à jour le contexte global
193
- this.context = structuredClone(context);
194
- return;
195
- } catch (error) {
196
- attempts++;
197
- if (attempts >= maxAttempts) {
198
- this.eventEmitter.emit("nodeError", { nodeName, error });
199
- node.onError?.(error as Error);
200
- this.globalErrorHandler?.(error as Error, context);
201
- throw error;
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`);
202
340
  }
203
341
 
204
- console.warn(
205
- `[Graph ${this.name}] Retry attempt ${attempts} for node ${nodeName}`,
206
- { error }
207
- );
208
- await new Promise((resolve) => setTimeout(resolve, delay));
342
+ // Mettre à jour le contexte global avec le résultat final des branches
343
+ Object.assign(context, branchContext);
344
+ this.context = structuredClone(context);
209
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;
210
350
  }
211
351
  }
212
352
 
@@ -231,37 +371,32 @@ export class GraphFlow<T extends ZodSchema> {
231
371
  */
232
372
  async execute(
233
373
  startNode: string,
234
- inputContext?: Partial<GraphContext<T>>,
235
- inputParams?: any
374
+ inputParams?: any,
375
+ inputContext?: Partial<GraphContext<T>>
236
376
  ): Promise<GraphContext<T>> {
237
- // Fresh local context from the global
238
- const context = this.createNewContext();
239
- 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
+ }
240
385
 
241
- // Emit "graphStarted"
242
386
  this.eventEmitter.emit("graphStarted", { name: this.name });
243
387
 
244
388
  try {
245
- // Because we're calling explicitly, it's NOT triggered by an event
246
- await this.executeNode(
247
- startNode,
248
- context,
249
- inputParams,
250
- /* triggeredByEvent= */ false
251
- );
389
+ await this.executeNode(startNode, this.context, inputParams, false);
252
390
 
253
- // Emit "graphCompleted"
254
391
  this.eventEmitter.emit("graphCompleted", {
255
392
  name: this.name,
256
393
  context: this.context,
257
394
  });
258
395
 
259
- // Return a snapshot of the final global context
260
- return structuredClone(this.context);
396
+ return this.getContext();
261
397
  } catch (error) {
262
- // Emit "graphError"
263
398
  this.eventEmitter.emit("graphError", { name: this.name, error });
264
- this.globalErrorHandler?.(error as Error, context);
399
+ this.globalErrorHandler?.(error as Error, this.context);
265
400
  throw error;
266
401
  }
267
402
  }
@@ -276,15 +411,29 @@ export class GraphFlow<T extends ZodSchema> {
276
411
  eventName: string,
277
412
  data?: Partial<GraphContext<T>>
278
413
  ): Promise<GraphContext<T>> {
279
- // Merge data into a fresh copy of the global context if desired
280
- const context = this.createNewContext();
281
- if (data) Object.assign(context, data);
414
+ const workingContext = structuredClone(this.context);
282
415
 
283
- // Just emit the event; the node-based event listeners in setupEventListeners()
284
- // will handle calling "executeNode(...)"
285
- this.eventEmitter.emit(eventName, context);
416
+ if (data) {
417
+ Object.assign(workingContext, data);
418
+ }
419
+
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,
435
+ });
286
436
 
287
- // Return the updated global context
288
437
  return this.getContext();
289
438
  }
290
439
 
@@ -351,10 +500,10 @@ export class GraphFlow<T extends ZodSchema> {
351
500
  }
352
501
 
353
502
  /**
354
- * Returns the current context
355
- * @returns {GraphContext<T>} Current graph context
503
+ * Gets a copy of the current context
504
+ * @returns {GraphContext<T>} A deep copy of the current context
356
505
  */
357
- getContext(): GraphContext<T> {
506
+ public getContext(): GraphContext<T> {
358
507
  return structuredClone(this.context);
359
508
  }
360
509
 
@@ -372,7 +521,7 @@ export class GraphFlow<T extends ZodSchema> {
372
521
  * @param {Node<T>} node - Node to add
373
522
  * @throws {Error} If node with same name already exists
374
523
  */
375
- addNode(node: Node<T>): void {
524
+ addNode(node: Node<T, any>): void {
376
525
  this.nodes.set(node.name, node);
377
526
  if (node.events && node.events.length > 0) {
378
527
  for (const evt of node.events) {
@@ -423,7 +572,7 @@ export class GraphFlow<T extends ZodSchema> {
423
572
  * Returns all nodes in the graph
424
573
  * @returns {Node<T>[]} Array of all nodes
425
574
  */
426
- getNodes(): Node<T>[] {
575
+ getNodes(): Node<T, any>[] {
427
576
  return Array.from(this.nodes.values());
428
577
  }
429
578
 
@@ -465,4 +614,12 @@ export class GraphFlow<T extends ZodSchema> {
465
614
  });
466
615
  }
467
616
  }
617
+
618
+ getLogs(): string[] {
619
+ return [...this.logs];
620
+ }
621
+
622
+ clearLogs(): void {
623
+ this.logs = [];
624
+ }
468
625
  }
package/graph.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { Node } from "types";
2
+ import { z } from "zod";
3
+ import { GraphController, GraphFlow } from "./index";
4
+
5
+ // 🏗 Définition des schémas pour chaque graphe
6
+ const schemaA = z.object({
7
+ input: z.string(),
8
+ result: z.string().optional(),
9
+ });
10
+
11
+ const schemaB = z.object({
12
+ number: z.number(),
13
+ result: z.number().optional(),
14
+ });
15
+
16
+ // 🔹 **Graph A** : Convertit une chaîne en majuscules
17
+ const processText: Node<typeof schemaA> = {
18
+ name: "processText",
19
+ execute: async (context) => {
20
+ context.result = context.input.toUpperCase();
21
+ console.log("📢 Graphe A : Texte transformé →", context.result);
22
+ },
23
+ };
24
+
25
+ // 🔹 **Graph B** : Multiplie un nombre par 10
26
+ const multiplyNumber: Node<typeof schemaB> = {
27
+ name: "multiplyNumber",
28
+ execute: async (context) => {
29
+ context.result = context.number * 10;
30
+ console.log("🔢 Graphe B : Nombre multiplié →", context.result);
31
+ },
32
+ };
33
+
34
+ // 🔗 **Création des graphes**
35
+ const graphA = new GraphFlow("GraphA", {
36
+ name: "GraphA",
37
+ nodes: [processText],
38
+ context: { input: "" },
39
+ schema: schemaA,
40
+ });
41
+
42
+ const graphB = new GraphFlow("GraphB", {
43
+ name: "GraphB",
44
+ nodes: [multiplyNumber],
45
+ context: { number: 0 },
46
+ schema: schemaB,
47
+ });
48
+
49
+ (async () => {
50
+ try {
51
+ console.log("🚀 **Exécution Séquentielle** des graphes...");
52
+ const sequentialResults = await GraphController.executeSequential(
53
+ [graphA, graphB],
54
+ ["processText", "multiplyNumber"],
55
+ [{ input: "hello world" }, { number: 5 }]
56
+ );
57
+
58
+ console.log("🟢 **Résultats Séquentiels :**", sequentialResults);
59
+
60
+ console.log(
61
+ "\n⚡ **Exécution Parallèle** avec limitation de concurrence..."
62
+ );
63
+ const parallelResults = await GraphController.executeParallel(
64
+ [graphA, graphB],
65
+ ["processText", "multiplyNumber"],
66
+ 1, // ⚠️ Limite de concurrence (1 à la fois)
67
+ [{ input: "parallel execution" }, { number: 7 }]
68
+ );
69
+
70
+ console.log("🟢 **Résultats Parallèles :**", parallelResults);
71
+ } catch (error) {
72
+ console.error("❌ Erreur lors de l’exécution :", error);
73
+ }
74
+ })();
package/index.ts CHANGED
@@ -11,8 +11,8 @@
11
11
  * - Utility functions for action schema generation and header building
12
12
  */
13
13
 
14
- export * from "./graph";
15
14
  export * from "./graph/controller";
15
+ export * from "./graph/index";
16
16
  export * from "./modules/memory";
17
17
  export * from "./modules/memory/adapters/meilisearch";
18
18
  export * from "./modules/memory/adapters/redis";
@@ -331,6 +331,13 @@ export interface IEventEmitter {
331
331
  * @returns {Function[]} Array of listener functions
332
332
  */
333
333
  rawListeners(event: string): Function[];
334
+
335
+ /**
336
+ * Registers an event listener that will be called only once
337
+ * @param {string} event - Event name
338
+ * @param {Function} listener - Event handler
339
+ */
340
+ once(event: string, listener: (...args: any[]) => void): void;
334
341
  }
335
342
 
336
343
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai.ntellect/core",
3
- "version": "0.6.19",
3
+ "version": "0.6.20",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -2,9 +2,8 @@ import { expect } from "chai";
2
2
  import EventEmitter from "events";
3
3
  import sinon from "sinon";
4
4
  import { z } from "zod";
5
- import { GraphFlow } from "../../graph";
5
+ import { GraphFlow } from "../../graph/index";
6
6
  import { GraphDefinition, Node } from "../../types";
7
-
8
7
  /**
9
8
  * ✅ Define a valid schema using Zod.
10
9
  */
@@ -23,13 +22,17 @@ describe("Graph", function () {
23
22
 
24
23
  beforeEach(() => {
25
24
  eventEmitter = new EventEmitter();
26
- graph = new GraphFlow("TestGraph", {
27
- name: "TestGraph",
28
- nodes: [],
29
- context: { value: 0 },
30
- schema: TestSchema,
31
- eventEmitter: eventEmitter,
32
- });
25
+ graph = new GraphFlow(
26
+ "TestGraph",
27
+ {
28
+ name: "TestGraph",
29
+ nodes: [],
30
+ context: { value: 0 },
31
+ schema: TestSchema,
32
+ eventEmitter: eventEmitter,
33
+ },
34
+ { verbose: true }
35
+ );
33
36
  });
34
37
 
35
38
  /**
@@ -39,7 +42,7 @@ describe("Graph", function () {
39
42
  const simpleNode: Node<TestSchema> = {
40
43
  name: "simpleNode",
41
44
  execute: async (context) => {
42
- context.value += 1;
45
+ context.value = (context.value ?? 0) + 1;
43
46
  },
44
47
  next: [],
45
48
  };
@@ -64,7 +67,7 @@ describe("Graph", function () {
64
67
  const testNode: Node<TestSchema> = {
65
68
  name: "testNode",
66
69
  execute: async (context) => {
67
- context.value += 1;
70
+ context.value = (context.value ?? 0) + 1;
68
71
  },
69
72
  next: [],
70
73
  };
@@ -101,35 +104,6 @@ describe("Graph", function () {
101
104
  expect(nodeErrorSpy.calledOnce).to.be.true;
102
105
  });
103
106
 
104
- /**
105
- * ✅ Ensure a node requiring user confirmation waits before execution.
106
- */
107
- it("should execute a node requiring user confirmation", async function () {
108
- const confirmationNode: Node<TestSchema> = {
109
- name: "waitUserConfirmation",
110
- execute: async (context) => {
111
- return new Promise<void>((resolve) => {
112
- graph.on("userConfirmed", () => {
113
- context.value += 1;
114
- resolve();
115
- });
116
- });
117
- },
118
- next: [],
119
- };
120
-
121
- graph.addNode(confirmationNode);
122
- const executionPromise = graph.execute("waitUserConfirmation");
123
-
124
- setTimeout(() => {
125
- graph.emit("userConfirmed");
126
- }, 100);
127
-
128
- await executionPromise;
129
- const context = graph.getContext();
130
- expect(context.value).to.equal(1);
131
- });
132
-
133
107
  /**
134
108
  * ✅ Ensure that context validation using Zod works correctly.
135
109
  */
@@ -140,7 +114,7 @@ describe("Graph", function () {
140
114
  const simpleNode: Node<TestSchema> = {
141
115
  name: "simpleNode",
142
116
  execute: async (context) => {
143
- context.value += 1;
117
+ context.value = (context.value ?? 0) + 1;
144
118
  },
145
119
  next: [],
146
120
  };
@@ -158,7 +132,7 @@ describe("Graph", function () {
158
132
  * ✅ Ensure a node with validated inputs and outputs executes correctly.
159
133
  */
160
134
  it("should execute a node with validated inputs and outputs", async function () {
161
- const paramNode: Node<TestSchema> = {
135
+ const paramNode: Node<TestSchema, { increment: number }> = {
162
136
  name: "paramNode",
163
137
  inputs: z.object({
164
138
  increment: z.number(),
@@ -167,13 +141,13 @@ describe("Graph", function () {
167
141
  value: z.number().min(5),
168
142
  }),
169
143
  execute: async (context, inputs: { increment: number }) => {
170
- context.value += inputs.increment;
144
+ context.value = (context.value ?? 0) + inputs.increment;
171
145
  },
172
146
  next: [],
173
147
  };
174
148
 
175
149
  graph.addNode(paramNode);
176
- await graph.execute("paramNode", {}, { increment: 5 });
150
+ await graph.execute("paramNode", { increment: 5 });
177
151
 
178
152
  const context = graph.getContext();
179
153
  expect(context.value).to.equal(5);
@@ -185,9 +159,9 @@ describe("Graph", function () {
185
159
  it("should not execute a node when condition is false", async function () {
186
160
  const conditionalNode: Node<TestSchema> = {
187
161
  name: "conditionalNode",
188
- condition: (context) => context.value > 0,
162
+ condition: (context) => (context.value ?? 0) > 0,
189
163
  execute: async (context) => {
190
- context.value += 10;
164
+ context.value = (context.value ?? 0) + 10;
191
165
  },
192
166
  next: [],
193
167
  };
@@ -212,7 +186,7 @@ describe("Graph", function () {
212
186
  if (attemptCount < 3) {
213
187
  throw new Error("Temporary failure");
214
188
  }
215
- context.value += 1;
189
+ context.value = (context.value ?? 0) + 1;
216
190
  },
217
191
  next: [],
218
192
  };
@@ -235,7 +209,7 @@ describe("Graph", function () {
235
209
  name: "eventNode",
236
210
  events: ["customEvent"],
237
211
  execute: async (context) => {
238
- context.value += 1;
212
+ context.value = (context.value ?? 0) + 1;
239
213
  },
240
214
  next: [],
241
215
  };
@@ -304,13 +278,13 @@ describe("Graph", function () {
304
278
  * ✅ Test input validation failure
305
279
  */
306
280
  it("should throw error when node input validation fails", async function () {
307
- const nodeWithInput: Node<TestSchema> = {
281
+ const nodeWithInput: Node<TestSchema, { amount: number }> = {
308
282
  name: "inputNode",
309
283
  inputs: z.object({
310
284
  amount: z.number().min(0),
311
285
  }),
312
286
  execute: async (context, inputs: { amount: number }) => {
313
- context.value += inputs.amount;
287
+ context.value = (context.value ?? 0) + inputs.amount;
314
288
  },
315
289
  next: [],
316
290
  };
@@ -318,7 +292,7 @@ describe("Graph", function () {
318
292
  graph.addNode(nodeWithInput);
319
293
 
320
294
  try {
321
- await graph.execute("inputNode", {}, { amount: -1 });
295
+ await graph.execute("inputNode", { amount: -1 });
322
296
  expect.fail("Should have thrown an error");
323
297
  } catch (error) {
324
298
  expect((error as Error).message).to.include(
@@ -358,7 +332,7 @@ describe("Graph", function () {
358
332
  * ✅ Test successful input and output validation
359
333
  */
360
334
  it("should successfully validate both inputs and outputs", async function () {
361
- const validatedNode: Node<TestSchema> = {
335
+ const validatedNode: Node<TestSchema, { increment: number }> = {
362
336
  name: "validatedNode",
363
337
  inputs: z.object({
364
338
  increment: z.number().min(0).max(5),
@@ -367,7 +341,7 @@ describe("Graph", function () {
367
341
  value: z.number().min(0).max(10),
368
342
  }),
369
343
  execute: async (context, inputs: { increment: number }) => {
370
- context.value += inputs.increment;
344
+ context.value = (context.value ?? 0) + inputs.increment;
371
345
  },
372
346
  next: [],
373
347
  };
@@ -375,12 +349,12 @@ describe("Graph", function () {
375
349
  graph.addNode(validatedNode);
376
350
 
377
351
  // Test with valid input that produces valid output
378
- await graph.execute("validatedNode", {}, { increment: 3 });
352
+ await graph.execute("validatedNode", { increment: 3 });
379
353
  expect(graph.getContext().value).to.equal(3);
380
354
 
381
355
  // Test with valid input that would produce invalid output
382
356
  try {
383
- await graph.execute("validatedNode", { value: 7 }, { increment: 5 });
357
+ await graph.execute("validatedNode", { increment: 5 }, { value: 7 });
384
358
  expect.fail("Should have thrown an error");
385
359
  } catch (error) {
386
360
  expect((error as Error).message).to.include(
@@ -393,7 +367,7 @@ describe("Graph", function () {
393
367
  * ✅ Test missing required inputs
394
368
  */
395
369
  it("should throw error when required inputs are missing", async function () {
396
- const nodeWithRequiredInput: Node<TestSchema> = {
370
+ const nodeWithRequiredInput: Node<TestSchema, { required: string }> = {
397
371
  name: "requiredInputNode",
398
372
  inputs: z.object({
399
373
  required: z.string(),
@@ -419,7 +393,7 @@ describe("Graph", function () {
419
393
  const nodeA: Node<TestSchema> = {
420
394
  name: "nodeA",
421
395
  execute: async (context) => {
422
- context.value += 1;
396
+ context.value = (context.value ?? 0) + 1;
423
397
  },
424
398
  next: ["nodeB1", "nodeB2"],
425
399
  };
@@ -427,7 +401,7 @@ describe("Graph", function () {
427
401
  const nodeB1: Node<TestSchema> = {
428
402
  name: "nodeB1",
429
403
  execute: async (context) => {
430
- context.value *= 2;
404
+ context.value = (context.value ?? 0) * 2;
431
405
  },
432
406
  next: ["nodeC"],
433
407
  };
@@ -435,7 +409,7 @@ describe("Graph", function () {
435
409
  const nodeB2: Node<TestSchema> = {
436
410
  name: "nodeB2",
437
411
  execute: async (context) => {
438
- context.value += 3;
412
+ context.value = (context.value ?? 0) + 3;
439
413
  },
440
414
  next: ["nodeC"],
441
415
  };
@@ -444,7 +418,7 @@ describe("Graph", function () {
444
418
  name: "nodeC",
445
419
  execute: async (context) => {
446
420
  // Créer une copie du contexte pour éviter les modifications concurrentes
447
- const newValue = context.value + 5;
421
+ const newValue = (context.value ?? 0) + 5;
448
422
  context.value = newValue;
449
423
  },
450
424
  };
@@ -462,25 +436,25 @@ describe("Graph", function () {
462
436
  const startNode: Node<TestSchema> = {
463
437
  name: "start",
464
438
  execute: async (context) => {
465
- context.value = 5;
439
+ context.value = (context.value ?? 0) + 5;
466
440
  },
467
441
  next: ["branchA", "branchB"],
468
442
  };
469
443
 
470
444
  const branchA: Node<TestSchema> = {
471
445
  name: "branchA",
472
- condition: (context) => context.value < 10,
446
+ condition: (context) => (context.value ?? 0) < 10,
473
447
  execute: async (context) => {
474
- context.value *= 2;
448
+ context.value = (context.value ?? 0) * 2;
475
449
  },
476
450
  next: ["end"],
477
451
  };
478
452
 
479
453
  const branchB: Node<TestSchema> = {
480
454
  name: "branchB",
481
- condition: (context) => context.value >= 10,
455
+ condition: (context) => (context.value ?? 0) >= 10,
482
456
  execute: async (context) => {
483
- context.value += 10;
457
+ context.value = (context.value ?? 0) + 10;
484
458
  },
485
459
  next: ["end"],
486
460
  };
@@ -488,7 +462,7 @@ describe("Graph", function () {
488
462
  const endNode: Node<TestSchema> = {
489
463
  name: "end",
490
464
  execute: async (context) => {
491
- context.value = context.value + 1;
465
+ context.value = (context.value ?? 0) + 1;
492
466
  },
493
467
  };
494
468
 
@@ -496,6 +470,13 @@ describe("Graph", function () {
496
470
  graph.addNode(node)
497
471
  );
498
472
 
473
+ await graph.load({
474
+ name: "TestGraph",
475
+ nodes: [startNode, branchA, branchB, endNode],
476
+ context: { value: 0 },
477
+ schema: TestSchema,
478
+ });
479
+
499
480
  await graph.execute("start");
500
481
  expect(graph.getContext().value).to.equal(11);
501
482
  });
@@ -520,7 +501,7 @@ describe("Graph", function () {
520
501
  name: "process",
521
502
  events: ["processData"],
522
503
  execute: async (context) => {
523
- context.value *= 2;
504
+ context.value = (context.value ?? 0) * 2;
524
505
  },
525
506
  next: ["finalize"],
526
507
  };
@@ -529,7 +510,7 @@ describe("Graph", function () {
529
510
  name: "finalize",
530
511
  events: ["complete"],
531
512
  execute: async (context) => {
532
- context.value += 3;
513
+ context.value = (context.value ?? 0) + 3;
533
514
  eventCounter.count++;
534
515
  },
535
516
  };
@@ -573,7 +554,7 @@ describe("Graph", function () {
573
554
  const cycleNode: Node<TestSchema> = {
574
555
  name: "cycle",
575
556
  execute: async (context) => {
576
- context.value += 1;
557
+ context.value = (context.value ?? 0) + 1;
577
558
  iterationCount.count++;
578
559
  },
579
560
  next: ["checkExit"],
@@ -581,9 +562,10 @@ describe("Graph", function () {
581
562
 
582
563
  const checkExitNode: Node<TestSchema> = {
583
564
  name: "checkExit",
584
- execute: async (context) => {},
585
- condition: (context) => context.value < 5,
586
- next: ["cycle"],
565
+ execute: async () => {},
566
+ next: (context) => {
567
+ return (context.value ?? 0) < 5 ? ["cycle"] : [];
568
+ },
587
569
  };
588
570
 
589
571
  [cycleNode, checkExitNode].forEach((node) => graph.addNode(node));
@@ -593,54 +575,4 @@ describe("Graph", function () {
593
575
  expect(graph.getContext().value).to.equal(5);
594
576
  expect(iterationCount.count).to.equal(5);
595
577
  });
596
-
597
- /**
598
- * ✅ Test executing entire graph when triggered by event
599
- */
600
- it("should execute entire graph when triggered by event", async function () {
601
- const nodeA: Node<TestSchema> = {
602
- name: "nodeA",
603
- execute: async (context) => {
604
- context.value += 1;
605
- },
606
- next: ["nodeB"],
607
- };
608
-
609
- const nodeB: Node<TestSchema> = {
610
- name: "nodeB",
611
- execute: async (context) => {
612
- context.value *= 2;
613
- },
614
- next: [],
615
- };
616
-
617
- const graphDefinition: GraphDefinition<TestSchema> = {
618
- name: "EventGraph",
619
- nodes: [nodeA, nodeB],
620
- context: { value: 0 },
621
- schema: TestSchema,
622
- entryNode: "nodeA",
623
- events: ["startGraph"],
624
- };
625
-
626
- graph.load(graphDefinition);
627
-
628
- // Use a promise to ensure the event is properly handled
629
- await new Promise<void>((resolve, reject) => {
630
- const timeout = setTimeout(
631
- () => reject(new Error("Graph event did not complete")),
632
- 1500
633
- );
634
-
635
- graph.on("graphCompleted", () => {
636
- clearTimeout(timeout);
637
- resolve();
638
- });
639
-
640
- graph.emit("startGraph").catch(reject);
641
- });
642
-
643
- const context = graph.getContext();
644
- expect(context.value).to.equal(2); // (0 + 1) * 2
645
- });
646
578
  });
package/types/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { EventEmitter } from "events";
1
2
  import { IEventEmitter } from "interfaces";
2
3
  import { ZodSchema } from "zod";
3
4
 
@@ -62,7 +63,7 @@ export type ScheduledRequest = {
62
63
  * Utility type for extracting schema type from Zod schema
63
64
  * @template T - Zod schema type
64
65
  */
65
- export type SchemaType<T> = T extends ZodSchema<infer U> ? Required<U> : never;
66
+ export type SchemaType<T> = T extends ZodSchema<infer U> ? U : never;
66
67
 
67
68
  /**
68
69
  * Type for graph context based on schema
@@ -74,28 +75,39 @@ export type GraphContext<T> = SchemaType<T>;
74
75
  * Interface representing a node in the graph
75
76
  * @interface
76
77
  * @template T - Schema type
78
+ * @template I - Input schema type
79
+ * @template O - Output schema type
77
80
  */
78
- export interface Node<T> {
81
+ export interface Node<T extends ZodSchema, I = any> {
79
82
  /** Name of the node */
80
83
  name: string;
81
84
  /** Schema for node inputs */
82
- inputs?: ZodSchema;
85
+ inputs?: I extends void ? never : ZodSchema<I>;
83
86
  /** Schema for node outputs */
84
87
  outputs?: ZodSchema;
85
88
  /** Execute function for the node */
86
- execute: (context: GraphContext<T>, inputs?: any) => Promise<void>;
89
+ execute: (
90
+ context: GraphContext<T>,
91
+ inputs: I extends void ? never : I
92
+ ) => Promise<void>;
87
93
  /** Optional condition for node execution */
88
94
  condition?: (context: GraphContext<T>) => boolean;
89
95
  /** Array of next node names */
90
- next?: string[];
96
+ next?: string[] | ((context: GraphContext<T>) => string[]);
91
97
  /** Array of event names */
92
98
  events?: string[];
99
+ /** Wait for event */
100
+ waitForEvent?: boolean;
93
101
  /** Retry configuration */
94
102
  retry?: {
95
103
  /** Maximum number of retry attempts */
96
104
  maxAttempts: number;
97
105
  /** Delay between retries in milliseconds */
98
106
  delay: number;
107
+ /** Error handler function */
108
+ onRetryFailed?: (error: Error, context: GraphContext<T>) => Promise<void>;
109
+ /** Continue execution on failed retry */
110
+ continueOnFailed?: boolean;
99
111
  };
100
112
  /** Error handler function */
101
113
  onError?: (error: Error) => void;
@@ -106,11 +118,11 @@ export interface Node<T> {
106
118
  * @interface
107
119
  * @template T - Schema type
108
120
  */
109
- export interface GraphDefinition<T> {
121
+ export interface GraphDefinition<T extends ZodSchema> {
110
122
  /** Name of the graph */
111
123
  name: string;
112
124
  /** Array of nodes in the graph */
113
- nodes: Node<T>[];
125
+ nodes: Node<T, any>[];
114
126
  /** Initial context */
115
127
  context: SchemaType<T>;
116
128
  /** Schema for validation */
@@ -120,7 +132,7 @@ export interface GraphDefinition<T> {
120
132
  /** Entry node name */
121
133
  entryNode?: string;
122
134
  /** Event emitter instance */
123
- eventEmitter?: IEventEmitter;
135
+ eventEmitter?: IEventEmitter | EventEmitter;
124
136
  /** Array of events */
125
137
  events?: string[];
126
138
  }