graphai 0.0.3 → 0.0.5
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/.github/workflows/node.js.yml +27 -0
- package/README.md +1 -2
- package/lib/graphai.d.ts +39 -14
- package/lib/graphai.js +127 -38
- package/package.json +5 -4
- package/{tests → samples}/sample_gpt.ts +6 -6
- package/src/graphai.ts +164 -46
- package/tests/graphs/test_error.yml +22 -0
- package/tests/graphs/test_multiple_functions_1.yml +27 -0
- package/tests/graphs/test_timeout.yml +22 -0
- package/tests/test_multiple_functions.ts +57 -0
- package/tests/test_sample_flow.ts +87 -0
- package/lib/file_utils.d.ts +0 -3
- package/lib/file_utils.js +0 -30
- package/lib/flow.d.ts +0 -51
- package/lib/flow.js +0 -118
- package/tests/sample_flow.ts +0 -57
- /package/{tests/graphs/sample3.yml → samples/graphs/slash_gpt.yml} +0 -0
- /package/{src → tests}/file_utils.ts +0 -0
- /package/tests/graphs/{sample1.yml → test_base.yml} +0 -0
- /package/tests/graphs/{sample2.yml → test_retry.yml} +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
|
3
|
+
|
|
4
|
+
name: Node.js CI
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
pull_request
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
node-version: [18.x, 20.x]
|
|
17
|
+
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
22
|
+
uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: ${{ matrix.node-version }}
|
|
25
|
+
cache: 'npm'
|
|
26
|
+
- run: yarn install
|
|
27
|
+
- run: yarn test
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GraphAI
|
|
2
2
|
|
|
3
|
-
GraphAI is
|
|
3
|
+
GraphAI is an asynchronous data flow execution engine, which makes it easy to create AI applications that need to make asynchronous AI API calls multiple times with some dependencies among them, such as giving the answer from one LLM call to another LLM call as a prompt.
|
|
4
4
|
|
|
5
5
|
You just need to describe dependencies among those API calls in a single graph definition file (in JSON or YAML), create a GraphAI object with it, and run it.
|
|
6
6
|
|
|
@@ -38,7 +38,6 @@ const nodeExecute = async (context: NodeExecuteContext) => {
|
|
|
38
38
|
const graph = new GraphAI(graph_data, nodeExecute);
|
|
39
39
|
const results = await graph.run();
|
|
40
40
|
return results["taskC"];
|
|
41
|
-
|
|
42
41
|
```
|
|
43
42
|
|
|
44
43
|
|
package/lib/graphai.d.ts
CHANGED
|
@@ -5,24 +5,36 @@ export declare enum NodeState {
|
|
|
5
5
|
TimedOut = 3,
|
|
6
6
|
Completed = 4
|
|
7
7
|
}
|
|
8
|
-
type ResultData = Record<string, any
|
|
9
|
-
|
|
8
|
+
type ResultData<ResultType = Record<string, any>> = ResultType | undefined;
|
|
9
|
+
type ResultDataDictonary<ResultType = Record<string, any>> = Record<string, ResultData<ResultType>>;
|
|
10
|
+
export type NodeDataParams<ParamsType = Record<string, any>> = ParamsType;
|
|
10
11
|
type NodeData = {
|
|
11
12
|
inputs: undefined | Array<string>;
|
|
12
13
|
params: NodeDataParams;
|
|
13
14
|
retry: undefined | number;
|
|
14
15
|
timeout: undefined | number;
|
|
16
|
+
functionName: undefined | string;
|
|
15
17
|
};
|
|
16
18
|
type GraphData = {
|
|
17
19
|
nodes: Record<string, NodeData>;
|
|
20
|
+
concurrency: number;
|
|
18
21
|
};
|
|
19
|
-
|
|
22
|
+
type NodeExecuteContext<ResultType, ParamsType> = {
|
|
20
23
|
nodeId: string;
|
|
21
24
|
retry: number;
|
|
22
|
-
params: NodeDataParams
|
|
23
|
-
payload:
|
|
25
|
+
params: NodeDataParams<ParamsType>;
|
|
26
|
+
payload: ResultDataDictonary<ResultType>;
|
|
27
|
+
};
|
|
28
|
+
export type TransactionLog = {
|
|
29
|
+
nodeId: string;
|
|
30
|
+
state: NodeState;
|
|
31
|
+
startTime: undefined | number;
|
|
32
|
+
endTime: undefined | number;
|
|
33
|
+
retryCount: number;
|
|
34
|
+
error: undefined | Error;
|
|
35
|
+
result?: ResultData;
|
|
24
36
|
};
|
|
25
|
-
type NodeExecute = (context: NodeExecuteContext) => Promise<ResultData
|
|
37
|
+
export type NodeExecute<ResultType = Record<string, any>, ParamsType = Record<string, any>> = (context: NodeExecuteContext<ResultType, ParamsType>) => Promise<ResultData<ResultType>>;
|
|
26
38
|
declare class Node {
|
|
27
39
|
nodeId: string;
|
|
28
40
|
params: NodeDataParams;
|
|
@@ -30,29 +42,42 @@ declare class Node {
|
|
|
30
42
|
pendings: Set<string>;
|
|
31
43
|
waitlist: Set<string>;
|
|
32
44
|
state: NodeState;
|
|
45
|
+
functionName: string;
|
|
33
46
|
result: ResultData;
|
|
34
47
|
retryLimit: number;
|
|
35
48
|
retryCount: number;
|
|
36
49
|
transactionId: undefined | number;
|
|
37
50
|
timeout: number;
|
|
38
|
-
|
|
51
|
+
error: undefined | Error;
|
|
52
|
+
private graph;
|
|
53
|
+
constructor(nodeId: string, data: NodeData, graph: GraphAI);
|
|
39
54
|
asString(): string;
|
|
40
55
|
private retry;
|
|
41
|
-
removePending(nodeId: string
|
|
42
|
-
payload(
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
removePending(nodeId: string): void;
|
|
57
|
+
payload(): ResultDataDictonary<Record<string, any>>;
|
|
58
|
+
pushQueueIfReady(): void;
|
|
59
|
+
execute(): Promise<void>;
|
|
45
60
|
}
|
|
46
61
|
type GraphNodes = Record<string, Node>;
|
|
62
|
+
type NodeExecuteDictonary = Record<string, NodeExecute>;
|
|
47
63
|
export declare class GraphAI {
|
|
48
64
|
nodes: GraphNodes;
|
|
49
|
-
|
|
65
|
+
callbackDictonary: NodeExecuteDictonary;
|
|
50
66
|
private runningNodes;
|
|
67
|
+
private nodeQueue;
|
|
51
68
|
private onComplete;
|
|
52
|
-
|
|
69
|
+
private concurrency;
|
|
70
|
+
private logs;
|
|
71
|
+
constructor(data: GraphData, callbackDictonary: NodeExecuteDictonary | NodeExecute);
|
|
72
|
+
getCallback(functionName: string): NodeExecute<Record<string, any>, Record<string, any>>;
|
|
53
73
|
asString(): string;
|
|
74
|
+
results(): ResultDataDictonary<Record<string, any>>;
|
|
75
|
+
errors(): Record<string, Error>;
|
|
54
76
|
run(): Promise<unknown>;
|
|
55
|
-
|
|
77
|
+
private runNode;
|
|
78
|
+
pushQueue(node: Node): void;
|
|
56
79
|
removeRunning(node: Node): void;
|
|
80
|
+
appendLog(log: TransactionLog): void;
|
|
81
|
+
transactionLogs(): TransactionLog[];
|
|
57
82
|
}
|
|
58
83
|
export {};
|
package/lib/graphai.js
CHANGED
|
@@ -10,90 +10,131 @@ var NodeState;
|
|
|
10
10
|
NodeState[NodeState["Completed"] = 4] = "Completed";
|
|
11
11
|
})(NodeState || (exports.NodeState = NodeState = {}));
|
|
12
12
|
class Node {
|
|
13
|
-
constructor(nodeId, data) {
|
|
13
|
+
constructor(nodeId, data, graph) {
|
|
14
14
|
this.nodeId = nodeId;
|
|
15
15
|
this.inputs = data.inputs ?? [];
|
|
16
16
|
this.pendings = new Set(this.inputs);
|
|
17
17
|
this.params = data.params;
|
|
18
18
|
this.waitlist = new Set();
|
|
19
19
|
this.state = NodeState.Waiting;
|
|
20
|
-
this.
|
|
20
|
+
this.functionName = data.functionName ?? "default";
|
|
21
|
+
this.result = undefined;
|
|
21
22
|
this.retryLimit = data.retry ?? 0;
|
|
22
23
|
this.retryCount = 0;
|
|
23
24
|
this.timeout = data.timeout ?? 0;
|
|
25
|
+
this.graph = graph;
|
|
24
26
|
}
|
|
25
27
|
asString() {
|
|
26
28
|
return `${this.nodeId}: ${this.state} ${[...this.waitlist]}`;
|
|
27
29
|
}
|
|
28
|
-
retry(
|
|
30
|
+
retry(state, error) {
|
|
29
31
|
if (this.retryCount < this.retryLimit) {
|
|
30
32
|
this.retryCount++;
|
|
31
|
-
this.execute(
|
|
33
|
+
this.execute();
|
|
32
34
|
}
|
|
33
35
|
else {
|
|
34
36
|
this.state = state;
|
|
35
|
-
this.result =
|
|
36
|
-
|
|
37
|
+
this.result = undefined;
|
|
38
|
+
this.error = error;
|
|
39
|
+
this.transactionId = 0; // This is necessary for timeout case
|
|
40
|
+
this.graph.removeRunning(this);
|
|
37
41
|
}
|
|
38
42
|
}
|
|
39
|
-
removePending(nodeId
|
|
43
|
+
removePending(nodeId) {
|
|
40
44
|
this.pendings.delete(nodeId);
|
|
41
|
-
this.
|
|
45
|
+
this.pushQueueIfReady();
|
|
42
46
|
}
|
|
43
|
-
payload(
|
|
47
|
+
payload() {
|
|
44
48
|
return this.inputs.reduce((results, nodeId) => {
|
|
45
|
-
results[nodeId] = graph.nodes[nodeId].result;
|
|
49
|
+
results[nodeId] = this.graph.nodes[nodeId].result;
|
|
46
50
|
return results;
|
|
47
51
|
}, {});
|
|
48
52
|
}
|
|
49
|
-
|
|
50
|
-
if (this.pendings.size
|
|
51
|
-
graph.
|
|
52
|
-
this.execute(graph);
|
|
53
|
+
pushQueueIfReady() {
|
|
54
|
+
if (this.pendings.size === 0) {
|
|
55
|
+
this.graph.pushQueue(this);
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
|
-
async execute(
|
|
58
|
+
async execute() {
|
|
59
|
+
const log = {
|
|
60
|
+
nodeId: this.nodeId,
|
|
61
|
+
retryCount: this.retryCount,
|
|
62
|
+
state: NodeState.Executing,
|
|
63
|
+
startTime: Date.now(),
|
|
64
|
+
endTime: undefined,
|
|
65
|
+
error: undefined,
|
|
66
|
+
};
|
|
67
|
+
this.graph.appendLog(log);
|
|
56
68
|
this.state = NodeState.Executing;
|
|
57
|
-
const transactionId =
|
|
69
|
+
const transactionId = log.startTime;
|
|
58
70
|
this.transactionId = transactionId;
|
|
59
71
|
if (this.timeout > 0) {
|
|
60
72
|
setTimeout(() => {
|
|
61
|
-
if (this.state
|
|
62
|
-
console.log(
|
|
63
|
-
|
|
73
|
+
if (this.state === NodeState.Executing && this.transactionId === transactionId) {
|
|
74
|
+
console.log(`-- ${this.nodeId}: timeout ${this.timeout}`);
|
|
75
|
+
log.error = Error("Timeout");
|
|
76
|
+
log.state = NodeState.TimedOut;
|
|
77
|
+
log.endTime = Date.now();
|
|
78
|
+
this.retry(NodeState.TimedOut, Error("Timeout"));
|
|
64
79
|
}
|
|
65
80
|
}, this.timeout);
|
|
66
81
|
}
|
|
67
82
|
try {
|
|
68
|
-
const
|
|
83
|
+
const callback = this.graph.getCallback(this.functionName);
|
|
84
|
+
const result = await callback({
|
|
85
|
+
nodeId: this.nodeId,
|
|
86
|
+
retry: this.retryCount,
|
|
87
|
+
params: this.params,
|
|
88
|
+
payload: this.payload(),
|
|
89
|
+
});
|
|
69
90
|
if (this.transactionId !== transactionId) {
|
|
70
|
-
console.log(
|
|
91
|
+
console.log(`-- ${this.nodeId}: transactionId mismatch`);
|
|
71
92
|
return;
|
|
72
93
|
}
|
|
94
|
+
log.state = NodeState.Completed;
|
|
95
|
+
log.endTime = Date.now();
|
|
96
|
+
log.result = result;
|
|
73
97
|
this.state = NodeState.Completed;
|
|
74
98
|
this.result = result;
|
|
75
99
|
this.waitlist.forEach((nodeId) => {
|
|
76
|
-
const node = graph.nodes[nodeId];
|
|
77
|
-
node.removePending(this.nodeId
|
|
100
|
+
const node = this.graph.nodes[nodeId];
|
|
101
|
+
node.removePending(this.nodeId);
|
|
78
102
|
});
|
|
79
|
-
graph.removeRunning(this);
|
|
103
|
+
this.graph.removeRunning(this);
|
|
80
104
|
}
|
|
81
|
-
catch (
|
|
105
|
+
catch (error) {
|
|
82
106
|
if (this.transactionId !== transactionId) {
|
|
83
|
-
console.log(
|
|
107
|
+
console.log(`-- ${this.nodeId}: transactionId mismatch(error)`);
|
|
84
108
|
return;
|
|
85
109
|
}
|
|
86
|
-
|
|
110
|
+
log.state = NodeState.Failed;
|
|
111
|
+
log.endTime = Date.now();
|
|
112
|
+
if (error instanceof Error) {
|
|
113
|
+
log.error = error;
|
|
114
|
+
this.retry(NodeState.Failed, error);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
console.error(`-- ${this.nodeId}: Unexpecrted error was caught`);
|
|
118
|
+
log.error = Error("Unknown");
|
|
119
|
+
this.retry(NodeState.Failed, Error("Unknown"));
|
|
120
|
+
}
|
|
87
121
|
}
|
|
88
122
|
}
|
|
89
123
|
}
|
|
124
|
+
const defaultConcurrency = 8;
|
|
90
125
|
class GraphAI {
|
|
91
|
-
constructor(data,
|
|
92
|
-
this.
|
|
126
|
+
constructor(data, callbackDictonary) {
|
|
127
|
+
this.logs = [];
|
|
128
|
+
this.callbackDictonary = typeof callbackDictonary === "function" ? { default: callbackDictonary } : callbackDictonary;
|
|
129
|
+
if (this.callbackDictonary["default"] === undefined) {
|
|
130
|
+
throw new Error("No default function");
|
|
131
|
+
}
|
|
132
|
+
this.concurrency = data.concurrency ?? defaultConcurrency;
|
|
93
133
|
this.runningNodes = new Set();
|
|
134
|
+
this.nodeQueue = [];
|
|
94
135
|
this.onComplete = () => { };
|
|
95
136
|
this.nodes = Object.keys(data.nodes).reduce((nodes, nodeId) => {
|
|
96
|
-
nodes[nodeId] = new Node(nodeId, data.nodes[nodeId]);
|
|
137
|
+
nodes[nodeId] = new Node(nodeId, data.nodes[nodeId], this);
|
|
97
138
|
return nodes;
|
|
98
139
|
}, {});
|
|
99
140
|
// Generate the waitlist for each node
|
|
@@ -105,6 +146,12 @@ class GraphAI {
|
|
|
105
146
|
});
|
|
106
147
|
});
|
|
107
148
|
}
|
|
149
|
+
getCallback(functionName) {
|
|
150
|
+
if (functionName && this.callbackDictonary[functionName]) {
|
|
151
|
+
return this.callbackDictonary[functionName];
|
|
152
|
+
}
|
|
153
|
+
return this.callbackDictonary["default"];
|
|
154
|
+
}
|
|
108
155
|
asString() {
|
|
109
156
|
return Object.keys(this.nodes)
|
|
110
157
|
.map((nodeId) => {
|
|
@@ -112,30 +159,72 @@ class GraphAI {
|
|
|
112
159
|
})
|
|
113
160
|
.join("\n");
|
|
114
161
|
}
|
|
162
|
+
results() {
|
|
163
|
+
return Object.keys(this.nodes).reduce((results, nodeId) => {
|
|
164
|
+
const node = this.nodes[nodeId];
|
|
165
|
+
if (node.result !== undefined) {
|
|
166
|
+
results[nodeId] = node.result;
|
|
167
|
+
}
|
|
168
|
+
return results;
|
|
169
|
+
}, {});
|
|
170
|
+
}
|
|
171
|
+
errors() {
|
|
172
|
+
return Object.keys(this.nodes).reduce((errors, nodeId) => {
|
|
173
|
+
const node = this.nodes[nodeId];
|
|
174
|
+
if (node.error !== undefined) {
|
|
175
|
+
errors[nodeId] = node.error;
|
|
176
|
+
}
|
|
177
|
+
return errors;
|
|
178
|
+
}, {});
|
|
179
|
+
}
|
|
115
180
|
async run() {
|
|
116
181
|
// Nodes without pending data should run immediately.
|
|
117
182
|
Object.keys(this.nodes).forEach((nodeId) => {
|
|
118
183
|
const node = this.nodes[nodeId];
|
|
119
|
-
node.
|
|
184
|
+
node.pushQueueIfReady();
|
|
120
185
|
});
|
|
121
186
|
return new Promise((resolve, reject) => {
|
|
122
187
|
this.onComplete = () => {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
188
|
+
const errors = this.errors();
|
|
189
|
+
const nodeIds = Object.keys(errors);
|
|
190
|
+
if (nodeIds.length > 0) {
|
|
191
|
+
reject(errors[nodeIds[0]]);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
resolve(this.results());
|
|
195
|
+
}
|
|
128
196
|
};
|
|
129
197
|
});
|
|
130
198
|
}
|
|
131
|
-
|
|
199
|
+
runNode(node) {
|
|
132
200
|
this.runningNodes.add(node.nodeId);
|
|
201
|
+
node.execute();
|
|
202
|
+
}
|
|
203
|
+
pushQueue(node) {
|
|
204
|
+
if (this.runningNodes.size < this.concurrency) {
|
|
205
|
+
this.runNode(node);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
this.nodeQueue.push(node);
|
|
209
|
+
}
|
|
133
210
|
}
|
|
134
211
|
removeRunning(node) {
|
|
135
212
|
this.runningNodes.delete(node.nodeId);
|
|
136
|
-
if (this.
|
|
213
|
+
if (this.nodeQueue.length > 0) {
|
|
214
|
+
const n = this.nodeQueue.shift();
|
|
215
|
+
if (n) {
|
|
216
|
+
this.runNode(n);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (this.runningNodes.size === 0) {
|
|
137
220
|
this.onComplete();
|
|
138
221
|
}
|
|
139
222
|
}
|
|
223
|
+
appendLog(log) {
|
|
224
|
+
this.logs.push(log);
|
|
225
|
+
}
|
|
226
|
+
transactionLogs() {
|
|
227
|
+
return this.logs;
|
|
228
|
+
}
|
|
140
229
|
}
|
|
141
230
|
exports.GraphAI = GraphAI;
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphai",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Asynchronous data flow execution engine to make it simple to build LLM apps.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsc",
|
|
8
8
|
"eslint": "eslint --fix --ext src/**/*.{ts} tests/**/*.ts",
|
|
9
9
|
"format": "prettier --write '{src,tests}/**/*.ts'",
|
|
10
|
-
"test": "node --test --require ts-node/register ./tests/
|
|
10
|
+
"test": "node --test --require ts-node/register ./tests/test_*.ts",
|
|
11
|
+
"gpt": "npx ts-node ./samples/sample_gpt.ts"
|
|
11
12
|
},
|
|
12
13
|
"repository": {
|
|
13
14
|
"type": "git",
|
|
@@ -25,13 +26,13 @@
|
|
|
25
26
|
"@typescript-eslint/parser": "^6.8.0",
|
|
26
27
|
"eslint": "^7.32.0 || ^8.2.0",
|
|
27
28
|
"eslint-plugin-import": "^2.25.2",
|
|
29
|
+
"openai": "^4.12.4",
|
|
28
30
|
"prettier": "^3.0.3",
|
|
29
|
-
"slashgpt": "^0.0.
|
|
31
|
+
"slashgpt": "^0.0.8",
|
|
30
32
|
"ts-node": "^10.9.1",
|
|
31
33
|
"typescript": "^5.2.2"
|
|
32
34
|
},
|
|
33
35
|
"dependencies": {
|
|
34
|
-
"openai": "^4.12.4",
|
|
35
36
|
"yaml": "^2.3.3"
|
|
36
37
|
},
|
|
37
38
|
"types": "./lib/index.d.ts",
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import { GraphAI,
|
|
2
|
+
import { GraphAI, NodeExecute } from "../src/graphai";
|
|
3
3
|
import { ChatSession, ChatConfig } from "slashgpt";
|
|
4
|
-
import { readManifestData } from "../
|
|
4
|
+
import { readManifestData } from "../tests/file_utils";
|
|
5
5
|
|
|
6
6
|
const config = new ChatConfig(path.resolve(__dirname));
|
|
7
7
|
|
|
8
|
-
const testFunction = async (context
|
|
9
|
-
console.log("executing", context.nodeId, context.params
|
|
8
|
+
const testFunction: NodeExecute<Record<string, string>> = async (context) => {
|
|
9
|
+
console.log("executing", context.nodeId, context.params);
|
|
10
10
|
const session = new ChatSession(config, context.params.manifest ?? {});
|
|
11
11
|
const prompt = Object.keys(context.payload).reduce((prompt, key) => {
|
|
12
|
-
return prompt.replace("${" + key + "}", context.payload[key]["answer"]);
|
|
12
|
+
return prompt.replace("${" + key + "}", context.payload[key]!["answer"]);
|
|
13
13
|
}, context.params.prompt);
|
|
14
14
|
session.append_user_question(prompt);
|
|
15
15
|
|
|
@@ -31,7 +31,7 @@ const test = async (file: string) => {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
const main = async () => {
|
|
34
|
-
await test("/graphs/
|
|
34
|
+
await test("/graphs/slash_gpt.yml");
|
|
35
35
|
console.log("COMPLETE 1");
|
|
36
36
|
};
|
|
37
37
|
main();
|
package/src/graphai.ts
CHANGED
|
@@ -7,28 +7,44 @@ export enum NodeState {
|
|
|
7
7
|
TimedOut,
|
|
8
8
|
Completed,
|
|
9
9
|
}
|
|
10
|
-
type ResultData = Record<string, any
|
|
11
|
-
|
|
10
|
+
type ResultData<ResultType = Record<string, any>> = ResultType | undefined;
|
|
11
|
+
type ResultDataDictonary<ResultType = Record<string, any>> = Record<string, ResultData<ResultType>>;
|
|
12
|
+
|
|
13
|
+
export type NodeDataParams<ParamsType = Record<string, any>> = ParamsType; // App-specific parameters
|
|
12
14
|
|
|
13
15
|
type NodeData = {
|
|
14
16
|
inputs: undefined | Array<string>;
|
|
15
17
|
params: NodeDataParams;
|
|
16
18
|
retry: undefined | number;
|
|
17
19
|
timeout: undefined | number; // msec
|
|
20
|
+
functionName: undefined | string;
|
|
18
21
|
};
|
|
19
22
|
|
|
20
23
|
type GraphData = {
|
|
21
24
|
nodes: Record<string, NodeData>;
|
|
25
|
+
concurrency: number;
|
|
22
26
|
};
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
type NodeExecuteContext<ResultType, ParamsType> = {
|
|
25
29
|
nodeId: string;
|
|
26
30
|
retry: number;
|
|
27
|
-
params: NodeDataParams
|
|
28
|
-
payload:
|
|
31
|
+
params: NodeDataParams<ParamsType>;
|
|
32
|
+
payload: ResultDataDictonary<ResultType>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type TransactionLog = {
|
|
36
|
+
nodeId: string;
|
|
37
|
+
state: NodeState;
|
|
38
|
+
startTime: undefined | number;
|
|
39
|
+
endTime: undefined | number;
|
|
40
|
+
retryCount: number;
|
|
41
|
+
error: undefined | Error;
|
|
42
|
+
result?: ResultData;
|
|
29
43
|
};
|
|
30
44
|
|
|
31
|
-
type NodeExecute =
|
|
45
|
+
export type NodeExecute<ResultType = Record<string, any>, ParamsType = Record<string, any>> = (
|
|
46
|
+
context: NodeExecuteContext<ResultType, ParamsType>,
|
|
47
|
+
) => Promise<ResultData<ResultType>>;
|
|
32
48
|
|
|
33
49
|
class Node {
|
|
34
50
|
public nodeId: string;
|
|
@@ -37,110 +53,160 @@ class Node {
|
|
|
37
53
|
public pendings: Set<string>; // List of nodes this node is waiting data from.
|
|
38
54
|
public waitlist: Set<string>; // List of nodes which need data from this node.
|
|
39
55
|
public state: NodeState;
|
|
56
|
+
public functionName: string;
|
|
40
57
|
public result: ResultData;
|
|
41
58
|
public retryLimit: number;
|
|
42
59
|
public retryCount: number;
|
|
43
60
|
public transactionId: undefined | number; // To reject callbacks from timed-out transactions
|
|
44
61
|
public timeout: number; // msec
|
|
62
|
+
public error: undefined | Error;
|
|
45
63
|
|
|
46
|
-
|
|
64
|
+
private graph: GraphAI;
|
|
65
|
+
|
|
66
|
+
constructor(nodeId: string, data: NodeData, graph: GraphAI) {
|
|
47
67
|
this.nodeId = nodeId;
|
|
48
68
|
this.inputs = data.inputs ?? [];
|
|
49
69
|
this.pendings = new Set(this.inputs);
|
|
50
70
|
this.params = data.params;
|
|
51
71
|
this.waitlist = new Set<string>();
|
|
52
72
|
this.state = NodeState.Waiting;
|
|
53
|
-
this.
|
|
73
|
+
this.functionName = data.functionName ?? "default";
|
|
74
|
+
this.result = undefined;
|
|
54
75
|
this.retryLimit = data.retry ?? 0;
|
|
55
76
|
this.retryCount = 0;
|
|
56
77
|
this.timeout = data.timeout ?? 0;
|
|
78
|
+
|
|
79
|
+
this.graph = graph;
|
|
57
80
|
}
|
|
58
81
|
|
|
59
82
|
public asString() {
|
|
60
83
|
return `${this.nodeId}: ${this.state} ${[...this.waitlist]}`;
|
|
61
84
|
}
|
|
62
85
|
|
|
63
|
-
private retry(
|
|
86
|
+
private retry(state: NodeState, error: Error) {
|
|
64
87
|
if (this.retryCount < this.retryLimit) {
|
|
65
88
|
this.retryCount++;
|
|
66
|
-
this.execute(
|
|
89
|
+
this.execute();
|
|
67
90
|
} else {
|
|
68
91
|
this.state = state;
|
|
69
|
-
this.result =
|
|
70
|
-
|
|
92
|
+
this.result = undefined;
|
|
93
|
+
this.error = error;
|
|
94
|
+
this.transactionId = 0; // This is necessary for timeout case
|
|
95
|
+
this.graph.removeRunning(this);
|
|
71
96
|
}
|
|
72
97
|
}
|
|
73
98
|
|
|
74
|
-
public removePending(nodeId: string
|
|
99
|
+
public removePending(nodeId: string) {
|
|
75
100
|
this.pendings.delete(nodeId);
|
|
76
|
-
this.
|
|
101
|
+
this.pushQueueIfReady();
|
|
77
102
|
}
|
|
78
103
|
|
|
79
|
-
public payload(
|
|
80
|
-
return this.inputs.reduce((results:
|
|
81
|
-
results[nodeId] = graph.nodes[nodeId].result;
|
|
104
|
+
public payload() {
|
|
105
|
+
return this.inputs.reduce((results: ResultDataDictonary, nodeId) => {
|
|
106
|
+
results[nodeId] = this.graph.nodes[nodeId].result;
|
|
82
107
|
return results;
|
|
83
108
|
}, {});
|
|
84
109
|
}
|
|
85
110
|
|
|
86
|
-
public
|
|
87
|
-
if (this.pendings.size
|
|
88
|
-
graph.
|
|
89
|
-
this.execute(graph);
|
|
111
|
+
public pushQueueIfReady() {
|
|
112
|
+
if (this.pendings.size === 0) {
|
|
113
|
+
this.graph.pushQueue(this);
|
|
90
114
|
}
|
|
91
115
|
}
|
|
92
116
|
|
|
93
|
-
|
|
117
|
+
public async execute() {
|
|
118
|
+
const log: TransactionLog = {
|
|
119
|
+
nodeId: this.nodeId,
|
|
120
|
+
retryCount: this.retryCount,
|
|
121
|
+
state: NodeState.Executing,
|
|
122
|
+
startTime: Date.now(),
|
|
123
|
+
endTime: undefined,
|
|
124
|
+
error: undefined,
|
|
125
|
+
};
|
|
126
|
+
this.graph.appendLog(log);
|
|
94
127
|
this.state = NodeState.Executing;
|
|
95
|
-
const transactionId =
|
|
128
|
+
const transactionId = log.startTime;
|
|
96
129
|
this.transactionId = transactionId;
|
|
97
130
|
|
|
98
131
|
if (this.timeout > 0) {
|
|
99
132
|
setTimeout(() => {
|
|
100
|
-
if (this.state
|
|
101
|
-
console.log(
|
|
102
|
-
|
|
133
|
+
if (this.state === NodeState.Executing && this.transactionId === transactionId) {
|
|
134
|
+
console.log(`-- ${this.nodeId}: timeout ${this.timeout}`);
|
|
135
|
+
log.error = Error("Timeout");
|
|
136
|
+
log.state = NodeState.TimedOut;
|
|
137
|
+
log.endTime = Date.now();
|
|
138
|
+
this.retry(NodeState.TimedOut, Error("Timeout"));
|
|
103
139
|
}
|
|
104
140
|
}, this.timeout);
|
|
105
141
|
}
|
|
106
142
|
|
|
107
143
|
try {
|
|
108
|
-
const
|
|
144
|
+
const callback = this.graph.getCallback(this.functionName);
|
|
145
|
+
const result = await callback({
|
|
146
|
+
nodeId: this.nodeId,
|
|
147
|
+
retry: this.retryCount,
|
|
148
|
+
params: this.params,
|
|
149
|
+
payload: this.payload(),
|
|
150
|
+
});
|
|
109
151
|
if (this.transactionId !== transactionId) {
|
|
110
|
-
console.log(
|
|
152
|
+
console.log(`-- ${this.nodeId}: transactionId mismatch`);
|
|
111
153
|
return;
|
|
112
154
|
}
|
|
155
|
+
log.state = NodeState.Completed;
|
|
156
|
+
log.endTime = Date.now();
|
|
157
|
+
log.result = result;
|
|
113
158
|
this.state = NodeState.Completed;
|
|
114
159
|
this.result = result;
|
|
115
160
|
this.waitlist.forEach((nodeId) => {
|
|
116
|
-
const node = graph.nodes[nodeId];
|
|
117
|
-
node.removePending(this.nodeId
|
|
161
|
+
const node = this.graph.nodes[nodeId];
|
|
162
|
+
node.removePending(this.nodeId);
|
|
118
163
|
});
|
|
119
|
-
graph.removeRunning(this);
|
|
120
|
-
} catch (
|
|
164
|
+
this.graph.removeRunning(this);
|
|
165
|
+
} catch (error) {
|
|
121
166
|
if (this.transactionId !== transactionId) {
|
|
122
|
-
console.log(
|
|
167
|
+
console.log(`-- ${this.nodeId}: transactionId mismatch(error)`);
|
|
123
168
|
return;
|
|
124
169
|
}
|
|
125
|
-
|
|
170
|
+
log.state = NodeState.Failed;
|
|
171
|
+
log.endTime = Date.now();
|
|
172
|
+
if (error instanceof Error) {
|
|
173
|
+
log.error = error;
|
|
174
|
+
this.retry(NodeState.Failed, error);
|
|
175
|
+
} else {
|
|
176
|
+
console.error(`-- ${this.nodeId}: Unexpecrted error was caught`);
|
|
177
|
+
log.error = Error("Unknown");
|
|
178
|
+
this.retry(NodeState.Failed, Error("Unknown"));
|
|
179
|
+
}
|
|
126
180
|
}
|
|
127
181
|
}
|
|
128
182
|
}
|
|
129
183
|
|
|
130
184
|
type GraphNodes = Record<string, Node>;
|
|
131
185
|
|
|
186
|
+
type NodeExecuteDictonary = Record<string, NodeExecute>;
|
|
187
|
+
|
|
188
|
+
const defaultConcurrency = 8;
|
|
189
|
+
|
|
132
190
|
export class GraphAI {
|
|
133
191
|
public nodes: GraphNodes;
|
|
134
|
-
public
|
|
192
|
+
public callbackDictonary: NodeExecuteDictonary;
|
|
135
193
|
private runningNodes: Set<string>;
|
|
194
|
+
private nodeQueue: Array<Node>;
|
|
136
195
|
private onComplete: () => void;
|
|
196
|
+
private concurrency: number;
|
|
197
|
+
private logs: Array<TransactionLog> = [];
|
|
137
198
|
|
|
138
|
-
constructor(data: GraphData,
|
|
139
|
-
this.
|
|
199
|
+
constructor(data: GraphData, callbackDictonary: NodeExecuteDictonary | NodeExecute) {
|
|
200
|
+
this.callbackDictonary = typeof callbackDictonary === "function" ? { default: callbackDictonary } : callbackDictonary;
|
|
201
|
+
if (this.callbackDictonary["default"] === undefined) {
|
|
202
|
+
throw new Error("No default function");
|
|
203
|
+
}
|
|
204
|
+
this.concurrency = data.concurrency ?? defaultConcurrency;
|
|
140
205
|
this.runningNodes = new Set<string>();
|
|
206
|
+
this.nodeQueue = [];
|
|
141
207
|
this.onComplete = () => {};
|
|
142
208
|
this.nodes = Object.keys(data.nodes).reduce((nodes: GraphNodes, nodeId: string) => {
|
|
143
|
-
nodes[nodeId] = new Node(nodeId, data.nodes[nodeId]);
|
|
209
|
+
nodes[nodeId] = new Node(nodeId, data.nodes[nodeId], this);
|
|
144
210
|
return nodes;
|
|
145
211
|
}, {});
|
|
146
212
|
|
|
@@ -154,6 +220,13 @@ export class GraphAI {
|
|
|
154
220
|
});
|
|
155
221
|
}
|
|
156
222
|
|
|
223
|
+
public getCallback(functionName: string) {
|
|
224
|
+
if (functionName && this.callbackDictonary[functionName]) {
|
|
225
|
+
return this.callbackDictonary[functionName];
|
|
226
|
+
}
|
|
227
|
+
return this.callbackDictonary["default"];
|
|
228
|
+
}
|
|
229
|
+
|
|
157
230
|
public asString() {
|
|
158
231
|
return Object.keys(this.nodes)
|
|
159
232
|
.map((nodeId) => {
|
|
@@ -162,32 +235,77 @@ export class GraphAI {
|
|
|
162
235
|
.join("\n");
|
|
163
236
|
}
|
|
164
237
|
|
|
238
|
+
public results() {
|
|
239
|
+
return Object.keys(this.nodes).reduce((results: ResultDataDictonary, nodeId) => {
|
|
240
|
+
const node = this.nodes[nodeId];
|
|
241
|
+
if (node.result !== undefined) {
|
|
242
|
+
results[nodeId] = node.result;
|
|
243
|
+
}
|
|
244
|
+
return results;
|
|
245
|
+
}, {});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public errors() {
|
|
249
|
+
return Object.keys(this.nodes).reduce((errors: Record<string, Error>, nodeId) => {
|
|
250
|
+
const node = this.nodes[nodeId];
|
|
251
|
+
if (node.error !== undefined) {
|
|
252
|
+
errors[nodeId] = node.error;
|
|
253
|
+
}
|
|
254
|
+
return errors;
|
|
255
|
+
}, {});
|
|
256
|
+
}
|
|
257
|
+
|
|
165
258
|
public async run() {
|
|
166
259
|
// Nodes without pending data should run immediately.
|
|
167
260
|
Object.keys(this.nodes).forEach((nodeId) => {
|
|
168
261
|
const node = this.nodes[nodeId];
|
|
169
|
-
node.
|
|
262
|
+
node.pushQueueIfReady();
|
|
170
263
|
});
|
|
171
264
|
|
|
172
265
|
return new Promise((resolve, reject) => {
|
|
173
266
|
this.onComplete = () => {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
267
|
+
const errors = this.errors();
|
|
268
|
+
const nodeIds = Object.keys(errors);
|
|
269
|
+
if (nodeIds.length > 0) {
|
|
270
|
+
reject(errors[nodeIds[0]]);
|
|
271
|
+
} else {
|
|
272
|
+
resolve(this.results());
|
|
273
|
+
}
|
|
179
274
|
};
|
|
180
275
|
});
|
|
181
276
|
}
|
|
182
277
|
|
|
183
|
-
|
|
278
|
+
private runNode(node: Node) {
|
|
184
279
|
this.runningNodes.add(node.nodeId);
|
|
280
|
+
node.execute();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
public pushQueue(node: Node) {
|
|
284
|
+
if (this.runningNodes.size < this.concurrency) {
|
|
285
|
+
this.runNode(node);
|
|
286
|
+
} else {
|
|
287
|
+
this.nodeQueue.push(node);
|
|
288
|
+
}
|
|
185
289
|
}
|
|
186
290
|
|
|
187
291
|
public removeRunning(node: Node) {
|
|
188
292
|
this.runningNodes.delete(node.nodeId);
|
|
189
|
-
if (this.
|
|
293
|
+
if (this.nodeQueue.length > 0) {
|
|
294
|
+
const n = this.nodeQueue.shift();
|
|
295
|
+
if (n) {
|
|
296
|
+
this.runNode(n);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (this.runningNodes.size === 0) {
|
|
190
300
|
this.onComplete();
|
|
191
301
|
}
|
|
192
302
|
}
|
|
303
|
+
|
|
304
|
+
public appendLog(log: TransactionLog) {
|
|
305
|
+
this.logs.push(log);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
public transactionLogs() {
|
|
309
|
+
return this.logs;
|
|
310
|
+
}
|
|
193
311
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
nodes:
|
|
2
|
+
node1:
|
|
3
|
+
params:
|
|
4
|
+
delay: 500
|
|
5
|
+
node2:
|
|
6
|
+
params:
|
|
7
|
+
delay: 100
|
|
8
|
+
node3:
|
|
9
|
+
params:
|
|
10
|
+
delay: 500
|
|
11
|
+
fail: true
|
|
12
|
+
inputs: [node1, node2]
|
|
13
|
+
node4:
|
|
14
|
+
timeout: 200
|
|
15
|
+
retry: 2
|
|
16
|
+
params:
|
|
17
|
+
delay: 300
|
|
18
|
+
inputs: [node3]
|
|
19
|
+
node5:
|
|
20
|
+
params:
|
|
21
|
+
delay: 100
|
|
22
|
+
inputs: [node4]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
nodes:
|
|
2
|
+
node1:
|
|
3
|
+
params:
|
|
4
|
+
delay: 500
|
|
5
|
+
node2:
|
|
6
|
+
params:
|
|
7
|
+
delay: 100
|
|
8
|
+
node3:
|
|
9
|
+
params:
|
|
10
|
+
delay: 500
|
|
11
|
+
inputs: [node1, node2]
|
|
12
|
+
functionName: test2
|
|
13
|
+
node4:
|
|
14
|
+
params:
|
|
15
|
+
delay: 100
|
|
16
|
+
inputs: [node3]
|
|
17
|
+
node5:
|
|
18
|
+
params:
|
|
19
|
+
delay: 500
|
|
20
|
+
inputs: [node2, node4]
|
|
21
|
+
functionName: test2
|
|
22
|
+
node6:
|
|
23
|
+
params:
|
|
24
|
+
delay: 100
|
|
25
|
+
number: 10
|
|
26
|
+
inputs: [node4]
|
|
27
|
+
functionName: numberTestFunction
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
nodes:
|
|
2
|
+
node1:
|
|
3
|
+
params:
|
|
4
|
+
delay: 500
|
|
5
|
+
node2:
|
|
6
|
+
params:
|
|
7
|
+
delay: 100
|
|
8
|
+
node3:
|
|
9
|
+
params:
|
|
10
|
+
delay: 500
|
|
11
|
+
fail: true
|
|
12
|
+
inputs: [node1, node2]
|
|
13
|
+
retry: 2
|
|
14
|
+
node4:
|
|
15
|
+
timeout: 200
|
|
16
|
+
params:
|
|
17
|
+
delay: 300
|
|
18
|
+
inputs: [node3]
|
|
19
|
+
node5:
|
|
20
|
+
params:
|
|
21
|
+
delay: 100
|
|
22
|
+
inputs: [node4]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { GraphAI, NodeExecute } from "../src/graphai";
|
|
3
|
+
import { readManifestData } from "./file_utils";
|
|
4
|
+
import { sleep } from "./utils";
|
|
5
|
+
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import assert from "node:assert";
|
|
8
|
+
|
|
9
|
+
const testFunction1: NodeExecute<Record<string, string>> = async (context) => {
|
|
10
|
+
const { nodeId, retry, params } = context;
|
|
11
|
+
console.log("executing", nodeId, params);
|
|
12
|
+
|
|
13
|
+
const result = { [nodeId]: "output 1" };
|
|
14
|
+
console.log("completing", nodeId, result);
|
|
15
|
+
return result;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const testFunction2: NodeExecute<Record<string, string>> = async (context) => {
|
|
19
|
+
const { nodeId, retry, params } = context;
|
|
20
|
+
console.log("executing", nodeId, params);
|
|
21
|
+
|
|
22
|
+
const result = { [nodeId]: "output 2" };
|
|
23
|
+
console.log("completing", nodeId, result);
|
|
24
|
+
return result;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const numberTestFunction: NodeExecute<Record<string, number>, Record<"number", number>> = async (context) => {
|
|
28
|
+
const { nodeId, retry, params } = context;
|
|
29
|
+
console.log("executing", nodeId, params);
|
|
30
|
+
|
|
31
|
+
const result = { [nodeId]: params.number };
|
|
32
|
+
console.log("completing", nodeId, result);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const runTest = async (file: string) => {
|
|
37
|
+
const file_path = path.resolve(__dirname) + file;
|
|
38
|
+
const graph_data = readManifestData(file_path);
|
|
39
|
+
|
|
40
|
+
const graph = new GraphAI(graph_data, { default: testFunction1, test2: testFunction2, numberTestFunction });
|
|
41
|
+
|
|
42
|
+
const results = await graph.run();
|
|
43
|
+
console.log(results);
|
|
44
|
+
return results;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
test("test sample1", async () => {
|
|
48
|
+
const result = await runTest("/graphs/test_multiple_functions_1.yml");
|
|
49
|
+
assert.deepStrictEqual(result, {
|
|
50
|
+
node1: { node1: "output 1" },
|
|
51
|
+
node2: { node2: "output 1" },
|
|
52
|
+
node3: { node3: "output 2" },
|
|
53
|
+
node4: { node4: "output 1" },
|
|
54
|
+
node5: { node5: "output 2" },
|
|
55
|
+
node6: { node6: 10 },
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { GraphAI, NodeExecute } from "../src/graphai";
|
|
3
|
+
import { readManifestData } from "./file_utils";
|
|
4
|
+
import { sleep } from "./utils";
|
|
5
|
+
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import assert from "node:assert";
|
|
8
|
+
|
|
9
|
+
const testFunction: NodeExecute<Record<string, string>> = async (context) => {
|
|
10
|
+
const { nodeId, retry, params, payload } = context;
|
|
11
|
+
console.log("executing", nodeId);
|
|
12
|
+
await sleep(params.delay / (retry + 1));
|
|
13
|
+
|
|
14
|
+
if (params.fail && retry < 2) {
|
|
15
|
+
const result = { [nodeId]: "failed" };
|
|
16
|
+
console.log("failed (intentional)", nodeId, retry);
|
|
17
|
+
throw new Error("Intentional Failure");
|
|
18
|
+
} else {
|
|
19
|
+
const result = Object.keys(payload).reduce(
|
|
20
|
+
(result, key) => {
|
|
21
|
+
result = { ...result, ...payload[key] };
|
|
22
|
+
return result;
|
|
23
|
+
},
|
|
24
|
+
{ [nodeId]: "output" },
|
|
25
|
+
);
|
|
26
|
+
console.log("completing", nodeId);
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const runTest = async (file: string) => {
|
|
32
|
+
const file_path = path.resolve(__dirname) + file;
|
|
33
|
+
const graph_data = readManifestData(file_path);
|
|
34
|
+
|
|
35
|
+
const graph = new GraphAI(graph_data, testFunction);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const results = await graph.run();
|
|
39
|
+
console.log(graph.transactionLogs());
|
|
40
|
+
return results;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error instanceof Error) {
|
|
43
|
+
console.log("Error:", error.message);
|
|
44
|
+
}
|
|
45
|
+
console.log(graph.transactionLogs());
|
|
46
|
+
return graph.results();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
test("test base", async () => {
|
|
51
|
+
const result = await runTest("/graphs/test_base.yml");
|
|
52
|
+
assert.deepStrictEqual(result, {
|
|
53
|
+
node1: { node1: "output" },
|
|
54
|
+
node2: { node2: "output" },
|
|
55
|
+
node3: { node3: "output", node1: "output", node2: "output" },
|
|
56
|
+
node4: { node4: "output", node3: "output", node1: "output", node2: "output" },
|
|
57
|
+
node5: { node5: "output", node4: "output", node3: "output", node1: "output", node2: "output" },
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("test retry", async () => {
|
|
62
|
+
const result = await runTest("/graphs/test_retry.yml");
|
|
63
|
+
assert.deepStrictEqual(result, {
|
|
64
|
+
node1: { node1: "output" },
|
|
65
|
+
node2: { node2: "output" },
|
|
66
|
+
node3: { node3: "output", node1: "output", node2: "output" },
|
|
67
|
+
node4: { node4: "output", node3: "output", node1: "output", node2: "output" },
|
|
68
|
+
node5: { node5: "output", node4: "output", node3: "output", node1: "output", node2: "output" },
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("test error", async () => {
|
|
73
|
+
const result = await runTest("/graphs/test_error.yml");
|
|
74
|
+
assert.deepStrictEqual(result, {
|
|
75
|
+
node1: { node1: "output" },
|
|
76
|
+
node2: { node2: "output" },
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("test timeout", async () => {
|
|
81
|
+
const result = await runTest("/graphs/test_timeout.yml");
|
|
82
|
+
assert.deepStrictEqual(result, {
|
|
83
|
+
node1: { node1: "output" },
|
|
84
|
+
node2: { node2: "output" },
|
|
85
|
+
node3: { node3: "output", node1: "output", node2: "output" },
|
|
86
|
+
});
|
|
87
|
+
});
|
package/lib/file_utils.d.ts
DELETED
package/lib/file_utils.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.readYamlManifest = exports.readJsonManifest = exports.readManifestData = void 0;
|
|
7
|
-
const fs_1 = __importDefault(require("fs"));
|
|
8
|
-
const yaml_1 = __importDefault(require("yaml"));
|
|
9
|
-
const readManifestData = (file) => {
|
|
10
|
-
if (file.endsWith(".yaml") || file.endsWith(".yml")) {
|
|
11
|
-
return (0, exports.readYamlManifest)(file);
|
|
12
|
-
}
|
|
13
|
-
if (file.endsWith(".json")) {
|
|
14
|
-
return (0, exports.readJsonManifest)(file);
|
|
15
|
-
}
|
|
16
|
-
throw new Error("No file exists " + file);
|
|
17
|
-
};
|
|
18
|
-
exports.readManifestData = readManifestData;
|
|
19
|
-
const readJsonManifest = (fileName) => {
|
|
20
|
-
const manifest_file = fs_1.default.readFileSync(fileName, "utf8");
|
|
21
|
-
const manifest = JSON.parse(manifest_file);
|
|
22
|
-
return manifest;
|
|
23
|
-
};
|
|
24
|
-
exports.readJsonManifest = readJsonManifest;
|
|
25
|
-
const readYamlManifest = (fileName) => {
|
|
26
|
-
const manifest_file = fs_1.default.readFileSync(fileName, "utf8");
|
|
27
|
-
const manifest = yaml_1.default.parse(manifest_file);
|
|
28
|
-
return manifest;
|
|
29
|
-
};
|
|
30
|
-
exports.readYamlManifest = readYamlManifest;
|
package/lib/flow.d.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
export declare enum NodeState {
|
|
2
|
-
Waiting = 0,
|
|
3
|
-
Executing = 1,
|
|
4
|
-
Failed = 2,
|
|
5
|
-
Completed = 3
|
|
6
|
-
}
|
|
7
|
-
type NodeData = {
|
|
8
|
-
inputs: undefined | Array<string>;
|
|
9
|
-
params: any;
|
|
10
|
-
retry: undefined | number;
|
|
11
|
-
};
|
|
12
|
-
type FlowData = {
|
|
13
|
-
nodes: Record<string, NodeData>;
|
|
14
|
-
};
|
|
15
|
-
export declare enum FlowCommand {
|
|
16
|
-
Log = 0,
|
|
17
|
-
Execute = 1,
|
|
18
|
-
OnComplete = 2
|
|
19
|
-
}
|
|
20
|
-
type FlowCallback = (params: Record<string, any>) => void;
|
|
21
|
-
declare class Node {
|
|
22
|
-
key: string;
|
|
23
|
-
inputs: Array<string>;
|
|
24
|
-
pendings: Set<string>;
|
|
25
|
-
params: any;
|
|
26
|
-
waitlist: Set<string>;
|
|
27
|
-
state: NodeState;
|
|
28
|
-
result: Record<string, any>;
|
|
29
|
-
retryLimit: number;
|
|
30
|
-
retryCount: number;
|
|
31
|
-
constructor(key: string, data: NodeData);
|
|
32
|
-
asString(): string;
|
|
33
|
-
complete(result: Record<string, any>, nodes: Record<string, Node>, graph: GraphAI): void;
|
|
34
|
-
reportError(result: Record<string, any>, nodes: Record<string, Node>, graph: GraphAI): void;
|
|
35
|
-
removePending(key: string, graph: GraphAI): void;
|
|
36
|
-
payload(graph: GraphAI): Record<string, any>;
|
|
37
|
-
executeIfReady(graph: GraphAI): void;
|
|
38
|
-
}
|
|
39
|
-
export declare class GraphAI {
|
|
40
|
-
nodes: Record<string, Node>;
|
|
41
|
-
callback: FlowCallback;
|
|
42
|
-
private runningNodes;
|
|
43
|
-
constructor(data: FlowData, callback: FlowCallback);
|
|
44
|
-
asString(): string;
|
|
45
|
-
run(): void;
|
|
46
|
-
feed(key: string, result: Record<string, any>): void;
|
|
47
|
-
reportError(key: string, result: Record<string, any>): void;
|
|
48
|
-
add(node: Node): void;
|
|
49
|
-
remove(node: Node): void;
|
|
50
|
-
}
|
|
51
|
-
export {};
|
package/lib/flow.js
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GraphAI = exports.FlowCommand = exports.NodeState = void 0;
|
|
4
|
-
var NodeState;
|
|
5
|
-
(function (NodeState) {
|
|
6
|
-
NodeState[NodeState["Waiting"] = 0] = "Waiting";
|
|
7
|
-
NodeState[NodeState["Executing"] = 1] = "Executing";
|
|
8
|
-
NodeState[NodeState["Failed"] = 2] = "Failed";
|
|
9
|
-
NodeState[NodeState["Completed"] = 3] = "Completed";
|
|
10
|
-
})(NodeState || (exports.NodeState = NodeState = {}));
|
|
11
|
-
var FlowCommand;
|
|
12
|
-
(function (FlowCommand) {
|
|
13
|
-
FlowCommand[FlowCommand["Log"] = 0] = "Log";
|
|
14
|
-
FlowCommand[FlowCommand["Execute"] = 1] = "Execute";
|
|
15
|
-
FlowCommand[FlowCommand["OnComplete"] = 2] = "OnComplete";
|
|
16
|
-
})(FlowCommand || (exports.FlowCommand = FlowCommand = {}));
|
|
17
|
-
class Node {
|
|
18
|
-
constructor(key, data) {
|
|
19
|
-
this.key = key;
|
|
20
|
-
this.inputs = data.inputs ?? [];
|
|
21
|
-
this.pendings = new Set(this.inputs);
|
|
22
|
-
this.params = data.params;
|
|
23
|
-
this.waitlist = new Set();
|
|
24
|
-
this.state = NodeState.Waiting;
|
|
25
|
-
this.result = {};
|
|
26
|
-
this.retryLimit = data.retry ?? 0;
|
|
27
|
-
this.retryCount = 0;
|
|
28
|
-
}
|
|
29
|
-
asString() {
|
|
30
|
-
return `${this.key}: ${this.state} ${[...this.waitlist]}`;
|
|
31
|
-
}
|
|
32
|
-
complete(result, nodes, graph) {
|
|
33
|
-
this.state = NodeState.Completed;
|
|
34
|
-
this.result = result;
|
|
35
|
-
this.waitlist.forEach(key => {
|
|
36
|
-
const node = nodes[key];
|
|
37
|
-
node.removePending(this.key, graph);
|
|
38
|
-
});
|
|
39
|
-
graph.remove(this);
|
|
40
|
-
}
|
|
41
|
-
reportError(result, nodes, graph) {
|
|
42
|
-
this.state = NodeState.Failed;
|
|
43
|
-
this.result = result;
|
|
44
|
-
if (this.retryCount < this.retryLimit) {
|
|
45
|
-
this.retryCount++;
|
|
46
|
-
this.state = NodeState.Executing;
|
|
47
|
-
graph.callback({ cmd: FlowCommand.Execute, node: this.key, params: this.params, retry: this.retryCount, payload: this.payload(graph) });
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
graph.remove(this);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
removePending(key, graph) {
|
|
54
|
-
this.pendings.delete(key);
|
|
55
|
-
this.executeIfReady(graph);
|
|
56
|
-
}
|
|
57
|
-
payload(graph) {
|
|
58
|
-
const foo = {};
|
|
59
|
-
return this.inputs.reduce((payload, key) => {
|
|
60
|
-
payload[key] = graph.nodes[key].result;
|
|
61
|
-
return payload;
|
|
62
|
-
}, foo);
|
|
63
|
-
}
|
|
64
|
-
executeIfReady(graph) {
|
|
65
|
-
if (this.pendings.size == 0) {
|
|
66
|
-
this.state = NodeState.Executing;
|
|
67
|
-
graph.add(this);
|
|
68
|
-
graph.callback({ cmd: FlowCommand.Execute, node: this.key, params: this.params, retry: 0, payload: this.payload(graph) });
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
class GraphAI {
|
|
73
|
-
constructor(data, callback) {
|
|
74
|
-
this.callback = callback;
|
|
75
|
-
this.runningNodes = new Set();
|
|
76
|
-
const foo = {}; // HACK: Work around
|
|
77
|
-
this.nodes = Object.keys(data.nodes).reduce((nodes, key) => {
|
|
78
|
-
nodes[key] = new Node(key, data.nodes[key]);
|
|
79
|
-
return nodes;
|
|
80
|
-
}, foo);
|
|
81
|
-
// Generate the waitlist for each node
|
|
82
|
-
Object.keys(this.nodes).forEach(key => {
|
|
83
|
-
const node = this.nodes[key];
|
|
84
|
-
node.pendings.forEach(pending => {
|
|
85
|
-
const node2 = this.nodes[pending];
|
|
86
|
-
node2.waitlist.add(key);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
asString() {
|
|
91
|
-
return Object.keys(this.nodes).map((key) => { return this.nodes[key].asString(); }).join('\n');
|
|
92
|
-
}
|
|
93
|
-
run() {
|
|
94
|
-
// Nodes without pending data should run immediately.
|
|
95
|
-
Object.keys(this.nodes).forEach(key => {
|
|
96
|
-
const node = this.nodes[key];
|
|
97
|
-
node.executeIfReady(this);
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
feed(key, result) {
|
|
101
|
-
const node = this.nodes[key];
|
|
102
|
-
node.complete(result, this.nodes, this);
|
|
103
|
-
}
|
|
104
|
-
reportError(key, result) {
|
|
105
|
-
const node = this.nodes[key];
|
|
106
|
-
node.reportError(result, this.nodes, this);
|
|
107
|
-
}
|
|
108
|
-
add(node) {
|
|
109
|
-
this.runningNodes.add(node.key);
|
|
110
|
-
}
|
|
111
|
-
remove(node) {
|
|
112
|
-
this.runningNodes.delete(node.key);
|
|
113
|
-
if (this.runningNodes.size == 0) {
|
|
114
|
-
this.callback({ cmd: FlowCommand.OnComplete });
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
exports.GraphAI = GraphAI;
|
package/tests/sample_flow.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import { GraphAI, NodeExecuteContext } from "../src/graphai";
|
|
3
|
-
import { readManifestData } from "../src/file_utils";
|
|
4
|
-
import { sleep } from "./utils";
|
|
5
|
-
|
|
6
|
-
import test from "node:test";
|
|
7
|
-
import assert from "node:assert";
|
|
8
|
-
|
|
9
|
-
const testFunction = async (context: NodeExecuteContext) => {
|
|
10
|
-
const { nodeId, retry, params } = context;
|
|
11
|
-
console.log("executing", nodeId, params);
|
|
12
|
-
await sleep(params.delay / (retry + 1));
|
|
13
|
-
|
|
14
|
-
if (params.fail && retry < 2) {
|
|
15
|
-
const result = { [nodeId]: "failed" };
|
|
16
|
-
console.log("failed", nodeId, result, retry);
|
|
17
|
-
throw new Error("Intentional Failure");
|
|
18
|
-
} else {
|
|
19
|
-
const result = { [nodeId]: "output" };
|
|
20
|
-
console.log("completing", nodeId, result);
|
|
21
|
-
return result;
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const runTest = async (file: string) => {
|
|
26
|
-
const file_path = path.resolve(__dirname) + file;
|
|
27
|
-
const graph_data = readManifestData(file_path);
|
|
28
|
-
|
|
29
|
-
const graph = new GraphAI(graph_data, testFunction);
|
|
30
|
-
|
|
31
|
-
const results = await graph.run();
|
|
32
|
-
console.log(results);
|
|
33
|
-
return results;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
test("test sample1", async () => {
|
|
37
|
-
const result = await runTest("/graphs/sample1.yml");
|
|
38
|
-
assert.deepStrictEqual(result, {
|
|
39
|
-
node1: { node1: "output" },
|
|
40
|
-
node2: { node2: "output" },
|
|
41
|
-
node3: { node3: "output" },
|
|
42
|
-
node4: { node4: "output" },
|
|
43
|
-
node5: { node5: "output" },
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("test sample1", async () => {
|
|
48
|
-
const result = await runTest("/graphs/sample2.yml");
|
|
49
|
-
assert.deepStrictEqual(result, {
|
|
50
|
-
node1: { node1: "output" },
|
|
51
|
-
node2: { node2: "output" },
|
|
52
|
-
node3: { node3: "output" },
|
|
53
|
-
node4: { node4: "output" },
|
|
54
|
-
node5: { node5: "output" },
|
|
55
|
-
});
|
|
56
|
-
console.log("COMPLETE 2");
|
|
57
|
-
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|