graphai 0.0.4 → 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/README.md +1 -2
- package/lib/graphai.d.ts +32 -17
- package/lib/graphai.js +71 -15
- package/package.json +4 -3
- package/{tests → samples}/sample_gpt.ts +5 -5
- package/src/graphai.ts +108 -36
- package/tests/graphs/test_error.yml +22 -0
- package/tests/graphs/test_multiple_functions_1.yml +6 -0
- package/tests/graphs/test_timeout.yml +22 -0
- package/tests/test_multiple_functions.ts +15 -5
- package/tests/test_sample_flow.ts +37 -13
- /package/{tests/graphs/sample3.yml → samples/graphs/slash_gpt.yml} +0 -0
- /package/tests/graphs/{sample1.yml → test_base.yml} +0 -0
- /package/tests/graphs/{sample2.yml → test_retry.yml} +0 -0
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
|
@@ -7,7 +7,7 @@ export declare enum NodeState {
|
|
|
7
7
|
}
|
|
8
8
|
type ResultData<ResultType = Record<string, any>> = ResultType | undefined;
|
|
9
9
|
type ResultDataDictonary<ResultType = Record<string, any>> = Record<string, ResultData<ResultType>>;
|
|
10
|
-
export type NodeDataParams = Record<string, any
|
|
10
|
+
export type NodeDataParams<ParamsType = Record<string, any>> = ParamsType;
|
|
11
11
|
type NodeData = {
|
|
12
12
|
inputs: undefined | Array<string>;
|
|
13
13
|
params: NodeDataParams;
|
|
@@ -19,14 +19,23 @@ type GraphData = {
|
|
|
19
19
|
nodes: Record<string, NodeData>;
|
|
20
20
|
concurrency: number;
|
|
21
21
|
};
|
|
22
|
-
|
|
22
|
+
type NodeExecuteContext<ResultType, ParamsType> = {
|
|
23
23
|
nodeId: string;
|
|
24
24
|
retry: number;
|
|
25
|
-
params: NodeDataParams
|
|
25
|
+
params: NodeDataParams<ParamsType>;
|
|
26
26
|
payload: ResultDataDictonary<ResultType>;
|
|
27
27
|
};
|
|
28
|
-
type
|
|
29
|
-
|
|
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;
|
|
36
|
+
};
|
|
37
|
+
export type NodeExecute<ResultType = Record<string, any>, ParamsType = Record<string, any>> = (context: NodeExecuteContext<ResultType, ParamsType>) => Promise<ResultData<ResultType>>;
|
|
38
|
+
declare class Node {
|
|
30
39
|
nodeId: string;
|
|
31
40
|
params: NodeDataParams;
|
|
32
41
|
inputs: Array<string>;
|
|
@@ -34,35 +43,41 @@ declare class Node<ResultType = Record<string, any>> {
|
|
|
34
43
|
waitlist: Set<string>;
|
|
35
44
|
state: NodeState;
|
|
36
45
|
functionName: string;
|
|
37
|
-
result: ResultData
|
|
46
|
+
result: ResultData;
|
|
38
47
|
retryLimit: number;
|
|
39
48
|
retryCount: number;
|
|
40
49
|
transactionId: undefined | number;
|
|
41
50
|
timeout: number;
|
|
51
|
+
error: undefined | Error;
|
|
42
52
|
private graph;
|
|
43
|
-
constructor(nodeId: string, data: NodeData, graph: GraphAI
|
|
53
|
+
constructor(nodeId: string, data: NodeData, graph: GraphAI);
|
|
44
54
|
asString(): string;
|
|
45
55
|
private retry;
|
|
46
56
|
removePending(nodeId: string): void;
|
|
47
|
-
payload(): ResultDataDictonary<
|
|
57
|
+
payload(): ResultDataDictonary<Record<string, any>>;
|
|
48
58
|
pushQueueIfReady(): void;
|
|
49
59
|
execute(): Promise<void>;
|
|
50
60
|
}
|
|
51
|
-
type GraphNodes
|
|
52
|
-
type NodeExecuteDictonary
|
|
53
|
-
export declare class GraphAI
|
|
54
|
-
nodes: GraphNodes
|
|
55
|
-
callbackDictonary: NodeExecuteDictonary
|
|
61
|
+
type GraphNodes = Record<string, Node>;
|
|
62
|
+
type NodeExecuteDictonary = Record<string, NodeExecute>;
|
|
63
|
+
export declare class GraphAI {
|
|
64
|
+
nodes: GraphNodes;
|
|
65
|
+
callbackDictonary: NodeExecuteDictonary;
|
|
56
66
|
private runningNodes;
|
|
57
67
|
private nodeQueue;
|
|
58
68
|
private onComplete;
|
|
59
69
|
private concurrency;
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
private logs;
|
|
71
|
+
constructor(data: GraphData, callbackDictonary: NodeExecuteDictonary | NodeExecute);
|
|
72
|
+
getCallback(functionName: string): NodeExecute<Record<string, any>, Record<string, any>>;
|
|
62
73
|
asString(): string;
|
|
74
|
+
results(): ResultDataDictonary<Record<string, any>>;
|
|
75
|
+
errors(): Record<string, Error>;
|
|
63
76
|
run(): Promise<unknown>;
|
|
64
77
|
private runNode;
|
|
65
|
-
pushQueue(node: Node
|
|
66
|
-
removeRunning(node: Node
|
|
78
|
+
pushQueue(node: Node): void;
|
|
79
|
+
removeRunning(node: Node): void;
|
|
80
|
+
appendLog(log: TransactionLog): void;
|
|
81
|
+
transactionLogs(): TransactionLog[];
|
|
67
82
|
}
|
|
68
83
|
export {};
|
package/lib/graphai.js
CHANGED
|
@@ -27,14 +27,16 @@ class Node {
|
|
|
27
27
|
asString() {
|
|
28
28
|
return `${this.nodeId}: ${this.state} ${[...this.waitlist]}`;
|
|
29
29
|
}
|
|
30
|
-
retry(state,
|
|
30
|
+
retry(state, error) {
|
|
31
31
|
if (this.retryCount < this.retryLimit) {
|
|
32
32
|
this.retryCount++;
|
|
33
33
|
this.execute();
|
|
34
34
|
}
|
|
35
35
|
else {
|
|
36
36
|
this.state = state;
|
|
37
|
-
this.result =
|
|
37
|
+
this.result = undefined;
|
|
38
|
+
this.error = error;
|
|
39
|
+
this.transactionId = 0; // This is necessary for timeout case
|
|
38
40
|
this.graph.removeRunning(this);
|
|
39
41
|
}
|
|
40
42
|
}
|
|
@@ -54,14 +56,26 @@ class Node {
|
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
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);
|
|
57
68
|
this.state = NodeState.Executing;
|
|
58
|
-
const transactionId =
|
|
69
|
+
const transactionId = log.startTime;
|
|
59
70
|
this.transactionId = transactionId;
|
|
60
71
|
if (this.timeout > 0) {
|
|
61
72
|
setTimeout(() => {
|
|
62
73
|
if (this.state === NodeState.Executing && this.transactionId === transactionId) {
|
|
63
|
-
console.log(
|
|
64
|
-
|
|
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"));
|
|
65
79
|
}
|
|
66
80
|
}, this.timeout);
|
|
67
81
|
}
|
|
@@ -74,9 +88,12 @@ class Node {
|
|
|
74
88
|
payload: this.payload(),
|
|
75
89
|
});
|
|
76
90
|
if (this.transactionId !== transactionId) {
|
|
77
|
-
console.log(
|
|
91
|
+
console.log(`-- ${this.nodeId}: transactionId mismatch`);
|
|
78
92
|
return;
|
|
79
93
|
}
|
|
94
|
+
log.state = NodeState.Completed;
|
|
95
|
+
log.endTime = Date.now();
|
|
96
|
+
log.result = result;
|
|
80
97
|
this.state = NodeState.Completed;
|
|
81
98
|
this.result = result;
|
|
82
99
|
this.waitlist.forEach((nodeId) => {
|
|
@@ -85,22 +102,34 @@ class Node {
|
|
|
85
102
|
});
|
|
86
103
|
this.graph.removeRunning(this);
|
|
87
104
|
}
|
|
88
|
-
catch (
|
|
105
|
+
catch (error) {
|
|
89
106
|
if (this.transactionId !== transactionId) {
|
|
90
|
-
console.log(
|
|
107
|
+
console.log(`-- ${this.nodeId}: transactionId mismatch(error)`);
|
|
91
108
|
return;
|
|
92
109
|
}
|
|
93
|
-
|
|
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
|
+
}
|
|
94
121
|
}
|
|
95
122
|
}
|
|
96
123
|
}
|
|
124
|
+
const defaultConcurrency = 8;
|
|
97
125
|
class GraphAI {
|
|
98
126
|
constructor(data, callbackDictonary) {
|
|
127
|
+
this.logs = [];
|
|
99
128
|
this.callbackDictonary = typeof callbackDictonary === "function" ? { default: callbackDictonary } : callbackDictonary;
|
|
100
129
|
if (this.callbackDictonary["default"] === undefined) {
|
|
101
130
|
throw new Error("No default function");
|
|
102
131
|
}
|
|
103
|
-
this.concurrency = data.concurrency ??
|
|
132
|
+
this.concurrency = data.concurrency ?? defaultConcurrency;
|
|
104
133
|
this.runningNodes = new Set();
|
|
105
134
|
this.nodeQueue = [];
|
|
106
135
|
this.onComplete = () => { };
|
|
@@ -130,6 +159,24 @@ class GraphAI {
|
|
|
130
159
|
})
|
|
131
160
|
.join("\n");
|
|
132
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
|
+
}
|
|
133
180
|
async run() {
|
|
134
181
|
// Nodes without pending data should run immediately.
|
|
135
182
|
Object.keys(this.nodes).forEach((nodeId) => {
|
|
@@ -138,11 +185,14 @@ class GraphAI {
|
|
|
138
185
|
});
|
|
139
186
|
return new Promise((resolve, reject) => {
|
|
140
187
|
this.onComplete = () => {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
}
|
|
146
196
|
};
|
|
147
197
|
});
|
|
148
198
|
}
|
|
@@ -170,5 +220,11 @@ class GraphAI {
|
|
|
170
220
|
this.onComplete();
|
|
171
221
|
}
|
|
172
222
|
}
|
|
223
|
+
appendLog(log) {
|
|
224
|
+
this.logs.push(log);
|
|
225
|
+
}
|
|
226
|
+
transactionLogs() {
|
|
227
|
+
return this.logs;
|
|
228
|
+
}
|
|
173
229
|
}
|
|
174
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/test_*.ts"
|
|
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",
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
"eslint-plugin-import": "^2.25.2",
|
|
28
29
|
"openai": "^4.12.4",
|
|
29
30
|
"prettier": "^3.0.3",
|
|
30
|
-
"slashgpt": "^0.0.
|
|
31
|
+
"slashgpt": "^0.0.8",
|
|
31
32
|
"ts-node": "^10.9.1",
|
|
32
33
|
"typescript": "^5.2.2"
|
|
33
34
|
},
|
|
@@ -1,12 +1,12 @@
|
|
|
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
|
|
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
12
|
return prompt.replace("${" + key + "}", context.payload[key]!["answer"]);
|
|
@@ -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
|
@@ -10,7 +10,7 @@ export enum NodeState {
|
|
|
10
10
|
type ResultData<ResultType = Record<string, any>> = ResultType | undefined;
|
|
11
11
|
type ResultDataDictonary<ResultType = Record<string, any>> = Record<string, ResultData<ResultType>>;
|
|
12
12
|
|
|
13
|
-
export type NodeDataParams = Record<string, any
|
|
13
|
+
export type NodeDataParams<ParamsType = Record<string, any>> = ParamsType; // App-specific parameters
|
|
14
14
|
|
|
15
15
|
type NodeData = {
|
|
16
16
|
inputs: undefined | Array<string>;
|
|
@@ -25,16 +25,28 @@ type GraphData = {
|
|
|
25
25
|
concurrency: number;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
type NodeExecuteContext<ResultType, ParamsType> = {
|
|
29
29
|
nodeId: string;
|
|
30
30
|
retry: number;
|
|
31
|
-
params: NodeDataParams
|
|
31
|
+
params: NodeDataParams<ParamsType>;
|
|
32
32
|
payload: ResultDataDictonary<ResultType>;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
type
|
|
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;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type NodeExecute<ResultType = Record<string, any>, ParamsType = Record<string, any>> = (
|
|
46
|
+
context: NodeExecuteContext<ResultType, ParamsType>,
|
|
47
|
+
) => Promise<ResultData<ResultType>>;
|
|
36
48
|
|
|
37
|
-
class Node
|
|
49
|
+
class Node {
|
|
38
50
|
public nodeId: string;
|
|
39
51
|
public params: NodeDataParams; // App-specific parameters
|
|
40
52
|
public inputs: Array<string>; // List of nodes this node needs data from.
|
|
@@ -42,15 +54,16 @@ class Node<ResultType = Record<string, any>> {
|
|
|
42
54
|
public waitlist: Set<string>; // List of nodes which need data from this node.
|
|
43
55
|
public state: NodeState;
|
|
44
56
|
public functionName: string;
|
|
45
|
-
public result: ResultData
|
|
57
|
+
public result: ResultData;
|
|
46
58
|
public retryLimit: number;
|
|
47
59
|
public retryCount: number;
|
|
48
60
|
public transactionId: undefined | number; // To reject callbacks from timed-out transactions
|
|
49
61
|
public timeout: number; // msec
|
|
62
|
+
public error: undefined | Error;
|
|
50
63
|
|
|
51
|
-
private graph: GraphAI
|
|
64
|
+
private graph: GraphAI;
|
|
52
65
|
|
|
53
|
-
constructor(nodeId: string, data: NodeData, graph: GraphAI
|
|
66
|
+
constructor(nodeId: string, data: NodeData, graph: GraphAI) {
|
|
54
67
|
this.nodeId = nodeId;
|
|
55
68
|
this.inputs = data.inputs ?? [];
|
|
56
69
|
this.pendings = new Set(this.inputs);
|
|
@@ -70,13 +83,15 @@ class Node<ResultType = Record<string, any>> {
|
|
|
70
83
|
return `${this.nodeId}: ${this.state} ${[...this.waitlist]}`;
|
|
71
84
|
}
|
|
72
85
|
|
|
73
|
-
private retry(state: NodeState,
|
|
86
|
+
private retry(state: NodeState, error: Error) {
|
|
74
87
|
if (this.retryCount < this.retryLimit) {
|
|
75
88
|
this.retryCount++;
|
|
76
89
|
this.execute();
|
|
77
90
|
} else {
|
|
78
91
|
this.state = state;
|
|
79
|
-
this.result =
|
|
92
|
+
this.result = undefined;
|
|
93
|
+
this.error = error;
|
|
94
|
+
this.transactionId = 0; // This is necessary for timeout case
|
|
80
95
|
this.graph.removeRunning(this);
|
|
81
96
|
}
|
|
82
97
|
}
|
|
@@ -87,7 +102,7 @@ class Node<ResultType = Record<string, any>> {
|
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
public payload() {
|
|
90
|
-
return this.inputs.reduce((results: ResultDataDictonary
|
|
105
|
+
return this.inputs.reduce((results: ResultDataDictonary, nodeId) => {
|
|
91
106
|
results[nodeId] = this.graph.nodes[nodeId].result;
|
|
92
107
|
return results;
|
|
93
108
|
}, {});
|
|
@@ -100,15 +115,27 @@ class Node<ResultType = Record<string, any>> {
|
|
|
100
115
|
}
|
|
101
116
|
|
|
102
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);
|
|
103
127
|
this.state = NodeState.Executing;
|
|
104
|
-
const transactionId =
|
|
128
|
+
const transactionId = log.startTime;
|
|
105
129
|
this.transactionId = transactionId;
|
|
106
130
|
|
|
107
131
|
if (this.timeout > 0) {
|
|
108
132
|
setTimeout(() => {
|
|
109
133
|
if (this.state === NodeState.Executing && this.transactionId === transactionId) {
|
|
110
|
-
console.log(
|
|
111
|
-
|
|
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"));
|
|
112
139
|
}
|
|
113
140
|
}, this.timeout);
|
|
114
141
|
}
|
|
@@ -122,9 +149,12 @@ class Node<ResultType = Record<string, any>> {
|
|
|
122
149
|
payload: this.payload(),
|
|
123
150
|
});
|
|
124
151
|
if (this.transactionId !== transactionId) {
|
|
125
|
-
console.log(
|
|
152
|
+
console.log(`-- ${this.nodeId}: transactionId mismatch`);
|
|
126
153
|
return;
|
|
127
154
|
}
|
|
155
|
+
log.state = NodeState.Completed;
|
|
156
|
+
log.endTime = Date.now();
|
|
157
|
+
log.result = result;
|
|
128
158
|
this.state = NodeState.Completed;
|
|
129
159
|
this.result = result;
|
|
130
160
|
this.waitlist.forEach((nodeId) => {
|
|
@@ -132,39 +162,51 @@ class Node<ResultType = Record<string, any>> {
|
|
|
132
162
|
node.removePending(this.nodeId);
|
|
133
163
|
});
|
|
134
164
|
this.graph.removeRunning(this);
|
|
135
|
-
} catch (
|
|
165
|
+
} catch (error) {
|
|
136
166
|
if (this.transactionId !== transactionId) {
|
|
137
|
-
console.log(
|
|
167
|
+
console.log(`-- ${this.nodeId}: transactionId mismatch(error)`);
|
|
138
168
|
return;
|
|
139
169
|
}
|
|
140
|
-
|
|
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
|
+
}
|
|
141
180
|
}
|
|
142
181
|
}
|
|
143
182
|
}
|
|
144
183
|
|
|
145
|
-
type GraphNodes
|
|
184
|
+
type GraphNodes = Record<string, Node>;
|
|
146
185
|
|
|
147
|
-
type NodeExecuteDictonary
|
|
186
|
+
type NodeExecuteDictonary = Record<string, NodeExecute>;
|
|
148
187
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
188
|
+
const defaultConcurrency = 8;
|
|
189
|
+
|
|
190
|
+
export class GraphAI {
|
|
191
|
+
public nodes: GraphNodes;
|
|
192
|
+
public callbackDictonary: NodeExecuteDictonary;
|
|
152
193
|
private runningNodes: Set<string>;
|
|
153
|
-
private nodeQueue: Array<Node
|
|
194
|
+
private nodeQueue: Array<Node>;
|
|
154
195
|
private onComplete: () => void;
|
|
155
196
|
private concurrency: number;
|
|
197
|
+
private logs: Array<TransactionLog> = [];
|
|
156
198
|
|
|
157
|
-
constructor(data: GraphData, callbackDictonary: NodeExecuteDictonary
|
|
199
|
+
constructor(data: GraphData, callbackDictonary: NodeExecuteDictonary | NodeExecute) {
|
|
158
200
|
this.callbackDictonary = typeof callbackDictonary === "function" ? { default: callbackDictonary } : callbackDictonary;
|
|
159
201
|
if (this.callbackDictonary["default"] === undefined) {
|
|
160
202
|
throw new Error("No default function");
|
|
161
203
|
}
|
|
162
|
-
this.concurrency = data.concurrency ??
|
|
204
|
+
this.concurrency = data.concurrency ?? defaultConcurrency;
|
|
163
205
|
this.runningNodes = new Set<string>();
|
|
164
206
|
this.nodeQueue = [];
|
|
165
207
|
this.onComplete = () => {};
|
|
166
|
-
this.nodes = Object.keys(data.nodes).reduce((nodes: GraphNodes
|
|
167
|
-
nodes[nodeId] = new Node
|
|
208
|
+
this.nodes = Object.keys(data.nodes).reduce((nodes: GraphNodes, nodeId: string) => {
|
|
209
|
+
nodes[nodeId] = new Node(nodeId, data.nodes[nodeId], this);
|
|
168
210
|
return nodes;
|
|
169
211
|
}, {});
|
|
170
212
|
|
|
@@ -193,6 +235,26 @@ export class GraphAI<ResultType = Record<string, any>> {
|
|
|
193
235
|
.join("\n");
|
|
194
236
|
}
|
|
195
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
|
+
|
|
196
258
|
public async run() {
|
|
197
259
|
// Nodes without pending data should run immediately.
|
|
198
260
|
Object.keys(this.nodes).forEach((nodeId) => {
|
|
@@ -202,21 +264,23 @@ export class GraphAI<ResultType = Record<string, any>> {
|
|
|
202
264
|
|
|
203
265
|
return new Promise((resolve, reject) => {
|
|
204
266
|
this.onComplete = () => {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
}
|
|
210
274
|
};
|
|
211
275
|
});
|
|
212
276
|
}
|
|
213
277
|
|
|
214
|
-
private runNode(node: Node
|
|
278
|
+
private runNode(node: Node) {
|
|
215
279
|
this.runningNodes.add(node.nodeId);
|
|
216
280
|
node.execute();
|
|
217
281
|
}
|
|
218
282
|
|
|
219
|
-
public pushQueue(node: Node
|
|
283
|
+
public pushQueue(node: Node) {
|
|
220
284
|
if (this.runningNodes.size < this.concurrency) {
|
|
221
285
|
this.runNode(node);
|
|
222
286
|
} else {
|
|
@@ -224,7 +288,7 @@ export class GraphAI<ResultType = Record<string, any>> {
|
|
|
224
288
|
}
|
|
225
289
|
}
|
|
226
290
|
|
|
227
|
-
public removeRunning(node: Node
|
|
291
|
+
public removeRunning(node: Node) {
|
|
228
292
|
this.runningNodes.delete(node.nodeId);
|
|
229
293
|
if (this.nodeQueue.length > 0) {
|
|
230
294
|
const n = this.nodeQueue.shift();
|
|
@@ -236,4 +300,12 @@ export class GraphAI<ResultType = Record<string, any>> {
|
|
|
236
300
|
this.onComplete();
|
|
237
301
|
}
|
|
238
302
|
}
|
|
303
|
+
|
|
304
|
+
public appendLog(log: TransactionLog) {
|
|
305
|
+
this.logs.push(log);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
public transactionLogs() {
|
|
309
|
+
return this.logs;
|
|
310
|
+
}
|
|
239
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,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]
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import { GraphAI,
|
|
3
|
-
import { readManifestData } from "
|
|
2
|
+
import { GraphAI, NodeExecute } from "../src/graphai";
|
|
3
|
+
import { readManifestData } from "./file_utils";
|
|
4
4
|
import { sleep } from "./utils";
|
|
5
5
|
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
import assert from "node:assert";
|
|
8
8
|
|
|
9
|
-
const testFunction1
|
|
9
|
+
const testFunction1: NodeExecute<Record<string, string>> = async (context) => {
|
|
10
10
|
const { nodeId, retry, params } = context;
|
|
11
11
|
console.log("executing", nodeId, params);
|
|
12
12
|
|
|
@@ -15,7 +15,7 @@ const testFunction1 = async (context: NodeExecuteContext<Record<string, string>>
|
|
|
15
15
|
return result;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
const testFunction2
|
|
18
|
+
const testFunction2: NodeExecute<Record<string, string>> = async (context) => {
|
|
19
19
|
const { nodeId, retry, params } = context;
|
|
20
20
|
console.log("executing", nodeId, params);
|
|
21
21
|
|
|
@@ -24,11 +24,20 @@ const testFunction2 = async (context: NodeExecuteContext<Record<string, string>>
|
|
|
24
24
|
return result;
|
|
25
25
|
};
|
|
26
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
|
+
|
|
27
36
|
const runTest = async (file: string) => {
|
|
28
37
|
const file_path = path.resolve(__dirname) + file;
|
|
29
38
|
const graph_data = readManifestData(file_path);
|
|
30
39
|
|
|
31
|
-
const graph = new GraphAI(graph_data, { default: testFunction1, test2: testFunction2 });
|
|
40
|
+
const graph = new GraphAI(graph_data, { default: testFunction1, test2: testFunction2, numberTestFunction });
|
|
32
41
|
|
|
33
42
|
const results = await graph.run();
|
|
34
43
|
console.log(results);
|
|
@@ -43,5 +52,6 @@ test("test sample1", async () => {
|
|
|
43
52
|
node3: { node3: "output 2" },
|
|
44
53
|
node4: { node4: "output 1" },
|
|
45
54
|
node5: { node5: "output 2" },
|
|
55
|
+
node6: { node6: 10 },
|
|
46
56
|
});
|
|
47
57
|
});
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import { GraphAI,
|
|
2
|
+
import { GraphAI, NodeExecute } from "../src/graphai";
|
|
3
3
|
import { readManifestData } from "./file_utils";
|
|
4
4
|
import { sleep } from "./utils";
|
|
5
5
|
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
import assert from "node:assert";
|
|
8
8
|
|
|
9
|
-
const testFunction
|
|
9
|
+
const testFunction: NodeExecute<Record<string, string>> = async (context) => {
|
|
10
10
|
const { nodeId, retry, params, payload } = context;
|
|
11
|
-
console.log("executing", nodeId
|
|
11
|
+
console.log("executing", nodeId);
|
|
12
12
|
await sleep(params.delay / (retry + 1));
|
|
13
13
|
|
|
14
14
|
if (params.fail && retry < 2) {
|
|
15
15
|
const result = { [nodeId]: "failed" };
|
|
16
|
-
console.log("failed", nodeId,
|
|
16
|
+
console.log("failed (intentional)", nodeId, retry);
|
|
17
17
|
throw new Error("Intentional Failure");
|
|
18
18
|
} else {
|
|
19
19
|
const result = Object.keys(payload).reduce(
|
|
@@ -23,7 +23,7 @@ const testFunction = async (context: NodeExecuteContext<Record<string, string>>)
|
|
|
23
23
|
},
|
|
24
24
|
{ [nodeId]: "output" },
|
|
25
25
|
);
|
|
26
|
-
console.log("completing", nodeId
|
|
26
|
+
console.log("completing", nodeId);
|
|
27
27
|
return result;
|
|
28
28
|
}
|
|
29
29
|
};
|
|
@@ -34,13 +34,21 @@ const runTest = async (file: string) => {
|
|
|
34
34
|
|
|
35
35
|
const graph = new GraphAI(graph_data, testFunction);
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
}
|
|
40
48
|
};
|
|
41
49
|
|
|
42
|
-
test("test
|
|
43
|
-
const result = await runTest("/graphs/
|
|
50
|
+
test("test base", async () => {
|
|
51
|
+
const result = await runTest("/graphs/test_base.yml");
|
|
44
52
|
assert.deepStrictEqual(result, {
|
|
45
53
|
node1: { node1: "output" },
|
|
46
54
|
node2: { node2: "output" },
|
|
@@ -50,8 +58,8 @@ test("test sample1", async () => {
|
|
|
50
58
|
});
|
|
51
59
|
});
|
|
52
60
|
|
|
53
|
-
test("test
|
|
54
|
-
const result = await runTest("/graphs/
|
|
61
|
+
test("test retry", async () => {
|
|
62
|
+
const result = await runTest("/graphs/test_retry.yml");
|
|
55
63
|
assert.deepStrictEqual(result, {
|
|
56
64
|
node1: { node1: "output" },
|
|
57
65
|
node2: { node2: "output" },
|
|
@@ -59,5 +67,21 @@ test("test sample1", async () => {
|
|
|
59
67
|
node4: { node4: "output", node3: "output", node1: "output", node2: "output" },
|
|
60
68
|
node5: { node5: "output", node4: "output", node3: "output", node1: "output", node2: "output" },
|
|
61
69
|
});
|
|
62
|
-
|
|
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
|
+
});
|
|
63
87
|
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|