@ai.ntellect/core 0.6.15 → 0.6.17
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/dist/graph/{graph.js → index.js} +2 -7
- package/dist/index.js +1 -1
- package/graph/controller.ts +2 -2
- package/graph/{graph.ts → index.ts} +1 -2
- package/index.ts +1 -1
- package/interfaces/index.ts +1 -2
- package/memory/adapters/meilisearch/index.ts +5 -1
- package/memory/adapters/redis/index.ts +2 -2
- package/memory/index.ts +2 -2
- package/package.json +2 -4
- package/services/agenda.ts +1 -1
- package/services/embedding.ts +1 -1
- package/test/memory/adapters/meilisearch.test.ts +17 -35
- package/test/memory/adapters/redis.test.ts +3 -4
- package/test/memory/base.test.ts +3 -7
- package/test/services/queue.test.ts +2 -2
- package/tsconfig.json +0 -3
- package/utils/experimental-graph-rag.ts +2 -2
- package/utils/queue-item-transformer.ts +1 -1
- package/test/.env.test +0 -4
- package/test/graph/engine.test.ts +0 -533
@@ -13,10 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
13
|
};
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
15
15
|
exports.Graph = void 0;
|
16
|
-
const dotenv_1 = require("dotenv");
|
17
16
|
const events_1 = __importDefault(require("events"));
|
18
|
-
(0, dotenv_1.configDotenv)();
|
19
|
-
// Classe Graph avec un contexte typé
|
20
17
|
class Graph {
|
21
18
|
constructor(name, config) {
|
22
19
|
this.name = name;
|
@@ -64,7 +61,7 @@ class Graph {
|
|
64
61
|
}
|
65
62
|
validatedParams = node.parameters.parse(params);
|
66
63
|
}
|
67
|
-
|
64
|
+
this.eventEmitter.emit("nodeStarted", { name: nodeName, context });
|
68
65
|
if (node.execute) {
|
69
66
|
yield node.execute(context);
|
70
67
|
}
|
@@ -75,7 +72,7 @@ class Graph {
|
|
75
72
|
yield node.executeWithParams(context, validatedParams);
|
76
73
|
}
|
77
74
|
this.validateContext(context);
|
78
|
-
this.eventEmitter.emit("nodeCompleted", { nodeName, context });
|
75
|
+
this.eventEmitter.emit("nodeCompleted", { name: nodeName, context });
|
79
76
|
if (node.next) {
|
80
77
|
yield Promise.all(node.next.map((nextNode) => this.executeNode(nextNode, context)));
|
81
78
|
}
|
@@ -148,11 +145,9 @@ class Graph {
|
|
148
145
|
getContext() {
|
149
146
|
return structuredClone(this.context);
|
150
147
|
}
|
151
|
-
// Journalisation (logging)
|
152
148
|
log(message, data) {
|
153
149
|
console.log(`[Graph ${this.name}] ${message}`, data);
|
154
150
|
}
|
155
|
-
// Modification dynamique du graph
|
156
151
|
addNode(node) {
|
157
152
|
this.nodes.set(node.name, node);
|
158
153
|
this.setupEventListeners();
|
package/dist/index.js
CHANGED
@@ -14,8 +14,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
15
15
|
};
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
17
|
+
__exportStar(require("./graph"), exports);
|
17
18
|
__exportStar(require("./graph/controller"), exports);
|
18
|
-
__exportStar(require("./graph/graph"), exports);
|
19
19
|
__exportStar(require("./memory"), exports);
|
20
20
|
__exportStar(require("./memory/adapters/meilisearch"), exports);
|
21
21
|
__exportStar(require("./memory/adapters/redis"), exports);
|
package/graph/controller.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
import { GraphContext } from "@/types";
|
2
1
|
import { ZodSchema } from "zod";
|
3
|
-
import { Graph } from "
|
2
|
+
import { Graph } from ".";
|
3
|
+
import { GraphContext } from "../types";
|
4
4
|
|
5
5
|
export class GraphController {
|
6
6
|
static async executeSequential<T extends ZodSchema>(
|
@@ -1,8 +1,7 @@
|
|
1
|
-
import { GraphConfig, GraphContext, GraphDefinition, Node } from "
|
1
|
+
import { GraphConfig, GraphContext, GraphDefinition, Node } from "../types";
|
2
2
|
import EventEmitter from "events";
|
3
3
|
import { ZodSchema } from "zod";
|
4
4
|
|
5
|
-
// Classe Graph avec un contexte typé
|
6
5
|
export class Graph<T extends ZodSchema> {
|
7
6
|
private nodes: Map<string, Node<T>>;
|
8
7
|
private context: GraphContext<T>;
|
package/index.ts
CHANGED
package/interfaces/index.ts
CHANGED
@@ -1,4 +1,8 @@
|
|
1
|
-
import {
|
1
|
+
import {
|
2
|
+
BaseMemoryType,
|
3
|
+
CreateMemoryInput,
|
4
|
+
MeilisearchConfig,
|
5
|
+
} from "../../../types";
|
2
6
|
|
3
7
|
export class MeilisearchAdapter {
|
4
8
|
constructor(private readonly config: MeilisearchConfig) {}
|
@@ -1,6 +1,6 @@
|
|
1
|
-
import { BaseMemoryService } from "@/interfaces";
|
2
|
-
import { BaseMemoryType } from "@/types";
|
3
1
|
import { createClient } from "redis";
|
2
|
+
import { BaseMemoryService } from "../../../interfaces";
|
3
|
+
import { BaseMemoryType } from "../../../types";
|
4
4
|
|
5
5
|
export class RedisAdapter implements BaseMemoryService {
|
6
6
|
private redis;
|
package/memory/index.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
import { BaseMemoryService } from "
|
2
|
-
import { BaseMemoryType, CreateMemoryInput } from "
|
1
|
+
import { BaseMemoryService } from "../interfaces";
|
2
|
+
import { BaseMemoryType, CreateMemoryInput } from "../types";
|
3
3
|
|
4
4
|
export abstract class BaseMemory {
|
5
5
|
constructor(protected readonly cacheService: BaseMemoryService) {}
|
package/package.json
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
{
|
2
2
|
"name": "@ai.ntellect/core",
|
3
|
-
"version": "0.6.
|
3
|
+
"version": "0.6.17",
|
4
4
|
"description": "",
|
5
5
|
"main": "dist/index.js",
|
6
6
|
"scripts": {
|
7
|
-
"build": "rm -rf dist && tsc
|
7
|
+
"build": "rm -rf dist && tsc",
|
8
8
|
"test": "mocha --require ts-node/register",
|
9
9
|
"test:watch": "mocha --require ts-node/register 'test/**/*.test.ts' --watch"
|
10
10
|
},
|
@@ -40,8 +40,6 @@
|
|
40
40
|
"mocha": "^10.0.0",
|
41
41
|
"redis": "^4.6.13",
|
42
42
|
"ts-node": "^10.9.0",
|
43
|
-
"tsc-alias": "^1.8.10",
|
44
|
-
"tsconfig-paths": "^4.2.0",
|
45
43
|
"typescript": "^5.7.2"
|
46
44
|
},
|
47
45
|
"repository": {
|
package/services/agenda.ts
CHANGED
package/services/embedding.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
import { BaseMemoryService } from "
|
2
|
-
import { MeilisearchAdapter } from "
|
3
|
-
import { BaseMemoryType } from "
|
1
|
+
import { BaseMemoryService } from "../../../interfaces";
|
2
|
+
import { MeilisearchAdapter } from "../../../memory/adapters/meilisearch";
|
3
|
+
import { BaseMemoryType } from "../../../types";
|
4
4
|
import { expect } from "chai";
|
5
5
|
import dotenv from "dotenv";
|
6
6
|
|
@@ -9,13 +9,11 @@ dotenv.config();
|
|
9
9
|
|
10
10
|
describe("MeilisearchAdapter", () => {
|
11
11
|
let meilisearchAdapter: MeilisearchAdapter;
|
12
|
-
let mockBaseMemoryService: BaseMemoryService;
|
13
12
|
const TEST_ROOM_ID = "test-room";
|
14
13
|
|
15
14
|
const testMemory: BaseMemoryType = {
|
16
15
|
id: "test-id",
|
17
16
|
data: "test data",
|
18
|
-
query: "test query",
|
19
17
|
embedding: [0.1, 0.2, 0.3],
|
20
18
|
roomId: "test-room",
|
21
19
|
createdAt: new Date(),
|
@@ -26,15 +24,12 @@ describe("MeilisearchAdapter", () => {
|
|
26
24
|
if (process.env.MEILISEARCH_HOST && process.env.MEILISEARCH_API_KEY) {
|
27
25
|
// Real Meilisearch configuration
|
28
26
|
// console.log("Real Meilisearch configuration");
|
29
|
-
meilisearchAdapter = new MeilisearchAdapter(
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
},
|
36
|
-
mockBaseMemoryService
|
37
|
-
);
|
27
|
+
meilisearchAdapter = new MeilisearchAdapter({
|
28
|
+
host: process.env.MEILISEARCH_HOST,
|
29
|
+
apiKey: process.env.MEILISEARCH_API_KEY,
|
30
|
+
searchableAttributes: ["content"],
|
31
|
+
sortableAttributes: ["createdAt"],
|
32
|
+
});
|
38
33
|
} else {
|
39
34
|
// Mock fetch implementation
|
40
35
|
// console.log("Mock Meilisearch configuration");
|
@@ -87,31 +82,18 @@ describe("MeilisearchAdapter", () => {
|
|
87
82
|
return new Response(JSON.stringify({}));
|
88
83
|
};
|
89
84
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
clearMemoryById: async () => {},
|
97
|
-
clearAllMemories: async () => {},
|
98
|
-
};
|
99
|
-
|
100
|
-
meilisearchAdapter = new MeilisearchAdapter(
|
101
|
-
{
|
102
|
-
host: "http://localhost:7700",
|
103
|
-
apiKey: "aSampleMasterKey",
|
104
|
-
searchableAttributes: ["content"],
|
105
|
-
sortableAttributes: ["createdAt"],
|
106
|
-
},
|
107
|
-
mockBaseMemoryService
|
108
|
-
);
|
85
|
+
meilisearchAdapter = new MeilisearchAdapter({
|
86
|
+
host: "http://localhost:7700",
|
87
|
+
apiKey: "aSampleMasterKey",
|
88
|
+
searchableAttributes: ["content"],
|
89
|
+
sortableAttributes: ["createdAt"],
|
90
|
+
});
|
109
91
|
}
|
110
92
|
});
|
111
93
|
|
112
94
|
describe("Initialization", () => {
|
113
95
|
it("should initialize storage", async () => {
|
114
|
-
await expect(meilisearchAdapter.init()).to.not.throw;
|
96
|
+
await expect(meilisearchAdapter.init("test-room")).to.not.throw;
|
115
97
|
});
|
116
98
|
});
|
117
99
|
|
@@ -170,7 +152,7 @@ describe("MeilisearchAdapter", () => {
|
|
170
152
|
}
|
171
153
|
|
172
154
|
try {
|
173
|
-
await meilisearchAdapter.init();
|
155
|
+
await meilisearchAdapter.init(TEST_ROOM_ID);
|
174
156
|
await meilisearchAdapter.initializeStorage(TEST_ROOM_ID);
|
175
157
|
} catch (error) {
|
176
158
|
console.error("Failed to initialize:", error);
|
@@ -1,6 +1,6 @@
|
|
1
|
-
import { BaseMemoryService } from "
|
2
|
-
import { RedisAdapter } from "
|
3
|
-
import { BaseMemoryType } from "
|
1
|
+
import { BaseMemoryService } from "../../../interfaces";
|
2
|
+
import { RedisAdapter } from "../../../memory/adapters/redis";
|
3
|
+
import { BaseMemoryType } from "../../../types";
|
4
4
|
import { expect } from "chai";
|
5
5
|
import dotenv from "dotenv";
|
6
6
|
import Redis from "ioredis";
|
@@ -21,7 +21,6 @@ describe("RedisAdapter", () => {
|
|
21
21
|
const testMemory: BaseMemoryType = {
|
22
22
|
id: "test-id",
|
23
23
|
data: "test data",
|
24
|
-
query: "test query",
|
25
24
|
embedding: [0.1, 0.2, 0.3],
|
26
25
|
roomId: "test-room",
|
27
26
|
createdAt: fixedDate,
|
package/test/memory/base.test.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
import { BaseMemoryService } from "
|
2
|
-
import { BaseMemory } from "
|
3
|
-
import { BaseMemoryType, CreateMemoryInput } from "
|
1
|
+
import { BaseMemoryService } from "../../interfaces";
|
2
|
+
import { BaseMemory } from "../../memory";
|
3
|
+
import { BaseMemoryType, CreateMemoryInput } from "../../types";
|
4
4
|
import { expect } from "chai";
|
5
5
|
|
6
6
|
/**
|
@@ -20,7 +20,6 @@ class TestMemory extends BaseMemory {
|
|
20
20
|
const memory: BaseMemoryType = {
|
21
21
|
id: crypto.randomUUID(),
|
22
22
|
data: input.data,
|
23
|
-
query: input.query,
|
24
23
|
embedding: input.embedding || null,
|
25
24
|
roomId: input.roomId,
|
26
25
|
createdAt: new Date(),
|
@@ -65,7 +64,6 @@ describe("BaseMemory", () => {
|
|
65
64
|
const testMemory: BaseMemoryType = {
|
66
65
|
id: "test-id",
|
67
66
|
data: "test data",
|
68
|
-
query: "test query",
|
69
67
|
embedding: [0.1, 0.2, 0.3],
|
70
68
|
roomId: "test-room",
|
71
69
|
createdAt: new Date(),
|
@@ -112,7 +110,6 @@ describe("BaseMemory", () => {
|
|
112
110
|
|
113
111
|
expect(result).to.exist;
|
114
112
|
expect(result?.data).to.equal(input.data);
|
115
|
-
expect(result?.query).to.equal(input.query);
|
116
113
|
expect(result?.roomId).to.equal(input.roomId);
|
117
114
|
expect(result?.id).to.be.a("string");
|
118
115
|
});
|
@@ -172,7 +169,6 @@ describe("BaseMemory", () => {
|
|
172
169
|
try {
|
173
170
|
await memory.createMemory({
|
174
171
|
data: "test",
|
175
|
-
query: "test",
|
176
172
|
roomId: "test",
|
177
173
|
});
|
178
174
|
expect.fail("Should have thrown an error");
|
@@ -1,5 +1,5 @@
|
|
1
|
-
import { Queue } from "
|
2
|
-
import { ActionSchema, QueueCallbacks, QueueItem } from "
|
1
|
+
import { Queue } from "../../services/queue";
|
2
|
+
import { ActionSchema, QueueCallbacks, QueueItem } from "../../types";
|
3
3
|
import { expect } from "chai";
|
4
4
|
import { z } from "zod";
|
5
5
|
|
package/tsconfig.json
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
import { MeilisearchAdapter } from "
|
2
|
-
import { AIEmbeddingService } from "
|
1
|
+
import { MeilisearchAdapter } from "../memory/adapters/meilisearch";
|
2
|
+
import { AIEmbeddingService } from "../services/embedding";
|
3
3
|
import { openai } from "@ai-sdk/openai";
|
4
4
|
import { generateObject } from "ai";
|
5
5
|
import { z } from "zod";
|
package/test/.env.test
DELETED
@@ -1,533 +0,0 @@
|
|
1
|
-
import { GraphEngine } from "@/graph/engine";
|
2
|
-
import { Persistence, RealTimeNotifier } from "@/interfaces";
|
3
|
-
import { GraphDefinition, SharedState } from "@/types";
|
4
|
-
import { expect } from "chai";
|
5
|
-
import { z } from "zod";
|
6
|
-
|
7
|
-
/**
|
8
|
-
* Test suite for the Graph service
|
9
|
-
* This suite tests the workflow execution engine that manages state transitions and node execution
|
10
|
-
*/
|
11
|
-
describe("Graph", () => {
|
12
|
-
/**
|
13
|
-
* Test schema definition using Zod
|
14
|
-
* Defines the structure and validation rules for the workflow state
|
15
|
-
*/
|
16
|
-
const TestSchema = z.object({
|
17
|
-
status: z.string(),
|
18
|
-
step: z.number(),
|
19
|
-
});
|
20
|
-
|
21
|
-
type TestState = z.infer<typeof TestSchema>;
|
22
|
-
|
23
|
-
let graph: GraphEngine<TestState>;
|
24
|
-
/**
|
25
|
-
* Test definition of a simple workflow graph
|
26
|
-
* Contains 3 nodes: start -> process -> end
|
27
|
-
* Each node updates the state with new status and step values
|
28
|
-
*/
|
29
|
-
const testDefinition: GraphDefinition<TestState> = {
|
30
|
-
name: "simple-workflow",
|
31
|
-
entryNode: "start",
|
32
|
-
nodes: {
|
33
|
-
start: {
|
34
|
-
name: "start",
|
35
|
-
description: "Starting node",
|
36
|
-
execute: async (state: SharedState<TestState>) => {
|
37
|
-
return graph.updateState({
|
38
|
-
...state,
|
39
|
-
status: "started",
|
40
|
-
step: 1,
|
41
|
-
});
|
42
|
-
},
|
43
|
-
relationships: [{ name: "process" }],
|
44
|
-
},
|
45
|
-
process: {
|
46
|
-
name: "process",
|
47
|
-
description: "Processing node",
|
48
|
-
execute: async (state: SharedState<TestState>) => {
|
49
|
-
return graph.updateState({
|
50
|
-
...state,
|
51
|
-
status: "processing",
|
52
|
-
step: 2,
|
53
|
-
});
|
54
|
-
},
|
55
|
-
condition: (state) => state.step === 1,
|
56
|
-
relationships: [{ name: "end" }],
|
57
|
-
},
|
58
|
-
end: {
|
59
|
-
name: "end",
|
60
|
-
description: "End node",
|
61
|
-
execute: async (state: SharedState<TestState>) => {
|
62
|
-
return graph.updateState({
|
63
|
-
...state,
|
64
|
-
status: "completed",
|
65
|
-
step: 3,
|
66
|
-
});
|
67
|
-
},
|
68
|
-
relationships: [],
|
69
|
-
},
|
70
|
-
},
|
71
|
-
schema: TestSchema,
|
72
|
-
};
|
73
|
-
|
74
|
-
beforeEach(() => {
|
75
|
-
graph = new GraphEngine(testDefinition);
|
76
|
-
});
|
77
|
-
|
78
|
-
describe("Workflow Execution", () => {
|
79
|
-
/**
|
80
|
-
* Tests the complete execution flow of the workflow
|
81
|
-
* Verifies that state transitions occur correctly from start to end
|
82
|
-
*/
|
83
|
-
it("should execute the complete workflow sequence", async () => {
|
84
|
-
const initialState: SharedState<TestState> = {
|
85
|
-
status: "init",
|
86
|
-
step: 0,
|
87
|
-
};
|
88
|
-
|
89
|
-
// Initialiser le graph avec l'état initial
|
90
|
-
graph = new GraphEngine(testDefinition, {
|
91
|
-
schema: TestSchema,
|
92
|
-
initialState,
|
93
|
-
});
|
94
|
-
|
95
|
-
// Exécuter le workflow
|
96
|
-
await graph.execute(initialState, "start");
|
97
|
-
const result = graph.getState();
|
98
|
-
|
99
|
-
expect(result).to.deep.equal({
|
100
|
-
status: "completed",
|
101
|
-
step: 3,
|
102
|
-
});
|
103
|
-
});
|
104
|
-
|
105
|
-
/**
|
106
|
-
* Tests that conditional logic in nodes is respected
|
107
|
-
* The process node should only execute when step === 1
|
108
|
-
*/
|
109
|
-
it("should respect conditions in workflow", async () => {
|
110
|
-
const initialState: SharedState<TestState> = {
|
111
|
-
status: "init",
|
112
|
-
step: 2,
|
113
|
-
};
|
114
|
-
|
115
|
-
// Initialiser le graph avec l'état initial
|
116
|
-
graph = new GraphEngine(testDefinition, {
|
117
|
-
schema: TestSchema,
|
118
|
-
initialState,
|
119
|
-
});
|
120
|
-
|
121
|
-
await graph.execute(initialState, "process");
|
122
|
-
const result = graph.getState();
|
123
|
-
|
124
|
-
expect(result).to.deep.equal({
|
125
|
-
status: "init",
|
126
|
-
step: 2,
|
127
|
-
});
|
128
|
-
});
|
129
|
-
});
|
130
|
-
|
131
|
-
describe("Graph Management", () => {
|
132
|
-
it("should add a new node to the graph", () => {
|
133
|
-
const newNode = {
|
134
|
-
name: "new-node",
|
135
|
-
description: "A new test node",
|
136
|
-
execute: async (state: SharedState<TestState>) => {
|
137
|
-
return graph.updateState({
|
138
|
-
...state,
|
139
|
-
status: "new",
|
140
|
-
step: 4,
|
141
|
-
});
|
142
|
-
},
|
143
|
-
relationships: [{ name: "end" }],
|
144
|
-
};
|
145
|
-
|
146
|
-
graph.addNode(newNode);
|
147
|
-
|
148
|
-
expect(graph.nodes.has("new-node")).to.be.true;
|
149
|
-
const addedNode = graph.nodes.get("new-node");
|
150
|
-
expect(addedNode?.relationships).to.have.lengthOf(1);
|
151
|
-
});
|
152
|
-
|
153
|
-
it("should update existing graph with new definition", () => {
|
154
|
-
const newDefinition: GraphDefinition<TestState> = {
|
155
|
-
name: "updated-workflow",
|
156
|
-
entryNode: "start",
|
157
|
-
nodes: {
|
158
|
-
...testDefinition.nodes,
|
159
|
-
"new-step": {
|
160
|
-
name: "new-step",
|
161
|
-
description: "New step node",
|
162
|
-
execute: async (state: SharedState<TestState>) => {
|
163
|
-
return graph.updateState({
|
164
|
-
...state,
|
165
|
-
status: "new-step",
|
166
|
-
step: 4,
|
167
|
-
});
|
168
|
-
},
|
169
|
-
relationships: [],
|
170
|
-
},
|
171
|
-
},
|
172
|
-
schema: TestSchema,
|
173
|
-
};
|
174
|
-
|
175
|
-
graph.updateGraph(newDefinition);
|
176
|
-
expect(graph.nodes.has("new-step")).to.be.true;
|
177
|
-
});
|
178
|
-
});
|
179
|
-
|
180
|
-
describe("State Management", () => {
|
181
|
-
it("should properly update and retrieve state", async () => {
|
182
|
-
const newState: SharedState<TestState> = {
|
183
|
-
status: "test",
|
184
|
-
step: 5,
|
185
|
-
};
|
186
|
-
|
187
|
-
graph.setState(newState);
|
188
|
-
const retrievedState = graph.getState();
|
189
|
-
|
190
|
-
expect(retrievedState).to.deep.equal(newState);
|
191
|
-
});
|
192
|
-
|
193
|
-
it("should merge states correctly when updating partially", () => {
|
194
|
-
const initialState: SharedState<TestState> = {
|
195
|
-
status: "initial",
|
196
|
-
step: 1,
|
197
|
-
};
|
198
|
-
|
199
|
-
graph.setState(initialState);
|
200
|
-
|
201
|
-
const partialUpdate = {
|
202
|
-
status: "updated",
|
203
|
-
};
|
204
|
-
|
205
|
-
const updatedState = graph.updateState(partialUpdate);
|
206
|
-
|
207
|
-
expect(updatedState).to.deep.equal({
|
208
|
-
status: "updated",
|
209
|
-
step: 1,
|
210
|
-
});
|
211
|
-
});
|
212
|
-
});
|
213
|
-
|
214
|
-
describe("Error Handling", () => {
|
215
|
-
it("should handle execution errors gracefully", async () => {
|
216
|
-
const errorNode = {
|
217
|
-
name: "error-node",
|
218
|
-
execute: async () => {
|
219
|
-
throw new Error("Test error");
|
220
|
-
},
|
221
|
-
};
|
222
|
-
|
223
|
-
graph.addNode(errorNode);
|
224
|
-
|
225
|
-
let errorCaught = false;
|
226
|
-
try {
|
227
|
-
await graph.execute(
|
228
|
-
{ status: "test", step: 1 },
|
229
|
-
"error-node",
|
230
|
-
undefined,
|
231
|
-
(error) => {
|
232
|
-
expect(error.message).to.equal("Test error");
|
233
|
-
errorCaught = true;
|
234
|
-
}
|
235
|
-
);
|
236
|
-
} catch (error) {
|
237
|
-
expect(error).to.be.instanceOf(Error);
|
238
|
-
expect((error as Error).message).to.equal("Test error");
|
239
|
-
}
|
240
|
-
|
241
|
-
expect(errorCaught).to.be.true;
|
242
|
-
});
|
243
|
-
|
244
|
-
it("should validate state against schema", async () => {
|
245
|
-
// Créer un nouveau graph avec validation stricte
|
246
|
-
const strictGraph = new GraphEngine(testDefinition, {
|
247
|
-
schema: TestSchema,
|
248
|
-
initialState: {
|
249
|
-
status: "init",
|
250
|
-
step: 0,
|
251
|
-
},
|
252
|
-
});
|
253
|
-
|
254
|
-
const invalidState: SharedState<any> = {
|
255
|
-
context: {
|
256
|
-
status: 123, // Should be string
|
257
|
-
step: "invalid", // Should be number
|
258
|
-
},
|
259
|
-
};
|
260
|
-
|
261
|
-
try {
|
262
|
-
await strictGraph.execute(invalidState, "start");
|
263
|
-
// Si on arrive ici, le test doit échouer car on s'attend à une erreur
|
264
|
-
expect.fail("Expected validation error but none was thrown");
|
265
|
-
} catch (error) {
|
266
|
-
expect(error).to.be.instanceOf(Error);
|
267
|
-
const errorMessage = (error as Error).message;
|
268
|
-
expect(
|
269
|
-
errorMessage.includes("Expected string") ||
|
270
|
-
errorMessage.includes("Expected number") ||
|
271
|
-
errorMessage.includes("validation")
|
272
|
-
).to.be.true;
|
273
|
-
}
|
274
|
-
});
|
275
|
-
});
|
276
|
-
|
277
|
-
describe("Parallel Execution", () => {
|
278
|
-
/**
|
279
|
-
* Tests concurrent execution of multiple nodes
|
280
|
-
* Important: The execution order is not guaranteed due to async nature
|
281
|
-
* @param concurrency - Maximum number of nodes that can execute simultaneously
|
282
|
-
*/
|
283
|
-
it("should execute multiple nodes in parallel", async () => {
|
284
|
-
const executionOrder: string[] = [];
|
285
|
-
|
286
|
-
const parallelNodes = ["node1", "node2", "node3"].map((name) => ({
|
287
|
-
name,
|
288
|
-
execute: async (state: SharedState<TestState>) => {
|
289
|
-
executionOrder.push(name);
|
290
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
291
|
-
return state;
|
292
|
-
},
|
293
|
-
}));
|
294
|
-
|
295
|
-
parallelNodes.forEach((node) => {
|
296
|
-
graph.addNode(node);
|
297
|
-
});
|
298
|
-
|
299
|
-
await graph.executeParallel(
|
300
|
-
{ status: "test", step: 1 },
|
301
|
-
["node1", "node2", "node3"],
|
302
|
-
2
|
303
|
-
);
|
304
|
-
|
305
|
-
expect(executionOrder).to.have.lengthOf(3);
|
306
|
-
expect(executionOrder).to.include.members(["node1", "node2", "node3"]);
|
307
|
-
});
|
308
|
-
});
|
309
|
-
|
310
|
-
describe("Event Handling", () => {
|
311
|
-
/**
|
312
|
-
* Tests the event emission and handling system
|
313
|
-
* Events can trigger node execution asynchronously
|
314
|
-
* Note: Uses setTimeout to ensure event processing completes
|
315
|
-
*/
|
316
|
-
it("should emit and handle events correctly", async () => {
|
317
|
-
const eventNode = {
|
318
|
-
name: "event-node",
|
319
|
-
execute: async (state: SharedState<TestState>) => {
|
320
|
-
return graph.updateState({
|
321
|
-
...state,
|
322
|
-
status: "event-triggered",
|
323
|
-
step: 10,
|
324
|
-
});
|
325
|
-
},
|
326
|
-
events: ["test-event"],
|
327
|
-
};
|
328
|
-
|
329
|
-
graph.addNode(eventNode);
|
330
|
-
|
331
|
-
// Émettre l'événement
|
332
|
-
graph.emit("test-event", {
|
333
|
-
state: { context: { status: "init", step: 0 } },
|
334
|
-
});
|
335
|
-
|
336
|
-
// Attendre un peu pour que l'événement soit traité
|
337
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
338
|
-
|
339
|
-
const state = graph.getState();
|
340
|
-
expect(state.status).to.equal("event-triggered");
|
341
|
-
});
|
342
|
-
});
|
343
|
-
|
344
|
-
describe("Subgraph Integration", () => {
|
345
|
-
/**
|
346
|
-
* Tests nested workflow execution through subgraphs
|
347
|
-
* Subgraphs allow modular workflow composition
|
348
|
-
* The main graph can delegate execution to subgraphs
|
349
|
-
*/
|
350
|
-
it("should execute subgraph as part of main graph", async () => {
|
351
|
-
const subGraphDef: GraphDefinition<TestState> = {
|
352
|
-
name: "sub-workflow",
|
353
|
-
entryNode: "sub-start",
|
354
|
-
nodes: {
|
355
|
-
"sub-start": {
|
356
|
-
name: "sub-start",
|
357
|
-
execute: async (state: SharedState<TestState>) => {
|
358
|
-
return graph.updateState({
|
359
|
-
...state,
|
360
|
-
status: "sub-completed",
|
361
|
-
step: 100,
|
362
|
-
});
|
363
|
-
},
|
364
|
-
relationships: [],
|
365
|
-
},
|
366
|
-
},
|
367
|
-
schema: TestSchema,
|
368
|
-
};
|
369
|
-
|
370
|
-
const subGraph = new GraphEngine(subGraphDef);
|
371
|
-
graph.addSubGraph(subGraph, "sub-start", "sub-workflow");
|
372
|
-
|
373
|
-
const initialState: SharedState<TestState> = {
|
374
|
-
status: "init",
|
375
|
-
step: 0,
|
376
|
-
};
|
377
|
-
|
378
|
-
await graph.execute(initialState, "sub-workflow");
|
379
|
-
const state = graph.getState();
|
380
|
-
|
381
|
-
expect(state.status).to.equal("sub-completed");
|
382
|
-
expect(state.step).to.equal(100);
|
383
|
-
});
|
384
|
-
});
|
385
|
-
|
386
|
-
describe("Global Context Management", () => {
|
387
|
-
it("should manage global context correctly", () => {
|
388
|
-
graph.addToContext("testKey", "testValue");
|
389
|
-
expect(graph.getContext("testKey")).to.equal("testValue");
|
390
|
-
|
391
|
-
graph.removeFromContext("testKey");
|
392
|
-
expect(graph.getContext("testKey")).to.be.undefined;
|
393
|
-
});
|
394
|
-
|
395
|
-
it("should handle multiple context values", () => {
|
396
|
-
graph.addToContext("key1", "value1");
|
397
|
-
graph.addToContext("key2", { nested: "value2" });
|
398
|
-
|
399
|
-
expect(graph.getContext("key1")).to.equal("value1");
|
400
|
-
expect(graph.getContext("key2")).to.deep.equal({ nested: "value2" });
|
401
|
-
});
|
402
|
-
});
|
403
|
-
|
404
|
-
describe("Graph Visualization", () => {
|
405
|
-
it("should generate valid mermaid diagram", () => {
|
406
|
-
const diagram = graph.generateMermaidDiagram("Test Workflow");
|
407
|
-
expect(diagram).to.include("flowchart TD");
|
408
|
-
expect(diagram).to.include("subgraph Test Workflow");
|
409
|
-
expect(diagram).to.include("start");
|
410
|
-
expect(diagram).to.include("process");
|
411
|
-
expect(diagram).to.include("end");
|
412
|
-
});
|
413
|
-
});
|
414
|
-
|
415
|
-
describe("Schema Visualization", () => {
|
416
|
-
/**
|
417
|
-
* Tests the schema visualization functionality
|
418
|
-
* This helps developers understand the workflow structure
|
419
|
-
* The visualization includes:
|
420
|
-
* - Node relationships
|
421
|
-
* - State schema
|
422
|
-
* - Validation rules
|
423
|
-
*/
|
424
|
-
it("should generate schema visualization", () => {
|
425
|
-
// Créer un nouveau graph avec un schéma pour le test
|
426
|
-
const graphWithSchema = new GraphEngine(testDefinition, {
|
427
|
-
schema: TestSchema,
|
428
|
-
});
|
429
|
-
|
430
|
-
const schemaVisualization = graphWithSchema.visualizeSchema();
|
431
|
-
|
432
|
-
// Vérifier les sections attendues dans la visualisation
|
433
|
-
expect(schemaVisualization).to.include("📋 Graph:");
|
434
|
-
expect(schemaVisualization).to.include("🔷 Nodes:");
|
435
|
-
|
436
|
-
// Vérifier les détails du schéma
|
437
|
-
expect(schemaVisualization).to.satisfy((text: string) => {
|
438
|
-
return text.includes("status:") && text.includes("step:");
|
439
|
-
});
|
440
|
-
|
441
|
-
// Vérifier la présence des nœuds
|
442
|
-
expect(schemaVisualization).to.include("start");
|
443
|
-
expect(schemaVisualization).to.include("process");
|
444
|
-
expect(schemaVisualization).to.include("end");
|
445
|
-
});
|
446
|
-
});
|
447
|
-
|
448
|
-
describe("Persistence Integration", () => {
|
449
|
-
it("should work with persistence layer", async () => {
|
450
|
-
const mockPersistence: Persistence<TestState> = {
|
451
|
-
saveState: async (graphName, state, currentNode) => {
|
452
|
-
expect(graphName).to.equal("simple-workflow");
|
453
|
-
expect(state).to.exist;
|
454
|
-
expect(currentNode).to.exist;
|
455
|
-
},
|
456
|
-
loadState: async () => null,
|
457
|
-
};
|
458
|
-
|
459
|
-
graph.setPersistence(mockPersistence);
|
460
|
-
await graph.execute({ status: "init", step: 0 }, "start");
|
461
|
-
});
|
462
|
-
});
|
463
|
-
|
464
|
-
describe("Real-time Notifications", () => {
|
465
|
-
/**
|
466
|
-
* Tests the notification system during workflow execution
|
467
|
-
* Notifications are sent for:
|
468
|
-
* - Node execution start
|
469
|
-
* - Node execution completion
|
470
|
-
* - State updates
|
471
|
-
* - Error events
|
472
|
-
*/
|
473
|
-
it("should send notifications during execution", async () => {
|
474
|
-
const notifications: any[] = [];
|
475
|
-
const mockNotifier: RealTimeNotifier = {
|
476
|
-
notify: (event, data) => {
|
477
|
-
notifications.push({ event, data });
|
478
|
-
},
|
479
|
-
};
|
480
|
-
|
481
|
-
graph.setNotifier(mockNotifier);
|
482
|
-
await graph.execute({ status: "init", step: 0 }, "start");
|
483
|
-
|
484
|
-
expect(notifications).to.have.length.greaterThan(0);
|
485
|
-
expect(notifications[0].event).to.equal("nodeExecutionStarted");
|
486
|
-
expect(notifications).to.deep.include.members([
|
487
|
-
{
|
488
|
-
event: "nodeExecutionCompleted",
|
489
|
-
data: {
|
490
|
-
workflow: "simple-workflow",
|
491
|
-
node: "start",
|
492
|
-
state: {
|
493
|
-
context: {
|
494
|
-
status: "started",
|
495
|
-
step: 1,
|
496
|
-
},
|
497
|
-
},
|
498
|
-
},
|
499
|
-
},
|
500
|
-
]);
|
501
|
-
});
|
502
|
-
});
|
503
|
-
|
504
|
-
describe("Cycle Detection", () => {
|
505
|
-
/**
|
506
|
-
* Tests the cycle detection mechanism
|
507
|
-
* Cycles in workflow definitions can cause infinite loops
|
508
|
-
* The graph constructor should detect and prevent cyclic dependencies
|
509
|
-
*/
|
510
|
-
it("should detect cycles in graph", () => {
|
511
|
-
const cyclicDefinition: GraphDefinition<TestState> = {
|
512
|
-
name: "cyclic-workflow",
|
513
|
-
entryNode: "node1",
|
514
|
-
nodes: {
|
515
|
-
node1: {
|
516
|
-
name: "node1",
|
517
|
-
execute: async (state) => state,
|
518
|
-
relationships: [{ name: "node2" }],
|
519
|
-
},
|
520
|
-
node2: {
|
521
|
-
name: "node2",
|
522
|
-
execute: async (state) => state,
|
523
|
-
relationships: [{ name: "node1" }],
|
524
|
-
},
|
525
|
-
},
|
526
|
-
};
|
527
|
-
|
528
|
-
expect(
|
529
|
-
() => new GraphEngine(cyclicDefinition, { autoDetectCycles: true })
|
530
|
-
).to.throw;
|
531
|
-
});
|
532
|
-
});
|
533
|
-
});
|