@ai.ntellect/core 0.7.5 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/graph/node.ts CHANGED
@@ -2,13 +2,22 @@ import { BehaviorSubject, Subject } from "rxjs";
2
2
  import { ZodSchema } from "zod";
3
3
  import { GraphContext, GraphEvent, Node } from "../types";
4
4
  import { GraphEventManager } from "./event-manager";
5
- import { GraphLogger } from "./logger";
6
5
 
7
6
  /**
8
7
  * Represents a node in the graph that can execute operations and manage state
9
8
  * @template T - The Zod schema type for validation
10
9
  */
10
+ export interface NodeParams<T = any> {
11
+ [key: string]: T;
12
+ }
13
+
14
+ export interface GraphLogger {
15
+ addLog: (message: string, data?: any) => void;
16
+ }
17
+
11
18
  export class GraphNode<T extends ZodSchema> {
19
+ private lastStateEvent: GraphEvent<T> | null = null;
20
+
12
21
  /**
13
22
  * Creates a new GraphNode instance
14
23
  * @param nodes - Map of all nodes in the graph
@@ -32,15 +41,22 @@ export class GraphNode<T extends ZodSchema> {
32
41
  * @private
33
42
  */
34
43
  private emitEvent(type: string, payload: any) {
35
- this.logger.addLog(`📢 Event: ${type}`);
44
+ if (type === "nodeStateChanged") {
45
+ if (
46
+ this.lastStateEvent?.type === type &&
47
+ this.lastStateEvent.payload.property === payload.property &&
48
+ this.lastStateEvent.payload.newValue === payload.newValue &&
49
+ this.lastStateEvent.payload.nodeName === payload.nodeName
50
+ ) {
51
+ return;
52
+ }
53
+ }
54
+
36
55
  const event = {
37
56
  type,
38
57
  payload: {
39
58
  ...payload,
40
- name:
41
- type === "nodeStateChanged"
42
- ? payload.name || payload.nodeName
43
- : payload.name,
59
+ name: type === "nodeStateChanged" ? payload.nodeName : payload.name,
44
60
  context: { ...payload.context },
45
61
  },
46
62
  timestamp: Date.now(),
@@ -49,8 +65,8 @@ export class GraphNode<T extends ZodSchema> {
49
65
  this.eventSubject.next(event);
50
66
  this.eventManager.emitEvent(type, event);
51
67
 
52
- // Update state subject only for state changes
53
68
  if (type === "nodeStateChanged") {
69
+ this.lastStateEvent = event;
54
70
  this.stateSubject.next({ ...payload.context });
55
71
  }
56
72
  }
@@ -59,125 +75,93 @@ export class GraphNode<T extends ZodSchema> {
59
75
  * Executes a node with the given name and context
60
76
  * @param nodeName - The name of the node to execute
61
77
  * @param context - The current graph context
62
- * @param inputs - Input data for the node
78
+ * @param params - Input data for the node
63
79
  * @param triggeredByEvent - Whether the execution was triggered by an event
64
80
  * @throws Error if the node is not found or execution fails
65
81
  */
66
- async executeNode(
82
+ public async executeNode(
67
83
  nodeName: string,
68
84
  context: GraphContext<T>,
69
- inputs: any,
85
+ params: any,
70
86
  triggeredByEvent: boolean = false
71
87
  ): Promise<void> {
72
88
  const node = this.nodes.get(nodeName);
73
89
  if (!node) throw new Error(`Node "${nodeName}" not found.`);
74
90
 
75
- this.logger.addLog(`🚀 Starting node "${nodeName}`);
76
- this.emitEvent("nodeStarted", { name: nodeName, context });
91
+ // Créer une copie du contexte pour ce nœud
92
+ const nodeContext = { ...context };
93
+ this.emitEvent("nodeStarted", { name: nodeName, context: nodeContext });
77
94
 
78
95
  try {
79
- // Vérifier la condition avant d'exécuter
80
- if (node.condition && !node.condition(context)) {
81
- this.logger.addLog(
82
- `⏭️ Skipping node "${nodeName}" - condition not met`
83
- );
84
- return;
85
- }
86
-
87
- const contextProxy = new Proxy(context, {
96
+ const contextProxy = new Proxy(nodeContext, {
88
97
  set: (target, prop, value) => {
89
98
  const oldValue = target[prop];
90
99
  if (oldValue === value) return true;
91
100
 
92
101
  target[prop] = value;
102
+ // Mettre à jour le contexte global
103
+ context[prop as keyof typeof context] = value;
104
+
93
105
  this.emitEvent("nodeStateChanged", {
94
106
  nodeName,
95
107
  property: prop.toString(),
96
108
  oldValue,
97
109
  newValue: value,
98
- context: target,
110
+ context: { ...target },
99
111
  });
100
-
101
112
  return true;
102
113
  },
103
- get: (target, prop) => target[prop],
104
114
  });
105
115
 
106
- // Exécuter le nœud
107
- await node.execute(contextProxy, inputs);
116
+ if (node.condition && !node.condition(contextProxy, params)) {
117
+ return;
118
+ }
119
+
120
+ await this.executeWithRetry(node, contextProxy, nodeName, params);
121
+ this.emitEvent("nodeCompleted", { name: nodeName, context: nodeContext });
108
122
 
109
- // Gérer la suite uniquement si pas déclenché par un événement
110
- if (!triggeredByEvent) {
123
+ if (!triggeredByEvent && node.next) {
111
124
  const nextNodes =
112
- typeof node.next === "function"
113
- ? node.next(contextProxy)
114
- : node.next || [];
115
-
125
+ typeof node.next === "function" ? node.next(contextProxy) : node.next;
116
126
  for (const nextNodeName of nextNodes) {
117
127
  await this.executeNode(nextNodeName, context, undefined, false);
118
128
  }
119
129
  }
120
-
121
- this.logger.addLog(`✅ Node "${nodeName}" executed successfully`);
122
- this.emitEvent("nodeCompleted", { name: nodeName, context });
123
130
  } catch (error) {
124
- this.logger.addLog(
125
- `❌ Error in node "${nodeName}": ${
126
- error instanceof Error ? error.message : String(error)
127
- }`
128
- );
129
- this.emitEvent("nodeError", { name: nodeName, error, context });
131
+ this.emitEvent("nodeError", {
132
+ name: nodeName,
133
+ error,
134
+ context: nodeContext,
135
+ });
130
136
  throw error;
131
137
  }
132
138
  }
133
139
 
134
140
  /**
135
- * Validates the inputs for a node using its schema
136
- * @param node - The node whose inputs need validation
137
- * @param inputs - The input data to validate
141
+ * Validates the params for a node using its schema
142
+ * @param node - The node whose params need validation
143
+ * @param params - The input data to validate
138
144
  * @param nodeName - The name of the node (for error messages)
139
145
  * @throws Error if validation fails
140
146
  * @private
141
147
  */
142
- private async validateInputs(
148
+ private async validateParams(
143
149
  node: Node<T, any>,
144
- inputs: any,
150
+ params: any,
145
151
  nodeName: string
146
152
  ): Promise<void> {
147
- if (!inputs) {
148
- throw new Error(`Inputs required for node "${nodeName}"`);
149
- }
153
+ // Si pas de schéma de validation ou si le schéma est optionnel, accepter n'importe quels params
154
+ if (!node.params || node.params.isOptional?.()) return;
150
155
 
151
- try {
152
- return node.inputs!.parse(inputs);
153
- } catch (error: any) {
154
- throw new Error(
155
- error.errors?.[0]?.message || error.message || "Input validation failed"
156
- );
156
+ // Vérifier les params uniquement si un schéma est défini et non optionnel
157
+ if (!params) {
158
+ throw new Error(`Params required for node "${nodeName}"`);
157
159
  }
158
- }
159
160
 
160
- /**
161
- * Validates the outputs of a node against its schema
162
- * @param node - The node whose outputs need validation
163
- * @param context - The current graph context
164
- * @param nodeName - The name of the node (for error messages)
165
- * @throws Error if validation fails
166
- * @private
167
- */
168
- private async validateOutputs(
169
- node: Node<T, any>,
170
- context: GraphContext<T>,
171
- nodeName: string
172
- ): Promise<void> {
173
161
  try {
174
- node.outputs!.parse(context);
162
+ return node.params.parse(params);
175
163
  } catch (error: any) {
176
- throw new Error(
177
- error.errors?.[0]?.message ||
178
- error.message ||
179
- "Output validation failed"
180
- );
164
+ throw error;
181
165
  }
182
166
  }
183
167
 
@@ -206,42 +190,41 @@ export class GraphNode<T extends ZodSchema> {
206
190
  * Executes a node with retry logic
207
191
  * @param node - The node to execute
208
192
  * @param contextProxy - The proxied graph context
209
- * @param inputs - Input data for the node
193
+ * @param params - Input data for the node
210
194
  * @param nodeName - The name of the node
195
+ * @param params - Parameters for the node
211
196
  * @throws Error if all retry attempts fail
212
197
  * @private
213
198
  */
214
199
  private async executeWithRetry(
215
200
  node: Node<T, any>,
216
201
  contextProxy: GraphContext<T>,
217
- inputs: any,
218
- nodeName: string
202
+ nodeName: string,
203
+ params?: NodeParams
219
204
  ): Promise<void> {
220
205
  let attempts = 0;
221
- let lastError: Error | null = null;
206
+ let lastError: Error = new Error("Unknown error");
222
207
 
223
- while (attempts < node.retry!.maxAttempts) {
208
+ while (attempts < (node.retry?.maxAttempts || 1)) {
224
209
  try {
225
- this.logger.addLog(
226
- `🔄 Attempt ${attempts + 1}/${node.retry!.maxAttempts}`
227
- );
228
- await node.execute(contextProxy, inputs);
210
+ // Valider les params uniquement si un schéma est défini
211
+ if (node.params) {
212
+ await this.validateParams(node, params, nodeName);
213
+ }
214
+
215
+ await node.execute(contextProxy, params);
229
216
  return;
230
217
  } catch (error: any) {
231
- lastError = error instanceof Error ? error : new Error(error.message);
218
+ lastError =
219
+ error instanceof Error
220
+ ? error
221
+ : new Error(error?.message || "Unknown error");
232
222
  attempts++;
233
- this.logger.addLog(
234
- `❌ Attempt ${attempts} failed: ${lastError.message}`
235
- );
236
223
 
237
- if (attempts === node.retry!.maxAttempts) {
238
- if (node.retry!.onRetryFailed && lastError) {
239
- await this.handleRetryFailure(
240
- node,
241
- lastError,
242
- contextProxy,
243
- nodeName
244
- );
224
+ if (attempts === (node.retry?.maxAttempts || 1)) {
225
+ if (node.retry?.onRetryFailed) {
226
+ await node.retry.onRetryFailed(lastError, contextProxy);
227
+ if (node.retry.continueOnFailed) return;
245
228
  }
246
229
  throw lastError;
247
230
  }
@@ -253,44 +236,6 @@ export class GraphNode<T extends ZodSchema> {
253
236
  }
254
237
  }
255
238
 
256
- /**
257
- * Handles the failure of retry attempts
258
- * @param node - The node that failed
259
- * @param error - The error that caused the failure
260
- * @param context - The current graph context
261
- * @param nodeName - The name of the node
262
- * @private
263
- */
264
- private async handleRetryFailure(
265
- node: Node<T, any>,
266
- error: Error,
267
- context: GraphContext<T>,
268
- nodeName: string
269
- ): Promise<void> {
270
- this.logger.addLog(
271
- `🔄 Executing retry failure handler for node "${nodeName}"`
272
- );
273
- try {
274
- if (node.retry?.onRetryFailed) {
275
- await node.retry.onRetryFailed(error, context);
276
- if (node.retry.continueOnFailed) {
277
- this.logger.addLog(
278
- `✅ Retry failure handler succeeded - continuing execution`
279
- );
280
- return;
281
- }
282
- this.logger.addLog(
283
- `⚠️ Retry failure handler executed but node will still fail`
284
- );
285
- }
286
- } catch (handlerError: any) {
287
- this.logger.addLog(
288
- `❌ Retry failure handler failed: ${handlerError.message}`
289
- );
290
- throw handlerError;
291
- }
292
- }
293
-
294
239
  /**
295
240
  * Handles correlated events for a node
296
241
  * @param node - The node with correlated events
package/graph/observer.ts CHANGED
@@ -56,7 +56,6 @@ export class GraphObserver<T extends ZodSchema> {
56
56
  } = {}
57
57
  ): GraphObservable<T> {
58
58
  const baseObservable = new Observable<any>((subscriber) => {
59
- // Combine les événements avec l'état actuel
60
59
  const subscription = combineLatest([
61
60
  this.eventSubject.pipe(
62
61
  filter((event) => event.type === "nodeStateChanged"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai.ntellect/core",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -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
+ });