graphai 0.0.8 → 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 CHANGED
@@ -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, outputs, retry, timeout, source, agentId) that dictate the node's behavior and its relationship with other nodes.
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
 
@@ -67,10 +77,13 @@ An agent function receives two set of parameters via AgentFunctionContext, agent
67
77
 
68
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.
69
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'.
70
- - '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.
80
+ - 'result': An optional object, which injects the result into the source node (equivalent to calling the injectResult method).
71
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.
72
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.
73
83
  - 'params': An optional parameters to the associated agent function, which are agent specific.
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.
74
87
 
75
88
  ## GraphAI class
76
89
 
package/lib/graphai.d.ts CHANGED
@@ -12,11 +12,13 @@ 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: NodeDataParams;
15
+ params?: NodeDataParams;
16
16
  retry?: number;
17
17
  timeout?: number;
18
18
  agentId?: string;
19
+ fork?: number;
19
20
  source?: boolean;
21
+ result?: ResultData;
20
22
  outputs?: Record<string, string>;
21
23
  };
22
24
  export type GraphData = {
@@ -37,6 +39,7 @@ export type TransactionLog = {
37
39
  };
38
40
  export type AgentFunctionContext<ParamsType, ResultType, PreviousResultType> = {
39
41
  nodeId: string;
42
+ forkIndex?: number;
40
43
  retry: number;
41
44
  params: NodeDataParams<ParamsType>;
42
45
  inputs: Array<PreviousResultType>;
@@ -51,6 +54,8 @@ declare class Node {
51
54
  waitlist: Set<string>;
52
55
  state: NodeState;
53
56
  agentId?: string;
57
+ fork?: number;
58
+ forkIndex?: number;
54
59
  result: ResultData;
55
60
  retryLimit: number;
56
61
  retryCount: number;
@@ -60,7 +65,7 @@ declare class Node {
60
65
  source: boolean;
61
66
  outputs?: Record<string, string>;
62
67
  private graph;
63
- constructor(nodeId: string, data: NodeData, graph: GraphAI);
68
+ constructor(nodeId: string, forkIndex: number | undefined, data: NodeData, graph: GraphAI);
64
69
  asString(): string;
65
70
  private retry;
66
71
  removePending(nodeId: string): void;
package/lib/graphai.js CHANGED
@@ -12,16 +12,18 @@ 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
23
  this.pendings = new Set(this.inputs);
23
- this.params = data.params;
24
+ this.params = data.params ?? {};
24
25
  this.agentId = data.agentId;
26
+ this.fork = data.fork;
25
27
  this.retryLimit = data.retry ?? 0;
26
28
  this.timeout = data.timeout;
27
29
  this.source = data.source === true;
@@ -62,9 +64,9 @@ class Node {
62
64
  retryCount: this.retryCount,
63
65
  state: NodeState.Injected,
64
66
  startTime: Date.now(),
67
+ endTime: Date.now(),
65
68
  result,
66
69
  };
67
- log.endTime = log.startTime;
68
70
  this.graph.appendLog(log);
69
71
  this.setResult(result, NodeState.Injected);
70
72
  }
@@ -82,21 +84,19 @@ class Node {
82
84
  });
83
85
  }
84
86
  async execute() {
87
+ const results = this.graph.resultsOf(this.inputs);
88
+ const transactionId = Date.now();
85
89
  const log = {
86
90
  nodeId: this.nodeId,
87
91
  retryCount: this.retryCount,
88
92
  state: NodeState.Executing,
89
- startTime: Date.now(),
93
+ startTime: transactionId,
90
94
  agentId: this.agentId,
91
95
  params: this.params,
96
+ inputs: results,
92
97
  };
93
- const results = this.graph.resultsOf(this.inputs);
94
- if (results.length > 0) {
95
- log.inputs = results;
96
- }
97
98
  this.graph.appendLog(log);
98
99
  this.state = NodeState.Executing;
99
- const transactionId = log.startTime;
100
100
  this.transactionId = transactionId;
101
101
  if (this.timeout && this.timeout > 0) {
102
102
  setTimeout(() => {
@@ -116,6 +116,7 @@ class Node {
116
116
  retry: this.retryCount,
117
117
  params: this.params,
118
118
  inputs: results,
119
+ forkIndex: this.forkIndex,
119
120
  });
120
121
  if (this.transactionId !== transactionId) {
121
122
  console.log(`-- ${this.nodeId}: transactionId mismatch`);
@@ -169,22 +170,65 @@ class GraphAI {
169
170
  this.onComplete = () => {
170
171
  console.error("-- SOMETHING IS WRONG: onComplete is called without run()");
171
172
  };
173
+ const nodeId2forkedNodeIds = {};
174
+ const forkedNodeId2Index = {};
175
+ // Create node instances from data.nodes
172
176
  this.nodes = Object.keys(data.nodes).reduce((nodes, nodeId) => {
173
- nodes[nodeId] = new Node(nodeId, data.nodes[nodeId], this);
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
+ }
174
191
  return nodes;
175
192
  }, {});
176
- // Generate the waitlist for each node
193
+ // Generate the waitlist for each node, and update the pendings in case of forked node.
177
194
  Object.keys(this.nodes).forEach((nodeId) => {
178
195
  const node = this.nodes[nodeId];
179
196
  node.pendings.forEach((pending) => {
180
- const node2 = this.nodes[pending];
181
- node2.waitlist.add(nodeId);
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
+ }
182
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
+ }
183
228
  });
184
229
  }
185
230
  getCallback(_agentId) {
186
231
  const agentId = _agentId ?? "_default";
187
- console.log(agentId);
188
232
  if (this.callbackDictonary[agentId]) {
189
233
  return this.callbackDictonary[agentId];
190
234
  }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -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.8",
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
- "gpt": "npx ts-node -r tsconfig-paths/register ./samples/sample_gpt.ts"
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",
@@ -14,7 +14,8 @@ export const slashGPTFuncitons2TextAgent: AgentFunction<
14
14
  const { title, description } = r;
15
15
  return ["title:", title, "description:", description].join("\n");
16
16
  });
17
- return result[params.result_key];
17
+
18
+ return result[context.forkIndex ?? 0];
18
19
  };
19
20
 
20
21
  export const slashGPTAgent: AgentFunction<
@@ -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();
@@ -51,6 +51,7 @@ const graph_data = {
51
51
  },
52
52
  },
53
53
  function2prompt0: {
54
+ fork: 10,
54
55
  params: {
55
56
  function_data_key: "methods",
56
57
  result_key: 0,
@@ -60,6 +61,7 @@ const graph_data = {
60
61
  },
61
62
  slashGPTAgent0: {
62
63
  agentId: "slashGPTAgent",
64
+ fork: 10,
63
65
  params: {
64
66
  debug: true,
65
67
  manifest: {
@@ -68,26 +70,6 @@ const graph_data = {
68
70
  },
69
71
  inputs: ["function2prompt0"],
70
72
  },
71
- // 1
72
- function2prompt1: {
73
- params: {
74
- debug: true,
75
- function_data_key: "methods",
76
- result_key: 1,
77
- },
78
- inputs: ["slashGPTAgent"],
79
- agentId: "slashGPTFuncitons2TextAgent",
80
- },
81
- slashGPTAgent1: {
82
- agentId: "slashGPTAgent",
83
- params: {
84
- debug: true,
85
- manifest: {
86
- prompt: "ユーザの問い合わせにある文章の専門家です。専門家として、ユーザのアイデアに対して実現可能なシナリオを800文字で書いてください。",
87
- },
88
- },
89
- inputs: ["function2prompt1"],
90
- },
91
73
  },
92
74
  };
93
75
  const runAgent = async () => {
@@ -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 }, { answer: string }> = async (context) => {
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
12
  const prompt = context.inputs.reduce((prompt, input, index) => {
12
- return prompt.replace("${" + index + "}", input["answer"]);
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
- const result = { answer: message.content };
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 result = await graph.run();
30
- console.log(result);
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();
package/src/graphai.ts CHANGED
@@ -14,11 +14,13 @@ export type NodeDataParams<ParamsType = Record<string, any>> = ParamsType; // Ag
14
14
 
15
15
  type NodeData = {
16
16
  inputs?: Array<string>;
17
- params: NodeDataParams;
17
+ params?: NodeDataParams;
18
18
  retry?: number;
19
19
  timeout?: number; // msec
20
20
  agentId?: string;
21
+ fork?: number;
21
22
  source?: boolean;
23
+ result?: ResultData; // preset result for source node.
22
24
  outputs?: Record<string, string>; // mapping from routeId to nodeId
23
25
  };
24
26
 
@@ -42,6 +44,7 @@ export type TransactionLog = {
42
44
 
43
45
  export type AgentFunctionContext<ParamsType, ResultType, PreviousResultType> = {
44
46
  nodeId: string;
47
+ forkIndex?: number;
45
48
  retry: number;
46
49
  params: NodeDataParams<ParamsType>;
47
50
  inputs: Array<PreviousResultType>;
@@ -61,6 +64,8 @@ class Node {
61
64
  public waitlist = new Set<string>(); // List of nodes which need data from this node.
62
65
  public state = NodeState.Waiting;
63
66
  public agentId?: string;
67
+ public fork?: number;
68
+ public forkIndex?: number;
64
69
  public result: ResultData = undefined;
65
70
  public retryLimit: number;
66
71
  public retryCount: number = 0;
@@ -72,12 +77,14 @@ class Node {
72
77
 
73
78
  private graph: GraphAI;
74
79
 
75
- constructor(nodeId: string, data: NodeData, graph: GraphAI) {
80
+ constructor(nodeId: string, forkIndex: number | undefined, data: NodeData, graph: GraphAI) {
76
81
  this.nodeId = nodeId;
82
+ this.forkIndex = forkIndex;
77
83
  this.inputs = data.inputs ?? [];
78
84
  this.pendings = new Set(this.inputs);
79
- this.params = data.params;
85
+ this.params = data.params ?? {};
80
86
  this.agentId = data.agentId;
87
+ this.fork = data.fork;
81
88
  this.retryLimit = data.retry ?? 0;
82
89
  this.timeout = data.timeout;
83
90
  this.source = data.source === true;
@@ -122,9 +129,9 @@ class Node {
122
129
  retryCount: this.retryCount,
123
130
  state: NodeState.Injected,
124
131
  startTime: Date.now(),
132
+ endTime: Date.now(),
125
133
  result,
126
134
  };
127
- log.endTime = log.startTime;
128
135
  this.graph.appendLog(log);
129
136
  this.setResult(result, NodeState.Injected);
130
137
  } else {
@@ -143,21 +150,19 @@ class Node {
143
150
  }
144
151
 
145
152
  public async execute() {
153
+ const results = this.graph.resultsOf(this.inputs);
154
+ const transactionId = Date.now();
146
155
  const log: TransactionLog = {
147
156
  nodeId: this.nodeId,
148
157
  retryCount: this.retryCount,
149
158
  state: NodeState.Executing,
150
- startTime: Date.now(),
159
+ startTime: transactionId,
151
160
  agentId: this.agentId,
152
161
  params: this.params,
162
+ inputs: results,
153
163
  };
154
- const results = this.graph.resultsOf(this.inputs);
155
- if (results.length > 0) {
156
- log.inputs = results;
157
- }
158
164
  this.graph.appendLog(log);
159
165
  this.state = NodeState.Executing;
160
- const transactionId = log.startTime;
161
166
  this.transactionId = transactionId;
162
167
 
163
168
  if (this.timeout && this.timeout > 0) {
@@ -179,6 +184,7 @@ class Node {
179
184
  retry: this.retryCount,
180
185
  params: this.params,
181
186
  inputs: results,
187
+ forkIndex: this.forkIndex,
182
188
  });
183
189
  if (this.transactionId !== transactionId) {
184
190
  console.log(`-- ${this.nodeId}: transactionId mismatch`);
@@ -241,24 +247,66 @@ export class GraphAI {
241
247
  this.onComplete = () => {
242
248
  console.error("-- SOMETHING IS WRONG: onComplete is called without run()");
243
249
  };
250
+ const nodeId2forkedNodeIds: Record<string, string[]> = {};
251
+ const forkedNodeId2Index: Record<string, number> = {};
252
+
253
+ // Create node instances from data.nodes
244
254
  this.nodes = Object.keys(data.nodes).reduce((nodes: GraphNodes, nodeId: string) => {
245
- nodes[nodeId] = new Node(nodeId, data.nodes[nodeId], this);
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
+ }
246
268
  return nodes;
247
269
  }, {});
248
270
 
249
- // Generate the waitlist for each node
271
+ // Generate the waitlist for each node, and update the pendings in case of forked node.
250
272
  Object.keys(this.nodes).forEach((nodeId) => {
251
273
  const node = this.nodes[nodeId];
252
274
  node.pendings.forEach((pending) => {
253
- const node2 = this.nodes[pending];
254
- node2.waitlist.add(nodeId);
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
+ }
255
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
+ }
256
305
  });
257
306
  }
258
307
 
259
308
  public getCallback(_agentId?: string) {
260
309
  const agentId = _agentId ?? "_default";
261
- console.log(agentId);
262
310
  if (this.callbackDictonary[agentId]) {
263
311
  return this.callbackDictonary[agentId];
264
312
  }
@@ -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();
@@ -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
+ });
@@ -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: "injected" },
68
- node3: { node3: "output", node1: "injected", node2: "injected" },
69
- node4: { node4: "output", node3: "output", node1: "injected", node2: "injected" },
70
- node5: { node5: "output", node4: "output", node3: "output", node1: "injected", node2: "injected" },
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
  });
@@ -0,0 +1,4 @@
1
+ nodes:
2
+ node1:
3
+ agentId: test
4
+
@@ -3,6 +3,8 @@ nodes:
3
3
  source: true
4
4
  node2:
5
5
  source: true
6
+ result:
7
+ node2: preset
6
8
  node3:
7
9
  params:
8
10
  delay: 500