llmjs2 1.3.9 → 1.6.1
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 +31 -476
- package/chain/AGENT_STEP_README.md +102 -0
- package/chain/README.md +257 -0
- package/chain/WORKFLOW_README.md +85 -0
- package/chain/agent-step-example.js +232 -0
- package/chain/docs/AGENT.md +126 -0
- package/chain/docs/GRAPH.md +490 -0
- package/chain/examples.js +314 -0
- package/chain/index.js +31 -0
- package/chain/lib/agent.js +338 -0
- package/chain/lib/flow/agent-step.js +119 -0
- package/chain/lib/flow/edge.js +24 -0
- package/chain/lib/flow/flow.js +76 -0
- package/chain/lib/flow/graph.js +331 -0
- package/chain/lib/flow/index.js +7 -0
- package/chain/lib/flow/step.js +63 -0
- package/chain/lib/memory/in-memory.js +117 -0
- package/chain/lib/memory/index.js +36 -0
- package/chain/lib/memory/lance-memory.js +225 -0
- package/chain/lib/memory/sqlite-memory.js +309 -0
- package/chain/simple-agent-step-example.js +168 -0
- package/chain/workflow-example-usage.js +70 -0
- package/chain/workflow-example.json +59 -0
- package/core/README.md +485 -0
- package/core/cli.js +275 -0
- package/core/docs/BASIC_USAGE.md +62 -0
- package/core/docs/CLI.md +104 -0
- package/{docs → core/docs}/GET_STARTED.md +129 -129
- package/{docs → core/docs}/GUARDRAILS_GUIDE.md +734 -734
- package/{docs → core/docs}/README.md +47 -47
- package/core/docs/ROUTER_GUIDE.md +199 -0
- package/{docs → core/docs}/SERVER_MODE.md +358 -350
- package/core/index.js +115 -0
- package/{providers → core/providers}/ollama.js +14 -6
- package/{providers → core/providers}/openai.js +14 -6
- package/{providers → core/providers}/openrouter.js +14 -6
- package/core/router.js +252 -0
- package/{server.js → core/server.js} +15 -5
- package/package.json +43 -27
- package/cli.js +0 -195
- package/docs/BASIC_USAGE.md +0 -296
- package/docs/CLI.md +0 -455
- package/docs/ROUTER_GUIDE.md +0 -402
- package/index.js +0 -267
- package/router.js +0 -273
- package/test-completion.js +0 -99
- package/test.js +0 -246
- /package/{config.yaml → core/config.yaml} +0 -0
- /package/{logger.js → core/logger.js} +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const { Step } = require('./step');
|
|
2
|
+
const { Agent } = require('../agent');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AgentStep - A specialized step that wraps an AI agent for use in graph workflows
|
|
6
|
+
* Allows seamless integration of AI agents into workflow execution
|
|
7
|
+
*/
|
|
8
|
+
class AgentStep extends Step {
|
|
9
|
+
/**
|
|
10
|
+
* Create a new AgentStep
|
|
11
|
+
* @param {Object} options - Step configuration
|
|
12
|
+
* @param {string} options.name - Step name
|
|
13
|
+
* @param {string} options.instruction - System instruction for the agent
|
|
14
|
+
* @param {Array} options.tools - Array of tool definitions
|
|
15
|
+
* @param {string} options.model - Model name (optional)
|
|
16
|
+
* @param {string} options.apikey - API key (optional)
|
|
17
|
+
* @param {Object} options.memory - Memory instance (optional)
|
|
18
|
+
* @param {string} options.userPrompt - Template string to map workflow context to agent input
|
|
19
|
+
*/
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
super({
|
|
22
|
+
name: options.name,
|
|
23
|
+
execute: options.execute || (async (context) => {
|
|
24
|
+
return this._defaultExecute(context);
|
|
25
|
+
})
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
this.agent = new Agent({
|
|
29
|
+
model: options.model,
|
|
30
|
+
instruction: options.instruction,
|
|
31
|
+
apikey: options.apikey,
|
|
32
|
+
tools: options.tools || [],
|
|
33
|
+
memory: options.memory
|
|
34
|
+
});
|
|
35
|
+
this.userPrompt = options.userPrompt || '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Default execute method for AgentStep
|
|
40
|
+
* @param {Object} context - Workflow context
|
|
41
|
+
* @returns {Promise<Object>} Agent response mapped to workflow context
|
|
42
|
+
*/
|
|
43
|
+
async _defaultExecute(context) {
|
|
44
|
+
if (!this.agent) {
|
|
45
|
+
throw new Error(`AgentStep ${this.name}: No agent provided`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Map workflow context to agent input
|
|
49
|
+
let agentInput;
|
|
50
|
+
if (this.userPrompt) {
|
|
51
|
+
// userPrompt is a template string with {{variable}} placeholders
|
|
52
|
+
agentInput = this._interpolateTemplate(this.userPrompt, context);
|
|
53
|
+
} else {
|
|
54
|
+
// Default: pass the entire context as a formatted string
|
|
55
|
+
agentInput = this._contextToString(context);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`[${this.name}] Agent input:`, agentInput);
|
|
59
|
+
|
|
60
|
+
// Call the agent
|
|
61
|
+
const agentResponse = await this.agent.generate(agentInput);
|
|
62
|
+
|
|
63
|
+
console.log(`[${this.name}] Agent response:`, agentResponse);
|
|
64
|
+
|
|
65
|
+
return agentResponse;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Convert workflow context to a readable string for agent input
|
|
70
|
+
* @param {Object} context - Workflow context
|
|
71
|
+
* @returns {string} Formatted context string
|
|
72
|
+
*/
|
|
73
|
+
_contextToString(context) {
|
|
74
|
+
const relevantKeys = Object.keys(context).filter(key =>
|
|
75
|
+
!key.startsWith('_') && key !== 'executedSteps' && key !== 'results'
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (relevantKeys.length === 0) {
|
|
79
|
+
return "Please process the available data.";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let result = "Based on the following context:\n";
|
|
83
|
+
for (const key of relevantKeys) {
|
|
84
|
+
const value = context[key];
|
|
85
|
+
if (typeof value === 'object' && value !== null) {
|
|
86
|
+
result += `${key}: ${JSON.stringify(value, null, 2)}\n`;
|
|
87
|
+
} else {
|
|
88
|
+
result += `${key}: ${value}\n`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
result += "\nPlease provide your analysis or response.";
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Simple template interpolation
|
|
97
|
+
* @param {string} template - Template string with {{variable}} placeholders
|
|
98
|
+
* @param {Object} context - Context object for interpolation
|
|
99
|
+
* @returns {string} Interpolated string
|
|
100
|
+
*/
|
|
101
|
+
_interpolateTemplate(template, context) {
|
|
102
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
103
|
+
const value = context[key];
|
|
104
|
+
return value !== undefined ? String(value) : match;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the agent instance
|
|
110
|
+
* @returns {Agent} The wrapped agent
|
|
111
|
+
*/
|
|
112
|
+
getAgent() {
|
|
113
|
+
return this.agent;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { AgentStep };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge - Represents a connection between nodes in a graph
|
|
3
|
+
*/
|
|
4
|
+
class Edge {
|
|
5
|
+
constructor(from, to, condition = null) {
|
|
6
|
+
this.from = from;
|
|
7
|
+
this.to = to;
|
|
8
|
+
this.condition = condition; // Optional condition function
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if this edge should be followed based on result and context
|
|
13
|
+
*/
|
|
14
|
+
shouldFollow(result, context) {
|
|
15
|
+
// Handle both direct function and {when: function} format
|
|
16
|
+
const conditionFn = typeof this.condition === 'function'
|
|
17
|
+
? this.condition
|
|
18
|
+
: this.condition?.when;
|
|
19
|
+
|
|
20
|
+
return !conditionFn || conditionFn(result, context);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { Edge };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const { Step } = require('./step');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Flow - Linear sequence of steps
|
|
5
|
+
*/
|
|
6
|
+
class Flow {
|
|
7
|
+
constructor(name, steps = []) {
|
|
8
|
+
this.name = name;
|
|
9
|
+
this.steps = steps;
|
|
10
|
+
this.context = {
|
|
11
|
+
executedSteps: new Set(),
|
|
12
|
+
results: new Map(),
|
|
13
|
+
variables: new Map()
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Add a step to the flow
|
|
19
|
+
*/
|
|
20
|
+
addStep(step) {
|
|
21
|
+
this.steps.push(step);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Execute the flow sequentially
|
|
26
|
+
*/
|
|
27
|
+
async execute(initialInputs = {}) {
|
|
28
|
+
console.log(`[${this.name}] Starting flow execution`);
|
|
29
|
+
|
|
30
|
+
this.context.variables.set('initial', initialInputs);
|
|
31
|
+
|
|
32
|
+
for (const step of this.steps) {
|
|
33
|
+
if (!step.canExecute(this.context)) {
|
|
34
|
+
throw new Error(`Cannot execute step ${step.name}: dependencies not satisfied`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const inputs = this.prepareInputs(step, initialInputs);
|
|
38
|
+
const result = await step.execute(this.context, inputs);
|
|
39
|
+
|
|
40
|
+
this.context.executedSteps.add(step.name);
|
|
41
|
+
this.context.results.set(step.name, result);
|
|
42
|
+
|
|
43
|
+
// Store result in variables for use by subsequent steps
|
|
44
|
+
this.context.variables.set(step.name, result);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(`[${this.name}] Flow execution completed`);
|
|
48
|
+
return this.getFinalResult();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Prepare inputs for a step based on context
|
|
53
|
+
*/
|
|
54
|
+
prepareInputs(step, initialInputs) {
|
|
55
|
+
const inputs = { ...initialInputs };
|
|
56
|
+
|
|
57
|
+
// Include results from previous steps if needed
|
|
58
|
+
for (const [key, value] of this.context.variables) {
|
|
59
|
+
if (key !== 'initial') {
|
|
60
|
+
inputs[key] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return inputs;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the final result of the flow
|
|
69
|
+
*/
|
|
70
|
+
getFinalResult() {
|
|
71
|
+
if (this.steps.length === 0) return null;
|
|
72
|
+
return this.context.results.get(this.steps[this.steps.length - 1].name);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { Flow };
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { Step } = require('./step');
|
|
3
|
+
const { Edge } = require('./edge');
|
|
4
|
+
const { AgentStep } = require('./agent-step');
|
|
5
|
+
const { Agent } = require('../agent');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Graph - Graph-flow execution system
|
|
9
|
+
*/
|
|
10
|
+
class Graph {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
const normalizedOptions = typeof options === 'string' ? { name: options } : (options || {});
|
|
13
|
+
|
|
14
|
+
this.name = normalizedOptions.name || 'graph';
|
|
15
|
+
this.nodes = new Map(); // step name -> step
|
|
16
|
+
this.edges = new Map(); // from -> [Edge]
|
|
17
|
+
this.startNodes = new Set();
|
|
18
|
+
this.compiled = false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load a graph from JSON configuration file
|
|
23
|
+
* @param {string} path - Path to JSON configuration file
|
|
24
|
+
* @returns {Graph} Configured Graph instance
|
|
25
|
+
*
|
|
26
|
+
* JSON format:
|
|
27
|
+
* {
|
|
28
|
+
* "name": "graph-name",
|
|
29
|
+
* "steps": [
|
|
30
|
+
* {
|
|
31
|
+
* "name": "step-name",
|
|
32
|
+
* "execute": "async (context) => { ... }"
|
|
33
|
+
* },
|
|
34
|
+
* {
|
|
35
|
+
* "name": "agent-step-name",
|
|
36
|
+
* "type": "agent",
|
|
37
|
+
* "instruction": "You are a helpful assistant",
|
|
38
|
+
* "tools": [...],
|
|
39
|
+
* "model": "gpt-4",
|
|
40
|
+
* "userPrompt": "template string with {{variable}} placeholders"
|
|
41
|
+
* }
|
|
42
|
+
* ],
|
|
43
|
+
* "edges": [...]
|
|
44
|
+
* }
|
|
45
|
+
*/
|
|
46
|
+
static load(path) {
|
|
47
|
+
const config = JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
48
|
+
const { name, steps = [], edges = [] } = config;
|
|
49
|
+
const graph = new Graph({ name });
|
|
50
|
+
|
|
51
|
+
// Load steps
|
|
52
|
+
for (const stepConfig of steps) {
|
|
53
|
+
const { name: stepName, type, execute, instruction, tools, model, apikey, memory, userPrompt } = stepConfig;
|
|
54
|
+
|
|
55
|
+
let step;
|
|
56
|
+
|
|
57
|
+
if (type === 'agent') {
|
|
58
|
+
// Create agent step
|
|
59
|
+
if (!instruction) {
|
|
60
|
+
throw new Error(`Instruction required for agent step ${stepName}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
step = new AgentStep({
|
|
64
|
+
name: stepName,
|
|
65
|
+
instruction,
|
|
66
|
+
tools,
|
|
67
|
+
model,
|
|
68
|
+
apikey,
|
|
69
|
+
memory,
|
|
70
|
+
userPrompt
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
// Create regular step
|
|
74
|
+
// Handle execute function - can be string or function
|
|
75
|
+
let executeFn;
|
|
76
|
+
if (typeof execute === 'string') {
|
|
77
|
+
// If execute is a string, create a function from it
|
|
78
|
+
try {
|
|
79
|
+
executeFn = new Function('context', `return (${execute})(context);`);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new Error(`Invalid execute function for step ${stepName}: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
} else if (typeof execute === 'function') {
|
|
84
|
+
executeFn = execute;
|
|
85
|
+
} else {
|
|
86
|
+
throw new Error(`Execute must be a function or string for step ${stepName}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
step = new Step({
|
|
90
|
+
name: stepName,
|
|
91
|
+
execute: executeFn
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
graph.step(step);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Load edges
|
|
99
|
+
for (const edgeConfig of edges) {
|
|
100
|
+
const { from, to, condition } = edgeConfig;
|
|
101
|
+
|
|
102
|
+
let conditionFn = null;
|
|
103
|
+
if (condition) {
|
|
104
|
+
if (typeof condition === 'string') {
|
|
105
|
+
try {
|
|
106
|
+
conditionFn = new Function('result', 'context', `return (${condition})(result, context);`);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw new Error(`Invalid condition function for edge ${from}->${to}: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
} else if (typeof condition === 'function') {
|
|
111
|
+
conditionFn = condition;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
graph.edge(from, to, conditionFn ? { when: conditionFn } : {});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return graph;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Add steps to the graph (fluent API)
|
|
123
|
+
* Can add single step or multiple steps
|
|
124
|
+
*/
|
|
125
|
+
step(...steps) {
|
|
126
|
+
for (const step of steps) {
|
|
127
|
+
if (step instanceof Step) {
|
|
128
|
+
this.nodes.set(step.name, step);
|
|
129
|
+
if (!this.edges.has(step.name)) {
|
|
130
|
+
this.edges.set(step.name, []);
|
|
131
|
+
}
|
|
132
|
+
// Mark as potential start node if no incoming edges
|
|
133
|
+
this.startNodes.add(step.name);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Backward-compatible alias for adding a single node.
|
|
141
|
+
*/
|
|
142
|
+
addNode(step) {
|
|
143
|
+
this.step(step);
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Add an edge between steps (fluent API)
|
|
149
|
+
* Supports conditional edges with {when: condition}
|
|
150
|
+
* Can connect to single step or array of steps for parallel execution
|
|
151
|
+
*/
|
|
152
|
+
edge(from, to, options = {}) {
|
|
153
|
+
if (typeof from === 'string') {
|
|
154
|
+
from = this.nodes.get(from);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!from) {
|
|
158
|
+
throw new Error('Source step must exist in the graph');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!this.edges.has(from.name)) {
|
|
162
|
+
this.edges.set(from.name, []);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Handle both single step and array of steps
|
|
166
|
+
const targets = Array.isArray(to) ? to : [to];
|
|
167
|
+
const condition = options.when || null;
|
|
168
|
+
|
|
169
|
+
for (const target of targets) {
|
|
170
|
+
let targetStep = target;
|
|
171
|
+
if (typeof target === 'string') {
|
|
172
|
+
targetStep = this.nodes.get(target);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!targetStep) {
|
|
176
|
+
throw new Error(`Target step ${target} must exist in the graph`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const edge = new Edge(from.name, targetStep.name, condition);
|
|
180
|
+
this.edges.get(from.name).push(edge);
|
|
181
|
+
|
|
182
|
+
// Remove target from start nodes since it has incoming edges
|
|
183
|
+
this.startNodes.delete(targetStep.name);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Backward-compatible alias for adding edges with an optional condition function.
|
|
191
|
+
*/
|
|
192
|
+
addEdge(from, to, conditionOrOptions) {
|
|
193
|
+
if (typeof conditionOrOptions === 'function') {
|
|
194
|
+
return this.edge(from, to, { when: conditionOrOptions });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return this.edge(from, to, conditionOrOptions || {});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Compile the graph (fluent API)
|
|
202
|
+
*/
|
|
203
|
+
compile() {
|
|
204
|
+
this.compiled = true;
|
|
205
|
+
console.log(`[${this.name}] Graph compiled with ${this.nodes.size} steps`);
|
|
206
|
+
return this;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Run the graph with initial context
|
|
211
|
+
*/
|
|
212
|
+
async run(initialContext = {}) {
|
|
213
|
+
if (!this.compiled) {
|
|
214
|
+
throw new Error('Graph must be compiled before running. Call .compile() first.');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(`[${this.name}] Starting graph execution`);
|
|
218
|
+
|
|
219
|
+
const context = {
|
|
220
|
+
...initialContext,
|
|
221
|
+
executedSteps: new Set(),
|
|
222
|
+
results: new Map(),
|
|
223
|
+
pendingNodes: new Set(this.startNodes)
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const maxIterations = 1000; // Prevent infinite loops
|
|
227
|
+
let iterations = 0;
|
|
228
|
+
|
|
229
|
+
while (context.pendingNodes.size > 0 && iterations < maxIterations) {
|
|
230
|
+
iterations++;
|
|
231
|
+
const currentNodeName = Array.from(context.pendingNodes)[0];
|
|
232
|
+
context.pendingNodes.delete(currentNodeName);
|
|
233
|
+
|
|
234
|
+
if (context.executedSteps.has(currentNodeName)) {
|
|
235
|
+
continue; // Already executed
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const step = this.nodes.get(currentNodeName);
|
|
239
|
+
if (!step) {
|
|
240
|
+
throw new Error(`Step ${currentNodeName} not found in graph`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check if all dependencies are satisfied
|
|
244
|
+
const dependencies = this.getDependencies(currentNodeName);
|
|
245
|
+
const depsSatisfied = dependencies.every(dep => context.executedSteps.has(dep));
|
|
246
|
+
|
|
247
|
+
if (!depsSatisfied) {
|
|
248
|
+
// Put back in pending and try later
|
|
249
|
+
context.pendingNodes.add(currentNodeName);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Execute the step
|
|
254
|
+
console.log(`[${this.name}] Executing step: ${currentNodeName}`);
|
|
255
|
+
const result = await step.run(context, context);
|
|
256
|
+
|
|
257
|
+
// Store result in context
|
|
258
|
+
context[currentNodeName] = result;
|
|
259
|
+
context.results.set(currentNodeName, result);
|
|
260
|
+
context.executedSteps.add(currentNodeName);
|
|
261
|
+
|
|
262
|
+
// Determine next nodes to execute
|
|
263
|
+
const outgoingEdges = this.edges.get(currentNodeName) || [];
|
|
264
|
+
for (const edge of outgoingEdges) {
|
|
265
|
+
if (edge.shouldFollow(result, context)) {
|
|
266
|
+
context.pendingNodes.add(edge.to);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (iterations >= maxIterations) {
|
|
272
|
+
throw new Error('Graph execution exceeded maximum iterations (possible infinite loop)');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log(`[${this.name}] Graph execution completed`);
|
|
276
|
+
return this.getFinalResults(context);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Backward-compatible execute entrypoint.
|
|
281
|
+
* Supports execute(startNodes, initialContext) and execute(initialContext).
|
|
282
|
+
*/
|
|
283
|
+
async execute(startNodesOrContext = [], maybeInitialContext = {}) {
|
|
284
|
+
let startNodes = [];
|
|
285
|
+
let initialContext = maybeInitialContext;
|
|
286
|
+
|
|
287
|
+
if (Array.isArray(startNodesOrContext)) {
|
|
288
|
+
startNodes = startNodesOrContext;
|
|
289
|
+
} else {
|
|
290
|
+
initialContext = startNodesOrContext || {};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (startNodes.length > 0) {
|
|
294
|
+
this.startNodes = new Set(startNodes);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!this.compiled) {
|
|
298
|
+
this.compile();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return this.run(initialContext);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get dependencies for a node
|
|
306
|
+
*/
|
|
307
|
+
getDependencies(nodeName) {
|
|
308
|
+
const dependencies = [];
|
|
309
|
+
for (const [fromNode, edges] of this.edges) {
|
|
310
|
+
for (const edge of edges) {
|
|
311
|
+
if (edge.to === nodeName) {
|
|
312
|
+
dependencies.push(fromNode);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return dependencies;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get final results from graph execution
|
|
321
|
+
*/
|
|
322
|
+
getFinalResults(context) {
|
|
323
|
+
const results = {};
|
|
324
|
+
for (const [nodeName, result] of context.results) {
|
|
325
|
+
results[nodeName] = result;
|
|
326
|
+
}
|
|
327
|
+
return results;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
module.exports = { Graph };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step - Basic unit of work in a graph-flow system
|
|
3
|
+
* A step executes a function with the current context
|
|
4
|
+
*/
|
|
5
|
+
class Step {
|
|
6
|
+
constructor(nameOrOptions = {}, config = {}) {
|
|
7
|
+
const options = typeof nameOrOptions === 'string'
|
|
8
|
+
? { ...config, name: nameOrOptions }
|
|
9
|
+
: (nameOrOptions || {});
|
|
10
|
+
|
|
11
|
+
this.name = options.name;
|
|
12
|
+
this.dependsOn = Array.isArray(options.dependsOn) ? options.dependsOn : [];
|
|
13
|
+
|
|
14
|
+
const executeFn = typeof options.execute === 'function'
|
|
15
|
+
? options.execute
|
|
16
|
+
: (typeof options.processor === 'function'
|
|
17
|
+
? options.processor
|
|
18
|
+
: async () => ({}));
|
|
19
|
+
|
|
20
|
+
this._execute = executeFn;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check whether this step can execute with current flow context.
|
|
25
|
+
* If no dependencies are configured, the step can always execute.
|
|
26
|
+
*/
|
|
27
|
+
canExecute(flowContext = {}) {
|
|
28
|
+
if (this.dependsOn.length === 0) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const executedSteps = flowContext.executedSteps;
|
|
33
|
+
if (!(executedSteps instanceof Set)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return this.dependsOn.every((dependency) => executedSteps.has(dependency));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Execute this step with the given context and inputs
|
|
42
|
+
*/
|
|
43
|
+
async execute(context = {}, inputs = {}) {
|
|
44
|
+
return this._execute(context, inputs);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run with logging and error handling
|
|
49
|
+
*/
|
|
50
|
+
async run(context = {}, inputs = context) {
|
|
51
|
+
try {
|
|
52
|
+
console.log(`[${this.name}] Executing step`);
|
|
53
|
+
const result = await this.execute(context, inputs);
|
|
54
|
+
console.log(`[${this.name}] Step completed`);
|
|
55
|
+
return result;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(`[${this.name}] Step failed:`, error.message);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { Step };
|