node-red-contrib-ai-agent 0.5.15 → 0.5.16
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 +137 -4
- package/package.json +1 -1
|
@@ -216,6 +216,14 @@ Example:
|
|
|
216
216
|
const response = await callAI(node, msg.aiagent, prompt, "You are an AI Orchestrator that creates non-linear plans with dependencies.");
|
|
217
217
|
debugLog(node, 'Planning Response', response);
|
|
218
218
|
const planData = parseJsonResponse(response);
|
|
219
|
+
|
|
220
|
+
if (!planData || typeof planData !== 'object') {
|
|
221
|
+
throw new Error('Planning response was not valid JSON.');
|
|
222
|
+
}
|
|
223
|
+
if (!Array.isArray(planData.tasks)) {
|
|
224
|
+
throw new Error('Planning response must include a tasks array.');
|
|
225
|
+
}
|
|
226
|
+
|
|
219
227
|
msg.orchestration.plan = planData;
|
|
220
228
|
msg.orchestration.status = 'executing';
|
|
221
229
|
} catch (error) {
|
|
@@ -303,6 +311,10 @@ Return a JSON object:
|
|
|
303
311
|
const response = await callAI(node, msg.aiagent, prompt, "You are an AI Orchestrator that reflects on progress and manages plan revisions.");
|
|
304
312
|
const reflection = parseJsonResponse(response);
|
|
305
313
|
|
|
314
|
+
if (!reflection || typeof reflection !== 'object') {
|
|
315
|
+
throw new Error('Reflection response was not valid JSON.');
|
|
316
|
+
}
|
|
317
|
+
|
|
306
318
|
msg.orchestration.status = reflection.status;
|
|
307
319
|
if (reflection.updatedPlan) {
|
|
308
320
|
msg.orchestration.plan = reflection.updatedPlan;
|
|
@@ -425,23 +437,89 @@ Return a JSON object:
|
|
|
425
437
|
* @returns {string} The extracted JSON string
|
|
426
438
|
*/
|
|
427
439
|
function extractJson(text) {
|
|
440
|
+
if (typeof text !== 'string') return '';
|
|
441
|
+
|
|
442
|
+
// Prefer a balanced-brace extraction to avoid greedy matching issues.
|
|
443
|
+
const extracted = extractBalancedJsonObject(text);
|
|
444
|
+
if (extracted) return extracted;
|
|
445
|
+
|
|
446
|
+
// Fallback to a simple greedy match if needed.
|
|
428
447
|
const match = text.match(/\{[\s\S]*\}/);
|
|
429
448
|
return match ? match[0] : text;
|
|
430
449
|
}
|
|
431
450
|
|
|
451
|
+
/**
|
|
452
|
+
* Extract the first balanced JSON object (starting at the first '{') from arbitrary text.
|
|
453
|
+
* @param {string} text - The input text
|
|
454
|
+
* @returns {string} Extracted JSON object text or empty string if not found
|
|
455
|
+
*/
|
|
456
|
+
function extractBalancedJsonObject(text) {
|
|
457
|
+
const start = text.indexOf('{');
|
|
458
|
+
if (start === -1) return '';
|
|
459
|
+
|
|
460
|
+
let depth = 0;
|
|
461
|
+
let inString = false;
|
|
462
|
+
let escape = false;
|
|
463
|
+
|
|
464
|
+
for (let i = start; i < text.length; i++) {
|
|
465
|
+
const ch = text[i];
|
|
466
|
+
|
|
467
|
+
if (inString) {
|
|
468
|
+
if (escape) {
|
|
469
|
+
escape = false;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (ch === '\\') {
|
|
473
|
+
escape = true;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (ch === '"') {
|
|
477
|
+
inString = false;
|
|
478
|
+
}
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (ch === '"') {
|
|
483
|
+
inString = true;
|
|
484
|
+
escape = false;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (ch === '{') {
|
|
489
|
+
depth++;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (ch === '}') {
|
|
494
|
+
depth--;
|
|
495
|
+
if (depth === 0) {
|
|
496
|
+
return text.slice(start, i + 1);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return '';
|
|
502
|
+
}
|
|
503
|
+
|
|
432
504
|
/**
|
|
433
505
|
* Parses a JSON object from an AI response, tolerating common non-JSON wrappers.
|
|
506
|
+
* Returns an empty string if parsing fails after sanitization attempts.
|
|
434
507
|
* @param {string} text - The AI response
|
|
435
|
-
* @returns {any} Parsed JSON
|
|
508
|
+
* @returns {any|string} Parsed JSON or empty string on failure
|
|
436
509
|
*/
|
|
437
510
|
function parseJsonResponse(text) {
|
|
438
511
|
const extracted = extractJson(text);
|
|
439
512
|
try {
|
|
440
513
|
return JSON.parse(extracted);
|
|
441
|
-
} catch (
|
|
514
|
+
} catch (err1) {
|
|
442
515
|
const sanitized = sanitizeJsonLikeText(extracted);
|
|
443
|
-
|
|
516
|
+
try {
|
|
517
|
+
return JSON.parse(sanitized);
|
|
518
|
+
} catch (err2) {
|
|
519
|
+
// no need to catch anything, default to empty string
|
|
520
|
+
}
|
|
444
521
|
}
|
|
522
|
+
return "";
|
|
445
523
|
}
|
|
446
524
|
|
|
447
525
|
/**
|
|
@@ -528,7 +606,62 @@ Return a JSON object:
|
|
|
528
606
|
out += ch;
|
|
529
607
|
}
|
|
530
608
|
|
|
531
|
-
return out.trim();
|
|
609
|
+
return removeTrailingCommas(out).trim();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Removes trailing commas before closing braces/brackets outside of string literals.
|
|
614
|
+
* Example: {"a":1,} -> {"a":1}
|
|
615
|
+
* @param {string} input - JSON-like string
|
|
616
|
+
* @returns {string}
|
|
617
|
+
*/
|
|
618
|
+
function removeTrailingCommas(input) {
|
|
619
|
+
if (typeof input !== 'string') return '';
|
|
620
|
+
|
|
621
|
+
let out = '';
|
|
622
|
+
let inString = false;
|
|
623
|
+
let escape = false;
|
|
624
|
+
|
|
625
|
+
for (let i = 0; i < input.length; i++) {
|
|
626
|
+
const ch = input[i];
|
|
627
|
+
|
|
628
|
+
if (inString) {
|
|
629
|
+
out += ch;
|
|
630
|
+
if (escape) {
|
|
631
|
+
escape = false;
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (ch === '\\') {
|
|
635
|
+
escape = true;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (ch === '"') {
|
|
639
|
+
inString = false;
|
|
640
|
+
}
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (ch === '"') {
|
|
645
|
+
out += ch;
|
|
646
|
+
inString = true;
|
|
647
|
+
escape = false;
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (ch === ',') {
|
|
652
|
+
// Look ahead for the next non-whitespace character.
|
|
653
|
+
let j = i + 1;
|
|
654
|
+
while (j < input.length && /\s/.test(input[j])) j++;
|
|
655
|
+
const nextNonWs = j < input.length ? input[j] : '';
|
|
656
|
+
if (nextNonWs === '}' || nextNonWs === ']') {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
out += ch;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return out;
|
|
532
665
|
}
|
|
533
666
|
|
|
534
667
|
function debugLog(node, label, payload) {
|