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.
@@ -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 (_err) {
514
+ } catch (err1) {
442
515
  const sanitized = sanitizeJsonLikeText(extracted);
443
- return JSON.parse(sanitized);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-ai-agent",
3
- "version": "0.5.15",
3
+ "version": "0.5.16",
4
4
  "description": "AI Agent for Node-RED",
5
5
  "repository": {
6
6
  "type": "git",