node-red-contrib-ai-agent 0.5.11 → 0.5.12
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/orchestrator/orchestrator.js +83 -24
- package/package.json +1 -1
|
@@ -76,32 +76,32 @@ module.exports = function (RED) {
|
|
|
76
76
|
|
|
77
77
|
// 3. Execution Phase (Direct Call)
|
|
78
78
|
msg.orchestration.currentTaskId = nextTask.id;
|
|
79
|
-
const agentInfo = msg.orchestration
|
|
80
|
-
a.capabilities.some(cap => cap.toLowerCase() === nextTask.type.toLowerCase())
|
|
81
|
-
);
|
|
79
|
+
const agentInfo = selectAgentForTask(msg.orchestration, nextTask);
|
|
82
80
|
|
|
83
81
|
if (!agentInfo) {
|
|
84
82
|
node.warn(`No registered agent found for capability: ${nextTask.type}`);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
83
|
+
msg.error = `Capability not provided by any wired agent: ${nextTask.type}`;
|
|
84
|
+
msg.payload = null;
|
|
85
|
+
} else {
|
|
86
|
+
const agentNode = RED.nodes.getNode(agentInfo.id);
|
|
87
|
+
if (!agentNode || typeof agentNode.executeTask !== 'function') {
|
|
88
|
+
msg.error = `Agent node ${agentInfo.name} [${agentInfo.id}] is not an AI Orchestrator Agent or is missing executeTask API.`;
|
|
89
|
+
msg.payload = null;
|
|
90
|
+
} else {
|
|
91
|
+
node.status({ fill: 'blue', shape: 'ring', text: `agent: ${agentInfo.name}` });
|
|
92
|
+
try {
|
|
93
|
+
const result = await agentNode.executeTask(nextTask.input, msg);
|
|
94
|
+
msg.payload = result;
|
|
95
|
+
msg.error = null;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// Strip 'AI API Error: ' prefix if present to match test expectations
|
|
98
|
+
let errorMessage = err.message;
|
|
99
|
+
if (errorMessage.startsWith('AI API Error: ')) {
|
|
100
|
+
errorMessage = errorMessage.substring('AI API Error: '.length);
|
|
101
|
+
}
|
|
102
|
+
msg.error = errorMessage;
|
|
103
|
+
}
|
|
103
104
|
}
|
|
104
|
-
msg.error = errorMessage;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// 4. Reflection Phase
|
|
@@ -133,6 +133,10 @@ module.exports = function (RED) {
|
|
|
133
133
|
const strategy = node.planningStrategy;
|
|
134
134
|
const agents = msg.orchestration.availableAgents || [];
|
|
135
135
|
const agentManifest = agents.map(a => `- ${a.name}: [${a.capabilities.join(', ')}]`).join('\n');
|
|
136
|
+
const allowedCapabilities = Array.from(new Set(
|
|
137
|
+
agents.flatMap(a => Array.isArray(a.capabilities) ? a.capabilities : []).map(String)
|
|
138
|
+
));
|
|
139
|
+
const allowedCapabilitiesJson = JSON.stringify(allowedCapabilities);
|
|
136
140
|
|
|
137
141
|
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.
|
|
138
142
|
Return a JSON object with a "tasks" array. Each task should have:
|
|
@@ -154,6 +158,11 @@ Return a JSON object with a "tasks" array. Each task should have:
|
|
|
154
158
|
- Do NOT include trailing commas
|
|
155
159
|
- All string values must be valid JSON strings (escape newlines as \\n if needed)
|
|
156
160
|
|
|
161
|
+
CAPABILITY RULES:
|
|
162
|
+
- The "type" field MUST be one of these EXACT strings (case-sensitive): ${allowedCapabilitiesJson}
|
|
163
|
+
- Do NOT invent new capabilities.
|
|
164
|
+
- If the goal seems to require a missing capability, still produce a plan using ONLY the allowed capabilities, and include a task whose input explains the limitation.
|
|
165
|
+
|
|
157
166
|
Example:
|
|
158
167
|
{
|
|
159
168
|
"tasks": [
|
|
@@ -181,8 +190,8 @@ Example:
|
|
|
181
190
|
*/
|
|
182
191
|
async function reflectAndRefine(node, msg) {
|
|
183
192
|
const currentTaskId = msg.orchestration.currentTaskId;
|
|
193
|
+
const isError = !!msg.error;
|
|
184
194
|
const taskResult = msg.payload;
|
|
185
|
-
const isError = msg.error ? true : false;
|
|
186
195
|
|
|
187
196
|
// Update history
|
|
188
197
|
msg.orchestration.history.push({
|
|
@@ -204,17 +213,38 @@ Example:
|
|
|
204
213
|
}
|
|
205
214
|
}
|
|
206
215
|
|
|
216
|
+
const hasHumanApprovalCapability = (msg.orchestration.availableAgents || []).some(a =>
|
|
217
|
+
Array.isArray(a.capabilities) && a.capabilities.some(c => String(c).toLowerCase() === 'human_approval')
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const allowedCapabilities = Array.from(new Set(
|
|
221
|
+
(msg.orchestration.availableAgents || [])
|
|
222
|
+
.flatMap(a => Array.isArray(a.capabilities) ? a.capabilities : [])
|
|
223
|
+
.map(String)
|
|
224
|
+
));
|
|
225
|
+
const allowedCapabilitiesJson = JSON.stringify(allowedCapabilities);
|
|
226
|
+
|
|
227
|
+
const humanApprovalInstruction = hasHumanApprovalCapability
|
|
228
|
+
? '3. If you need more information or approval from a human, add a task with type "human_approval".'
|
|
229
|
+
: '3. Do NOT request human approval tasks ("human_approval" is not available).';
|
|
230
|
+
|
|
207
231
|
const prompt = `Current Goal: ${msg.orchestration.goal}
|
|
208
232
|
Current Plan: ${JSON.stringify(msg.orchestration.plan)}
|
|
209
233
|
Last Task ID: ${currentTaskId}
|
|
210
234
|
Last Task ${isError ? 'Error' : 'Result'}: ${JSON.stringify(isError ? msg.error : taskResult)}
|
|
211
235
|
|
|
236
|
+
Available Capabilities (EXACT strings): ${allowedCapabilitiesJson}
|
|
237
|
+
|
|
212
238
|
Evaluate the progress.
|
|
213
239
|
1. If the last task failed, propose a recovery strategy (retry, alternative task, or fail the goal).
|
|
214
240
|
2. If the goal is achieved, set status to "completed".
|
|
215
|
-
|
|
241
|
+
${humanApprovalInstruction}
|
|
216
242
|
4. Otherwise, continue execution. You may refine the plan by adding, removing, or modifying tasks.
|
|
217
243
|
|
|
244
|
+
PLAN UPDATE RULES:
|
|
245
|
+
- In updatedPlan.tasks, every task.type MUST be one of the EXACT capability strings listed above.
|
|
246
|
+
- Do NOT invent or rename capabilities.
|
|
247
|
+
|
|
218
248
|
Return a JSON object:
|
|
219
249
|
{
|
|
220
250
|
"analysis": "detailed evaluation of progress and next steps",
|
|
@@ -269,6 +299,35 @@ Return a JSON object:
|
|
|
269
299
|
return eligibleTasks[0];
|
|
270
300
|
}
|
|
271
301
|
|
|
302
|
+
function selectAgentForTask(orchestration, task) {
|
|
303
|
+
if (!orchestration || !task || !task.type) return null;
|
|
304
|
+
const availableAgents = orchestration.availableAgents || [];
|
|
305
|
+
const taskType = String(task.type).toLowerCase();
|
|
306
|
+
|
|
307
|
+
const matchingAgents = availableAgents.filter(a =>
|
|
308
|
+
Array.isArray(a.capabilities) && a.capabilities.some(cap => String(cap).toLowerCase() === taskType)
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
if (matchingAgents.length === 0) return null;
|
|
312
|
+
if (matchingAgents.length === 1) return matchingAgents[0];
|
|
313
|
+
|
|
314
|
+
orchestration.agentUsage = orchestration.agentUsage || {};
|
|
315
|
+
orchestration.agentLastUsedAt = orchestration.agentLastUsedAt || {};
|
|
316
|
+
|
|
317
|
+
// Prefer least recently used among matching agents to avoid always picking the first.
|
|
318
|
+
matchingAgents.sort((a, b) => {
|
|
319
|
+
const aLast = orchestration.agentLastUsedAt[a.id] || 0;
|
|
320
|
+
const bLast = orchestration.agentLastUsedAt[b.id] || 0;
|
|
321
|
+
if (aLast !== bLast) return aLast - bLast;
|
|
322
|
+
return String(a.id).localeCompare(String(b.id));
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const selected = matchingAgents[0];
|
|
326
|
+
orchestration.agentUsage[selected.id] = (orchestration.agentUsage[selected.id] || 0) + 1;
|
|
327
|
+
orchestration.agentLastUsedAt[selected.id] = Date.now();
|
|
328
|
+
return selected;
|
|
329
|
+
}
|
|
330
|
+
|
|
272
331
|
/**
|
|
273
332
|
* Makes an API call to the AI model
|
|
274
333
|
* @param {Object} aiConfig - AI configuration containing model and API key
|