|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class JsonParser {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static parse(text, options = {}) {
|
|
|
const {
|
|
|
debug = false,
|
|
|
expectArray = false,
|
|
|
expectObject = true,
|
|
|
repairAttempts = true
|
|
|
} = options;
|
|
|
|
|
|
if (debug) {
|
|
|
console.log("\nRAW LLM OUTPUT:");
|
|
|
console.log("-".repeat(70));
|
|
|
console.log(text);
|
|
|
console.log("-".repeat(70) + "\n");
|
|
|
}
|
|
|
|
|
|
|
|
|
let cleaned = this.cleanText(text, debug);
|
|
|
|
|
|
|
|
|
let extracted = this.extractJson(cleaned, expectArray, expectObject, debug);
|
|
|
|
|
|
|
|
|
try {
|
|
|
const parsed = JSON.parse(extracted);
|
|
|
if (debug) console.log("Successfully parsed JSON\n");
|
|
|
return parsed;
|
|
|
} catch (firstError) {
|
|
|
if (debug) {
|
|
|
console.log("First parse attempt failed:", firstError.message);
|
|
|
}
|
|
|
|
|
|
if (!repairAttempts) {
|
|
|
throw new Error(`JSON parse failed: ${firstError.message}\n\nExtracted text:\n${extracted}`);
|
|
|
}
|
|
|
|
|
|
|
|
|
return this.attemptRepairs(extracted, debug);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static cleanText(text, debug = false) {
|
|
|
let cleaned = text;
|
|
|
|
|
|
|
|
|
cleaned = cleaned.replace(/```json\s*/gi, '');
|
|
|
cleaned = cleaned.replace(/```\s*/g, '');
|
|
|
|
|
|
|
|
|
cleaned = cleaned.replace(/^(Here's the plan:|JSON output:|Plan:|Output:)\s*/i, '');
|
|
|
|
|
|
|
|
|
cleaned = cleaned.trim();
|
|
|
|
|
|
if (debug && cleaned !== text) {
|
|
|
console.log("Cleaned text (removed markdown/prefixes)\n");
|
|
|
}
|
|
|
|
|
|
return cleaned;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static extractJson(text, expectArray = false, expectObject = true, debug = false) {
|
|
|
|
|
|
const startChar = expectArray ? '[' : '{';
|
|
|
const endChar = expectArray ? ']' : '}';
|
|
|
|
|
|
const startIdx = text.indexOf(startChar);
|
|
|
const lastIdx = text.lastIndexOf(endChar);
|
|
|
|
|
|
if (startIdx === -1 || lastIdx === -1 || startIdx >= lastIdx) {
|
|
|
if (debug) {
|
|
|
console.log(`Could not find valid ${startChar}...${endChar} boundaries`);
|
|
|
console.log(`Start index: ${startIdx}, End index: ${lastIdx}`);
|
|
|
}
|
|
|
|
|
|
|
|
|
if (expectObject && !text.trim().startsWith('{')) {
|
|
|
const withBraces = '{' + text.trim() + '}';
|
|
|
if (debug) console.log("Added missing opening brace");
|
|
|
return withBraces;
|
|
|
}
|
|
|
|
|
|
return text;
|
|
|
}
|
|
|
|
|
|
const extracted = text.substring(startIdx, lastIdx + 1);
|
|
|
|
|
|
if (debug && extracted !== text) {
|
|
|
console.log("Extracted JSON from surrounding text:");
|
|
|
console.log(extracted.substring(0, 100) + (extracted.length > 100 ? '...' : ''));
|
|
|
console.log();
|
|
|
}
|
|
|
|
|
|
return extracted;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static attemptRepairs(jsonString, debug = false) {
|
|
|
const repairs = [
|
|
|
|
|
|
(str) => {
|
|
|
const fixed = str.replace(/,(\s*[}\]])/g, '$1');
|
|
|
if (debug && fixed !== str) console.log("Repair 1: Removed trailing commas");
|
|
|
return fixed;
|
|
|
},
|
|
|
|
|
|
|
|
|
(str) => {
|
|
|
const fixed = str.replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
|
|
|
if (debug && fixed !== str) console.log("Repair 2: Added quotes around property names");
|
|
|
return fixed;
|
|
|
},
|
|
|
|
|
|
|
|
|
(str) => {
|
|
|
const fixed = str.replace(/'/g, '"');
|
|
|
if (debug && fixed !== str) console.log("Repair 3: Converted single quotes to double quotes");
|
|
|
return fixed;
|
|
|
},
|
|
|
|
|
|
|
|
|
(str) => {
|
|
|
const openBraces = (str.match(/{/g) || []).length;
|
|
|
const closeBraces = (str.match(/}/g) || []).length;
|
|
|
if (openBraces > closeBraces) {
|
|
|
const fixed = str + '}'.repeat(openBraces - closeBraces);
|
|
|
if (debug) console.log(`Repair 4: Added ${openBraces - closeBraces} missing closing brace(s)`);
|
|
|
return fixed;
|
|
|
}
|
|
|
return str;
|
|
|
},
|
|
|
|
|
|
|
|
|
(str) => {
|
|
|
const openBrackets = (str.match(/\[/g) || []).length;
|
|
|
const closeBrackets = (str.match(/]/g) || []).length;
|
|
|
if (openBrackets > closeBrackets) {
|
|
|
const fixed = str + ']'.repeat(openBrackets - closeBrackets);
|
|
|
if (debug) console.log(`Repair 5: Added ${openBrackets - closeBrackets} missing closing bracket(s)`);
|
|
|
return fixed;
|
|
|
}
|
|
|
return str;
|
|
|
},
|
|
|
|
|
|
|
|
|
(str) => {
|
|
|
const fixed = str.replace(/\\"/g, '"');
|
|
|
if (debug && fixed !== str) console.log("Repair 6: Fixed escaped quotes");
|
|
|
return fixed;
|
|
|
},
|
|
|
|
|
|
|
|
|
(str) => {
|
|
|
|
|
|
const fixed = str.replace(/[\x00-\x1F\x7F]/g, '');
|
|
|
if (debug && fixed !== str) console.log("Repair 7: Removed control characters");
|
|
|
return fixed;
|
|
|
}
|
|
|
];
|
|
|
|
|
|
let current = jsonString;
|
|
|
|
|
|
|
|
|
for (const repair of repairs) {
|
|
|
current = repair(current);
|
|
|
}
|
|
|
|
|
|
|
|
|
try {
|
|
|
const parsed = JSON.parse(current);
|
|
|
if (debug) console.log("Successfully parsed after repairs\n");
|
|
|
return parsed;
|
|
|
} catch (error) {
|
|
|
|
|
|
const atomsMatch = current.match(/"atoms"\s*:\s*(\[[\s\S]*\])/);
|
|
|
if (atomsMatch) {
|
|
|
try {
|
|
|
const atomsOnly = { atoms: JSON.parse(atomsMatch[1]) };
|
|
|
if (debug) console.log("Extracted and parsed atoms array\n");
|
|
|
return atomsOnly;
|
|
|
} catch (innerError) {
|
|
|
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
throw new Error(
|
|
|
`JSON parse failed after all repair attempts.\n\n` +
|
|
|
`Original error: ${error.message}\n\n` +
|
|
|
`Attempted repairs:\n${current.substring(0, 500)}${current.length > 500 ? '...' : ''}\n\n` +
|
|
|
`Tip: Check if the LLM is following the JSON schema correctly.`
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static validatePlan(plan, debug = false) {
|
|
|
if (!plan || typeof plan !== 'object') {
|
|
|
throw new Error('Plan must be an object');
|
|
|
}
|
|
|
|
|
|
if (!Array.isArray(plan.atoms)) {
|
|
|
throw new Error('Plan must have an "atoms" array');
|
|
|
}
|
|
|
|
|
|
if (plan.atoms.length === 0) {
|
|
|
throw new Error('Plan must have at least one atom');
|
|
|
}
|
|
|
|
|
|
for (const atom of plan.atoms) {
|
|
|
if (typeof atom.id !== 'number') {
|
|
|
throw new Error(`Atom missing or invalid id: ${JSON.stringify(atom)}`);
|
|
|
}
|
|
|
|
|
|
if (!atom.kind || !['tool', 'decision', 'final'].includes(atom.kind)) {
|
|
|
throw new Error(`Atom ${atom.id} has invalid kind: ${atom.kind}`);
|
|
|
}
|
|
|
|
|
|
if (!atom.name || typeof atom.name !== 'string') {
|
|
|
throw new Error(`Atom ${atom.id} missing or invalid name`);
|
|
|
}
|
|
|
|
|
|
if (atom.dependsOn && !Array.isArray(atom.dependsOn)) {
|
|
|
throw new Error(`Atom ${atom.id} dependsOn must be an array`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (debug) {
|
|
|
console.log(`Plan structure validated: ${plan.atoms.length} atoms\n`);
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static prettyPrint(plan) {
|
|
|
console.log("\nPLAN STRUCTURE:");
|
|
|
console.log("=".repeat(70));
|
|
|
|
|
|
for (const atom of plan.atoms) {
|
|
|
const deps = atom.dependsOn && atom.dependsOn.length > 0
|
|
|
? ` (depends on: ${atom.dependsOn.join(', ')})`
|
|
|
: '';
|
|
|
|
|
|
console.log(` ${atom.id}. [${atom.kind}] ${atom.name}${deps}`);
|
|
|
|
|
|
if (atom.input && Object.keys(atom.input).length > 0) {
|
|
|
console.log(` Input: ${JSON.stringify(atom.input)}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
console.log("=".repeat(70) + "\n");
|
|
|
}
|
|
|
} |