Self-contained Dockerized build for end users. Run via docker compose; see README.md for setup. Source-only, no sample data or build artifacts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
2.7 KiB
JavaScript
91 lines
2.7 KiB
JavaScript
// lib/json-utils.js — tolerant JSON extraction for LLM responses.
|
|
// Local models often wrap JSON in prose or code fences, or leave trailing commas.
|
|
|
|
export function extractJson(text) {
|
|
if (text == null) throw new Error("Model returned an empty response.");
|
|
let s = String(text).trim();
|
|
|
|
// Strip code fences anywhere
|
|
s = s.replace(/```(?:json|javascript|js)?/gi, "```");
|
|
const fenced = s.match(/```([\s\S]*?)```/);
|
|
if (fenced && fenced[1].trim().match(/^[\[{]/)) s = fenced[1].trim();
|
|
|
|
// Slice from first brace/bracket to its matching end; try whichever starts first, first.
|
|
const candidates = [];
|
|
const firstObj = s.indexOf("{");
|
|
const firstArr = s.indexOf("[");
|
|
const objSlice = firstObj !== -1 ? s.slice(firstObj, s.lastIndexOf("}") + 1) : null;
|
|
const arrSlice = firstArr !== -1 ? s.slice(firstArr, s.lastIndexOf("]") + 1) : null;
|
|
if (firstArr !== -1 && (firstObj === -1 || firstArr < firstObj)) {
|
|
if (arrSlice) candidates.push(arrSlice);
|
|
if (objSlice) candidates.push(objSlice);
|
|
} else {
|
|
if (objSlice) candidates.push(objSlice);
|
|
if (arrSlice) candidates.push(arrSlice);
|
|
}
|
|
candidates.unshift(s);
|
|
|
|
let lastErr = null;
|
|
for (const c of candidates) {
|
|
if (!c) continue;
|
|
for (const attempt of [c, repair(c)]) {
|
|
try {
|
|
return JSON.parse(attempt);
|
|
} catch (e) {
|
|
lastErr = e;
|
|
}
|
|
}
|
|
}
|
|
const preview = s.slice(0, 300).replace(/\s+/g, " ");
|
|
throw new Error(
|
|
"Could not read the model's response as JSON. Try again, lower the temperature, or use a stronger model. Response began: " + preview
|
|
);
|
|
}
|
|
|
|
function repair(s) {
|
|
let out = s;
|
|
// Smart quotes -> straight quotes
|
|
out = out.replace(/[\u201C\u201D]/g, '"').replace(/[\u2018\u2019]/g, "'");
|
|
// Remove trailing commas before } or ]
|
|
out = out.replace(/,\s*([}\]])/g, "$1");
|
|
// Remove JS-style comments
|
|
out = out.replace(/^\s*\/\/.*$/gm, "");
|
|
// Control characters inside strings break JSON.parse; replace bare newlines in strings
|
|
out = sanitizeNewlinesInStrings(out);
|
|
return out;
|
|
}
|
|
|
|
function sanitizeNewlinesInStrings(s) {
|
|
let result = "";
|
|
let inStr = false;
|
|
let escaped = false;
|
|
for (let i = 0; i < s.length; i++) {
|
|
const ch = s[i];
|
|
if (inStr) {
|
|
if (escaped) {
|
|
result += ch;
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
if (ch === "\\") {
|
|
result += ch;
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
if (ch === '"') {
|
|
inStr = false;
|
|
result += ch;
|
|
continue;
|
|
}
|
|
if (ch === "\n") { result += "\\n"; continue; }
|
|
if (ch === "\r") { continue; }
|
|
if (ch === "\t") { result += "\\t"; continue; }
|
|
result += ch;
|
|
} else {
|
|
if (ch === '"') inStr = true;
|
|
result += ch;
|
|
}
|
|
}
|
|
return result;
|
|
}
|