| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| 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");
|
| }
|
| } |