graphai 0.0.2 → 0.0.4
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 +25 -1
- package/lib/graphai.d.ts +68 -0
- package/lib/graphai.js +174 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +2 -2
- package/package.json +4 -3
- package/src/graphai.ts +239 -0
- package/src/index.ts +2 -2
- package/tests/graphs/sample2.yml +7 -1
- package/tests/graphs/sample3.yml +12 -0
- package/tests/graphs/test_multiple_functions_1.yml +21 -0
- package/tests/sample_gpt.ts +37 -0
- package/tests/test_multiple_functions.ts +47 -0
- package/tests/test_sample_flow.ts +63 -0
- package/tests/utils.ts +3 -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/src/flow.ts +0 -152
- package/tests/sample_flow.ts +0 -39
- /package/{src → tests}/file_utils.ts +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,8 @@
|
|
|
1
1
|
# GraphAI
|
|
2
2
|
|
|
3
|
-
GraphAI is a TypeScript library, which makes it easy to create AI applications that need to
|
|
3
|
+
GraphAI is a TypeScript library, 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
|
+
|
|
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.
|
|
4
6
|
|
|
5
7
|
Here is an example:
|
|
6
8
|
|
|
@@ -18,3 +20,25 @@ nodes:
|
|
|
18
20
|
inputs: [taskA, taskB]
|
|
19
21
|
```
|
|
20
22
|
|
|
23
|
+
``` TypeScript
|
|
24
|
+
const nodeExecute = async (context: NodeExecuteContext) => {
|
|
25
|
+
const {
|
|
26
|
+
nodeId, // taskA, taskB or taskC
|
|
27
|
+
params, // app-specific/task-specific parameters specified in the graph definition file
|
|
28
|
+
payload // for taskC, { taskA: resultA, taskB: resultB }
|
|
29
|
+
} = context;
|
|
30
|
+
// App-specific code (such as calling OpenAI's chat.completions API)
|
|
31
|
+
...
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
...
|
|
36
|
+
const file = fs.readFileSync(pathToYamlFile, "utf8");
|
|
37
|
+
const graphdata = YAML.parse(file);
|
|
38
|
+
const graph = new GraphAI(graph_data, nodeExecute);
|
|
39
|
+
const results = await graph.run();
|
|
40
|
+
return results["taskC"];
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
|
package/lib/graphai.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export declare enum NodeState {
|
|
2
|
+
Waiting = 0,
|
|
3
|
+
Executing = 1,
|
|
4
|
+
Failed = 2,
|
|
5
|
+
TimedOut = 3,
|
|
6
|
+
Completed = 4
|
|
7
|
+
}
|
|
8
|
+
type ResultData<ResultType = Record<string, any>> = ResultType | undefined;
|
|
9
|
+
type ResultDataDictonary<ResultType = Record<string, any>> = Record<string, ResultData<ResultType>>;
|
|
10
|
+
export type NodeDataParams = Record<string, any>;
|
|
11
|
+
type NodeData = {
|
|
12
|
+
inputs: undefined | Array<string>;
|
|
13
|
+
params: NodeDataParams;
|
|
14
|
+
retry: undefined | number;
|
|
15
|
+
timeout: undefined | number;
|
|
16
|
+
functionName: undefined | string;
|
|
17
|
+
};
|
|
18
|
+
type GraphData = {
|
|
19
|
+
nodes: Record<string, NodeData>;
|
|
20
|
+
concurrency: number;
|
|
21
|
+
};
|
|
22
|
+
export type NodeExecuteContext<ResultType> = {
|
|
23
|
+
nodeId: string;
|
|
24
|
+
retry: number;
|
|
25
|
+
params: NodeDataParams;
|
|
26
|
+
payload: ResultDataDictonary<ResultType>;
|
|
27
|
+
};
|
|
28
|
+
type NodeExecute<ResultType> = (context: NodeExecuteContext<ResultType>) => Promise<ResultData<ResultType>>;
|
|
29
|
+
declare class Node<ResultType = Record<string, any>> {
|
|
30
|
+
nodeId: string;
|
|
31
|
+
params: NodeDataParams;
|
|
32
|
+
inputs: Array<string>;
|
|
33
|
+
pendings: Set<string>;
|
|
34
|
+
waitlist: Set<string>;
|
|
35
|
+
state: NodeState;
|
|
36
|
+
functionName: string;
|
|
37
|
+
result: ResultData<ResultType>;
|
|
38
|
+
retryLimit: number;
|
|
39
|
+
retryCount: number;
|
|
40
|
+
transactionId: undefined | number;
|
|
41
|
+
timeout: number;
|
|
42
|
+
private graph;
|
|
43
|
+
constructor(nodeId: string, data: NodeData, graph: GraphAI<ResultType>);
|
|
44
|
+
asString(): string;
|
|
45
|
+
private retry;
|
|
46
|
+
removePending(nodeId: string): void;
|
|
47
|
+
payload(): ResultDataDictonary<ResultType>;
|
|
48
|
+
pushQueueIfReady(): void;
|
|
49
|
+
execute(): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
type GraphNodes<ResultType> = Record<string, Node<ResultType>>;
|
|
52
|
+
type NodeExecuteDictonary<ResultType> = Record<string, NodeExecute<ResultType>>;
|
|
53
|
+
export declare class GraphAI<ResultType = Record<string, any>> {
|
|
54
|
+
nodes: GraphNodes<ResultType>;
|
|
55
|
+
callbackDictonary: NodeExecuteDictonary<ResultType>;
|
|
56
|
+
private runningNodes;
|
|
57
|
+
private nodeQueue;
|
|
58
|
+
private onComplete;
|
|
59
|
+
private concurrency;
|
|
60
|
+
constructor(data: GraphData, callbackDictonary: NodeExecuteDictonary<ResultType> | NodeExecute<ResultType>);
|
|
61
|
+
getCallback(functionName: string): NodeExecute<ResultType>;
|
|
62
|
+
asString(): string;
|
|
63
|
+
run(): Promise<unknown>;
|
|
64
|
+
private runNode;
|
|
65
|
+
pushQueue(node: Node<ResultType>): void;
|
|
66
|
+
removeRunning(node: Node<ResultType>): void;
|
|
67
|
+
}
|
|
68
|
+
export {};
|
package/lib/graphai.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GraphAI = 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["TimedOut"] = 3] = "TimedOut";
|
|
10
|
+
NodeState[NodeState["Completed"] = 4] = "Completed";
|
|
11
|
+
})(NodeState || (exports.NodeState = NodeState = {}));
|
|
12
|
+
class Node {
|
|
13
|
+
constructor(nodeId, data, graph) {
|
|
14
|
+
this.nodeId = nodeId;
|
|
15
|
+
this.inputs = data.inputs ?? [];
|
|
16
|
+
this.pendings = new Set(this.inputs);
|
|
17
|
+
this.params = data.params;
|
|
18
|
+
this.waitlist = new Set();
|
|
19
|
+
this.state = NodeState.Waiting;
|
|
20
|
+
this.functionName = data.functionName ?? "default";
|
|
21
|
+
this.result = undefined;
|
|
22
|
+
this.retryLimit = data.retry ?? 0;
|
|
23
|
+
this.retryCount = 0;
|
|
24
|
+
this.timeout = data.timeout ?? 0;
|
|
25
|
+
this.graph = graph;
|
|
26
|
+
}
|
|
27
|
+
asString() {
|
|
28
|
+
return `${this.nodeId}: ${this.state} ${[...this.waitlist]}`;
|
|
29
|
+
}
|
|
30
|
+
retry(state, result) {
|
|
31
|
+
if (this.retryCount < this.retryLimit) {
|
|
32
|
+
this.retryCount++;
|
|
33
|
+
this.execute();
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
this.state = state;
|
|
37
|
+
this.result = result;
|
|
38
|
+
this.graph.removeRunning(this);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
removePending(nodeId) {
|
|
42
|
+
this.pendings.delete(nodeId);
|
|
43
|
+
this.pushQueueIfReady();
|
|
44
|
+
}
|
|
45
|
+
payload() {
|
|
46
|
+
return this.inputs.reduce((results, nodeId) => {
|
|
47
|
+
results[nodeId] = this.graph.nodes[nodeId].result;
|
|
48
|
+
return results;
|
|
49
|
+
}, {});
|
|
50
|
+
}
|
|
51
|
+
pushQueueIfReady() {
|
|
52
|
+
if (this.pendings.size === 0) {
|
|
53
|
+
this.graph.pushQueue(this);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async execute() {
|
|
57
|
+
this.state = NodeState.Executing;
|
|
58
|
+
const transactionId = Date.now();
|
|
59
|
+
this.transactionId = transactionId;
|
|
60
|
+
if (this.timeout > 0) {
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
if (this.state === NodeState.Executing && this.transactionId === transactionId) {
|
|
63
|
+
console.log("*** timeout", this.timeout);
|
|
64
|
+
this.retry(NodeState.TimedOut, undefined);
|
|
65
|
+
}
|
|
66
|
+
}, this.timeout);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const callback = this.graph.getCallback(this.functionName);
|
|
70
|
+
const result = await callback({
|
|
71
|
+
nodeId: this.nodeId,
|
|
72
|
+
retry: this.retryCount,
|
|
73
|
+
params: this.params,
|
|
74
|
+
payload: this.payload(),
|
|
75
|
+
});
|
|
76
|
+
if (this.transactionId !== transactionId) {
|
|
77
|
+
console.log("****** transactionId mismatch (success)");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.state = NodeState.Completed;
|
|
81
|
+
this.result = result;
|
|
82
|
+
this.waitlist.forEach((nodeId) => {
|
|
83
|
+
const node = this.graph.nodes[nodeId];
|
|
84
|
+
node.removePending(this.nodeId);
|
|
85
|
+
});
|
|
86
|
+
this.graph.removeRunning(this);
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
if (this.transactionId !== transactionId) {
|
|
90
|
+
console.log("****** transactionId mismatch (failed)");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.retry(NodeState.Failed, undefined);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
class GraphAI {
|
|
98
|
+
constructor(data, callbackDictonary) {
|
|
99
|
+
this.callbackDictonary = typeof callbackDictonary === "function" ? { default: callbackDictonary } : callbackDictonary;
|
|
100
|
+
if (this.callbackDictonary["default"] === undefined) {
|
|
101
|
+
throw new Error("No default function");
|
|
102
|
+
}
|
|
103
|
+
this.concurrency = data.concurrency ?? 2;
|
|
104
|
+
this.runningNodes = new Set();
|
|
105
|
+
this.nodeQueue = [];
|
|
106
|
+
this.onComplete = () => { };
|
|
107
|
+
this.nodes = Object.keys(data.nodes).reduce((nodes, nodeId) => {
|
|
108
|
+
nodes[nodeId] = new Node(nodeId, data.nodes[nodeId], this);
|
|
109
|
+
return nodes;
|
|
110
|
+
}, {});
|
|
111
|
+
// Generate the waitlist for each node
|
|
112
|
+
Object.keys(this.nodes).forEach((nodeId) => {
|
|
113
|
+
const node = this.nodes[nodeId];
|
|
114
|
+
node.pendings.forEach((pending) => {
|
|
115
|
+
const node2 = this.nodes[pending];
|
|
116
|
+
node2.waitlist.add(nodeId);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
getCallback(functionName) {
|
|
121
|
+
if (functionName && this.callbackDictonary[functionName]) {
|
|
122
|
+
return this.callbackDictonary[functionName];
|
|
123
|
+
}
|
|
124
|
+
return this.callbackDictonary["default"];
|
|
125
|
+
}
|
|
126
|
+
asString() {
|
|
127
|
+
return Object.keys(this.nodes)
|
|
128
|
+
.map((nodeId) => {
|
|
129
|
+
return this.nodes[nodeId].asString();
|
|
130
|
+
})
|
|
131
|
+
.join("\n");
|
|
132
|
+
}
|
|
133
|
+
async run() {
|
|
134
|
+
// Nodes without pending data should run immediately.
|
|
135
|
+
Object.keys(this.nodes).forEach((nodeId) => {
|
|
136
|
+
const node = this.nodes[nodeId];
|
|
137
|
+
node.pushQueueIfReady();
|
|
138
|
+
});
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
this.onComplete = () => {
|
|
141
|
+
const results = Object.keys(this.nodes).reduce((results, nodeId) => {
|
|
142
|
+
results[nodeId] = this.nodes[nodeId].result;
|
|
143
|
+
return results;
|
|
144
|
+
}, {});
|
|
145
|
+
resolve(results);
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
runNode(node) {
|
|
150
|
+
this.runningNodes.add(node.nodeId);
|
|
151
|
+
node.execute();
|
|
152
|
+
}
|
|
153
|
+
pushQueue(node) {
|
|
154
|
+
if (this.runningNodes.size < this.concurrency) {
|
|
155
|
+
this.runNode(node);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
this.nodeQueue.push(node);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
removeRunning(node) {
|
|
162
|
+
this.runningNodes.delete(node.nodeId);
|
|
163
|
+
if (this.nodeQueue.length > 0) {
|
|
164
|
+
const n = this.nodeQueue.shift();
|
|
165
|
+
if (n) {
|
|
166
|
+
this.runNode(n);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (this.runningNodes.size === 0) {
|
|
170
|
+
this.onComplete();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
exports.GraphAI = GraphAI;
|
package/lib/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { GraphAI } from "./
|
|
1
|
+
import { GraphAI } from "./graphai";
|
|
2
2
|
export { GraphAI };
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GraphAI = void 0;
|
|
4
|
-
const
|
|
5
|
-
Object.defineProperty(exports, "GraphAI", { enumerable: true, get: function () { return
|
|
4
|
+
const graphai_1 = require("./graphai");
|
|
5
|
+
Object.defineProperty(exports, "GraphAI", { enumerable: true, get: function () { return graphai_1.GraphAI; } });
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphai",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
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": "
|
|
10
|
+
"test": "node --test --require ts-node/register ./tests/test_*.ts"
|
|
11
11
|
},
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
"@typescript-eslint/parser": "^6.8.0",
|
|
26
26
|
"eslint": "^7.32.0 || ^8.2.0",
|
|
27
27
|
"eslint-plugin-import": "^2.25.2",
|
|
28
|
+
"openai": "^4.12.4",
|
|
28
29
|
"prettier": "^3.0.3",
|
|
30
|
+
"slashgpt": "^0.0.6",
|
|
29
31
|
"ts-node": "^10.9.1",
|
|
30
32
|
"typescript": "^5.2.2"
|
|
31
33
|
},
|
|
32
34
|
"dependencies": {
|
|
33
|
-
"openai": "^4.12.4",
|
|
34
35
|
"yaml": "^2.3.3"
|
|
35
36
|
},
|
|
36
37
|
"types": "./lib/index.d.ts",
|
package/src/graphai.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { AssertionError } from "assert";
|
|
2
|
+
|
|
3
|
+
export enum NodeState {
|
|
4
|
+
Waiting,
|
|
5
|
+
Executing,
|
|
6
|
+
Failed,
|
|
7
|
+
TimedOut,
|
|
8
|
+
Completed,
|
|
9
|
+
}
|
|
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 = Record<string, any>; // App-specific parameters
|
|
14
|
+
|
|
15
|
+
type NodeData = {
|
|
16
|
+
inputs: undefined | Array<string>;
|
|
17
|
+
params: NodeDataParams;
|
|
18
|
+
retry: undefined | number;
|
|
19
|
+
timeout: undefined | number; // msec
|
|
20
|
+
functionName: undefined | string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type GraphData = {
|
|
24
|
+
nodes: Record<string, NodeData>;
|
|
25
|
+
concurrency: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type NodeExecuteContext<ResultType> = {
|
|
29
|
+
nodeId: string;
|
|
30
|
+
retry: number;
|
|
31
|
+
params: NodeDataParams;
|
|
32
|
+
payload: ResultDataDictonary<ResultType>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type NodeExecute<ResultType> = (context: NodeExecuteContext<ResultType>) => Promise<ResultData<ResultType>>;
|
|
36
|
+
|
|
37
|
+
class Node<ResultType = Record<string, any>> {
|
|
38
|
+
public nodeId: string;
|
|
39
|
+
public params: NodeDataParams; // App-specific parameters
|
|
40
|
+
public inputs: Array<string>; // List of nodes this node needs data from.
|
|
41
|
+
public pendings: Set<string>; // List of nodes this node is waiting data from.
|
|
42
|
+
public waitlist: Set<string>; // List of nodes which need data from this node.
|
|
43
|
+
public state: NodeState;
|
|
44
|
+
public functionName: string;
|
|
45
|
+
public result: ResultData<ResultType>;
|
|
46
|
+
public retryLimit: number;
|
|
47
|
+
public retryCount: number;
|
|
48
|
+
public transactionId: undefined | number; // To reject callbacks from timed-out transactions
|
|
49
|
+
public timeout: number; // msec
|
|
50
|
+
|
|
51
|
+
private graph: GraphAI<ResultType>;
|
|
52
|
+
|
|
53
|
+
constructor(nodeId: string, data: NodeData, graph: GraphAI<ResultType>) {
|
|
54
|
+
this.nodeId = nodeId;
|
|
55
|
+
this.inputs = data.inputs ?? [];
|
|
56
|
+
this.pendings = new Set(this.inputs);
|
|
57
|
+
this.params = data.params;
|
|
58
|
+
this.waitlist = new Set<string>();
|
|
59
|
+
this.state = NodeState.Waiting;
|
|
60
|
+
this.functionName = data.functionName ?? "default";
|
|
61
|
+
this.result = undefined;
|
|
62
|
+
this.retryLimit = data.retry ?? 0;
|
|
63
|
+
this.retryCount = 0;
|
|
64
|
+
this.timeout = data.timeout ?? 0;
|
|
65
|
+
|
|
66
|
+
this.graph = graph;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public asString() {
|
|
70
|
+
return `${this.nodeId}: ${this.state} ${[...this.waitlist]}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private retry(state: NodeState, result: ResultData<ResultType>) {
|
|
74
|
+
if (this.retryCount < this.retryLimit) {
|
|
75
|
+
this.retryCount++;
|
|
76
|
+
this.execute();
|
|
77
|
+
} else {
|
|
78
|
+
this.state = state;
|
|
79
|
+
this.result = result;
|
|
80
|
+
this.graph.removeRunning(this);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public removePending(nodeId: string) {
|
|
85
|
+
this.pendings.delete(nodeId);
|
|
86
|
+
this.pushQueueIfReady();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public payload() {
|
|
90
|
+
return this.inputs.reduce((results: ResultDataDictonary<ResultType>, nodeId) => {
|
|
91
|
+
results[nodeId] = this.graph.nodes[nodeId].result;
|
|
92
|
+
return results;
|
|
93
|
+
}, {});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public pushQueueIfReady() {
|
|
97
|
+
if (this.pendings.size === 0) {
|
|
98
|
+
this.graph.pushQueue(this);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public async execute() {
|
|
103
|
+
this.state = NodeState.Executing;
|
|
104
|
+
const transactionId = Date.now();
|
|
105
|
+
this.transactionId = transactionId;
|
|
106
|
+
|
|
107
|
+
if (this.timeout > 0) {
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
if (this.state === NodeState.Executing && this.transactionId === transactionId) {
|
|
110
|
+
console.log("*** timeout", this.timeout);
|
|
111
|
+
this.retry(NodeState.TimedOut, undefined);
|
|
112
|
+
}
|
|
113
|
+
}, this.timeout);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const callback = this.graph.getCallback(this.functionName);
|
|
118
|
+
const result = await callback({
|
|
119
|
+
nodeId: this.nodeId,
|
|
120
|
+
retry: this.retryCount,
|
|
121
|
+
params: this.params,
|
|
122
|
+
payload: this.payload(),
|
|
123
|
+
});
|
|
124
|
+
if (this.transactionId !== transactionId) {
|
|
125
|
+
console.log("****** transactionId mismatch (success)");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.state = NodeState.Completed;
|
|
129
|
+
this.result = result;
|
|
130
|
+
this.waitlist.forEach((nodeId) => {
|
|
131
|
+
const node = this.graph.nodes[nodeId];
|
|
132
|
+
node.removePending(this.nodeId);
|
|
133
|
+
});
|
|
134
|
+
this.graph.removeRunning(this);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
if (this.transactionId !== transactionId) {
|
|
137
|
+
console.log("****** transactionId mismatch (failed)");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.retry(NodeState.Failed, undefined);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type GraphNodes<ResultType> = Record<string, Node<ResultType>>;
|
|
146
|
+
|
|
147
|
+
type NodeExecuteDictonary<ResultType> = Record<string, NodeExecute<ResultType>>;
|
|
148
|
+
|
|
149
|
+
export class GraphAI<ResultType = Record<string, any>> {
|
|
150
|
+
public nodes: GraphNodes<ResultType>;
|
|
151
|
+
public callbackDictonary: NodeExecuteDictonary<ResultType>;
|
|
152
|
+
private runningNodes: Set<string>;
|
|
153
|
+
private nodeQueue: Array<Node<ResultType>>;
|
|
154
|
+
private onComplete: () => void;
|
|
155
|
+
private concurrency: number;
|
|
156
|
+
|
|
157
|
+
constructor(data: GraphData, callbackDictonary: NodeExecuteDictonary<ResultType> | NodeExecute<ResultType>) {
|
|
158
|
+
this.callbackDictonary = typeof callbackDictonary === "function" ? { default: callbackDictonary } : callbackDictonary;
|
|
159
|
+
if (this.callbackDictonary["default"] === undefined) {
|
|
160
|
+
throw new Error("No default function");
|
|
161
|
+
}
|
|
162
|
+
this.concurrency = data.concurrency ?? 2;
|
|
163
|
+
this.runningNodes = new Set<string>();
|
|
164
|
+
this.nodeQueue = [];
|
|
165
|
+
this.onComplete = () => {};
|
|
166
|
+
this.nodes = Object.keys(data.nodes).reduce((nodes: GraphNodes<ResultType>, nodeId: string) => {
|
|
167
|
+
nodes[nodeId] = new Node<ResultType>(nodeId, data.nodes[nodeId], this);
|
|
168
|
+
return nodes;
|
|
169
|
+
}, {});
|
|
170
|
+
|
|
171
|
+
// Generate the waitlist for each node
|
|
172
|
+
Object.keys(this.nodes).forEach((nodeId) => {
|
|
173
|
+
const node = this.nodes[nodeId];
|
|
174
|
+
node.pendings.forEach((pending) => {
|
|
175
|
+
const node2 = this.nodes[pending];
|
|
176
|
+
node2.waitlist.add(nodeId);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public getCallback(functionName: string) {
|
|
182
|
+
if (functionName && this.callbackDictonary[functionName]) {
|
|
183
|
+
return this.callbackDictonary[functionName];
|
|
184
|
+
}
|
|
185
|
+
return this.callbackDictonary["default"];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public asString() {
|
|
189
|
+
return Object.keys(this.nodes)
|
|
190
|
+
.map((nodeId) => {
|
|
191
|
+
return this.nodes[nodeId].asString();
|
|
192
|
+
})
|
|
193
|
+
.join("\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public async run() {
|
|
197
|
+
// Nodes without pending data should run immediately.
|
|
198
|
+
Object.keys(this.nodes).forEach((nodeId) => {
|
|
199
|
+
const node = this.nodes[nodeId];
|
|
200
|
+
node.pushQueueIfReady();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
this.onComplete = () => {
|
|
205
|
+
const results = Object.keys(this.nodes).reduce((results: ResultDataDictonary<ResultType>, nodeId) => {
|
|
206
|
+
results[nodeId] = this.nodes[nodeId].result;
|
|
207
|
+
return results;
|
|
208
|
+
}, {});
|
|
209
|
+
resolve(results);
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private runNode(node: Node<ResultType>) {
|
|
215
|
+
this.runningNodes.add(node.nodeId);
|
|
216
|
+
node.execute();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public pushQueue(node: Node<ResultType>) {
|
|
220
|
+
if (this.runningNodes.size < this.concurrency) {
|
|
221
|
+
this.runNode(node);
|
|
222
|
+
} else {
|
|
223
|
+
this.nodeQueue.push(node);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
public removeRunning(node: Node<ResultType>) {
|
|
228
|
+
this.runningNodes.delete(node.nodeId);
|
|
229
|
+
if (this.nodeQueue.length > 0) {
|
|
230
|
+
const n = this.nodeQueue.shift();
|
|
231
|
+
if (n) {
|
|
232
|
+
this.runNode(n);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (this.runningNodes.size === 0) {
|
|
236
|
+
this.onComplete();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { GraphAI } from "./
|
|
1
|
+
import { GraphAI } from "./graphai";
|
|
2
2
|
|
|
3
|
-
export { GraphAI };
|
|
3
|
+
export { GraphAI };
|
package/tests/graphs/sample2.yml
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
nodes:
|
|
2
|
+
node1:
|
|
3
|
+
params:
|
|
4
|
+
prompt: Come up with ten business ideas for AI startup
|
|
5
|
+
node2:
|
|
6
|
+
inputs: [node1]
|
|
7
|
+
params:
|
|
8
|
+
prompt: Please evaluate following business ideas. ${node1}
|
|
9
|
+
node3:
|
|
10
|
+
inputs: [node1, node2]
|
|
11
|
+
params:
|
|
12
|
+
prompt: Please pick the winner of this business idea contest. ${node1} ${node2}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { GraphAI, NodeExecuteContext } from "../src/graphai";
|
|
3
|
+
import { ChatSession, ChatConfig } from "slashgpt";
|
|
4
|
+
import { readManifestData } from "./file_utils";
|
|
5
|
+
|
|
6
|
+
const config = new ChatConfig(path.resolve(__dirname));
|
|
7
|
+
|
|
8
|
+
const testFunction = async (context: NodeExecuteContext<Record<string, string>>) => {
|
|
9
|
+
console.log("executing", context.nodeId, context.params, context.payload);
|
|
10
|
+
const session = new ChatSession(config, context.params.manifest ?? {});
|
|
11
|
+
const prompt = Object.keys(context.payload).reduce((prompt, key) => {
|
|
12
|
+
return prompt.replace("${" + key + "}", context.payload[key]!["answer"]);
|
|
13
|
+
}, context.params.prompt);
|
|
14
|
+
session.append_user_question(prompt);
|
|
15
|
+
|
|
16
|
+
await session.call_loop(() => {});
|
|
17
|
+
const message = session.history.last_message();
|
|
18
|
+
if (message === undefined) {
|
|
19
|
+
throw new Error("No message in the history");
|
|
20
|
+
}
|
|
21
|
+
const result = { answer: message.content };
|
|
22
|
+
console.log(result);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const test = async (file: string) => {
|
|
27
|
+
const file_path = path.resolve(__dirname) + file;
|
|
28
|
+
const graph_data = readManifestData(file_path);
|
|
29
|
+
const graph = new GraphAI(graph_data, testFunction);
|
|
30
|
+
await graph.run();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const main = async () => {
|
|
34
|
+
await test("/graphs/sample3.yml");
|
|
35
|
+
console.log("COMPLETE 1");
|
|
36
|
+
};
|
|
37
|
+
main();
|
|
@@ -0,0 +1,47 @@
|
|
|
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 testFunction1 = async (context: NodeExecuteContext<Record<string, string>>) => {
|
|
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 = async (context: NodeExecuteContext<Record<string, string>>) => {
|
|
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 runTest = async (file: string) => {
|
|
28
|
+
const file_path = path.resolve(__dirname) + file;
|
|
29
|
+
const graph_data = readManifestData(file_path);
|
|
30
|
+
|
|
31
|
+
const graph = new GraphAI(graph_data, { default: testFunction1, test2: testFunction2 });
|
|
32
|
+
|
|
33
|
+
const results = await graph.run();
|
|
34
|
+
console.log(results);
|
|
35
|
+
return results;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
test("test sample1", async () => {
|
|
39
|
+
const result = await runTest("/graphs/test_multiple_functions_1.yml");
|
|
40
|
+
assert.deepStrictEqual(result, {
|
|
41
|
+
node1: { node1: "output 1" },
|
|
42
|
+
node2: { node2: "output 1" },
|
|
43
|
+
node3: { node3: "output 2" },
|
|
44
|
+
node4: { node4: "output 1" },
|
|
45
|
+
node5: { node5: "output 2" },
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { GraphAI, NodeExecuteContext } 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 = async (context: NodeExecuteContext<Record<string, string>>) => {
|
|
10
|
+
const { nodeId, retry, params, payload } = 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 = 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, result);
|
|
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
|
+
const results = await graph.run();
|
|
38
|
+
console.log(results);
|
|
39
|
+
return results;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
test("test sample1", async () => {
|
|
43
|
+
const result = await runTest("/graphs/sample1.yml");
|
|
44
|
+
assert.deepStrictEqual(result, {
|
|
45
|
+
node1: { node1: "output" },
|
|
46
|
+
node2: { node2: "output" },
|
|
47
|
+
node3: { node3: "output", node1: "output", node2: "output" },
|
|
48
|
+
node4: { node4: "output", node3: "output", node1: "output", node2: "output" },
|
|
49
|
+
node5: { node5: "output", node4: "output", node3: "output", node1: "output", node2: "output" },
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("test sample1", async () => {
|
|
54
|
+
const result = await runTest("/graphs/sample2.yml");
|
|
55
|
+
assert.deepStrictEqual(result, {
|
|
56
|
+
node1: { node1: "output" },
|
|
57
|
+
node2: { node2: "output" },
|
|
58
|
+
node3: { node3: "output", node1: "output", node2: "output" },
|
|
59
|
+
node4: { node4: "output", node3: "output", node1: "output", node2: "output" },
|
|
60
|
+
node5: { node5: "output", node4: "output", node3: "output", node1: "output", node2: "output" },
|
|
61
|
+
});
|
|
62
|
+
console.log("COMPLETE 2");
|
|
63
|
+
});
|
package/tests/utils.ts
ADDED
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/src/flow.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
export enum NodeState {
|
|
2
|
-
Waiting,
|
|
3
|
-
Executing,
|
|
4
|
-
Failed,
|
|
5
|
-
Completed
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
type NodeData = {
|
|
9
|
-
inputs: undefined | Array<string>;
|
|
10
|
-
params: any; // Application specific parameters
|
|
11
|
-
retry: undefined | number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type FlowData = {
|
|
15
|
-
nodes: Record<string, NodeData>;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export enum FlowCommand {
|
|
19
|
-
Log,
|
|
20
|
-
Execute,
|
|
21
|
-
OnComplete
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type FlowCallback = (params: Record<string, any>) => void;
|
|
25
|
-
|
|
26
|
-
class Node {
|
|
27
|
-
public key: string;
|
|
28
|
-
public inputs: Array<string>;
|
|
29
|
-
public pendings: Set<string>;
|
|
30
|
-
public params: any;
|
|
31
|
-
public waitlist: Set<string>;
|
|
32
|
-
public state: NodeState;
|
|
33
|
-
public result: Record<string, any>;
|
|
34
|
-
public retryLimit: number;
|
|
35
|
-
public retryCount: number;
|
|
36
|
-
constructor(key: string, data: NodeData) {
|
|
37
|
-
this.key = key;
|
|
38
|
-
this.inputs = data.inputs ?? [];
|
|
39
|
-
this.pendings = new Set(this.inputs);
|
|
40
|
-
this.params = data.params;
|
|
41
|
-
this.waitlist = new Set<string>();
|
|
42
|
-
this.state = NodeState.Waiting;
|
|
43
|
-
this.result = {};
|
|
44
|
-
this.retryLimit = data.retry ?? 0;
|
|
45
|
-
this.retryCount = 0;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
public asString() {
|
|
49
|
-
return `${this.key}: ${this.state} ${[...this.waitlist]}`
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
public complete(result: Record<string, any>, nodes: Record<string, Node>, graph: GraphAI) {
|
|
53
|
-
this.state = NodeState.Completed;
|
|
54
|
-
this.result = result;
|
|
55
|
-
this.waitlist.forEach(key => {
|
|
56
|
-
const node = nodes[key];
|
|
57
|
-
node.removePending(this.key, graph);
|
|
58
|
-
});
|
|
59
|
-
graph.remove(this);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
public reportError(result: Record<string, any>, nodes: Record<string, Node>, graph: GraphAI) {
|
|
63
|
-
this.state = NodeState.Failed;
|
|
64
|
-
this.result = result;
|
|
65
|
-
if (this.retryCount < this.retryLimit) {
|
|
66
|
-
this.retryCount++;
|
|
67
|
-
this.state = NodeState.Executing;
|
|
68
|
-
graph.callback({cmd: FlowCommand.Execute, node: this.key, params: this.params, retry: this.retryCount, payload: this.payload(graph) });
|
|
69
|
-
} else {
|
|
70
|
-
graph.remove(this);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
public removePending(key: string, graph: GraphAI) {
|
|
75
|
-
this.pendings.delete(key);
|
|
76
|
-
this.executeIfReady(graph);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
public payload(graph: GraphAI) {
|
|
80
|
-
const foo: Record<string, any> = {};
|
|
81
|
-
return this.inputs.reduce((payload, key) => {
|
|
82
|
-
payload[key] = graph.nodes[key].result;
|
|
83
|
-
return payload;
|
|
84
|
-
}, foo);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
public executeIfReady(graph: GraphAI) {
|
|
88
|
-
if (this.pendings.size == 0) {
|
|
89
|
-
this.state = NodeState.Executing;
|
|
90
|
-
graph.add(this);
|
|
91
|
-
graph.callback({cmd: FlowCommand.Execute, node: this.key, params: this.params, retry: 0, payload: this.payload(graph) });
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export class GraphAI {
|
|
97
|
-
public nodes: Record<string, Node>
|
|
98
|
-
public callback: FlowCallback;
|
|
99
|
-
private runningNodes: Set<string>;
|
|
100
|
-
|
|
101
|
-
constructor(data: FlowData, callback: FlowCallback) {
|
|
102
|
-
this.callback = callback;
|
|
103
|
-
this.runningNodes = new Set<string>();
|
|
104
|
-
const foo: Record<string, Node> = {}; // HACK: Work around
|
|
105
|
-
this.nodes = Object.keys(data.nodes).reduce((nodes, key) => {
|
|
106
|
-
nodes[key] = new Node(key, data.nodes[key]);
|
|
107
|
-
return nodes;
|
|
108
|
-
}, foo);
|
|
109
|
-
|
|
110
|
-
// Generate the waitlist for each node
|
|
111
|
-
Object.keys(this.nodes).forEach(key => {
|
|
112
|
-
const node = this.nodes[key];
|
|
113
|
-
node.pendings.forEach(pending => {
|
|
114
|
-
const node2 = this.nodes[pending]
|
|
115
|
-
node2.waitlist.add(key);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
public asString() {
|
|
121
|
-
return Object.keys(this.nodes).map((key) => { return this.nodes[key].asString() }).join('\n');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
public run() {
|
|
125
|
-
// Nodes without pending data should run immediately.
|
|
126
|
-
Object.keys(this.nodes).forEach(key => {
|
|
127
|
-
const node = this.nodes[key];
|
|
128
|
-
node.executeIfReady(this);
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
public feed(key: string, result: Record<string, any>) {
|
|
133
|
-
const node = this.nodes[key];
|
|
134
|
-
node.complete(result, this.nodes, this);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
public reportError(key: string, result: Record<string, any>) {
|
|
138
|
-
const node = this.nodes[key];
|
|
139
|
-
node.reportError(result, this.nodes, this);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
public add(node: Node) {
|
|
143
|
-
this.runningNodes.add(node.key);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
public remove(node: Node) {
|
|
147
|
-
this.runningNodes.delete(node.key);
|
|
148
|
-
if (this.runningNodes.size == 0) {
|
|
149
|
-
this.callback({cmd: FlowCommand.OnComplete});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
package/tests/sample_flow.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import { GraphAI, FlowCommand } from "../src/flow";
|
|
3
|
-
|
|
4
|
-
import { readManifestData } from "../src/file_utils";
|
|
5
|
-
|
|
6
|
-
const test = async (file: string) => {
|
|
7
|
-
const file_path = path.resolve(__dirname) + file;
|
|
8
|
-
const graph_data = readManifestData(file_path);
|
|
9
|
-
return new Promise((resolve, reject) => {
|
|
10
|
-
const graph = new GraphAI(graph_data, async (params) => {
|
|
11
|
-
if (params.cmd == FlowCommand.Execute) {
|
|
12
|
-
const node = params.node;
|
|
13
|
-
console.log("executing", node, params.params, params.payload)
|
|
14
|
-
setTimeout(() => {
|
|
15
|
-
if (params.params.fail && params.retry < 2) {
|
|
16
|
-
const result = { [node]:"failed" };
|
|
17
|
-
console.log("failed", node, result, params.retry)
|
|
18
|
-
graph.reportError(node, result);
|
|
19
|
-
} else {
|
|
20
|
-
const result = { [node]:"output" };
|
|
21
|
-
console.log("completing", node, result)
|
|
22
|
-
graph.feed(node, result)
|
|
23
|
-
}
|
|
24
|
-
}, params.params.delay);
|
|
25
|
-
} else if (params.cmd == FlowCommand.OnComplete) {
|
|
26
|
-
resolve(graph);
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
graph.run();
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const main = async () => {
|
|
34
|
-
await test("/graphs/sample1.yml");
|
|
35
|
-
console.log("COMPLETE 1");
|
|
36
|
-
await test("/graphs/sample2.yml");
|
|
37
|
-
console.log("COMPLETE 2");
|
|
38
|
-
};
|
|
39
|
-
main();
|
|
File without changes
|