@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.
- package/.mocharc.json +1 -2
- package/README.md +123 -178
- package/dist/graph/controller.js +29 -6
- package/dist/graph/index.js +302 -62
- package/dist/index.js +21 -6
- package/dist/interfaces/index.js +15 -0
- package/dist/modules/agenda/adapters/node-cron/index.js +29 -0
- package/dist/modules/agenda/index.js +140 -0
- package/dist/{services/embedding.js → modules/embedding/adapters/ai/index.js} +24 -7
- package/dist/modules/embedding/index.js +59 -0
- package/dist/modules/memory/adapters/in-memory/index.js +210 -0
- package/dist/{memory → modules/memory}/adapters/meilisearch/index.js +97 -2
- package/dist/{memory → modules/memory}/adapters/redis/index.js +77 -15
- package/dist/modules/memory/index.js +103 -0
- package/dist/utils/{stringifiy-zod-schema.js → generate-action-schema.js} +5 -5
- package/graph/controller.ts +46 -35
- package/graph/index.ts +534 -102
- package/graph.ts +74 -0
- package/index.ts +25 -7
- package/interfaces/index.ts +353 -27
- package/modules/agenda/adapters/node-cron/index.ts +25 -0
- package/modules/agenda/index.ts +159 -0
- package/modules/embedding/adapters/ai/index.ts +42 -0
- package/modules/embedding/index.ts +45 -0
- package/modules/memory/adapters/in-memory/index.ts +203 -0
- package/{memory → modules/memory}/adapters/meilisearch/index.ts +114 -12
- package/modules/memory/adapters/redis/index.ts +164 -0
- package/modules/memory/index.ts +93 -0
- package/package.json +3 -1
- package/test/graph/index.test.ts +578 -0
- package/test/modules/agenda/node-cron.test.ts +286 -0
- package/test/modules/embedding/ai.test.ts +78 -0
- package/test/modules/memory/adapters/in-memory.test.ts +153 -0
- package/test/{memory → modules/memory}/adapters/meilisearch.test.ts +79 -75
- package/test/modules/memory/adapters/redis.test.ts +169 -0
- package/test/modules/memory/base.test.ts +230 -0
- package/test/services/agenda.test.ts +279 -280
- package/types/index.ts +93 -202
- package/utils/{stringifiy-zod-schema.ts → generate-action-schema.ts} +3 -3
- package/app/README.md +0 -36
- package/app/app/favicon.ico +0 -0
- package/app/app/globals.css +0 -21
- package/app/app/gun.ts +0 -0
- package/app/app/layout.tsx +0 -18
- package/app/app/page.tsx +0 -321
- package/app/eslint.config.mjs +0 -16
- package/app/next.config.ts +0 -7
- package/app/package-lock.json +0 -5912
- package/app/package.json +0 -31
- package/app/pnpm-lock.yaml +0 -4031
- package/app/postcss.config.mjs +0 -8
- package/app/public/file.svg +0 -1
- package/app/public/globe.svg +0 -1
- package/app/public/next.svg +0 -1
- package/app/public/vercel.svg +0 -1
- package/app/public/window.svg +0 -1
- package/app/tailwind.config.ts +0 -18
- package/app/tsconfig.json +0 -27
- package/dist/memory/index.js +0 -9
- package/dist/services/agenda.js +0 -115
- package/dist/services/queue.js +0 -142
- package/dist/utils/experimental-graph-rag.js +0 -152
- package/dist/utils/generate-object.js +0 -111
- package/dist/utils/inject-actions.js +0 -16
- package/dist/utils/queue-item-transformer.js +0 -24
- package/dist/utils/sanitize-results.js +0 -60
- package/memory/adapters/redis/index.ts +0 -103
- package/memory/index.ts +0 -22
- package/services/agenda.ts +0 -118
- package/services/embedding.ts +0 -26
- package/services/queue.ts +0 -145
- package/test/memory/adapters/redis.test.ts +0 -159
- package/test/memory/base.test.ts +0 -225
- package/test/services/queue.test.ts +0 -286
- package/utils/experimental-graph-rag.ts +0 -170
- package/utils/generate-object.ts +0 -117
- package/utils/inject-actions.ts +0 -19
- package/utils/queue-item-transformer.ts +0 -38
- package/utils/sanitize-results.ts +0 -66
package/graph/index.ts
CHANGED
@@ -1,193 +1,625 @@
|
|
1
|
-
import {
|
2
|
-
import
|
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
|
-
|
6
|
-
|
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:
|
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
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
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(
|
159
|
+
if (!node) throw new Error(`Node "${nodeName}" not found.`);
|
43
160
|
|
44
|
-
|
161
|
+
this.addLog(`🚀 Starting node "${nodeName}"`);
|
162
|
+
this.eventEmitter.emit("nodeStarted", { name: nodeName });
|
45
163
|
|
46
|
-
|
47
|
-
|
48
|
-
const delay = node.retry?.delay || 0;
|
164
|
+
try {
|
165
|
+
const localContext = structuredClone(context);
|
49
166
|
|
50
|
-
|
51
|
-
|
52
|
-
|
167
|
+
if (node.condition && !node.condition(localContext)) {
|
168
|
+
this.addLog(`⏭️ Skipping node "${nodeName}" - condition not met`);
|
169
|
+
return;
|
170
|
+
}
|
53
171
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
244
|
+
// Validate outputs
|
245
|
+
if (node.outputs) {
|
246
|
+
this.addLog(`📤 Validating outputs for node "${nodeName}"`);
|
247
|
+
node.outputs.parse(localContext);
|
248
|
+
}
|
77
249
|
|
78
|
-
|
250
|
+
Object.assign(context, localContext);
|
79
251
|
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
-
|
97
|
-
|
98
|
-
|
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) =>
|
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
|
-
|
115
|
-
|
374
|
+
inputParams?: any,
|
375
|
+
inputContext?: Partial<GraphContext<T>>
|
116
376
|
): Promise<GraphContext<T>> {
|
117
|
-
|
118
|
-
|
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
|
-
|
124
|
-
|
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);
|
399
|
+
this.globalErrorHandler?.(error as Error, this.context);
|
128
400
|
throw error;
|
129
401
|
}
|
130
402
|
}
|
131
403
|
|
132
|
-
|
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
|
-
|
137
|
-
if (data) Object.assign(this.context, data); // ✅ Met à jour le contexte global
|
414
|
+
const workingContext = structuredClone(this.context);
|
138
415
|
|
139
|
-
|
416
|
+
if (data) {
|
417
|
+
Object.assign(workingContext, data);
|
418
|
+
}
|
140
419
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
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
|
-
|
168
|
-
|
169
|
-
)
|
170
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
}
|