node-red-contrib-ai-agent 0.4.0 → 0.5.0
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 +20 -12
- package/agent/ai-agent.html +95 -95
- package/agent/ai-agent.js +326 -326
- package/model/ai-model.html +260 -141
- package/orchestrator/orchestrator.html +2 -2
- package/orchestrator/orchestrator.js +98 -26
- package/package.json +4 -2
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* AI Orchestrator Node - Manages multi-agent task execution with planning and reflection
|
|
5
|
+
* @param {Object} config - Node configuration object
|
|
6
|
+
* @param {string} config.name - Node name
|
|
7
|
+
* @param {number} config.maxIterations - Maximum number of planning/execution iterations
|
|
8
|
+
* @param {string} config.planningStrategy - Strategy for plan creation ('simple' or 'advanced')
|
|
9
|
+
* @param {string} config.defaultGoal - Default goal if none provided in message
|
|
10
|
+
*/
|
|
3
11
|
module.exports = function (RED) {
|
|
4
12
|
function AiOrchestratorNode(config) {
|
|
5
13
|
RED.nodes.createNode(this, config);
|
|
@@ -12,16 +20,21 @@ module.exports = function (RED) {
|
|
|
12
20
|
|
|
13
21
|
node.on('input', async function (msg, send, done) {
|
|
14
22
|
send = send || function () { node.send.apply(node, arguments) };
|
|
15
|
-
node.status({ fill: 'blue', shape: 'dot', text: 'thinking...' });
|
|
16
23
|
|
|
17
24
|
try {
|
|
18
25
|
// Initialize orchestration state if not present
|
|
19
26
|
if (!msg.orchestration) {
|
|
27
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'initializing team...' });
|
|
28
|
+
|
|
29
|
+
// Pipeline Discovery: Extract agents from upstream chain
|
|
30
|
+
const availableAgents = msg.agents || [];
|
|
31
|
+
|
|
20
32
|
msg.orchestration = {
|
|
21
33
|
planId: 'plan-' + Date.now(),
|
|
22
34
|
iterations: 0,
|
|
23
35
|
goal: msg.payload || node.defaultGoal,
|
|
24
36
|
status: 'planning',
|
|
37
|
+
availableAgents: availableAgents,
|
|
25
38
|
history: [],
|
|
26
39
|
plan: null
|
|
27
40
|
};
|
|
@@ -35,7 +48,7 @@ module.exports = function (RED) {
|
|
|
35
48
|
msg.orchestration.status = 'failed';
|
|
36
49
|
msg.orchestration.error = 'Max iterations reached';
|
|
37
50
|
node.status({ fill: 'red', shape: 'dot', text: 'max iterations' });
|
|
38
|
-
send(
|
|
51
|
+
send(msg);
|
|
39
52
|
if (done) done();
|
|
40
53
|
return;
|
|
41
54
|
}
|
|
@@ -45,32 +58,61 @@ module.exports = function (RED) {
|
|
|
45
58
|
throw new Error('AI Model configuration missing or API key not found.');
|
|
46
59
|
}
|
|
47
60
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
await createInitialPlan(node, msg);
|
|
51
|
-
} else if (msg.orchestration.currentTaskId) {
|
|
52
|
-
await reflectAndRefine(node, msg);
|
|
53
|
-
}
|
|
61
|
+
// Inner loop for Zero-Wire execution
|
|
62
|
+
while (msg.orchestration.status !== 'completed' && msg.orchestration.status !== 'failed') {
|
|
54
63
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
// 1. Planning Phase
|
|
65
|
+
if (msg.orchestration.status === 'planning' || !msg.orchestration.plan) {
|
|
66
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'planning...' });
|
|
67
|
+
await createInitialPlan(node, msg);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 2. Find Next Task
|
|
60
71
|
const nextTask = getNextTask(msg.orchestration.plan);
|
|
61
|
-
if (nextTask) {
|
|
62
|
-
msg.payload = nextTask.input;
|
|
63
|
-
msg.topic = nextTask.type;
|
|
64
|
-
msg.orchestration.currentTaskId = nextTask.id;
|
|
65
|
-
node.status({ fill: 'blue', shape: 'ring', text: `dispatching: ${nextTask.id}` });
|
|
66
|
-
send([msg, null]); // Output 1
|
|
67
|
-
} else {
|
|
72
|
+
if (!nextTask) {
|
|
68
73
|
msg.orchestration.status = 'completed';
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 3. Execution Phase (Direct Call)
|
|
78
|
+
msg.orchestration.currentTaskId = nextTask.id;
|
|
79
|
+
const agentInfo = msg.orchestration.availableAgents.find(a =>
|
|
80
|
+
a.capabilities.some(cap => cap.toLowerCase() === nextTask.type.toLowerCase())
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (!agentInfo) {
|
|
84
|
+
node.warn(`No registered agent found for capability: ${nextTask.type}`);
|
|
85
|
+
throw new Error(`Capability not provided by any wired agent: ${nextTask.type}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const agentNode = RED.nodes.getNode(agentInfo.id);
|
|
89
|
+
if (!agentNode || typeof agentNode.executeTask !== 'function') {
|
|
90
|
+
throw new Error(`Agent node ${agentInfo.name} [${agentInfo.id}] is not an AI Agent Orchestrator or is missing executeTask API.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
node.status({ fill: 'blue', shape: 'ring', text: `agent: ${agentInfo.name}` });
|
|
94
|
+
try {
|
|
95
|
+
const result = await agentNode.executeTask(nextTask.input, msg);
|
|
96
|
+
msg.payload = result;
|
|
97
|
+
msg.error = null;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// Strip 'AI API Error: ' prefix if present to match test expectations
|
|
100
|
+
let errorMessage = err.message;
|
|
101
|
+
if (errorMessage.startsWith('AI API Error: ')) {
|
|
102
|
+
errorMessage = errorMessage.substring('AI API Error: '.length);
|
|
103
|
+
}
|
|
104
|
+
msg.error = errorMessage;
|
|
71
105
|
}
|
|
106
|
+
|
|
107
|
+
// 4. Reflection Phase
|
|
108
|
+
node.status({ fill: 'blue', shape: 'dot', text: 'reflecting...' });
|
|
109
|
+
await reflectAndRefine(node, msg);
|
|
72
110
|
}
|
|
73
111
|
|
|
112
|
+
// Final Output
|
|
113
|
+
node.status({ fill: 'green', shape: 'dot', text: msg.orchestration.status });
|
|
114
|
+
send(msg);
|
|
115
|
+
|
|
74
116
|
if (done) done();
|
|
75
117
|
} catch (error) {
|
|
76
118
|
node.status({ fill: 'red', shape: 'ring', text: 'error' });
|
|
@@ -80,20 +122,27 @@ module.exports = function (RED) {
|
|
|
80
122
|
});
|
|
81
123
|
}
|
|
82
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Creates an initial execution plan using AI
|
|
127
|
+
* @param {Object} node - The orchestrator node instance
|
|
128
|
+
* @param {Object} msg - The message object containing orchestration state
|
|
129
|
+
* @throws {Error} If planning fails
|
|
130
|
+
*/
|
|
83
131
|
async function createInitialPlan(node, msg) {
|
|
84
132
|
const goal = msg.orchestration.goal;
|
|
85
133
|
const strategy = node.planningStrategy;
|
|
134
|
+
const agents = msg.orchestration.availableAgents || [];
|
|
135
|
+
const agentManifest = agents.map(a => `- ${a.name}: [${a.capabilities.join(', ')}]`).join('\n');
|
|
86
136
|
|
|
87
|
-
let prompt = `Goal: ${goal}\n\nDecompose this goal into a series of tasks
|
|
137
|
+
let prompt = `Goal: ${goal}\n\nAvailable Agents and their Capabilities:\n${agentManifest}\n\nDecompose this goal into a series of tasks. You MUST ONLY use capabilities provided by the available agents listed above.
|
|
88
138
|
Return a JSON object with a "tasks" array. Each task should have:
|
|
89
139
|
- "id": a short string id (e.g., "t1", "t2")
|
|
90
|
-
- "type": the
|
|
140
|
+
- "type": the name of the REQUIRED capability from the list above
|
|
91
141
|
- "input": detailed instruction for the agent
|
|
92
142
|
- "status": "pending"
|
|
93
143
|
- "priority": a number (1-10, default 5)
|
|
94
144
|
- "dependsOn": an array of IDs of tasks that must be completed BEFORE this task can start (empty array if none)
|
|
95
|
-
|
|
96
|
-
Note: You can use "human_approval" as a task type if you need a human to verify something before proceeding.`;
|
|
145
|
+
`;
|
|
97
146
|
|
|
98
147
|
if (strategy === 'advanced') {
|
|
99
148
|
prompt += `\n\nThink about parallel execution. Group related tasks and identify bottlenecks. Ensure dependencies are logical.`;
|
|
@@ -117,6 +166,11 @@ Note: You can use "human_approval" as a task type if you need a human to verify
|
|
|
117
166
|
}
|
|
118
167
|
}
|
|
119
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Reflects on task execution results and refines the plan
|
|
171
|
+
* @param {Object} node - The orchestrator node instance
|
|
172
|
+
* @param {Object} msg - The message object containing orchestration state and task results
|
|
173
|
+
*/
|
|
120
174
|
async function reflectAndRefine(node, msg) {
|
|
121
175
|
const currentTaskId = msg.orchestration.currentTaskId;
|
|
122
176
|
const taskResult = msg.payload;
|
|
@@ -174,6 +228,11 @@ Return a JSON object:
|
|
|
174
228
|
}
|
|
175
229
|
}
|
|
176
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Gets the next executable task from the plan based on dependencies and priority
|
|
233
|
+
* @param {Object} plan - The execution plan containing tasks
|
|
234
|
+
* @returns {Object|null} The next task to execute or null if no eligible tasks
|
|
235
|
+
*/
|
|
177
236
|
function getNextTask(plan) {
|
|
178
237
|
if (!plan || !plan.tasks) return null;
|
|
179
238
|
|
|
@@ -202,6 +261,14 @@ Return a JSON object:
|
|
|
202
261
|
return eligibleTasks[0];
|
|
203
262
|
}
|
|
204
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Makes an API call to the AI model
|
|
266
|
+
* @param {Object} aiConfig - AI configuration containing model and API key
|
|
267
|
+
* @param {string} prompt - The user prompt to send
|
|
268
|
+
* @param {string} systemPrompt - The system prompt for context
|
|
269
|
+
* @returns {Promise<string>} The AI response content
|
|
270
|
+
* @throws {Error} If API call fails
|
|
271
|
+
*/
|
|
205
272
|
async function callAI(aiConfig, prompt, systemPrompt) {
|
|
206
273
|
const response = await axios.post(
|
|
207
274
|
'https://openrouter.ai/api/v1/chat/completions',
|
|
@@ -223,6 +290,11 @@ Return a JSON object:
|
|
|
223
290
|
return response.data.choices[0]?.message?.content || '';
|
|
224
291
|
}
|
|
225
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Extracts JSON from a text response
|
|
295
|
+
* @param {string} text - The text containing JSON
|
|
296
|
+
* @returns {string} The extracted JSON string
|
|
297
|
+
*/
|
|
226
298
|
function extractJson(text) {
|
|
227
299
|
const match = text.match(/\{[\s\S]*\}/);
|
|
228
300
|
return match ? match[0] : text;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-ai-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "AI Agent for Node-RED",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
],
|
|
16
16
|
"main": "ai-agent.js",
|
|
17
17
|
"scripts": {
|
|
18
|
-
"test": "mocha \"test/**/*.js\""
|
|
18
|
+
"test": "mocha \"test/**/*.js\"",
|
|
19
|
+
"update-models": "python scripts/update_models.py"
|
|
19
20
|
},
|
|
20
21
|
"author": "Milan Lesichkov",
|
|
21
22
|
"maintainers": [
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
"ai-memory-file": "./memory-file/memory-file.js",
|
|
64
65
|
"ai-memory-inmem": "./memory-inmem/memory-inmem.js",
|
|
65
66
|
"ai-orchestrator": "./orchestrator/orchestrator.js",
|
|
67
|
+
"ai-agent-orchestrator": "./agent-orchestrator/agent-orchestrator.js",
|
|
66
68
|
"ai-tool-approval": "./tool-approval/ai-tool-approval.js"
|
|
67
69
|
}
|
|
68
70
|
}
|