node-red-contrib-ai-agent 0.5.10 → 0.5.11
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 +111 -3
- package/package.json +1 -1
|
@@ -148,7 +148,13 @@ Return a JSON object with a "tasks" array. Each task should have:
|
|
|
148
148
|
prompt += `\n\nThink about parallel execution. Group related tasks and identify bottlenecks. Ensure dependencies are logical.`;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
prompt += `\n\
|
|
151
|
+
prompt += `\n\nIMPORTANT OUTPUT RULES:
|
|
152
|
+
- Return ONLY raw JSON (no markdown, no code fences, no explanations)
|
|
153
|
+
- Do NOT include comments (e.g. // ...)
|
|
154
|
+
- Do NOT include trailing commas
|
|
155
|
+
- All string values must be valid JSON strings (escape newlines as \\n if needed)
|
|
156
|
+
|
|
157
|
+
Example:
|
|
152
158
|
{
|
|
153
159
|
"tasks": [
|
|
154
160
|
{"id": "t1", "type": "research", "input": "...", "status": "pending", "priority": 10, "dependsOn": []},
|
|
@@ -160,7 +166,7 @@ Return a JSON object with a "tasks" array. Each task should have:
|
|
|
160
166
|
node.warn("Prompt: " + prompt);
|
|
161
167
|
const response = await callAI(msg.aiagent, prompt, "You are an AI Orchestrator that creates non-linear plans with dependencies.");
|
|
162
168
|
node.warn("Response: " + response);
|
|
163
|
-
const planData =
|
|
169
|
+
const planData = parseJsonResponse(response);
|
|
164
170
|
msg.orchestration.plan = planData;
|
|
165
171
|
msg.orchestration.status = 'executing';
|
|
166
172
|
} catch (error) {
|
|
@@ -218,7 +224,7 @@ Return a JSON object:
|
|
|
218
224
|
|
|
219
225
|
try {
|
|
220
226
|
const response = await callAI(msg.aiagent, prompt, "You are an AI Orchestrator that reflects on progress and manages plan revisions.");
|
|
221
|
-
const reflection =
|
|
227
|
+
const reflection = parseJsonResponse(response);
|
|
222
228
|
|
|
223
229
|
msg.orchestration.status = reflection.status;
|
|
224
230
|
if (reflection.updatedPlan) {
|
|
@@ -302,5 +308,107 @@ Return a JSON object:
|
|
|
302
308
|
return match ? match[0] : text;
|
|
303
309
|
}
|
|
304
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Parses a JSON object from an AI response, tolerating common non-JSON wrappers.
|
|
313
|
+
* @param {string} text - The AI response
|
|
314
|
+
* @returns {any} Parsed JSON
|
|
315
|
+
*/
|
|
316
|
+
function parseJsonResponse(text) {
|
|
317
|
+
const extracted = extractJson(text);
|
|
318
|
+
try {
|
|
319
|
+
return JSON.parse(extracted);
|
|
320
|
+
} catch (_err) {
|
|
321
|
+
const sanitized = sanitizeJsonLikeText(extracted);
|
|
322
|
+
return JSON.parse(sanitized);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Removes markdown fences and JS-style comments, and escapes raw newlines inside string literals.
|
|
328
|
+
* This is a best-effort repair for model outputs that are "almost JSON".
|
|
329
|
+
* @param {string} input - A string that should contain a JSON object
|
|
330
|
+
* @returns {string} A JSON string more likely to be parseable by JSON.parse
|
|
331
|
+
*/
|
|
332
|
+
function sanitizeJsonLikeText(input) {
|
|
333
|
+
if (typeof input !== 'string') return '';
|
|
334
|
+
|
|
335
|
+
// Remove common markdown code fences
|
|
336
|
+
let s = input
|
|
337
|
+
.replace(/^\s*```(?:json)?\s*/i, '')
|
|
338
|
+
.replace(/\s*```\s*$/i, '')
|
|
339
|
+
.trim();
|
|
340
|
+
|
|
341
|
+
// If we still have leading/trailing non-JSON, re-extract
|
|
342
|
+
s = extractJson(s).trim();
|
|
343
|
+
|
|
344
|
+
let out = '';
|
|
345
|
+
let inString = false;
|
|
346
|
+
let escape = false;
|
|
347
|
+
let inLineComment = false;
|
|
348
|
+
|
|
349
|
+
for (let i = 0; i < s.length; i++) {
|
|
350
|
+
const ch = s[i];
|
|
351
|
+
const next = i + 1 < s.length ? s[i + 1] : '';
|
|
352
|
+
|
|
353
|
+
if (inLineComment) {
|
|
354
|
+
if (ch === '\n') {
|
|
355
|
+
inLineComment = false;
|
|
356
|
+
out += ch;
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!inString && ch === '/' && next === '/') {
|
|
362
|
+
inLineComment = true;
|
|
363
|
+
i++;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!inString && ch === '`') {
|
|
368
|
+
// ignore stray backticks
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (inString) {
|
|
373
|
+
if (escape) {
|
|
374
|
+
out += ch;
|
|
375
|
+
escape = false;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (ch === '\\') {
|
|
379
|
+
out += ch;
|
|
380
|
+
escape = true;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (ch === '"') {
|
|
384
|
+
out += ch;
|
|
385
|
+
inString = false;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (ch === '\n') {
|
|
389
|
+
out += '\\n';
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (ch === '\r') {
|
|
393
|
+
// drop CR; newline will be handled by \n
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
out += ch;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (ch === '"') {
|
|
401
|
+
out += ch;
|
|
402
|
+
inString = true;
|
|
403
|
+
escape = false;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
out += ch;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return out.trim();
|
|
411
|
+
}
|
|
412
|
+
|
|
305
413
|
RED.nodes.registerType('ai-orchestrator', AiOrchestratorNode);
|
|
306
414
|
};
|