node-red-contrib-ai-agent 0.5.15 → 0.5.17
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.
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
debug: { value: false }
|
|
14
14
|
},
|
|
15
15
|
inputs: 1,
|
|
16
|
-
outputs:
|
|
17
|
-
outputLabels: ["
|
|
16
|
+
outputs: 2,
|
|
17
|
+
outputLabels: ["Success", "Failure"],
|
|
18
18
|
icon: "font-awesome/fa-sitemap",
|
|
19
19
|
label: function () {
|
|
20
20
|
return this.name || "AI Orchestrator";
|
|
@@ -76,16 +76,20 @@
|
|
|
76
76
|
|
|
77
77
|
<h3>Outputs</h3>
|
|
78
78
|
<ol class="node-ports">
|
|
79
|
-
<li>
|
|
79
|
+
<li>Success
|
|
80
80
|
<dl class="message-properties">
|
|
81
|
-
<dt>msg.payload <span class="property-type">string</span></dt>
|
|
82
|
-
<dd>The
|
|
81
|
+
<dt>msg.payload <span class="property-type">string | object</span></dt>
|
|
82
|
+
<dd>The final successful result once the plan completes.</dd>
|
|
83
|
+
<dt>msg.orchestration <span class="property-type">object</span></dt>
|
|
84
|
+
<dd>Complete orchestration metadata and history.</dd>
|
|
83
85
|
</dl>
|
|
84
86
|
</li>
|
|
85
|
-
<li>
|
|
87
|
+
<li>Failure
|
|
86
88
|
<dl class="message-properties">
|
|
89
|
+
<dt>msg.error <span class="property-type">string</span></dt>
|
|
90
|
+
<dd>Error message explaining why orchestration failed.</dd>
|
|
87
91
|
<dt>msg.orchestration <span class="property-type">object</span></dt>
|
|
88
|
-
<dd>
|
|
92
|
+
<dd>Orchestration state including failure details.</dd>
|
|
89
93
|
</dl>
|
|
90
94
|
</li>
|
|
91
95
|
</ol>
|
|
@@ -98,5 +102,5 @@
|
|
|
98
102
|
<li><strong>Priorities:</strong> Tasks with higher <code>priority</code> numbers are executed first.</li>
|
|
99
103
|
<li><strong>Error Recovery:</strong> If a task fails, the orchestrator reflects on the error and can revise the plan to retry or try an alternative approach.</li>
|
|
100
104
|
</ul>
|
|
101
|
-
<p>
|
|
105
|
+
<p>The first output fires when the orchestration succeeds. All failure paths (validation errors, missing agents, iteration limits, plan errors, etc.) send a message out of the second output with <code>msg.error</code> describing the issue.</p>
|
|
102
106
|
</script>
|
|
@@ -64,9 +64,11 @@ module.exports = function (RED) {
|
|
|
64
64
|
msg.orchestration._running = true;
|
|
65
65
|
setImmediate(() => processNextStep(RED, node, msg, send, done));
|
|
66
66
|
} catch (error) {
|
|
67
|
-
node.status({ fill: 'red', shape: 'ring', text: 'error' });
|
|
68
67
|
node.error(error.message, msg);
|
|
69
|
-
|
|
68
|
+
msg.orchestration = msg.orchestration || {};
|
|
69
|
+
msg.orchestration.status = 'failed';
|
|
70
|
+
msg.orchestration.error = error && error.message ? error.message : String(error);
|
|
71
|
+
finalizeOrchestration(node, msg, send, done, error);
|
|
70
72
|
}
|
|
71
73
|
});
|
|
72
74
|
}
|
|
@@ -155,9 +157,25 @@ module.exports = function (RED) {
|
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
function finalizeOrchestration(node, msg, send, done, error) {
|
|
160
|
+
send = send || function () { node.send.apply(node, arguments) };
|
|
161
|
+
msg.orchestration = msg.orchestration || {};
|
|
158
162
|
msg.orchestration._running = false;
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
|
|
164
|
+
const isSuccess = msg.orchestration.status === 'completed';
|
|
165
|
+
if (!isSuccess) {
|
|
166
|
+
const errorMessage = msg.orchestration.error || (error && error.message) || msg.error || 'Unknown error';
|
|
167
|
+
msg.error = errorMessage;
|
|
168
|
+
msg.orchestration.error = errorMessage;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
node.status({
|
|
172
|
+
fill: isSuccess ? 'green' : 'red',
|
|
173
|
+
shape: 'dot',
|
|
174
|
+
text: msg.orchestration.status || (isSuccess ? 'completed' : 'failed')
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const outputs = isSuccess ? [msg, null] : [null, msg];
|
|
178
|
+
send(outputs);
|
|
161
179
|
if (done) done(error);
|
|
162
180
|
}
|
|
163
181
|
|
|
@@ -216,6 +234,14 @@ Example:
|
|
|
216
234
|
const response = await callAI(node, msg.aiagent, prompt, "You are an AI Orchestrator that creates non-linear plans with dependencies.");
|
|
217
235
|
debugLog(node, 'Planning Response', response);
|
|
218
236
|
const planData = parseJsonResponse(response);
|
|
237
|
+
|
|
238
|
+
if (!planData || typeof planData !== 'object') {
|
|
239
|
+
throw new Error('Planning response was not valid JSON.');
|
|
240
|
+
}
|
|
241
|
+
if (!Array.isArray(planData.tasks)) {
|
|
242
|
+
throw new Error('Planning response must include a tasks array.');
|
|
243
|
+
}
|
|
244
|
+
|
|
219
245
|
msg.orchestration.plan = planData;
|
|
220
246
|
msg.orchestration.status = 'executing';
|
|
221
247
|
} catch (error) {
|
|
@@ -303,6 +329,10 @@ Return a JSON object:
|
|
|
303
329
|
const response = await callAI(node, msg.aiagent, prompt, "You are an AI Orchestrator that reflects on progress and manages plan revisions.");
|
|
304
330
|
const reflection = parseJsonResponse(response);
|
|
305
331
|
|
|
332
|
+
if (!reflection || typeof reflection !== 'object') {
|
|
333
|
+
throw new Error('Reflection response was not valid JSON.');
|
|
334
|
+
}
|
|
335
|
+
|
|
306
336
|
msg.orchestration.status = reflection.status;
|
|
307
337
|
if (reflection.updatedPlan) {
|
|
308
338
|
msg.orchestration.plan = reflection.updatedPlan;
|
|
@@ -425,23 +455,89 @@ Return a JSON object:
|
|
|
425
455
|
* @returns {string} The extracted JSON string
|
|
426
456
|
*/
|
|
427
457
|
function extractJson(text) {
|
|
458
|
+
if (typeof text !== 'string') return '';
|
|
459
|
+
|
|
460
|
+
// Prefer a balanced-brace extraction to avoid greedy matching issues.
|
|
461
|
+
const extracted = extractBalancedJsonObject(text);
|
|
462
|
+
if (extracted) return extracted;
|
|
463
|
+
|
|
464
|
+
// Fallback to a simple greedy match if needed.
|
|
428
465
|
const match = text.match(/\{[\s\S]*\}/);
|
|
429
466
|
return match ? match[0] : text;
|
|
430
467
|
}
|
|
431
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Extract the first balanced JSON object (starting at the first '{') from arbitrary text.
|
|
471
|
+
* @param {string} text - The input text
|
|
472
|
+
* @returns {string} Extracted JSON object text or empty string if not found
|
|
473
|
+
*/
|
|
474
|
+
function extractBalancedJsonObject(text) {
|
|
475
|
+
const start = text.indexOf('{');
|
|
476
|
+
if (start === -1) return '';
|
|
477
|
+
|
|
478
|
+
let depth = 0;
|
|
479
|
+
let inString = false;
|
|
480
|
+
let escape = false;
|
|
481
|
+
|
|
482
|
+
for (let i = start; i < text.length; i++) {
|
|
483
|
+
const ch = text[i];
|
|
484
|
+
|
|
485
|
+
if (inString) {
|
|
486
|
+
if (escape) {
|
|
487
|
+
escape = false;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (ch === '\\') {
|
|
491
|
+
escape = true;
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (ch === '"') {
|
|
495
|
+
inString = false;
|
|
496
|
+
}
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (ch === '"') {
|
|
501
|
+
inString = true;
|
|
502
|
+
escape = false;
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (ch === '{') {
|
|
507
|
+
depth++;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (ch === '}') {
|
|
512
|
+
depth--;
|
|
513
|
+
if (depth === 0) {
|
|
514
|
+
return text.slice(start, i + 1);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return '';
|
|
520
|
+
}
|
|
521
|
+
|
|
432
522
|
/**
|
|
433
523
|
* Parses a JSON object from an AI response, tolerating common non-JSON wrappers.
|
|
524
|
+
* Returns an empty string if parsing fails after sanitization attempts.
|
|
434
525
|
* @param {string} text - The AI response
|
|
435
|
-
* @returns {any} Parsed JSON
|
|
526
|
+
* @returns {any|string} Parsed JSON or empty string on failure
|
|
436
527
|
*/
|
|
437
528
|
function parseJsonResponse(text) {
|
|
438
529
|
const extracted = extractJson(text);
|
|
439
530
|
try {
|
|
440
531
|
return JSON.parse(extracted);
|
|
441
|
-
} catch (
|
|
532
|
+
} catch (err1) {
|
|
442
533
|
const sanitized = sanitizeJsonLikeText(extracted);
|
|
443
|
-
|
|
534
|
+
try {
|
|
535
|
+
return JSON.parse(sanitized);
|
|
536
|
+
} catch (err2) {
|
|
537
|
+
// no need to catch anything, default to empty string
|
|
538
|
+
}
|
|
444
539
|
}
|
|
540
|
+
return "";
|
|
445
541
|
}
|
|
446
542
|
|
|
447
543
|
/**
|
|
@@ -528,7 +624,62 @@ Return a JSON object:
|
|
|
528
624
|
out += ch;
|
|
529
625
|
}
|
|
530
626
|
|
|
531
|
-
return out.trim();
|
|
627
|
+
return removeTrailingCommas(out).trim();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Removes trailing commas before closing braces/brackets outside of string literals.
|
|
632
|
+
* Example: {"a":1,} -> {"a":1}
|
|
633
|
+
* @param {string} input - JSON-like string
|
|
634
|
+
* @returns {string}
|
|
635
|
+
*/
|
|
636
|
+
function removeTrailingCommas(input) {
|
|
637
|
+
if (typeof input !== 'string') return '';
|
|
638
|
+
|
|
639
|
+
let out = '';
|
|
640
|
+
let inString = false;
|
|
641
|
+
let escape = false;
|
|
642
|
+
|
|
643
|
+
for (let i = 0; i < input.length; i++) {
|
|
644
|
+
const ch = input[i];
|
|
645
|
+
|
|
646
|
+
if (inString) {
|
|
647
|
+
out += ch;
|
|
648
|
+
if (escape) {
|
|
649
|
+
escape = false;
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (ch === '\\') {
|
|
653
|
+
escape = true;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (ch === '"') {
|
|
657
|
+
inString = false;
|
|
658
|
+
}
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (ch === '"') {
|
|
663
|
+
out += ch;
|
|
664
|
+
inString = true;
|
|
665
|
+
escape = false;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (ch === ',') {
|
|
670
|
+
// Look ahead for the next non-whitespace character.
|
|
671
|
+
let j = i + 1;
|
|
672
|
+
while (j < input.length && /\s/.test(input[j])) j++;
|
|
673
|
+
const nextNonWs = j < input.length ? input[j] : '';
|
|
674
|
+
if (nextNonWs === '}' || nextNonWs === ']') {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
out += ch;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return out;
|
|
532
683
|
}
|
|
533
684
|
|
|
534
685
|
function debugLog(node, label, payload) {
|