Spaces:
Runtime error
Runtime error
File size: 4,420 Bytes
7fd62bd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | /**
* src/utils/jsonCleaner.ts
*
* LLM responses almost never contain bare JSON. They typically:
* - Wrap JSON in ```json β¦ ``` code fences
* - Prepend sentences like "Here is the JSON:"
* - Mix prose before/after the JSON block
* - Contain nested braces inside string literals
*
* This module extracts the first valid JSON object `{}` or array `[]`
* from any arbitrary LLM response string using a character-level scan
* that correctly handles strings, escapes, and nesting.
*/
// βββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/**
* Extracts and parses the first JSON object or array from an LLM response.
*
* @param raw - The full LLM response text
* @returns The parsed value
* @throws Error if no valid JSON can be extracted
*/
export function extractJson<T = unknown>(raw: string): T {
if (!raw || typeof raw !== 'string') {
throw new Error('extractJson: input must be a non-empty string');
}
// ββ Strategy 1: Strip a markdown code fence if present ββββββββββββββββββ
const fenceMatch = raw.match(
/```(?:json|yaml|js|javascript|typescript|dart)?\s*([\s\S]*?)```/i
);
if (fenceMatch) {
const inner = fenceMatch[1].trim();
try {
return JSON.parse(inner) as T;
} catch {
// Fenced block wasn't valid JSON β fall through
}
}
// ββ Strategy 2: Find first `{` or `[` and matching close bracket ββββββββ
const objStart = raw.indexOf('{');
const arrStart = raw.indexOf('[');
// Determine which comes first
let start: number;
let openChar: '{' | '[';
let closeChar: '}' | ']';
if (objStart === -1 && arrStart === -1) {
throw new Error(
`extractJson: no JSON object or array found in response.\n` +
`Response preview: ${raw.slice(0, 200)}`
);
}
if (arrStart === -1 || (objStart !== -1 && objStart < arrStart)) {
start = objStart;
openChar = '{';
closeChar = '}';
} else {
start = arrStart;
openChar = '[';
closeChar = ']';
}
// Character-level scan: track depth, skip string contents
let depth = 0;
let inString = false;
let escape = false;
let end = -1;
for (let i = start; i < raw.length; i++) {
const ch = raw[i];
if (escape) {
escape = false;
continue;
}
if (inString) {
if (ch === '\\') escape = true;
else if (ch === '"') inString = false;
continue;
}
// Not in a string
if (ch === '"') { inString = true; continue; }
if (ch === openChar) { depth++; continue; }
if (ch === closeChar) {
depth--;
if (depth === 0) { end = i; break; }
}
}
if (end === -1) {
throw new Error(
`extractJson: unbalanced brackets in LLM response.\n` +
`Response preview: ${raw.slice(start, start + 300)}`
);
}
const candidate = raw.slice(start, end + 1);
try {
return JSON.parse(candidate) as T;
} catch (parseErr) {
// Last resort: attempt to fix common LLM JSON mistakes
const repaired = attemptRepair(candidate);
if (repaired !== null) return repaired as T;
throw new Error(
`extractJson: JSON.parse failed.\n` +
`Error: ${String(parseErr)}\n` +
`Candidate (first 300 chars): ${candidate.slice(0, 300)}`
);
}
}
/**
* Non-throwing variant of extractJson.
* Returns `null` if extraction or parsing fails.
*/
export function tryExtractJson<T = unknown>(raw: string): T | null {
try {
return extractJson<T>(raw);
} catch {
return null;
}
}
// βββ Private Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/**
* Attempts common repairs to malformed LLM JSON:
* - Trailing commas before `}` or `]`
* - Single-quoted strings
* - Unquoted property keys (limited cases)
*/
function attemptRepair(json: string): unknown | null {
let repaired = json;
// Remove trailing commas in objects/arrays
repaired = repaired.replace(/,\s*([}\]])/g, '$1');
// Replace single-quoted strings with double-quoted
repaired = repaired.replace(/'/g, '"');
try {
return JSON.parse(repaired);
} catch {
return null;
}
}
|