|
@@ -4502,6 +4502,99 @@ static JSValue js_smtp_send(JSContext* ctx, JSValue this_val, int argc, JSValue*
|
|
|
// it came from. Since almost every such value is a server's XML or HTML reply,
|
|
// it came from. Since almost every such value is a server's XML or HTML reply,
|
|
|
// the parser is wrapped once for every node so the failure describes what
|
|
// the parser is wrapped once for every node so the failure describes what
|
|
|
// arrived instead of only which character stopped it.
|
|
// arrived instead of only which character stopped it.
|
|
|
|
|
+// smartbotic.utils.repairJson - best-effort recovery of JSON a model mangled.
|
|
|
|
|
+//
|
|
|
|
|
+// A model asked for JSON usually returns JSON, and when it does not the damage
|
|
|
|
|
+// is from a small, recurring set. Retrying the call does not help when the
|
|
|
|
|
+// fault is deterministic for that model, and where the API is paid the retry
|
|
|
|
|
+// costs real money for an answer that will be malformed the same way.
|
|
|
|
|
+//
|
|
|
|
|
+// It returns what it changed rather than only the value, so a caller can say so
|
|
|
|
|
+// out loud. Quietly accepting a repaired document is how a genuine problem
|
|
|
|
|
+// upstream - a truncating proxy, a model that has started ignoring the schema -
|
|
|
|
|
+// stays invisible.
|
|
|
|
|
+//
|
|
|
|
|
+// Note the shape this exists for is one the well-known jsonrepair library does
|
|
|
|
|
+// not handle: a document that begins at a key because the opening brace is
|
|
|
|
|
+// missing. Verified against jsonrepair 3.x, which answers
|
|
|
|
|
+// `Unexpected character ":"`. It fixes the four classic faults below; this
|
|
|
|
|
+// fixes those and that one.
|
|
|
|
|
+static const char* kJsonRepairPrelude = R"JS(
|
|
|
|
|
+(function () {
|
|
|
|
|
+ if (typeof smartbotic === 'undefined' || !smartbotic.utils) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ smartbotic.utils.repairJson = function (text) {
|
|
|
|
|
+ var repairs = [];
|
|
|
|
|
+ var raw = String(text == null ? '' : text);
|
|
|
|
|
+
|
|
|
|
|
+ function attempt(candidate) {
|
|
|
|
|
+ try { return { ok: true, value: JSON.parse(candidate) }; }
|
|
|
|
|
+ catch (e) { return { ok: false }; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var first = attempt(raw);
|
|
|
|
|
+ if (first.ok) { return { ok: true, value: first.value, repairs: [] }; }
|
|
|
|
|
+
|
|
|
|
|
+ var work = raw.trim();
|
|
|
|
|
+
|
|
|
|
|
+ // Code fences, and any prose either side of the document.
|
|
|
|
|
+ if (work.indexOf('```') !== -1) {
|
|
|
|
|
+ var open = work.indexOf('{');
|
|
|
|
|
+ var close = work.lastIndexOf('}');
|
|
|
|
|
+ if (open === -1) { open = work.indexOf('['); close = work.lastIndexOf(']'); }
|
|
|
|
|
+ if (open !== -1 && close > open) {
|
|
|
|
|
+ work = work.substring(open, close + 1);
|
|
|
|
|
+ repairs.push('removed code fences');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // A bare object body: the document starts at a key, the opening brace
|
|
|
|
|
+ // never arrived.
|
|
|
|
|
+ //
|
|
|
|
|
+ // Two forms, and the second is the one that actually occurs. The model
|
|
|
|
|
+ // drops `{"` as a pair, so the first key arrives without its opening
|
|
|
|
|
+ // quote as well - `humanCount": 0`, not `"humanCount": 0`. Handling
|
|
|
|
|
+ // only the tidier first form looks correct and fixes nothing real: it
|
|
|
|
|
+ // was caught by testing against the stored reply rather than a retyped
|
|
|
|
|
+ // one, which had quietly put the quote back.
|
|
|
|
|
+ if (/^"[^"]*"\s*:/.test(work)) {
|
|
|
|
|
+ work = '{' + work;
|
|
|
|
|
+ repairs.push('added the missing opening brace');
|
|
|
|
|
+ } else if (/^[A-Za-z_$][A-Za-z0-9_$]*"\s*:/.test(work)) {
|
|
|
|
|
+ work = '{"' + work;
|
|
|
|
|
+ repairs.push('added the missing opening brace and quote');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Trailing comma before a closer.
|
|
|
|
|
+ var without = work.replace(/,(\s*[}\]])/g, '$1');
|
|
|
|
|
+ if (without !== work) { work = without; repairs.push('removed a trailing comma'); }
|
|
|
|
|
+
|
|
|
|
|
+ // Unclosed strings, brackets and braces - counted outside strings, so a
|
|
|
|
|
+ // brace inside a description does not confuse the tally.
|
|
|
|
|
+ var curly = 0, square = 0, inString = false, escaped = false;
|
|
|
|
|
+ for (var i = 0; i < work.length; i++) {
|
|
|
|
|
+ var ch = work[i];
|
|
|
|
|
+ if (escaped) { escaped = false; continue; }
|
|
|
|
|
+ if (ch === '\\') { escaped = true; continue; }
|
|
|
|
|
+ if (ch === '"') { inString = !inString; continue; }
|
|
|
|
|
+ if (inString) { continue; }
|
|
|
|
|
+ if (ch === '{') { curly++; }
|
|
|
|
|
+ else if (ch === '}') { curly--; }
|
|
|
|
|
+ else if (ch === '[') { square++; }
|
|
|
|
|
+ else if (ch === ']') { square--; }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (inString) { work += '"'; repairs.push('closed an unterminated string'); }
|
|
|
|
|
+ if (square > 0) { work += new Array(square + 1).join(']'); repairs.push('closed ' + square + ' bracket(s)'); }
|
|
|
|
|
+ if (curly > 0) { work += new Array(curly + 1).join('}'); repairs.push('closed ' + curly + ' brace(s)'); }
|
|
|
|
|
+
|
|
|
|
|
+ var done = attempt(work);
|
|
|
|
|
+ if (done.ok) { return { ok: true, value: done.value, repairs: repairs }; }
|
|
|
|
|
+ return { ok: false, value: null, repairs: repairs };
|
|
|
|
|
+ };
|
|
|
|
|
+})();
|
|
|
|
|
+)JS";
|
|
|
|
|
+
|
|
|
static const char* kJsonParsePrelude = R"JS(
|
|
static const char* kJsonParsePrelude = R"JS(
|
|
|
(function () {
|
|
(function () {
|
|
|
var originalParse = JSON.parse;
|
|
var originalParse = JSON.parse;
|
|
@@ -4962,6 +5055,13 @@ ScriptResult ScriptEngine::execute(const std::string& script, const ScriptContex
|
|
|
}
|
|
}
|
|
|
JS_FreeValue(context_, smartbotic);
|
|
JS_FreeValue(context_, smartbotic);
|
|
|
|
|
|
|
|
|
|
+ JSValue repair_prelude = JS_Eval(context_, kJsonRepairPrelude, strlen(kJsonRepairPrelude),
|
|
|
|
|
+ "<prelude:repair>", JS_EVAL_TYPE_GLOBAL);
|
|
|
|
|
+ if (JS_IsException(repair_prelude)) {
|
|
|
|
|
+ JS_FreeValue(context_, JS_GetException(context_));
|
|
|
|
|
+ }
|
|
|
|
|
+ JS_FreeValue(context_, repair_prelude);
|
|
|
|
|
+
|
|
|
JSValue prelude = JS_Eval(context_, kJsonParsePrelude, strlen(kJsonParsePrelude),
|
|
JSValue prelude = JS_Eval(context_, kJsonParsePrelude, strlen(kJsonParsePrelude),
|
|
|
"<prelude>", JS_EVAL_TYPE_GLOBAL);
|
|
"<prelude>", JS_EVAL_TYPE_GLOBAL);
|
|
|
if (JS_IsException(prelude)) {
|
|
if (JS_IsException(prelude)) {
|