@ai.ntellect/core 0.7.5 → 0.7.6

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.
@@ -0,0 +1,187 @@
1
+ import { expect } from "chai";
2
+ import { z } from "zod";
3
+ import { GraphController } from "../../graph/controller";
4
+ import { GraphFlow } from "../../graph/index";
5
+ import { Node } from "../../types";
6
+
7
+ describe("GraphController", () => {
8
+ const TestSchema = z.object({
9
+ counter: z.number(),
10
+ message: z.string(),
11
+ });
12
+
13
+ const createTestGraph = (name: string): GraphFlow<typeof TestSchema> => {
14
+ const nodes: Node<typeof TestSchema>[] = [
15
+ {
16
+ name: "start",
17
+ execute: async (context, params) => {
18
+ context.counter = params?.value ?? 0;
19
+ context.message = params?.prefix ? `${params.prefix}-${name}` : name;
20
+ },
21
+ },
22
+ {
23
+ name: "increment",
24
+ execute: async (context) => {
25
+ context.counter += 1;
26
+ },
27
+ },
28
+ ];
29
+
30
+ return new GraphFlow(name, {
31
+ name,
32
+ nodes,
33
+ schema: TestSchema,
34
+ context: { counter: 0, message: "" },
35
+ });
36
+ };
37
+
38
+ describe("Sequential Execution", () => {
39
+ it("should execute graphs sequentially with different params and params", async () => {
40
+ const graph1 = createTestGraph("graph1");
41
+ const graph2 = createTestGraph("graph2");
42
+ const graph3 = createTestGraph("graph3");
43
+
44
+ const params = [{ value: 10 }, { value: 20 }, { value: 30 }];
45
+
46
+ const params2 = [
47
+ { prefix: "test1" },
48
+ { prefix: "test2" },
49
+ { prefix: "test3" },
50
+ ];
51
+
52
+ const results = await GraphController.executeSequential(
53
+ [graph1, graph2, graph3],
54
+ ["start", "start", "start"],
55
+ params.map((value, i) => ({ ...value, prefix: params2[i].prefix }))
56
+ );
57
+
58
+ expect(results).to.have.length(3);
59
+ expect(results[0].counter).to.equal(10);
60
+ expect(results[1].counter).to.equal(20);
61
+ expect(results[2].counter).to.equal(30);
62
+ expect(results[0].message).to.equal("test1-graph1");
63
+ expect(results[1].message).to.equal("test2-graph2");
64
+ expect(results[2].message).to.equal("test3-graph3");
65
+ });
66
+
67
+ it("should handle missing params and params gracefully", async () => {
68
+ const graph1 = createTestGraph("graph1");
69
+ const graph2 = createTestGraph("graph2");
70
+
71
+ const results = await GraphController.executeSequential(
72
+ [graph1, graph2],
73
+ ["start", "start"]
74
+ );
75
+
76
+ expect(results).to.have.length(2);
77
+ expect(results[0].counter).to.equal(0);
78
+ expect(results[1].counter).to.equal(0);
79
+ expect(results[0].message).to.equal("graph1");
80
+ expect(results[1].message).to.equal("graph2");
81
+ });
82
+ });
83
+
84
+ describe("Parallel Execution", () => {
85
+ it("should execute graphs in parallel with concurrency limit", async () => {
86
+ const graphs = Array.from({ length: 5 }, (_, i) =>
87
+ createTestGraph(`graph${i + 1}`)
88
+ );
89
+
90
+ const params = Array.from({ length: 5 }, (_, i) => ({
91
+ value: (i + 1) * 10,
92
+ prefix: `test${i + 1}`,
93
+ }));
94
+
95
+ // Ajouter un délai dans l'exécution
96
+ const originalExecute = graphs[0].execute;
97
+ graphs[0].execute = async (...args) => {
98
+ await new Promise((resolve) => setTimeout(resolve, 100));
99
+ return originalExecute.apply(graphs[0], args);
100
+ };
101
+
102
+ const startTime = Date.now();
103
+ const results = await GraphController.executeParallel(
104
+ graphs,
105
+ Array(5).fill("start"),
106
+ 2,
107
+ params
108
+ );
109
+ const executionTime = Date.now() - startTime;
110
+
111
+ expect(executionTime).to.be.greaterThan(0);
112
+ expect(results).to.have.length(5);
113
+ results.forEach((result, i) => {
114
+ expect(result.counter).to.equal((i + 1) * 10);
115
+ expect(result.message).to.equal(`test${i + 1}-graph${i + 1}`);
116
+ });
117
+ });
118
+
119
+ it("should handle errors in parallel execution", async () => {
120
+ const errorGraph = new GraphFlow("errorGraph", {
121
+ name: "errorGraph",
122
+ nodes: [
123
+ {
124
+ name: "start",
125
+ execute: async () => {
126
+ throw new Error("Test error");
127
+ },
128
+ },
129
+ ],
130
+ schema: TestSchema,
131
+ context: { counter: 0, message: "" },
132
+ });
133
+
134
+ const successGraph = createTestGraph("successGraph");
135
+
136
+ try {
137
+ await GraphController.executeParallel(
138
+ [errorGraph, successGraph],
139
+ ["start", "start"],
140
+ 2
141
+ );
142
+ expect.fail("Should have thrown an error");
143
+ } catch (error: any) {
144
+ expect(error.message).to.equal("Test error");
145
+ }
146
+ });
147
+ });
148
+
149
+ describe("Complex Workflows", () => {
150
+ it("should handle mixed sequential and parallel execution", async () => {
151
+ const graphs = Array.from({ length: 4 }, (_, i) =>
152
+ createTestGraph(`graph${i + 1}`)
153
+ );
154
+
155
+ // Exécuter les deux premiers graphes en parallèle
156
+ const parallelResults = await GraphController.executeParallel(
157
+ graphs.slice(0, 2),
158
+ ["start", "start"],
159
+ 2,
160
+ [
161
+ { value: 10, prefix: "parallel1" },
162
+ { value: 20, prefix: "parallel2" },
163
+ ]
164
+ );
165
+
166
+ // Puis exécuter les deux suivants séquentiellement
167
+ const sequentialResults = await GraphController.executeSequential(
168
+ graphs.slice(2),
169
+ ["start", "start"],
170
+ [
171
+ { value: 30, prefix: "seq1" },
172
+ { value: 40, prefix: "seq2" },
173
+ ]
174
+ );
175
+
176
+ const allResults = [...parallelResults, ...sequentialResults];
177
+ expect(allResults).to.have.length(4);
178
+ expect(allResults.map((r) => r.counter)).to.deep.equal([10, 20, 30, 40]);
179
+ expect(allResults.map((r) => r.message)).to.deep.equal([
180
+ "parallel1-graph1",
181
+ "parallel2-graph2",
182
+ "seq1-graph3",
183
+ "seq2-graph4",
184
+ ]);
185
+ });
186
+ });
187
+ });
@@ -181,7 +181,8 @@ describe("GraphFlow", function () {
181
181
  outputs: z.object({
182
182
  value: z.number().min(5),
183
183
  }),
184
- execute: async (context, inputs: { increment: number }) => {
184
+ execute: async (context, inputs?: { increment: number }) => {
185
+ if (!inputs) throw new Error("Inputs required");
185
186
  context.value = (context.value ?? 0) + inputs.increment;
186
187
  },
187
188
  next: [],
@@ -230,30 +231,31 @@ describe("GraphFlow", function () {
230
231
  */
231
232
  it("should retry a node execution when it fails", async () => {
232
233
  let attempts = 0;
233
- const nodes = new Map();
234
- nodes.set("test", {
235
- name: "test",
236
- execute: async () => {
234
+ const retryNode: Node<TestSchema> = {
235
+ name: "retryNode",
236
+ execute: async (context) => {
237
237
  attempts++;
238
238
  if (attempts < 3) {
239
239
  throw new Error("Temporary failure");
240
240
  }
241
+ context.value = 42;
241
242
  },
242
243
  retry: {
243
244
  maxAttempts: 3,
244
245
  delay: 100,
245
246
  },
246
- });
247
+ };
247
248
 
248
249
  const graph = new GraphFlow("test", {
249
250
  name: "test",
250
251
  schema: TestSchema,
251
252
  context: { value: 0 },
252
- nodes: Array.from(nodes.values()),
253
+ nodes: [retryNode],
253
254
  });
254
255
 
255
- await graph.execute("test");
256
+ await graph.execute("retryNode");
256
257
  expect(attempts).to.equal(3);
258
+ expect(graph.getContext().value).to.equal(42);
257
259
  });
258
260
 
259
261
  /**
@@ -300,21 +302,22 @@ describe("GraphFlow", function () {
300
302
  * Tests input validation error handling
301
303
  */
302
304
  it("should throw error when node input validation fails", async () => {
303
- const InputSchema = z.object({
304
- value: z.number().min(0),
305
- });
305
+ const node: Node<TestSchema> = {
306
+ name: "test",
307
+ inputs: z.object({
308
+ value: z.number().min(0),
309
+ }),
310
+ execute: async (context, inputs) => {
311
+ if (!inputs) throw new Error("Inputs required");
312
+ context.value = inputs.value;
313
+ },
314
+ };
306
315
 
307
316
  const graph = new GraphFlow("test", {
308
317
  name: "test",
309
318
  schema: TestSchema,
310
319
  context: { value: 0 },
311
- nodes: [
312
- {
313
- name: "test",
314
- inputs: InputSchema,
315
- execute: async () => {},
316
- },
317
- ],
320
+ nodes: [node],
318
321
  });
319
322
 
320
323
  try {
@@ -356,6 +359,13 @@ describe("GraphFlow", function () {
356
359
  * Tests successful input/output validation flow
357
360
  */
358
361
  it("should successfully validate both inputs and outputs", async function () {
362
+ const graph = new GraphFlow("test", {
363
+ name: "test",
364
+ schema: TestSchema,
365
+ context: { value: 0 },
366
+ nodes: [],
367
+ });
368
+
359
369
  const validatedNode: Node<TestSchema, { increment: number }> = {
360
370
  name: "validatedNode",
361
371
  inputs: z.object({
@@ -364,7 +374,8 @@ describe("GraphFlow", function () {
364
374
  outputs: z.object({
365
375
  value: z.number().min(0).max(10),
366
376
  }),
367
- execute: async (context, inputs: { increment: number }) => {
377
+ execute: async (context, inputs?: { increment: number }) => {
378
+ if (!inputs) throw new Error("Inputs required");
368
379
  context.value = (context.value ?? 0) + inputs.increment;
369
380
  },
370
381
  next: [],
@@ -5,7 +5,7 @@ import { BehaviorSubject, Subject } from "rxjs";
5
5
  import { z } from "zod";
6
6
  import { GraphEventManager } from "../../graph/event-manager";
7
7
  import { GraphLogger } from "../../graph/logger";
8
- import { GraphNode } from "../../graph/node";
8
+ import { GraphNode, NodeParams } from "../../graph/node";
9
9
  import { GraphContext } from "../../types";
10
10
 
11
11
  use(chaiAsPromised);
@@ -107,7 +107,7 @@ describe("GraphNode", () => {
107
107
  const nodes = new Map();
108
108
  nodes.set("test", {
109
109
  name: "test",
110
- execute: async (_context: TestContext) => {
110
+ execute: async () => {
111
111
  throw new Error("Test error");
112
112
  },
113
113
  });
@@ -121,13 +121,17 @@ describe("GraphNode", () => {
121
121
  );
122
122
 
123
123
  try {
124
- await node.executeNode("test", { counter: 0, message: "Hello" }, null);
125
- expect.fail("Should have thrown an error");
124
+ await node.executeNode(
125
+ "test",
126
+ { counter: 0, message: "Hello" },
127
+ null,
128
+ false
129
+ );
130
+ expect.fail("Test error");
126
131
  } catch (error: any) {
127
132
  expect(error.message).to.equal("Test error");
128
133
  const errorEvents = events.filter((e) => e.type === "nodeError");
129
134
  expect(errorEvents).to.have.lengthOf(1);
130
- expect(errorEvents[0].payload.error.message).to.equal("Test error");
131
135
  }
132
136
  });
133
137
 
@@ -175,7 +179,7 @@ describe("GraphNode", () => {
175
179
  const nodes = new Map();
176
180
  nodes.set("test", {
177
181
  name: "test",
178
- execute: async (context: TestContext) => {
182
+ execute: async (context: TestContext, inputs?: any) => {
179
183
  context.counter = context.counter; // Même valeur
180
184
  context.message = "New"; // Nouvelle valeur
181
185
  },
@@ -194,4 +198,135 @@ describe("GraphNode", () => {
194
198
  expect(stateChanges).to.have.lengthOf(1); // Seulement pour message
195
199
  expect(stateChanges[0].payload.property).to.equal("message");
196
200
  });
201
+
202
+ it("should execute node with parameters", async () => {
203
+ const nodes = new Map();
204
+ nodes.set("test", {
205
+ name: "test",
206
+ execute: async (context: TestContext, inputs?: any) => {
207
+ context.counter = inputs?.value ?? 0;
208
+ context.message = inputs?.message ?? "Default";
209
+ },
210
+ });
211
+
212
+ node = new GraphNode(
213
+ nodes,
214
+ logger,
215
+ eventManager,
216
+ eventSubject,
217
+ stateSubject
218
+ );
219
+
220
+ await node.executeNode(
221
+ "test",
222
+ { counter: 0, message: "Hello" },
223
+ { value: 5, message: "Custom" },
224
+ false
225
+ );
226
+
227
+ const stateChanges = events.filter((e) => e.type === "nodeStateChanged");
228
+ expect(stateChanges).to.have.lengthOf(2);
229
+ expect(stateChanges[0].payload.newValue).to.equal(5);
230
+ expect(stateChanges[1].payload.newValue).to.equal("Custom");
231
+ });
232
+
233
+ it("should use default values when no parameters provided", async () => {
234
+ const nodes = new Map();
235
+ nodes.set("test", {
236
+ name: "test",
237
+ execute: async (
238
+ context: TestContext,
239
+ _inputs: any,
240
+ params?: NodeParams
241
+ ) => {
242
+ context.counter = params?.increment || 1;
243
+ context.message = params?.message || "Default";
244
+ },
245
+ });
246
+
247
+ node = new GraphNode(
248
+ nodes,
249
+ logger,
250
+ eventManager,
251
+ eventSubject,
252
+ stateSubject
253
+ );
254
+
255
+ await node.executeNode("test", { counter: 0, message: "Hello" }, null);
256
+
257
+ const stateChanges = events.filter((e) => e.type === "nodeStateChanged");
258
+ expect(stateChanges).to.have.lengthOf(2);
259
+ expect(stateChanges[0].payload.newValue).to.equal(1); // counter (default)
260
+ expect(stateChanges[1].payload.newValue).to.equal("Default"); // message (default)
261
+ });
262
+
263
+ it("should properly handle node inputs", async () => {
264
+ const nodes = new Map();
265
+ nodes.set("test", {
266
+ name: "test",
267
+ execute: async (context: TestContext, inputs: any) => {
268
+ context.counter = inputs.value;
269
+ context.message = inputs.message;
270
+ },
271
+ });
272
+
273
+ node = new GraphNode(
274
+ nodes,
275
+ logger,
276
+ eventManager,
277
+ eventSubject,
278
+ stateSubject
279
+ );
280
+
281
+ const testInputs = {
282
+ value: 42,
283
+ message: "Test Input",
284
+ };
285
+
286
+ await node.executeNode(
287
+ "test",
288
+ { counter: 0, message: "Hello" },
289
+ testInputs
290
+ );
291
+
292
+ const stateChanges = events.filter((e) => e.type === "nodeStateChanged");
293
+ expect(stateChanges).to.have.lengthOf(2);
294
+ expect(stateChanges[0].payload.newValue).to.equal(42); // counter from input
295
+ expect(stateChanges[1].payload.newValue).to.equal("Test Input"); // message from input
296
+ });
297
+
298
+ it("should not emit duplicate state changes", async () => {
299
+ const nodes = new Map();
300
+ nodes.set("test", {
301
+ name: "test",
302
+ execute: async (context: TestContext) => {
303
+ context.counter = 1; // Valeur fixe au lieu d'incrémentations
304
+ context.counter = 1; // Même valeur
305
+ context.message = "New";
306
+ context.message = "New"; // Même valeur
307
+ },
308
+ });
309
+
310
+ node = new GraphNode(
311
+ nodes,
312
+ logger,
313
+ eventManager,
314
+ eventSubject,
315
+ stateSubject
316
+ );
317
+
318
+ await node.executeNode("test", { counter: 0, message: "Hello" }, null);
319
+
320
+ // Vérifier qu'il n'y a pas de doublons dans les événements
321
+ const stateChanges = events.filter((e) => e.type === "nodeStateChanged");
322
+ const uniqueChanges = new Set(
323
+ stateChanges.map(
324
+ (e) =>
325
+ `${e.payload.property}-${e.payload.oldValue}-${e.payload.newValue}`
326
+ )
327
+ );
328
+
329
+ expect(stateChanges.length).to.equal(uniqueChanges.size);
330
+ expect(stateChanges).to.have.lengthOf(2); // Un pour counter, un pour message
331
+ });
197
332
  });
package/types/index.ts CHANGED
@@ -86,12 +86,9 @@ export interface Node<T extends ZodSchema, I = any> {
86
86
  /** Schema for node outputs */
87
87
  outputs?: ZodSchema;
88
88
  /** Execute function for the node */
89
- execute: (
90
- context: GraphContext<T>,
91
- inputs: I extends void ? never : I
92
- ) => Promise<void>;
89
+ execute: (context: GraphContext<T>, params?: I) => Promise<void>;
93
90
  /** Optional condition for node execution */
94
- condition?: (context: GraphContext<T>) => boolean;
91
+ condition?: (context: GraphContext<T>, params?: I) => boolean;
95
92
  /** Array of next node names */
96
93
  next?: string[] | ((context: GraphContext<T>) => string[]);
97
94
  /** Array of event names that trigger this node */