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.
@@ -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([null, msg]); // Output 2 for final result
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
- // Logic based on current status
49
- if (msg.orchestration.status === 'planning' || !msg.orchestration.plan) {
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
- // Dispatch or Finalize
56
- if (msg.orchestration.status === 'completed' || msg.orchestration.status === 'failed') {
57
- node.status({ fill: 'green', shape: 'dot', text: msg.orchestration.status });
58
- send([null, msg]); // Output 2
59
- } else {
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
- node.status({ fill: 'green', shape: 'dot', text: 'completed' });
70
- send([null, msg]); // Output 2
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 for AI agents.
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 type of task (e.g., "research", "code", "review")
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.4.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
  }