graphai 0.0.7 → 0.0.9
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 +18 -8
- package/lib/graphai.d.ts +14 -11
- package/lib/graphai.js +75 -36
- package/lib/graphai_cli.d.ts +2 -0
- package/lib/graphai_cli.js +36 -0
- package/package.json +4 -2
- package/samples/agents/arxiv_agent.ts +1 -2
- package/samples/agents/parroting_agent.ts +5 -0
- package/samples/agents/slashgpt_agent.ts +35 -6
- package/samples/graphs/arxiv.yml +3 -7
- package/samples/graphs/slash_gpt.yml +9 -2
- package/samples/interaction.ts +49 -0
- package/samples/sample_co2.ts +85 -0
- package/samples/sample_gpt.ts +11 -8
- package/samples/sample_paper_ai.ts +1 -5
- package/src/graphai.ts +86 -44
- package/src/graphai_cli.ts +35 -0
- package/tests/agents/agents.ts +4 -5
- package/tests/graphai/test_dispatch.ts +6 -7
- package/tests/graphai/test_fork.ts +106 -0
- package/tests/graphai/test_http_client.ts +3 -3
- package/tests/graphai/test_multiple_functions.ts +2 -2
- package/tests/graphai/test_sample_flow.ts +4 -5
- package/tests/graphs/test_cli.yaml +4 -0
- package/tests/graphs/test_dispatch.yml +6 -1
- package/tests/graphs/test_multiple_functions_1.yml +3 -0
- package/tests/graphs/test_source2.yml +2 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
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.
|
|
6
6
|
|
|
7
|
-
You just need to describe dependencies among those API calls in a single data flow graph
|
|
7
|
+
You just need to describe dependencies among those API calls in a single data flow graph, create a GraphAI object with that graph, and run it.
|
|
8
8
|
|
|
9
9
|
Here is an example:
|
|
10
10
|
|
|
@@ -27,7 +27,7 @@ const sampleAgentFunction = async (context: AgentFunctionContext) => {
|
|
|
27
27
|
const {
|
|
28
28
|
nodeId, // taskA, taskB or taskC
|
|
29
29
|
params, // agent-specific parameters specified in the graph definition file
|
|
30
|
-
|
|
30
|
+
inputs // inputs from previous nodes
|
|
31
31
|
} = context;
|
|
32
32
|
// Agent-specific code (such as calling OpenAI's chat.completions API)
|
|
33
33
|
...
|
|
@@ -42,11 +42,21 @@ const sampleAgentFunction = async (context: AgentFunctionContext) => {
|
|
|
42
42
|
return results["taskC"];
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
## Background
|
|
46
|
+
|
|
47
|
+
As Andrew Ng has described in his article, "[The batch: Issue 242](https://www.deeplearning.ai/the-batch/issue-242/)", better results can often be achieved by making multiple calls to a Large Language Model (LLM) and allowing it to incrementally build towards a higher-quality output. Dr. Ng refers to this approach as 'agentic workflows.'
|
|
48
|
+
|
|
49
|
+
Building applications that employ these workflows, however, is challenging due to the complexities of managing multiple asynchronous API calls, including error handling, timeouts, retries, and logging.
|
|
50
|
+
|
|
51
|
+
GraphAI is designed to simplify this process by decoupling the complexity of multiple asynchronous calls from the application's core logic. It enables developers to model these calls and their dependencies within a single Data Flow Graph, enhancing development and debugging processes.
|
|
52
|
+
|
|
53
|
+
Furthermore, GraphAI's robust mechanisms for error handling, retry strategies, timeouts, and logging empower developers to concentrate on refining the application logic.
|
|
54
|
+
|
|
45
55
|
## Data Flow Graph
|
|
46
56
|
|
|
47
57
|
A Data Flow Graph (DFG) is a JavaScript object, which defines the flow of data. It is typically described in YAML file and loaded at runtime.
|
|
48
58
|
|
|
49
|
-
A DFG consists of a collection of 'nodes', which contains a series of nested keys representing individual nodes in the data flow. Each node is identified by a unique key (e.g., node1, node2) and can contain several predefined keys (params, inputs, retry, timeout, source,
|
|
59
|
+
A DFG consists of a collection of 'nodes', which contains a series of nested keys representing individual nodes in the data flow. Each node is identified by a unique key (e.g., node1, node2) and can contain several predefined keys (params, inputs, outputs, retry, timeout, source, agentId, result, fork) that dictate the node's behavior and its relationship with other nodes.
|
|
50
60
|
|
|
51
61
|
### DFG Structure
|
|
52
62
|
|
|
@@ -61,19 +71,19 @@ An agent is an abstract object, which takes some inputs and generates an output
|
|
|
61
71
|
|
|
62
72
|
An agent function is a TypeScript function, which implements an agent. A DFG is associated one or more agent functions. If the DFG is associated with multiple agent functions, each node needs to be associated only one of them (either explicitly with 'agentId' or implicitly to the default Agent function).
|
|
63
73
|
|
|
64
|
-
An agent function receives two set of parameters via AgentFunctionContext, agent specific parameters specified in the DFG and input data came from other nodes
|
|
65
|
-
|
|
66
|
-
A regular agent function returns the data (type: ```Record<string, any>```), but a dispatcher agent function returns the date with outputID(s) (type: ```Record<string: Record<string, any>>```).
|
|
74
|
+
An agent function receives two set of parameters via AgentFunctionContext, agent specific parameters specified in the DFG and input data came from other nodes.
|
|
67
75
|
|
|
68
76
|
## Node Structure
|
|
69
77
|
|
|
70
78
|
- 'inputs': An optional list of node identifiers that the current node depends on. This establishes a flow where the current node can only be executed after the completion of the nodes listed under 'inputs'. If this list is empty and the 'source' property is not set, the associated Agent Function will be immediatley executed.
|
|
71
79
|
- 'source': An optional flag, which specifies if the node is a 'source node' or not. A souce node is a special node, which receives data from either an external code (via GraphAI's injectResult method) or from a 'dispatcher node'.
|
|
72
|
-
- '
|
|
80
|
+
- 'result': An optional object, which injects the result into the source node (equivalent to calling the injectResult method).
|
|
73
81
|
- 'retry': An optional number, which specifies the maximum number of retries to be made. If the last attempt fails, that return value will be recorded.
|
|
74
82
|
- 'timeout': An optional number, which specifies the maximum waittime in msec. If the associated agent function does not return the value in time, the "Timeout" error will be recorded and the returned value will be discarded.
|
|
75
83
|
- 'params': An optional parameters to the associated agent function, which are agent specific.
|
|
76
|
-
- '
|
|
84
|
+
- 'agentId': An optional parameter, which specifies the id of the agent, when the graph is associated with multiple agents.
|
|
85
|
+
- 'fork': An optional paramter, which specifies the number of concurrent transactions to be created for the current node.
|
|
86
|
+
- 'outputs': An optinal property, which specifies the mapping from outputId to nodeId. If this property is set, the node become a special node called "dispatcher". A dispatcher node injects result(s) into specified nodes, enabling the dynamic flow of data.
|
|
77
87
|
|
|
78
88
|
## GraphAI class
|
|
79
89
|
|
package/lib/graphai.d.ts
CHANGED
|
@@ -12,13 +12,14 @@ type ResultDataDictonary<ResultType = Record<string, any>> = Record<string, Resu
|
|
|
12
12
|
export type NodeDataParams<ParamsType = Record<string, any>> = ParamsType;
|
|
13
13
|
type NodeData = {
|
|
14
14
|
inputs?: Array<string>;
|
|
15
|
-
params
|
|
16
|
-
payloadMapping?: Record<string, string>;
|
|
15
|
+
params?: NodeDataParams;
|
|
17
16
|
retry?: number;
|
|
18
17
|
timeout?: number;
|
|
19
18
|
agentId?: string;
|
|
19
|
+
fork?: number;
|
|
20
20
|
source?: boolean;
|
|
21
|
-
|
|
21
|
+
result?: ResultData;
|
|
22
|
+
outputs?: Record<string, string>;
|
|
22
23
|
};
|
|
23
24
|
export type GraphData = {
|
|
24
25
|
nodes: Record<string, NodeData>;
|
|
@@ -32,15 +33,16 @@ export type TransactionLog = {
|
|
|
32
33
|
retryCount: number;
|
|
33
34
|
agentId?: string;
|
|
34
35
|
params?: NodeDataParams;
|
|
35
|
-
|
|
36
|
+
inputs?: Array<ResultData>;
|
|
36
37
|
errorMessage?: string;
|
|
37
38
|
result?: ResultData;
|
|
38
39
|
};
|
|
39
40
|
export type AgentFunctionContext<ParamsType, ResultType, PreviousResultType> = {
|
|
40
41
|
nodeId: string;
|
|
42
|
+
forkIndex?: number;
|
|
41
43
|
retry: number;
|
|
42
44
|
params: NodeDataParams<ParamsType>;
|
|
43
|
-
|
|
45
|
+
inputs: Array<PreviousResultType>;
|
|
44
46
|
};
|
|
45
47
|
export type AgentFunction<ParamsType = Record<string, any>, ResultType = Record<string, any>, PreviousResultType = Record<string, any>> = (context: AgentFunctionContext<ParamsType, ResultType, PreviousResultType>) => Promise<ResultData<ResultType>>;
|
|
46
48
|
export type AgentFunctionDictonary = Record<string, AgentFunction<any, any, any>>;
|
|
@@ -48,11 +50,12 @@ declare class Node {
|
|
|
48
50
|
nodeId: string;
|
|
49
51
|
params: NodeDataParams;
|
|
50
52
|
inputs: Array<string>;
|
|
51
|
-
payloadMapping: Record<string, string>;
|
|
52
53
|
pendings: Set<string>;
|
|
53
54
|
waitlist: Set<string>;
|
|
54
55
|
state: NodeState;
|
|
55
|
-
agentId
|
|
56
|
+
agentId?: string;
|
|
57
|
+
fork?: number;
|
|
58
|
+
forkIndex?: number;
|
|
56
59
|
result: ResultData;
|
|
57
60
|
retryLimit: number;
|
|
58
61
|
retryCount: number;
|
|
@@ -60,13 +63,12 @@ declare class Node {
|
|
|
60
63
|
timeout?: number;
|
|
61
64
|
error?: Error;
|
|
62
65
|
source: boolean;
|
|
63
|
-
|
|
66
|
+
outputs?: Record<string, string>;
|
|
64
67
|
private graph;
|
|
65
|
-
constructor(nodeId: string, data: NodeData, graph: GraphAI);
|
|
68
|
+
constructor(nodeId: string, forkIndex: number | undefined, data: NodeData, graph: GraphAI);
|
|
66
69
|
asString(): string;
|
|
67
70
|
private retry;
|
|
68
71
|
removePending(nodeId: string): void;
|
|
69
|
-
payload(): ResultDataDictonary<Record<string, any>>;
|
|
70
72
|
pushQueueIfReady(): void;
|
|
71
73
|
injectResult(result: ResultData): void;
|
|
72
74
|
private setResult;
|
|
@@ -83,7 +85,7 @@ export declare class GraphAI {
|
|
|
83
85
|
private concurrency;
|
|
84
86
|
private logs;
|
|
85
87
|
constructor(data: GraphData, callbackDictonary: AgentFunctionDictonary | AgentFunction<any, any, any>);
|
|
86
|
-
getCallback(
|
|
88
|
+
getCallback(_agentId?: string): AgentFunction<any, any, any>;
|
|
87
89
|
asString(): string;
|
|
88
90
|
results(): ResultDataDictonary<Record<string, any>>;
|
|
89
91
|
errors(): Record<string, Error>;
|
|
@@ -94,5 +96,6 @@ export declare class GraphAI {
|
|
|
94
96
|
appendLog(log: TransactionLog): void;
|
|
95
97
|
transactionLogs(): TransactionLog[];
|
|
96
98
|
injectResult(nodeId: string, result: ResultData): void;
|
|
99
|
+
resultsOf(nodeIds: Array<string>): ResultData<Record<string, any>>[];
|
|
97
100
|
}
|
|
98
101
|
export {};
|
package/lib/graphai.js
CHANGED
|
@@ -12,21 +12,22 @@ var NodeState;
|
|
|
12
12
|
NodeState["Dispatched"] = "dispatched";
|
|
13
13
|
})(NodeState || (exports.NodeState = NodeState = {}));
|
|
14
14
|
class Node {
|
|
15
|
-
constructor(nodeId, data, graph) {
|
|
15
|
+
constructor(nodeId, forkIndex, data, graph) {
|
|
16
16
|
this.waitlist = new Set(); // List of nodes which need data from this node.
|
|
17
17
|
this.state = NodeState.Waiting;
|
|
18
18
|
this.result = undefined;
|
|
19
19
|
this.retryCount = 0;
|
|
20
20
|
this.nodeId = nodeId;
|
|
21
|
+
this.forkIndex = forkIndex;
|
|
21
22
|
this.inputs = data.inputs ?? [];
|
|
22
|
-
this.payloadMapping = data.payloadMapping ?? {};
|
|
23
23
|
this.pendings = new Set(this.inputs);
|
|
24
|
-
this.params = data.params;
|
|
25
|
-
this.agentId = data.agentId
|
|
24
|
+
this.params = data.params ?? {};
|
|
25
|
+
this.agentId = data.agentId;
|
|
26
|
+
this.fork = data.fork;
|
|
26
27
|
this.retryLimit = data.retry ?? 0;
|
|
27
28
|
this.timeout = data.timeout;
|
|
28
29
|
this.source = data.source === true;
|
|
29
|
-
this.
|
|
30
|
+
this.outputs = data.outputs;
|
|
30
31
|
this.graph = graph;
|
|
31
32
|
}
|
|
32
33
|
asString() {
|
|
@@ -51,17 +52,6 @@ class Node {
|
|
|
51
52
|
this.pushQueueIfReady();
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
|
-
payload() {
|
|
55
|
-
return this.inputs.reduce((results, nodeId) => {
|
|
56
|
-
if (this.payloadMapping && this.payloadMapping[nodeId]) {
|
|
57
|
-
results[this.payloadMapping[nodeId]] = this.graph.nodes[nodeId].result;
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
results[nodeId] = this.graph.nodes[nodeId].result;
|
|
61
|
-
}
|
|
62
|
-
return results;
|
|
63
|
-
}, {});
|
|
64
|
-
}
|
|
65
55
|
pushQueueIfReady() {
|
|
66
56
|
if (this.pendings.size === 0 && !this.source) {
|
|
67
57
|
this.graph.pushQueue(this);
|
|
@@ -74,8 +64,9 @@ class Node {
|
|
|
74
64
|
retryCount: this.retryCount,
|
|
75
65
|
state: NodeState.Injected,
|
|
76
66
|
startTime: Date.now(),
|
|
67
|
+
endTime: Date.now(),
|
|
68
|
+
result,
|
|
77
69
|
};
|
|
78
|
-
log.endTime = log.startTime;
|
|
79
70
|
this.graph.appendLog(log);
|
|
80
71
|
this.setResult(result, NodeState.Injected);
|
|
81
72
|
}
|
|
@@ -93,19 +84,19 @@ class Node {
|
|
|
93
84
|
});
|
|
94
85
|
}
|
|
95
86
|
async execute() {
|
|
96
|
-
const
|
|
87
|
+
const results = this.graph.resultsOf(this.inputs);
|
|
88
|
+
const transactionId = Date.now();
|
|
97
89
|
const log = {
|
|
98
90
|
nodeId: this.nodeId,
|
|
99
91
|
retryCount: this.retryCount,
|
|
100
92
|
state: NodeState.Executing,
|
|
101
|
-
startTime:
|
|
93
|
+
startTime: transactionId,
|
|
102
94
|
agentId: this.agentId,
|
|
103
95
|
params: this.params,
|
|
104
|
-
|
|
96
|
+
inputs: results,
|
|
105
97
|
};
|
|
106
98
|
this.graph.appendLog(log);
|
|
107
99
|
this.state = NodeState.Executing;
|
|
108
|
-
const transactionId = log.startTime;
|
|
109
100
|
this.transactionId = transactionId;
|
|
110
101
|
if (this.timeout && this.timeout > 0) {
|
|
111
102
|
setTimeout(() => {
|
|
@@ -124,7 +115,8 @@ class Node {
|
|
|
124
115
|
nodeId: this.nodeId,
|
|
125
116
|
retry: this.retryCount,
|
|
126
117
|
params: this.params,
|
|
127
|
-
|
|
118
|
+
inputs: results,
|
|
119
|
+
forkIndex: this.forkIndex,
|
|
128
120
|
});
|
|
129
121
|
if (this.transactionId !== transactionId) {
|
|
130
122
|
console.log(`-- ${this.nodeId}: transactionId mismatch`);
|
|
@@ -132,10 +124,10 @@ class Node {
|
|
|
132
124
|
}
|
|
133
125
|
log.endTime = Date.now();
|
|
134
126
|
log.result = result;
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
127
|
+
const outputs = this.outputs;
|
|
128
|
+
if (outputs !== undefined) {
|
|
137
129
|
Object.keys(result).forEach((outputId) => {
|
|
138
|
-
const nodeId =
|
|
130
|
+
const nodeId = outputs[outputId];
|
|
139
131
|
this.graph.injectResult(nodeId, result[outputId]);
|
|
140
132
|
});
|
|
141
133
|
log.state = NodeState.Dispatched;
|
|
@@ -173,32 +165,74 @@ class GraphAI {
|
|
|
173
165
|
this.runningNodes = new Set();
|
|
174
166
|
this.nodeQueue = [];
|
|
175
167
|
this.logs = [];
|
|
176
|
-
this.callbackDictonary = typeof callbackDictonary === "function" ? {
|
|
177
|
-
if (this.callbackDictonary["default"] === undefined) {
|
|
178
|
-
throw new Error("No default function");
|
|
179
|
-
}
|
|
168
|
+
this.callbackDictonary = typeof callbackDictonary === "function" ? { _default: callbackDictonary } : callbackDictonary;
|
|
180
169
|
this.concurrency = data.concurrency ?? defaultConcurrency;
|
|
181
170
|
this.onComplete = () => {
|
|
182
171
|
console.error("-- SOMETHING IS WRONG: onComplete is called without run()");
|
|
183
172
|
};
|
|
173
|
+
const nodeId2forkedNodeIds = {};
|
|
174
|
+
const forkedNodeId2Index = {};
|
|
175
|
+
// Create node instances from data.nodes
|
|
184
176
|
this.nodes = Object.keys(data.nodes).reduce((nodes, nodeId) => {
|
|
185
|
-
|
|
177
|
+
const fork = data.nodes[nodeId].fork;
|
|
178
|
+
if (fork) {
|
|
179
|
+
// For fork, change the nodeId and increase the node
|
|
180
|
+
nodeId2forkedNodeIds[nodeId] = new Array(fork).fill(undefined).map((_, i) => {
|
|
181
|
+
const forkedNodeId = `${nodeId}_${i}`;
|
|
182
|
+
nodes[forkedNodeId] = new Node(forkedNodeId, i, data.nodes[nodeId], this);
|
|
183
|
+
// Data for pending and waiting
|
|
184
|
+
forkedNodeId2Index[forkedNodeId] = i;
|
|
185
|
+
return forkedNodeId;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
nodes[nodeId] = new Node(nodeId, undefined, data.nodes[nodeId], this);
|
|
190
|
+
}
|
|
186
191
|
return nodes;
|
|
187
192
|
}, {});
|
|
188
|
-
// Generate the waitlist for each node
|
|
193
|
+
// Generate the waitlist for each node, and update the pendings in case of forked node.
|
|
189
194
|
Object.keys(this.nodes).forEach((nodeId) => {
|
|
190
195
|
const node = this.nodes[nodeId];
|
|
191
196
|
node.pendings.forEach((pending) => {
|
|
192
|
-
|
|
193
|
-
|
|
197
|
+
// If the pending(previous) node is forking
|
|
198
|
+
if (nodeId2forkedNodeIds[pending]) {
|
|
199
|
+
// update node.pending and pending(previous) node.wailtlist
|
|
200
|
+
if (node.fork) {
|
|
201
|
+
// 1:1 if current nodes are also forking.
|
|
202
|
+
const newPendingId = nodeId2forkedNodeIds[pending][forkedNodeId2Index[nodeId]];
|
|
203
|
+
this.nodes[newPendingId].waitlist.add(nodeId); // previousNode
|
|
204
|
+
node.pendings.add(newPendingId);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
// 1:n if current node is not forking.
|
|
208
|
+
nodeId2forkedNodeIds[pending].forEach((newPendingId) => {
|
|
209
|
+
this.nodes[newPendingId].waitlist.add(nodeId); // previousNode
|
|
210
|
+
node.pendings.add(newPendingId);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
node.pendings.delete(pending);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
this.nodes[pending].waitlist.add(nodeId); // previousNode
|
|
217
|
+
}
|
|
194
218
|
});
|
|
219
|
+
node.inputs = Array.from(node.pendings); // for fork.
|
|
220
|
+
});
|
|
221
|
+
// If the result property is specified, inject it.
|
|
222
|
+
// NOTE: This must be done at the end of this constructor
|
|
223
|
+
Object.keys(data.nodes).forEach((nodeId) => {
|
|
224
|
+
const result = data.nodes[nodeId].result;
|
|
225
|
+
if (result) {
|
|
226
|
+
this.injectResult(nodeId, result);
|
|
227
|
+
}
|
|
195
228
|
});
|
|
196
229
|
}
|
|
197
|
-
getCallback(
|
|
198
|
-
|
|
230
|
+
getCallback(_agentId) {
|
|
231
|
+
const agentId = _agentId ?? "_default";
|
|
232
|
+
if (this.callbackDictonary[agentId]) {
|
|
199
233
|
return this.callbackDictonary[agentId];
|
|
200
234
|
}
|
|
201
|
-
|
|
235
|
+
throw new Error("No agent: " + agentId);
|
|
202
236
|
}
|
|
203
237
|
asString() {
|
|
204
238
|
return Object.keys(this.nodes)
|
|
@@ -288,5 +322,10 @@ class GraphAI {
|
|
|
288
322
|
console.error("-- Invalid nodeId", nodeId);
|
|
289
323
|
}
|
|
290
324
|
}
|
|
325
|
+
resultsOf(nodeIds) {
|
|
326
|
+
return nodeIds.map((nodeId) => {
|
|
327
|
+
return this.nodes[nodeId].result;
|
|
328
|
+
});
|
|
329
|
+
}
|
|
291
330
|
}
|
|
292
331
|
exports.GraphAI = GraphAI;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const graphai_1 = require("./graphai");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
11
|
+
const testAgent = async (context) => {
|
|
12
|
+
return {};
|
|
13
|
+
};
|
|
14
|
+
const main = async () => {
|
|
15
|
+
const file = process.argv[2];
|
|
16
|
+
if (file === undefined) {
|
|
17
|
+
console.log("no file");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const file_path = path_1.default.resolve(process.cwd() + "/" + file);
|
|
21
|
+
if (!fs_1.default.existsSync(file_path)) {
|
|
22
|
+
console.log("no file");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const graph_data_file = fs_1.default.readFileSync(file_path, "utf8");
|
|
27
|
+
const graph_data = yaml_1.default.parse(graph_data_file);
|
|
28
|
+
const graph = new graphai_1.GraphAI(graph_data, { test: testAgent });
|
|
29
|
+
const results = await graph.run();
|
|
30
|
+
console.log(results);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
console.log("error", e);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphai",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"description": "Asynchronous data flow execution engine to make it simple to build LLM apps.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
|
+
"bin": "lib/graphai_cli.js",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"build": "tsc && tsc-alias",
|
|
8
9
|
"eslint": "eslint --fix --ext src/**/*.{ts} tests/**/*.ts",
|
|
9
10
|
"format": "prettier --write '{src,tests,samples}/**/*.ts' .eslintrc.js",
|
|
10
11
|
"test": "node --test -r tsconfig-paths/register --require ts-node/register ./tests/**/test_*.ts",
|
|
11
|
-
"
|
|
12
|
+
"cli": "npx ts-node -r tsconfig-paths/register ./src/graphai_cli.ts",
|
|
13
|
+
"sample": "npx ts-node -r tsconfig-paths/register"
|
|
12
14
|
},
|
|
13
15
|
"repository": {
|
|
14
16
|
"type": "git",
|
|
@@ -35,12 +35,11 @@ export const arxivAgent: AgentFunction<{ keywords: string[]; limit: number }, ar
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
export const arxiv2TextAgent: AgentFunction<{}, string, string[]> = async (context) => {
|
|
38
|
-
const result = (context
|
|
38
|
+
const result = (context.inputs[0] || [])
|
|
39
39
|
.map((r: any) => {
|
|
40
40
|
const { id, title, summary } = r;
|
|
41
41
|
return ["id:", id, "title:", title, "summary:", summary].join("\n");
|
|
42
42
|
})
|
|
43
43
|
.join("\n\n\n");
|
|
44
|
-
|
|
45
44
|
return result;
|
|
46
45
|
};
|
|
@@ -4,18 +4,47 @@ import { ChatSession, ChatConfig, ManifestData } from "slashgpt";
|
|
|
4
4
|
|
|
5
5
|
const config = new ChatConfig(path.resolve(__dirname));
|
|
6
6
|
|
|
7
|
-
export const
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
export const slashGPTFuncitons2TextAgent: AgentFunction<
|
|
8
|
+
{ function_data_key: string; result_key: number },
|
|
9
|
+
string,
|
|
10
|
+
{ function_data: { [key: string]: string[] } }
|
|
11
|
+
> = async (context) => {
|
|
12
|
+
const { params } = context;
|
|
13
|
+
const result = (context?.inputs[0].function_data[params.function_data_key] || []).map((r: any) => {
|
|
14
|
+
const { title, description } = r;
|
|
15
|
+
return ["title:", title, "description:", description].join("\n");
|
|
16
|
+
});
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
return result[context.forkIndex ?? 0];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const slashGPTAgent: AgentFunction<
|
|
22
|
+
{ manifest: ManifestData; prompt: string; function_result?: boolean; debug?: boolean },
|
|
23
|
+
{ answer: string },
|
|
24
|
+
string
|
|
25
|
+
> = async (context) => {
|
|
26
|
+
const { params } = context;
|
|
27
|
+
if (params.debug) {
|
|
28
|
+
console.log("executing", context.nodeId, context);
|
|
29
|
+
}
|
|
30
|
+
const session = new ChatSession(config, params?.manifest ?? {});
|
|
31
|
+
|
|
32
|
+
const prompt = params?.prompt ?? [context.inputs].filter((a) => a !== undefined).join("\n\n");
|
|
33
|
+
// console.log(prompt);
|
|
12
34
|
session.append_user_question(prompt);
|
|
13
35
|
|
|
14
36
|
await session.call_loop(() => {});
|
|
15
|
-
|
|
37
|
+
|
|
38
|
+
// console.log(session.history)
|
|
39
|
+
const message = (() => {
|
|
40
|
+
if (params.function_result) {
|
|
41
|
+
return session.history.messages().find((m) => m.role === "function_result");
|
|
42
|
+
}
|
|
43
|
+
return session.history.last_message();
|
|
44
|
+
})();
|
|
16
45
|
if (message === undefined) {
|
|
17
46
|
throw new Error("No message in the history");
|
|
18
47
|
}
|
|
19
|
-
const result = { answer: message.content };
|
|
48
|
+
const result = { answer: message.content, function_data: message.function_data };
|
|
20
49
|
return result;
|
|
21
50
|
};
|
package/samples/graphs/arxiv.yml
CHANGED
|
@@ -5,17 +5,13 @@ nodes:
|
|
|
5
5
|
- llm
|
|
6
6
|
- gpt
|
|
7
7
|
limit: 10
|
|
8
|
-
|
|
8
|
+
agentId: arxivAgent
|
|
9
9
|
arxiv2TextAgent:
|
|
10
10
|
inputs: [searchArxiv]
|
|
11
|
-
|
|
12
|
-
payloadMapping:
|
|
13
|
-
searchArxiv: inputData
|
|
11
|
+
agentId: arxiv2TextAgent
|
|
14
12
|
slashGPTAgent:
|
|
15
13
|
inputs: [arxiv2TextAgent]
|
|
16
|
-
|
|
17
|
-
arxiv2TextAgent: inputData
|
|
18
|
-
functionName: slashGPTAgent
|
|
14
|
+
agentId: slashGPTAgent
|
|
19
15
|
params:
|
|
20
16
|
prompt: |
|
|
21
17
|
与えられたそれぞれの論文の要点をまとめ、以下の項目で日本語で出力せよ。それぞれの項目は最大でも180文字以内に要約せよ。
|
|
@@ -5,8 +5,15 @@ nodes:
|
|
|
5
5
|
node2:
|
|
6
6
|
inputs: [node1]
|
|
7
7
|
params:
|
|
8
|
-
prompt:
|
|
8
|
+
prompt: |
|
|
9
|
+
Please evaluate following business ideas.
|
|
10
|
+
${0}
|
|
9
11
|
node3:
|
|
10
12
|
inputs: [node1, node2]
|
|
11
13
|
params:
|
|
12
|
-
prompt:
|
|
14
|
+
prompt: |
|
|
15
|
+
Please pick the winner of this business idea contest.
|
|
16
|
+
[ideas]
|
|
17
|
+
${0}
|
|
18
|
+
[evalutations]
|
|
19
|
+
${1}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { GraphAI, GraphData } from "@/graphai";
|
|
2
|
+
import * as readline from "readline";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import { testAgent } from "~/agents/agents";
|
|
6
|
+
|
|
7
|
+
const getUserInput = async (question: string): Promise<string> => {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const rl = readline.createInterface({
|
|
10
|
+
input: process.stdin,
|
|
11
|
+
output: process.stdout,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
rl.question(question, (answer) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
resolve(answer);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const graph_data: GraphData = {
|
|
22
|
+
nodes: {
|
|
23
|
+
node1: {
|
|
24
|
+
source: true,
|
|
25
|
+
},
|
|
26
|
+
node2: {
|
|
27
|
+
inputs: ["node1"],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const runAgent = async (query: string) => {
|
|
33
|
+
console.log("query=", query);
|
|
34
|
+
graph_data.nodes.node1.result = { query };
|
|
35
|
+
const graph = new GraphAI(graph_data, testAgent);
|
|
36
|
+
// graph.injectResult("node1", { query });
|
|
37
|
+
const result = await graph.run();
|
|
38
|
+
const log_path = path.resolve(__dirname) + "/../tests/logs/interaction.log";
|
|
39
|
+
fs.writeFileSync(log_path, JSON.stringify(graph.transactionLogs(), null, 2));
|
|
40
|
+
console.log(result);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const main = async () => {
|
|
44
|
+
const query = await getUserInput("Please enter your question: ");
|
|
45
|
+
await runAgent(query);
|
|
46
|
+
console.log("COMPLETE 1");
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
main();
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { GraphAI, AgentFunction } from "@/graphai";
|
|
2
|
+
import { readGraphaiData } from "~/utils/file_utils";
|
|
3
|
+
|
|
4
|
+
import { slashGPTAgent, slashGPTFuncitons2TextAgent } from "./agents/slashgpt_agent";
|
|
5
|
+
|
|
6
|
+
const graph_data = {
|
|
7
|
+
nodes: {
|
|
8
|
+
slashGPTAgent: {
|
|
9
|
+
agentId: "slashGPTAgent",
|
|
10
|
+
params: {
|
|
11
|
+
function_result: true,
|
|
12
|
+
prompt: "世界で協力してco2を減らす方法を教えて下さい",
|
|
13
|
+
manifest: {
|
|
14
|
+
prompt: "あなたは世界経済の専門家です。ユーザの問い合わせについて考え、10この結果をfunctionsの結果に返してください。",
|
|
15
|
+
skip_function_result: true,
|
|
16
|
+
actions: {
|
|
17
|
+
your_ideas: {
|
|
18
|
+
type: "message_template",
|
|
19
|
+
message: "dummy",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
functions: [
|
|
23
|
+
{
|
|
24
|
+
name: "your_ideas",
|
|
25
|
+
description: "Your answer to a user's inquiry",
|
|
26
|
+
parameters: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
methods: {
|
|
30
|
+
type: "array",
|
|
31
|
+
items: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
title: {
|
|
35
|
+
type: "string",
|
|
36
|
+
description: "title",
|
|
37
|
+
},
|
|
38
|
+
description: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "description",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
required: ["title", "description"],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
function2prompt0: {
|
|
54
|
+
fork: 10,
|
|
55
|
+
params: {
|
|
56
|
+
function_data_key: "methods",
|
|
57
|
+
result_key: 0,
|
|
58
|
+
},
|
|
59
|
+
inputs: ["slashGPTAgent"],
|
|
60
|
+
agentId: "slashGPTFuncitons2TextAgent",
|
|
61
|
+
},
|
|
62
|
+
slashGPTAgent0: {
|
|
63
|
+
agentId: "slashGPTAgent",
|
|
64
|
+
fork: 10,
|
|
65
|
+
params: {
|
|
66
|
+
debug: true,
|
|
67
|
+
manifest: {
|
|
68
|
+
prompt: "ユーザの問い合わせにある文章の専門家です。専門家として、ユーザのアイデアに対して実現可能なシナリオを800文字で書いてください。",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
inputs: ["function2prompt0"],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
const runAgent = async () => {
|
|
76
|
+
const graph = new GraphAI(graph_data, { slashGPTAgent, slashGPTFuncitons2TextAgent });
|
|
77
|
+
const result = await graph.run();
|
|
78
|
+
console.log(result);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const main = async () => {
|
|
82
|
+
await runAgent();
|
|
83
|
+
console.log("COMPLETE 1");
|
|
84
|
+
};
|
|
85
|
+
main();
|
package/samples/sample_gpt.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
2
3
|
import { GraphAI, AgentFunction } from "@/graphai";
|
|
3
4
|
import { ChatSession, ChatConfig, ManifestData } from "slashgpt";
|
|
4
5
|
import { readGraphaiData } from "~/utils/file_utils";
|
|
5
6
|
|
|
6
7
|
const config = new ChatConfig(path.resolve(__dirname));
|
|
7
8
|
|
|
8
|
-
const slashGPTAgent: AgentFunction<{ manifest: ManifestData; prompt: string }, {
|
|
9
|
+
const slashGPTAgent: AgentFunction<{ manifest: ManifestData; prompt: string }, { content: string }> = async (context) => {
|
|
9
10
|
console.log("executing", context.nodeId, context.params);
|
|
10
11
|
const session = new ChatSession(config, context.params.manifest ?? {});
|
|
11
|
-
const prompt =
|
|
12
|
-
return prompt.replace("${" +
|
|
12
|
+
const prompt = context.inputs.reduce((prompt, input, index) => {
|
|
13
|
+
return prompt.replace("${" + index + "}", input["content"]);
|
|
13
14
|
}, context.params.prompt);
|
|
14
15
|
session.append_user_question(prompt);
|
|
15
16
|
|
|
@@ -18,20 +19,22 @@ const slashGPTAgent: AgentFunction<{ manifest: ManifestData; prompt: string }, {
|
|
|
18
19
|
if (message === undefined) {
|
|
19
20
|
throw new Error("No message in the history");
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
return result;
|
|
22
|
+
return message;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
const runAgent = async (file: string) => {
|
|
26
26
|
const file_path = path.resolve(__dirname) + file;
|
|
27
27
|
const graph_data = readGraphaiData(file_path);
|
|
28
28
|
const graph = new GraphAI(graph_data, slashGPTAgent);
|
|
29
|
-
const
|
|
30
|
-
|
|
29
|
+
const results = (await graph.run()) as Record<string, any>;
|
|
30
|
+
|
|
31
|
+
const log_path = path.resolve(__dirname) + "/../tests/logs/" + path.basename(file_path).replace(/\.yml$/, ".log");
|
|
32
|
+
console.log(log_path);
|
|
33
|
+
fs.writeFileSync(log_path, JSON.stringify(graph.transactionLogs(), null, 2));
|
|
34
|
+
console.log(results["node3"]["content"]);
|
|
31
35
|
};
|
|
32
36
|
|
|
33
37
|
const main = async () => {
|
|
34
38
|
await runAgent("/graphs/slash_gpt.yml");
|
|
35
|
-
console.log("COMPLETE 1");
|
|
36
39
|
};
|
|
37
40
|
main();
|
|
@@ -7,14 +7,10 @@ import { readGraphaiData } from "~/utils/file_utils";
|
|
|
7
7
|
import { slashGPTAgent } from "./agents/slashgpt_agent";
|
|
8
8
|
import { arxivAgent, arxiv2TextAgent } from "./agents/arxiv_agent";
|
|
9
9
|
|
|
10
|
-
export const parrotingAgent: AgentFunction = async (context) => {
|
|
11
|
-
return {};
|
|
12
|
-
};
|
|
13
|
-
|
|
14
10
|
const runAgent = async (file: string) => {
|
|
15
11
|
const file_path = path.resolve(__dirname) + file;
|
|
16
12
|
const graph_data = readGraphaiData(file_path);
|
|
17
|
-
const graph = new GraphAI(graph_data, {
|
|
13
|
+
const graph = new GraphAI(graph_data, { arxivAgent: arxivAgent, arxiv2TextAgent, slashGPTAgent });
|
|
18
14
|
const result = await graph.run();
|
|
19
15
|
console.log(result);
|
|
20
16
|
};
|
package/src/graphai.ts
CHANGED
|
@@ -14,13 +14,14 @@ export type NodeDataParams<ParamsType = Record<string, any>> = ParamsType; // Ag
|
|
|
14
14
|
|
|
15
15
|
type NodeData = {
|
|
16
16
|
inputs?: Array<string>;
|
|
17
|
-
params
|
|
18
|
-
payloadMapping?: Record<string, string>;
|
|
17
|
+
params?: NodeDataParams;
|
|
19
18
|
retry?: number;
|
|
20
19
|
timeout?: number; // msec
|
|
21
20
|
agentId?: string;
|
|
21
|
+
fork?: number;
|
|
22
22
|
source?: boolean;
|
|
23
|
-
|
|
23
|
+
result?: ResultData; // preset result for source node.
|
|
24
|
+
outputs?: Record<string, string>; // mapping from routeId to nodeId
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
export type GraphData = {
|
|
@@ -36,16 +37,17 @@ export type TransactionLog = {
|
|
|
36
37
|
retryCount: number;
|
|
37
38
|
agentId?: string;
|
|
38
39
|
params?: NodeDataParams;
|
|
39
|
-
|
|
40
|
+
inputs?: Array<ResultData>;
|
|
40
41
|
errorMessage?: string;
|
|
41
42
|
result?: ResultData;
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
export type AgentFunctionContext<ParamsType, ResultType, PreviousResultType> = {
|
|
45
46
|
nodeId: string;
|
|
47
|
+
forkIndex?: number;
|
|
46
48
|
retry: number;
|
|
47
49
|
params: NodeDataParams<ParamsType>;
|
|
48
|
-
|
|
50
|
+
inputs: Array<PreviousResultType>;
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
export type AgentFunction<ParamsType = Record<string, any>, ResultType = Record<string, any>, PreviousResultType = Record<string, any>> = (
|
|
@@ -58,11 +60,12 @@ class Node {
|
|
|
58
60
|
public nodeId: string;
|
|
59
61
|
public params: NodeDataParams; // Agent-specific parameters
|
|
60
62
|
public inputs: Array<string>; // List of nodes this node needs data from.
|
|
61
|
-
public payloadMapping: Record<string, string>;
|
|
62
63
|
public pendings: Set<string>; // List of nodes this node is waiting data from.
|
|
63
64
|
public waitlist = new Set<string>(); // List of nodes which need data from this node.
|
|
64
65
|
public state = NodeState.Waiting;
|
|
65
|
-
public agentId
|
|
66
|
+
public agentId?: string;
|
|
67
|
+
public fork?: number;
|
|
68
|
+
public forkIndex?: number;
|
|
66
69
|
public result: ResultData = undefined;
|
|
67
70
|
public retryLimit: number;
|
|
68
71
|
public retryCount: number = 0;
|
|
@@ -70,21 +73,22 @@ class Node {
|
|
|
70
73
|
public timeout?: number; // msec
|
|
71
74
|
public error?: Error;
|
|
72
75
|
public source: boolean;
|
|
73
|
-
public
|
|
76
|
+
public outputs?: Record<string, string>; // Mapping from routeId to nodeId
|
|
74
77
|
|
|
75
78
|
private graph: GraphAI;
|
|
76
79
|
|
|
77
|
-
constructor(nodeId: string, data: NodeData, graph: GraphAI) {
|
|
80
|
+
constructor(nodeId: string, forkIndex: number | undefined, data: NodeData, graph: GraphAI) {
|
|
78
81
|
this.nodeId = nodeId;
|
|
82
|
+
this.forkIndex = forkIndex;
|
|
79
83
|
this.inputs = data.inputs ?? [];
|
|
80
|
-
this.payloadMapping = data.payloadMapping ?? {};
|
|
81
84
|
this.pendings = new Set(this.inputs);
|
|
82
|
-
this.params = data.params;
|
|
83
|
-
this.agentId = data.agentId
|
|
85
|
+
this.params = data.params ?? {};
|
|
86
|
+
this.agentId = data.agentId;
|
|
87
|
+
this.fork = data.fork;
|
|
84
88
|
this.retryLimit = data.retry ?? 0;
|
|
85
89
|
this.timeout = data.timeout;
|
|
86
90
|
this.source = data.source === true;
|
|
87
|
-
this.
|
|
91
|
+
this.outputs = data.outputs;
|
|
88
92
|
this.graph = graph;
|
|
89
93
|
}
|
|
90
94
|
|
|
@@ -112,17 +116,6 @@ class Node {
|
|
|
112
116
|
}
|
|
113
117
|
}
|
|
114
118
|
|
|
115
|
-
public payload() {
|
|
116
|
-
return this.inputs.reduce((results: ResultDataDictonary, nodeId) => {
|
|
117
|
-
if (this.payloadMapping && this.payloadMapping[nodeId]) {
|
|
118
|
-
results[this.payloadMapping[nodeId]] = this.graph.nodes[nodeId].result;
|
|
119
|
-
} else {
|
|
120
|
-
results[nodeId] = this.graph.nodes[nodeId].result;
|
|
121
|
-
}
|
|
122
|
-
return results;
|
|
123
|
-
}, {});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
119
|
public pushQueueIfReady() {
|
|
127
120
|
if (this.pendings.size === 0 && !this.source) {
|
|
128
121
|
this.graph.pushQueue(this);
|
|
@@ -136,8 +129,9 @@ class Node {
|
|
|
136
129
|
retryCount: this.retryCount,
|
|
137
130
|
state: NodeState.Injected,
|
|
138
131
|
startTime: Date.now(),
|
|
132
|
+
endTime: Date.now(),
|
|
133
|
+
result,
|
|
139
134
|
};
|
|
140
|
-
log.endTime = log.startTime;
|
|
141
135
|
this.graph.appendLog(log);
|
|
142
136
|
this.setResult(result, NodeState.Injected);
|
|
143
137
|
} else {
|
|
@@ -156,19 +150,19 @@ class Node {
|
|
|
156
150
|
}
|
|
157
151
|
|
|
158
152
|
public async execute() {
|
|
159
|
-
const
|
|
153
|
+
const results = this.graph.resultsOf(this.inputs);
|
|
154
|
+
const transactionId = Date.now();
|
|
160
155
|
const log: TransactionLog = {
|
|
161
156
|
nodeId: this.nodeId,
|
|
162
157
|
retryCount: this.retryCount,
|
|
163
158
|
state: NodeState.Executing,
|
|
164
|
-
startTime:
|
|
159
|
+
startTime: transactionId,
|
|
165
160
|
agentId: this.agentId,
|
|
166
161
|
params: this.params,
|
|
167
|
-
|
|
162
|
+
inputs: results,
|
|
168
163
|
};
|
|
169
164
|
this.graph.appendLog(log);
|
|
170
165
|
this.state = NodeState.Executing;
|
|
171
|
-
const transactionId = log.startTime;
|
|
172
166
|
this.transactionId = transactionId;
|
|
173
167
|
|
|
174
168
|
if (this.timeout && this.timeout > 0) {
|
|
@@ -189,7 +183,8 @@ class Node {
|
|
|
189
183
|
nodeId: this.nodeId,
|
|
190
184
|
retry: this.retryCount,
|
|
191
185
|
params: this.params,
|
|
192
|
-
|
|
186
|
+
inputs: results,
|
|
187
|
+
forkIndex: this.forkIndex,
|
|
193
188
|
});
|
|
194
189
|
if (this.transactionId !== transactionId) {
|
|
195
190
|
console.log(`-- ${this.nodeId}: transactionId mismatch`);
|
|
@@ -199,10 +194,10 @@ class Node {
|
|
|
199
194
|
log.endTime = Date.now();
|
|
200
195
|
log.result = result;
|
|
201
196
|
|
|
202
|
-
const
|
|
203
|
-
if (
|
|
197
|
+
const outputs = this.outputs;
|
|
198
|
+
if (outputs !== undefined) {
|
|
204
199
|
Object.keys(result).forEach((outputId) => {
|
|
205
|
-
const nodeId =
|
|
200
|
+
const nodeId = outputs[outputId];
|
|
206
201
|
this.graph.injectResult(nodeId, result[outputId]);
|
|
207
202
|
});
|
|
208
203
|
log.state = NodeState.Dispatched;
|
|
@@ -247,34 +242,75 @@ export class GraphAI {
|
|
|
247
242
|
private logs: Array<TransactionLog> = [];
|
|
248
243
|
|
|
249
244
|
constructor(data: GraphData, callbackDictonary: AgentFunctionDictonary | AgentFunction<any, any, any>) {
|
|
250
|
-
this.callbackDictonary = typeof callbackDictonary === "function" ? {
|
|
251
|
-
if (this.callbackDictonary["default"] === undefined) {
|
|
252
|
-
throw new Error("No default function");
|
|
253
|
-
}
|
|
245
|
+
this.callbackDictonary = typeof callbackDictonary === "function" ? { _default: callbackDictonary } : callbackDictonary;
|
|
254
246
|
this.concurrency = data.concurrency ?? defaultConcurrency;
|
|
255
247
|
this.onComplete = () => {
|
|
256
248
|
console.error("-- SOMETHING IS WRONG: onComplete is called without run()");
|
|
257
249
|
};
|
|
250
|
+
const nodeId2forkedNodeIds: Record<string, string[]> = {};
|
|
251
|
+
const forkedNodeId2Index: Record<string, number> = {};
|
|
252
|
+
|
|
253
|
+
// Create node instances from data.nodes
|
|
258
254
|
this.nodes = Object.keys(data.nodes).reduce((nodes: GraphNodes, nodeId: string) => {
|
|
259
|
-
|
|
255
|
+
const fork = data.nodes[nodeId].fork;
|
|
256
|
+
if (fork) {
|
|
257
|
+
// For fork, change the nodeId and increase the node
|
|
258
|
+
nodeId2forkedNodeIds[nodeId] = new Array(fork).fill(undefined).map((_, i) => {
|
|
259
|
+
const forkedNodeId = `${nodeId}_${i}`;
|
|
260
|
+
nodes[forkedNodeId] = new Node(forkedNodeId, i, data.nodes[nodeId], this);
|
|
261
|
+
// Data for pending and waiting
|
|
262
|
+
forkedNodeId2Index[forkedNodeId] = i;
|
|
263
|
+
return forkedNodeId;
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
nodes[nodeId] = new Node(nodeId, undefined, data.nodes[nodeId], this);
|
|
267
|
+
}
|
|
260
268
|
return nodes;
|
|
261
269
|
}, {});
|
|
262
270
|
|
|
263
|
-
// Generate the waitlist for each node
|
|
271
|
+
// Generate the waitlist for each node, and update the pendings in case of forked node.
|
|
264
272
|
Object.keys(this.nodes).forEach((nodeId) => {
|
|
265
273
|
const node = this.nodes[nodeId];
|
|
266
274
|
node.pendings.forEach((pending) => {
|
|
267
|
-
|
|
268
|
-
|
|
275
|
+
// If the pending(previous) node is forking
|
|
276
|
+
if (nodeId2forkedNodeIds[pending]) {
|
|
277
|
+
// update node.pending and pending(previous) node.wailtlist
|
|
278
|
+
if (node.fork) {
|
|
279
|
+
// 1:1 if current nodes are also forking.
|
|
280
|
+
const newPendingId = nodeId2forkedNodeIds[pending][forkedNodeId2Index[nodeId]];
|
|
281
|
+
this.nodes[newPendingId].waitlist.add(nodeId); // previousNode
|
|
282
|
+
node.pendings.add(newPendingId);
|
|
283
|
+
} else {
|
|
284
|
+
// 1:n if current node is not forking.
|
|
285
|
+
nodeId2forkedNodeIds[pending].forEach((newPendingId) => {
|
|
286
|
+
this.nodes[newPendingId].waitlist.add(nodeId); // previousNode
|
|
287
|
+
node.pendings.add(newPendingId);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
node.pendings.delete(pending);
|
|
291
|
+
} else {
|
|
292
|
+
this.nodes[pending].waitlist.add(nodeId); // previousNode
|
|
293
|
+
}
|
|
269
294
|
});
|
|
295
|
+
node.inputs = Array.from(node.pendings); // for fork.
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// If the result property is specified, inject it.
|
|
299
|
+
// NOTE: This must be done at the end of this constructor
|
|
300
|
+
Object.keys(data.nodes).forEach((nodeId) => {
|
|
301
|
+
const result = data.nodes[nodeId].result;
|
|
302
|
+
if (result) {
|
|
303
|
+
this.injectResult(nodeId, result);
|
|
304
|
+
}
|
|
270
305
|
});
|
|
271
306
|
}
|
|
272
307
|
|
|
273
|
-
public getCallback(
|
|
274
|
-
|
|
308
|
+
public getCallback(_agentId?: string) {
|
|
309
|
+
const agentId = _agentId ?? "_default";
|
|
310
|
+
if (this.callbackDictonary[agentId]) {
|
|
275
311
|
return this.callbackDictonary[agentId];
|
|
276
312
|
}
|
|
277
|
-
|
|
313
|
+
throw new Error("No agent: " + agentId);
|
|
278
314
|
}
|
|
279
315
|
|
|
280
316
|
public asString() {
|
|
@@ -372,4 +408,10 @@ export class GraphAI {
|
|
|
372
408
|
console.error("-- Invalid nodeId", nodeId);
|
|
373
409
|
}
|
|
374
410
|
}
|
|
411
|
+
|
|
412
|
+
public resultsOf(nodeIds: Array<string>) {
|
|
413
|
+
return nodeIds.map((nodeId) => {
|
|
414
|
+
return this.nodes[nodeId].result;
|
|
415
|
+
});
|
|
416
|
+
}
|
|
375
417
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { GraphAI, AgentFunction } from "./graphai";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import YAML from "yaml";
|
|
7
|
+
|
|
8
|
+
const testAgent: AgentFunction<{ delay: number; fail: boolean }> = async (context) => {
|
|
9
|
+
return {};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const main = async () => {
|
|
13
|
+
const file = process.argv[2];
|
|
14
|
+
if (file === undefined) {
|
|
15
|
+
console.log("no file");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const file_path = path.resolve(process.cwd() + "/" + file);
|
|
19
|
+
if (!fs.existsSync(file_path)) {
|
|
20
|
+
console.log("no file");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const graph_data_file = fs.readFileSync(file_path, "utf8");
|
|
25
|
+
const graph_data = YAML.parse(graph_data_file);
|
|
26
|
+
|
|
27
|
+
const graph = new GraphAI(graph_data, { test: testAgent });
|
|
28
|
+
const results = await graph.run();
|
|
29
|
+
console.log(results);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.log("error", e);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
main();
|
package/tests/agents/agents.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { AgentFunction } from "@/graphai";
|
|
|
2
2
|
import { sleep } from "~/utils/utils";
|
|
3
3
|
|
|
4
4
|
export const testAgent: AgentFunction<{ delay: number; fail: boolean }> = async (context) => {
|
|
5
|
-
const { nodeId, retry, params,
|
|
5
|
+
const { nodeId, retry, params, inputs } = context;
|
|
6
6
|
console.log("executing", nodeId);
|
|
7
7
|
await sleep(params.delay / (retry + 1));
|
|
8
8
|
|
|
@@ -11,10 +11,9 @@ export const testAgent: AgentFunction<{ delay: number; fail: boolean }> = async
|
|
|
11
11
|
console.log("failed (intentional)", nodeId, retry);
|
|
12
12
|
throw new Error("Intentional Failure");
|
|
13
13
|
} else {
|
|
14
|
-
const result =
|
|
15
|
-
(result,
|
|
16
|
-
|
|
17
|
-
return result;
|
|
14
|
+
const result = inputs.reduce(
|
|
15
|
+
(result: Record<string, any>, input: Record<string, any>) => {
|
|
16
|
+
return { ...result, ...input };
|
|
18
17
|
},
|
|
19
18
|
{ [nodeId]: "output" },
|
|
20
19
|
);
|
|
@@ -8,8 +8,8 @@ import { testAgent } from "~/agents/agents";
|
|
|
8
8
|
import test from "node:test";
|
|
9
9
|
import assert from "node:assert";
|
|
10
10
|
|
|
11
|
-
const dispatchAgent: AgentFunction<{ delay: number; fail: boolean }
|
|
12
|
-
const { nodeId, retry, params,
|
|
11
|
+
const dispatchAgent: AgentFunction<{ delay: number; fail: boolean }, Record<string, any>, Record<string, any>> = async (context) => {
|
|
12
|
+
const { nodeId, retry, params, inputs } = context;
|
|
13
13
|
console.log("executing", nodeId);
|
|
14
14
|
await sleep(params.delay / (retry + 1));
|
|
15
15
|
|
|
@@ -18,10 +18,9 @@ const dispatchAgent: AgentFunction<{ delay: number; fail: boolean }> = async (co
|
|
|
18
18
|
console.log("failed (intentional)", nodeId, retry);
|
|
19
19
|
throw new Error("Intentional Failure");
|
|
20
20
|
} else {
|
|
21
|
-
const result =
|
|
22
|
-
(result,
|
|
23
|
-
|
|
24
|
-
return result;
|
|
21
|
+
const result = inputs.reduce(
|
|
22
|
+
(result: Record<string, any>, input: Record<string, any>) => {
|
|
23
|
+
return { ...result, ...input };
|
|
25
24
|
},
|
|
26
25
|
{ [nodeId]: "dispatch" },
|
|
27
26
|
);
|
|
@@ -31,7 +30,7 @@ const dispatchAgent: AgentFunction<{ delay: number; fail: boolean }> = async (co
|
|
|
31
30
|
};
|
|
32
31
|
|
|
33
32
|
test("test dispatch", async () => {
|
|
34
|
-
const result = await fileTestRunner("/graphs/test_dispatch.yml", {
|
|
33
|
+
const result = await fileTestRunner("/graphs/test_dispatch.yml", { test: testAgent, alt: dispatchAgent });
|
|
35
34
|
assert.deepStrictEqual(result, {
|
|
36
35
|
node1: { node1: "output" },
|
|
37
36
|
node20: { node2: "dispatch" },
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { GraphAI, AgentFunction } from "@/graphai";
|
|
2
|
+
import { testAgent } from "~/agents/agents";
|
|
3
|
+
import { graphDataTestRunner } from "~/utils/runner";
|
|
4
|
+
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import assert from "node:assert";
|
|
7
|
+
|
|
8
|
+
const testAgent1: AgentFunction = async (context) => {
|
|
9
|
+
const { nodeId, retry, params, inputs } = context;
|
|
10
|
+
console.log("executing", nodeId, params, inputs);
|
|
11
|
+
|
|
12
|
+
const result = {
|
|
13
|
+
[nodeId]: [nodeId, inputs.map((a) => Object.values(a).flat())]
|
|
14
|
+
.flat()
|
|
15
|
+
.filter((a) => !!a)
|
|
16
|
+
.join(":"),
|
|
17
|
+
};
|
|
18
|
+
console.log("completing", nodeId, result);
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
test("test base", async () => {
|
|
23
|
+
const forkGraph = {
|
|
24
|
+
nodes: {
|
|
25
|
+
node1: {
|
|
26
|
+
params: {},
|
|
27
|
+
},
|
|
28
|
+
node2: {
|
|
29
|
+
params: {},
|
|
30
|
+
fork: 10,
|
|
31
|
+
inputs: ["node1"],
|
|
32
|
+
},
|
|
33
|
+
node3: {
|
|
34
|
+
params: {},
|
|
35
|
+
// fork: 10,
|
|
36
|
+
inputs: ["node2"],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const result = await graphDataTestRunner("fork.yml", forkGraph, testAgent1);
|
|
42
|
+
// console.log(result);
|
|
43
|
+
assert.deepStrictEqual(result, {
|
|
44
|
+
node1: { node1: "node1" },
|
|
45
|
+
node2_0: { node2_0: "node2_0:node1" },
|
|
46
|
+
node2_1: { node2_1: "node2_1:node1" },
|
|
47
|
+
node2_2: { node2_2: "node2_2:node1" },
|
|
48
|
+
node2_3: { node2_3: "node2_3:node1" },
|
|
49
|
+
node2_4: { node2_4: "node2_4:node1" },
|
|
50
|
+
node2_5: { node2_5: "node2_5:node1" },
|
|
51
|
+
node2_6: { node2_6: "node2_6:node1" },
|
|
52
|
+
node2_7: { node2_7: "node2_7:node1" },
|
|
53
|
+
node2_8: { node2_8: "node2_8:node1" },
|
|
54
|
+
node2_9: { node2_9: "node2_9:node1" },
|
|
55
|
+
node3: {
|
|
56
|
+
node3:
|
|
57
|
+
"node3:node2_0:node1:node2_1:node1:node2_2:node1:node2_3:node1:node2_4:node1:node2_5:node1:node2_6:node1:node2_7:node1:node2_8:node1:node2_9:node1",
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("test base", async () => {
|
|
63
|
+
const forkGraph = {
|
|
64
|
+
nodes: {
|
|
65
|
+
node1: {
|
|
66
|
+
params: {},
|
|
67
|
+
},
|
|
68
|
+
node2: {
|
|
69
|
+
params: {},
|
|
70
|
+
fork: 10,
|
|
71
|
+
inputs: ["node1"],
|
|
72
|
+
},
|
|
73
|
+
node3: {
|
|
74
|
+
params: {},
|
|
75
|
+
fork: 10,
|
|
76
|
+
inputs: ["node2"],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const result = await graphDataTestRunner("fork.yml", forkGraph, testAgent1);
|
|
82
|
+
// console.log(result);
|
|
83
|
+
assert.deepStrictEqual(result, {
|
|
84
|
+
node1: { node1: "node1" },
|
|
85
|
+
node2_0: { node2_0: "node2_0:node1" },
|
|
86
|
+
node2_1: { node2_1: "node2_1:node1" },
|
|
87
|
+
node2_2: { node2_2: "node2_2:node1" },
|
|
88
|
+
node2_3: { node2_3: "node2_3:node1" },
|
|
89
|
+
node2_4: { node2_4: "node2_4:node1" },
|
|
90
|
+
node2_5: { node2_5: "node2_5:node1" },
|
|
91
|
+
node2_6: { node2_6: "node2_6:node1" },
|
|
92
|
+
node2_7: { node2_7: "node2_7:node1" },
|
|
93
|
+
node2_8: { node2_8: "node2_8:node1" },
|
|
94
|
+
node2_9: { node2_9: "node2_9:node1" },
|
|
95
|
+
node3_0: { node3_0: "node3_0:node2_0:node1" },
|
|
96
|
+
node3_1: { node3_1: "node3_1:node2_1:node1" },
|
|
97
|
+
node3_2: { node3_2: "node3_2:node2_2:node1" },
|
|
98
|
+
node3_3: { node3_3: "node3_3:node2_3:node1" },
|
|
99
|
+
node3_4: { node3_4: "node3_4:node2_4:node1" },
|
|
100
|
+
node3_5: { node3_5: "node3_5:node2_5:node1" },
|
|
101
|
+
node3_6: { node3_6: "node3_6:node2_6:node1" },
|
|
102
|
+
node3_7: { node3_7: "node3_7:node2_7:node1" },
|
|
103
|
+
node3_8: { node3_8: "node3_8:node2_8:node1" },
|
|
104
|
+
node3_9: { node3_9: "node3_9:node2_9:node1" },
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -5,8 +5,8 @@ import test from "node:test";
|
|
|
5
5
|
import assert from "node:assert";
|
|
6
6
|
|
|
7
7
|
const httpClientAgent: AgentFunction<Record<string, string>> = async (context) => {
|
|
8
|
-
const { nodeId, retry, params,
|
|
9
|
-
console.log("executing", nodeId, params,
|
|
8
|
+
const { nodeId, retry, params, inputs } = context;
|
|
9
|
+
console.log("executing", nodeId, params, inputs);
|
|
10
10
|
|
|
11
11
|
const response = await fetch(params.url);
|
|
12
12
|
const result = await response.json();
|
|
@@ -31,7 +31,7 @@ const graph_data = {
|
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
test("test
|
|
34
|
+
test("test http client", async () => {
|
|
35
35
|
const result = await graphDataTestRunner("http.log", graph_data, httpClientAgent);
|
|
36
36
|
assert.deepStrictEqual(result, {
|
|
37
37
|
node1: { result: true, messages: ["hello"] },
|
|
@@ -33,8 +33,8 @@ const numberTestAgent: AgentFunction<{ number: number }, { [key: string]: number
|
|
|
33
33
|
return result;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
test("test
|
|
37
|
-
const result = await fileTestRunner("/graphs/test_multiple_functions_1.yml", {
|
|
36
|
+
test("test multiple function", async () => {
|
|
37
|
+
const result = await fileTestRunner("/graphs/test_multiple_functions_1.yml", { test1: testAgent1, test2: testAgent2, numberTestAgent });
|
|
38
38
|
assert.deepStrictEqual(result, {
|
|
39
39
|
node1: { node1: "output 1" },
|
|
40
40
|
node2: { node2: "output 1" },
|
|
@@ -60,13 +60,12 @@ test("test source", async () => {
|
|
|
60
60
|
test("test source2", async () => {
|
|
61
61
|
const result = await fileTestRunner("/graphs/test_source2.yml", testAgent, (graph: GraphAI) => {
|
|
62
62
|
graph.injectResult("node1", { node1: "injected" });
|
|
63
|
-
graph.injectResult("node2", { node2: "injected" });
|
|
64
63
|
});
|
|
65
64
|
assert.deepStrictEqual(result, {
|
|
66
65
|
node1: { node1: "injected" },
|
|
67
|
-
node2: { node2: "
|
|
68
|
-
node3: { node3: "output", node1: "injected", node2: "
|
|
69
|
-
node4: { node4: "output", node3: "output", node1: "injected", node2: "
|
|
70
|
-
node5: { node5: "output", node4: "output", node3: "output", node1: "injected", node2: "
|
|
66
|
+
node2: { node2: "preset" },
|
|
67
|
+
node3: { node3: "output", node1: "injected", node2: "preset" },
|
|
68
|
+
node4: { node4: "output", node3: "output", node1: "injected", node2: "preset" },
|
|
69
|
+
node5: { node5: "output", node4: "output", node3: "output", node1: "injected", node2: "preset" },
|
|
71
70
|
});
|
|
72
71
|
});
|
|
@@ -2,23 +2,28 @@ nodes:
|
|
|
2
2
|
node1:
|
|
3
3
|
params:
|
|
4
4
|
delay: 500
|
|
5
|
+
agentId: test
|
|
5
6
|
node2:
|
|
6
7
|
params:
|
|
7
8
|
delay: 100
|
|
8
9
|
agentId: alt
|
|
9
|
-
|
|
10
|
+
outputs:
|
|
10
11
|
output1: node20
|
|
11
12
|
node20:
|
|
12
13
|
source: true
|
|
14
|
+
agentId: test
|
|
13
15
|
node3:
|
|
14
16
|
params:
|
|
15
17
|
delay: 500
|
|
16
18
|
inputs: [node1, node20]
|
|
19
|
+
agentId: test
|
|
17
20
|
node4:
|
|
18
21
|
params:
|
|
19
22
|
delay: 100
|
|
20
23
|
inputs: [node3]
|
|
24
|
+
agentId: test
|
|
21
25
|
node5:
|
|
22
26
|
params:
|
|
23
27
|
delay: 500
|
|
24
28
|
inputs: [node20, node4]
|
|
29
|
+
agentId: test
|
|
@@ -2,9 +2,11 @@ nodes:
|
|
|
2
2
|
node1:
|
|
3
3
|
params:
|
|
4
4
|
delay: 500
|
|
5
|
+
agentId: test1
|
|
5
6
|
node2:
|
|
6
7
|
params:
|
|
7
8
|
delay: 100
|
|
9
|
+
agentId: test1
|
|
8
10
|
node3:
|
|
9
11
|
params:
|
|
10
12
|
delay: 500
|
|
@@ -14,6 +16,7 @@ nodes:
|
|
|
14
16
|
params:
|
|
15
17
|
delay: 100
|
|
16
18
|
inputs: [node3]
|
|
19
|
+
agentId: test1
|
|
17
20
|
node5:
|
|
18
21
|
params:
|
|
19
22
|
delay: 500
|