@ai.ntellect/core 0.6.21 → 0.7.1
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 +2 -1
- package/README.md +61 -85
- package/graph/controller.ts +1 -1
- package/graph/event-manager.ts +288 -0
- package/graph/index.ts +153 -367
- package/graph/logger.ts +70 -0
- package/graph/node.ts +398 -0
- package/graph/observer.ts +361 -0
- package/interfaces/index.ts +102 -1
- package/modules/agenda/index.ts +3 -16
- package/modules/memory/adapters/in-memory/index.ts +6 -2
- package/modules/memory/adapters/meilisearch/index.ts +6 -2
- package/modules/memory/adapters/redis/index.ts +2 -2
- package/modules/memory/index.ts +1 -1
- package/package.json +10 -5
- package/test/graph/index.test.ts +244 -113
- package/test/graph/observer.test.ts +398 -0
- package/test/modules/agenda/node-cron.test.ts +37 -16
- package/test/modules/memory/adapters/in-memory.test.ts +2 -2
- package/test/modules/memory/adapters/meilisearch.test.ts +28 -24
- package/test/modules/memory/base.test.ts +3 -3
- package/tsconfig.json +4 -2
- package/types/index.ts +24 -3
- package/dist/graph/controller.js +0 -72
- package/dist/graph/index.js +0 -501
- package/dist/index.js +0 -41
- package/dist/interfaces/index.js +0 -17
- package/dist/modules/agenda/adapters/node-cron/index.js +0 -29
- package/dist/modules/agenda/index.js +0 -140
- package/dist/modules/embedding/adapters/ai/index.js +0 -57
- package/dist/modules/embedding/index.js +0 -59
- package/dist/modules/memory/adapters/in-memory/index.js +0 -210
- package/dist/modules/memory/adapters/meilisearch/index.js +0 -320
- package/dist/modules/memory/adapters/redis/index.js +0 -158
- package/dist/modules/memory/index.js +0 -103
- package/dist/types/index.js +0 -2
- package/dist/utils/generate-action-schema.js +0 -43
- package/dist/utils/header-builder.js +0 -34
- package/test/modules/embedding/ai.test.ts +0 -78
- package/test/modules/memory/adapters/redis.test.ts +0 -169
- package/test/services/agenda.test.ts +0 -279
package/graph/logger.ts
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
/**
|
2
|
+
* Handles logging operations for a graph instance
|
3
|
+
* Provides methods for adding, retrieving, and managing logs with optional verbose output
|
4
|
+
*/
|
5
|
+
export class GraphLogger {
|
6
|
+
private logs: string[] = [];
|
7
|
+
private verbose: boolean = false;
|
8
|
+
|
9
|
+
/**
|
10
|
+
* Creates a new GraphLogger instance
|
11
|
+
* @param graphName - The name of the graph this logger is associated with
|
12
|
+
* @param verbose - Whether to output logs to console in real-time
|
13
|
+
*/
|
14
|
+
constructor(private graphName: string, verbose: boolean = false) {
|
15
|
+
this.verbose = verbose;
|
16
|
+
}
|
17
|
+
|
18
|
+
/**
|
19
|
+
* Adds a new log entry with timestamp
|
20
|
+
* @param message - The message to log
|
21
|
+
*/
|
22
|
+
public addLog(message: string): void {
|
23
|
+
const timestamp = new Date().toISOString();
|
24
|
+
const logMessage = `[${timestamp}] ${message}`;
|
25
|
+
this.logs.push(logMessage);
|
26
|
+
if (this.verbose) {
|
27
|
+
console.log(`${this.graphName} - ${message}`);
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
/**
|
32
|
+
* Returns a copy of all stored logs
|
33
|
+
* @returns Array of log messages
|
34
|
+
*/
|
35
|
+
public getLogs(): string[] {
|
36
|
+
return [...this.logs];
|
37
|
+
}
|
38
|
+
|
39
|
+
/**
|
40
|
+
* Clears all stored logs
|
41
|
+
*/
|
42
|
+
public clearLogs(): void {
|
43
|
+
this.logs = [];
|
44
|
+
}
|
45
|
+
|
46
|
+
/**
|
47
|
+
* Sets the verbose mode
|
48
|
+
* @param enabled - Whether to enable verbose mode
|
49
|
+
*/
|
50
|
+
public setVerbose(enabled: boolean): void {
|
51
|
+
this.verbose = enabled;
|
52
|
+
}
|
53
|
+
|
54
|
+
/**
|
55
|
+
* Gets the current verbose mode status
|
56
|
+
* @returns Current verbose mode state
|
57
|
+
*/
|
58
|
+
public isVerbose(): boolean {
|
59
|
+
return this.verbose;
|
60
|
+
}
|
61
|
+
|
62
|
+
/**
|
63
|
+
* Logs a message to console with graph name prefix
|
64
|
+
* @param message - The message to log
|
65
|
+
* @param data - Optional data to log
|
66
|
+
*/
|
67
|
+
log(message: string, data?: any): void {
|
68
|
+
console.log(`[Graph ${this.graphName}] ${message}`, data);
|
69
|
+
}
|
70
|
+
}
|
package/graph/node.ts
ADDED
@@ -0,0 +1,398 @@
|
|
1
|
+
import { BehaviorSubject, Subject } from "rxjs";
|
2
|
+
import { ZodSchema } from "zod";
|
3
|
+
import { GraphContext, GraphEvent, Node } from "../types";
|
4
|
+
import { GraphEventManager } from "./event-manager";
|
5
|
+
import { GraphLogger } from "./logger";
|
6
|
+
|
7
|
+
/**
|
8
|
+
* Represents a node in the graph that can execute operations and manage state
|
9
|
+
* @template T - The Zod schema type for validation
|
10
|
+
*/
|
11
|
+
export class GraphNode<T extends ZodSchema> {
|
12
|
+
/**
|
13
|
+
* Creates a new GraphNode instance
|
14
|
+
* @param nodes - Map of all nodes in the graph
|
15
|
+
* @param logger - Logger instance for tracking node operations
|
16
|
+
* @param eventManager - Manager for handling graph events
|
17
|
+
* @param eventSubject - Subject for emitting events
|
18
|
+
* @param stateSubject - Subject for managing graph state
|
19
|
+
*/
|
20
|
+
constructor(
|
21
|
+
private nodes: Map<string, Node<T, any>>,
|
22
|
+
private logger: GraphLogger,
|
23
|
+
private eventManager: GraphEventManager<T>,
|
24
|
+
private eventSubject: Subject<GraphEvent<T>>,
|
25
|
+
private stateSubject: BehaviorSubject<GraphContext<T>>
|
26
|
+
) {}
|
27
|
+
|
28
|
+
/**
|
29
|
+
* Emits an event with the specified type and payload
|
30
|
+
* @param type - The type of event to emit
|
31
|
+
* @param payload - The data associated with the event
|
32
|
+
* @private
|
33
|
+
*/
|
34
|
+
private emitEvent(type: string, payload: any) {
|
35
|
+
this.logger.addLog(`📢 Event: ${type}`);
|
36
|
+
const event = {
|
37
|
+
type,
|
38
|
+
payload: {
|
39
|
+
...payload,
|
40
|
+
name:
|
41
|
+
type === "nodeStateChanged"
|
42
|
+
? payload.name || payload.nodeName
|
43
|
+
: payload.name,
|
44
|
+
context: { ...payload.context },
|
45
|
+
},
|
46
|
+
timestamp: Date.now(),
|
47
|
+
};
|
48
|
+
|
49
|
+
this.eventSubject.next(event);
|
50
|
+
this.eventManager.emitEvent(type, event);
|
51
|
+
|
52
|
+
// Update state subject only for state changes
|
53
|
+
if (type === "nodeStateChanged") {
|
54
|
+
this.stateSubject.next({ ...payload.context });
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Executes a node with the given name and context
|
60
|
+
* @param nodeName - The name of the node to execute
|
61
|
+
* @param context - The current graph context
|
62
|
+
* @param inputs - Input data for the node
|
63
|
+
* @param triggeredByEvent - Whether the execution was triggered by an event
|
64
|
+
* @throws Error if the node is not found or execution fails
|
65
|
+
*/
|
66
|
+
async executeNode(
|
67
|
+
nodeName: string,
|
68
|
+
context: GraphContext<T>,
|
69
|
+
inputs: any,
|
70
|
+
triggeredByEvent: boolean = false
|
71
|
+
): Promise<void> {
|
72
|
+
const node = this.nodes.get(nodeName);
|
73
|
+
if (!node) throw new Error(`Node "${nodeName}" not found.`);
|
74
|
+
|
75
|
+
this.logger.addLog(`🚀 Starting node "${nodeName}"`);
|
76
|
+
this.emitEvent("nodeStarted", { name: nodeName, context: { ...context } });
|
77
|
+
|
78
|
+
try {
|
79
|
+
const contextProxy = new Proxy(context, {
|
80
|
+
set: (target, prop, value) => {
|
81
|
+
const oldValue = target[prop];
|
82
|
+
target[prop] = value;
|
83
|
+
|
84
|
+
this.emitEvent("nodeStateChanged", {
|
85
|
+
nodeName,
|
86
|
+
name: nodeName,
|
87
|
+
property: prop.toString(),
|
88
|
+
oldValue,
|
89
|
+
newValue: value,
|
90
|
+
context: { ...target },
|
91
|
+
});
|
92
|
+
|
93
|
+
return true;
|
94
|
+
},
|
95
|
+
get: (target, prop) => {
|
96
|
+
return target[prop];
|
97
|
+
},
|
98
|
+
});
|
99
|
+
|
100
|
+
if (node.condition && !node.condition(contextProxy)) {
|
101
|
+
this.logger.addLog(
|
102
|
+
`⏭️ Skipping node "${nodeName}" - condition not met`
|
103
|
+
);
|
104
|
+
return;
|
105
|
+
}
|
106
|
+
|
107
|
+
if (node.inputs) {
|
108
|
+
await this.validateInputs(node, inputs, nodeName);
|
109
|
+
}
|
110
|
+
|
111
|
+
if (node.retry && node.retry.maxAttempts > 0) {
|
112
|
+
await this.executeWithRetry(node, contextProxy, inputs, nodeName);
|
113
|
+
} else {
|
114
|
+
await node.execute(contextProxy, inputs);
|
115
|
+
}
|
116
|
+
|
117
|
+
if (node.outputs) {
|
118
|
+
await this.validateOutputs(node, contextProxy, nodeName);
|
119
|
+
}
|
120
|
+
|
121
|
+
if (!triggeredByEvent) {
|
122
|
+
const nextNodes =
|
123
|
+
typeof node.next === "function"
|
124
|
+
? node.next(contextProxy)
|
125
|
+
: node.next || [];
|
126
|
+
|
127
|
+
for (const nextNodeName of nextNodes) {
|
128
|
+
const nextNode = this.nodes.get(nextNodeName);
|
129
|
+
if (
|
130
|
+
nextNode &&
|
131
|
+
(!nextNode.condition || nextNode.condition(contextProxy))
|
132
|
+
) {
|
133
|
+
await this.executeNode(
|
134
|
+
nextNodeName,
|
135
|
+
contextProxy,
|
136
|
+
undefined,
|
137
|
+
false
|
138
|
+
);
|
139
|
+
}
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
if (!triggeredByEvent) {
|
144
|
+
await this.handleEvents(node, nodeName, contextProxy);
|
145
|
+
}
|
146
|
+
|
147
|
+
this.logger.addLog(`✅ Node "${nodeName}" executed successfully`);
|
148
|
+
this.emitEvent("nodeCompleted", {
|
149
|
+
name: nodeName,
|
150
|
+
context: { ...contextProxy },
|
151
|
+
});
|
152
|
+
} catch (error: any) {
|
153
|
+
const errorToThrow =
|
154
|
+
error instanceof Error
|
155
|
+
? error
|
156
|
+
: new Error(error.message || "Unknown error");
|
157
|
+
|
158
|
+
this.logger.addLog(
|
159
|
+
`❌ Error in node "${nodeName}": ${errorToThrow.message}`
|
160
|
+
);
|
161
|
+
|
162
|
+
this.emitEvent("nodeError", {
|
163
|
+
name: nodeName,
|
164
|
+
error: errorToThrow,
|
165
|
+
context,
|
166
|
+
});
|
167
|
+
|
168
|
+
throw errorToThrow;
|
169
|
+
}
|
170
|
+
}
|
171
|
+
|
172
|
+
/**
|
173
|
+
* Validates the inputs for a node using its schema
|
174
|
+
* @param node - The node whose inputs need validation
|
175
|
+
* @param inputs - The input data to validate
|
176
|
+
* @param nodeName - The name of the node (for error messages)
|
177
|
+
* @throws Error if validation fails
|
178
|
+
* @private
|
179
|
+
*/
|
180
|
+
private async validateInputs(
|
181
|
+
node: Node<T, any>,
|
182
|
+
inputs: any,
|
183
|
+
nodeName: string
|
184
|
+
): Promise<void> {
|
185
|
+
if (!inputs) {
|
186
|
+
throw new Error(`Inputs required for node "${nodeName}"`);
|
187
|
+
}
|
188
|
+
|
189
|
+
try {
|
190
|
+
return node.inputs!.parse(inputs);
|
191
|
+
} catch (error: any) {
|
192
|
+
throw new Error(
|
193
|
+
error.errors?.[0]?.message || error.message || "Input validation failed"
|
194
|
+
);
|
195
|
+
}
|
196
|
+
}
|
197
|
+
|
198
|
+
/**
|
199
|
+
* Validates the outputs of a node against its schema
|
200
|
+
* @param node - The node whose outputs need validation
|
201
|
+
* @param context - The current graph context
|
202
|
+
* @param nodeName - The name of the node (for error messages)
|
203
|
+
* @throws Error if validation fails
|
204
|
+
* @private
|
205
|
+
*/
|
206
|
+
private async validateOutputs(
|
207
|
+
node: Node<T, any>,
|
208
|
+
context: GraphContext<T>,
|
209
|
+
nodeName: string
|
210
|
+
): Promise<void> {
|
211
|
+
try {
|
212
|
+
node.outputs!.parse(context);
|
213
|
+
} catch (error: any) {
|
214
|
+
throw new Error(
|
215
|
+
error.errors?.[0]?.message ||
|
216
|
+
error.message ||
|
217
|
+
"Output validation failed"
|
218
|
+
);
|
219
|
+
}
|
220
|
+
}
|
221
|
+
|
222
|
+
/**
|
223
|
+
* Handles event-related operations for a node
|
224
|
+
* @param node - The node whose events need handling
|
225
|
+
* @param nodeName - The name of the node
|
226
|
+
* @param context - The current graph context
|
227
|
+
* @private
|
228
|
+
*/
|
229
|
+
private async handleEvents(
|
230
|
+
node: Node<T, any>,
|
231
|
+
nodeName: string,
|
232
|
+
context: GraphContext<T>
|
233
|
+
): Promise<void> {
|
234
|
+
if (node.correlateEvents) {
|
235
|
+
await this.handleCorrelatedEvents(node, nodeName);
|
236
|
+
}
|
237
|
+
|
238
|
+
if (node.waitForEvents) {
|
239
|
+
await this.handleWaitForEvents(node, nodeName);
|
240
|
+
}
|
241
|
+
}
|
242
|
+
|
243
|
+
/**
|
244
|
+
* Executes a node with retry logic
|
245
|
+
* @param node - The node to execute
|
246
|
+
* @param contextProxy - The proxied graph context
|
247
|
+
* @param inputs - Input data for the node
|
248
|
+
* @param nodeName - The name of the node
|
249
|
+
* @throws Error if all retry attempts fail
|
250
|
+
* @private
|
251
|
+
*/
|
252
|
+
private async executeWithRetry(
|
253
|
+
node: Node<T, any>,
|
254
|
+
contextProxy: GraphContext<T>,
|
255
|
+
inputs: any,
|
256
|
+
nodeName: string
|
257
|
+
): Promise<void> {
|
258
|
+
let attempts = 0;
|
259
|
+
let lastError: Error | null = null;
|
260
|
+
|
261
|
+
while (attempts < node.retry!.maxAttempts) {
|
262
|
+
try {
|
263
|
+
this.logger.addLog(
|
264
|
+
`🔄 Attempt ${attempts + 1}/${node.retry!.maxAttempts}`
|
265
|
+
);
|
266
|
+
await node.execute(contextProxy, inputs);
|
267
|
+
return;
|
268
|
+
} catch (error: any) {
|
269
|
+
lastError = error instanceof Error ? error : new Error(error.message);
|
270
|
+
attempts++;
|
271
|
+
this.logger.addLog(
|
272
|
+
`❌ Attempt ${attempts} failed: ${lastError.message}`
|
273
|
+
);
|
274
|
+
|
275
|
+
if (attempts === node.retry!.maxAttempts) {
|
276
|
+
if (node.retry!.onRetryFailed && lastError) {
|
277
|
+
await this.handleRetryFailure(
|
278
|
+
node,
|
279
|
+
lastError,
|
280
|
+
contextProxy,
|
281
|
+
nodeName
|
282
|
+
);
|
283
|
+
}
|
284
|
+
throw lastError;
|
285
|
+
}
|
286
|
+
|
287
|
+
await new Promise((resolve) =>
|
288
|
+
setTimeout(resolve, node.retry?.delay || 0)
|
289
|
+
);
|
290
|
+
}
|
291
|
+
}
|
292
|
+
}
|
293
|
+
|
294
|
+
/**
|
295
|
+
* Handles the failure of retry attempts
|
296
|
+
* @param node - The node that failed
|
297
|
+
* @param error - The error that caused the failure
|
298
|
+
* @param context - The current graph context
|
299
|
+
* @param nodeName - The name of the node
|
300
|
+
* @private
|
301
|
+
*/
|
302
|
+
private async handleRetryFailure(
|
303
|
+
node: Node<T, any>,
|
304
|
+
error: Error,
|
305
|
+
context: GraphContext<T>,
|
306
|
+
nodeName: string
|
307
|
+
): Promise<void> {
|
308
|
+
this.logger.addLog(
|
309
|
+
`🔄 Executing retry failure handler for node "${nodeName}"`
|
310
|
+
);
|
311
|
+
try {
|
312
|
+
if (node.retry?.onRetryFailed) {
|
313
|
+
await node.retry.onRetryFailed(error, context);
|
314
|
+
if (node.retry.continueOnFailed) {
|
315
|
+
this.logger.addLog(
|
316
|
+
`✅ Retry failure handler succeeded - continuing execution`
|
317
|
+
);
|
318
|
+
return;
|
319
|
+
}
|
320
|
+
this.logger.addLog(
|
321
|
+
`⚠️ Retry failure handler executed but node will still fail`
|
322
|
+
);
|
323
|
+
}
|
324
|
+
} catch (handlerError: any) {
|
325
|
+
this.logger.addLog(
|
326
|
+
`❌ Retry failure handler failed: ${handlerError.message}`
|
327
|
+
);
|
328
|
+
throw handlerError;
|
329
|
+
}
|
330
|
+
}
|
331
|
+
|
332
|
+
/**
|
333
|
+
* Handles correlated events for a node
|
334
|
+
* @param node - The node with correlated events
|
335
|
+
* @param nodeName - The name of the node
|
336
|
+
* @throws Error if correlation fails or timeout occurs
|
337
|
+
* @private
|
338
|
+
*/
|
339
|
+
private async handleCorrelatedEvents(
|
340
|
+
node: Node<T, any>,
|
341
|
+
nodeName: string
|
342
|
+
): Promise<void> {
|
343
|
+
if (node.correlateEvents) {
|
344
|
+
const { events, timeout, correlation } = node.correlateEvents;
|
345
|
+
this.logger.addLog(
|
346
|
+
`⏳ Node "${nodeName}" waiting for correlated events: ${events.join(
|
347
|
+
", "
|
348
|
+
)}`
|
349
|
+
);
|
350
|
+
|
351
|
+
try {
|
352
|
+
// Attendre les événements
|
353
|
+
const receivedEvents = await this.eventManager.waitForEvents(
|
354
|
+
events,
|
355
|
+
timeout
|
356
|
+
);
|
357
|
+
|
358
|
+
// Vérifier la corrélation
|
359
|
+
if (!correlation(receivedEvents)) {
|
360
|
+
this.logger.addLog(
|
361
|
+
`❌ Event correlation failed for node "${nodeName}"`
|
362
|
+
);
|
363
|
+
throw new Error(`Event correlation failed for node "${nodeName}"`);
|
364
|
+
}
|
365
|
+
|
366
|
+
this.logger.addLog(
|
367
|
+
`✅ Event correlation succeeded for node "${nodeName}"`
|
368
|
+
);
|
369
|
+
} catch (error) {
|
370
|
+
this.logger.addLog(
|
371
|
+
`❌ Error waiting for events: ${(error as Error).message}`
|
372
|
+
);
|
373
|
+
throw error;
|
374
|
+
}
|
375
|
+
}
|
376
|
+
}
|
377
|
+
|
378
|
+
/**
|
379
|
+
* Handles waiting for events
|
380
|
+
* @param node - The node waiting for events
|
381
|
+
* @param nodeName - The name of the node
|
382
|
+
* @throws Error if timeout occurs
|
383
|
+
* @private
|
384
|
+
*/
|
385
|
+
private async handleWaitForEvents(
|
386
|
+
node: Node<T, any>,
|
387
|
+
nodeName: string
|
388
|
+
): Promise<void> {
|
389
|
+
if (node.waitForEvents) {
|
390
|
+
const { events, timeout } = node.waitForEvents;
|
391
|
+
this.logger.addLog(
|
392
|
+
`⏳ Node "${nodeName}" waiting for events: ${events.join(", ")}`
|
393
|
+
);
|
394
|
+
await this.eventManager.waitForEvents(events, timeout);
|
395
|
+
this.logger.addLog(`✅ All events received for node "${nodeName}"`);
|
396
|
+
}
|
397
|
+
}
|
398
|
+
}
|