fix ai insight
CI / lint-and-test (push) Canceled after 0s

This commit is contained in:
Alberto-Audrix committed 2026-08-28 14:22:33 +07:00
1 parent 333b1cf370
commit 01b3178bc6
7 files changed
+333 -23

No files matched your search

+35
View File
@@ -53,3 +53,38 @@ TINDAKAN:
expect(parseAiResult('maaf, saya tidak bisa menjawab')).toBeNull();
});
});
/**
* The second habit, observed verbatim from qwen2.5:7b on the counting card:
* a complete answer whose last string value never gets its closing quote. The
* reply below is trimmed from a real capture — the TINDAKAN line ends at "]"
* and goes straight to the closing brace.
*/
describe('parseAiResult — balasan yang kutipnya tidak ditutup', () => {
const unterminated = `{
"kesimpulan": "Mortalitas kumulatif 9,91% (2.477 ekor) di atas standar CP 707 (5,75%).",
"insight": "PENYEBAB:
1. Suhu di atas target CP 707 (22°C) selama 36 hari, terparah pada hari ke-42. [Terbukti dari data]
TINDAKAN:
1. Lakukan pemeriksaan rutin program vaksinasi. [Dugaan — perlu dicek: Periksa riwayat penyakit]
}`;
it('memperbaikinya alih-alih membuang seluruh jawaban', () => {
const parsed = parseAiResult(unterminated);
expect(parsed).not.toBeNull();
expect(parsed?.kesimpulan).toContain('9,91%');
expect(parsed?.insight).toContain('PENYEBAB:');
// The last line must survive intact, without the stray brace.
expect(parsed?.insight).toContain('Periksa riwayat penyakit]');
expect(parsed?.insight).not.toContain('}');
});
it('tidak mengubah balasan yang kutipnya sudah benar', () => {
const wellFormed = `{"kesimpulan":"Mortalitas 2,68%.","insight":"PENYEBAB:
1. Suhu 34°C di hari ke-3."}`;
const parsed = parseAiResult(wellFormed);
expect(parsed?.kesimpulan).toBe('Mortalitas 2,68%.');
expect(parsed?.insight).toBe('PENYEBAB:\n1. Suhu 34°C di hari ke-3.');
});
});
+42 -3
View File
@@ -98,15 +98,30 @@ const takeFirst = (source: Record<string, unknown>, keys: string[]) => {
*
* Only characters inside string literals are touched; the JSON structure itself
* is left exactly as it was.
*
* It also reports back whether the text ENDED while still inside a string, and
* how many braces were still open, so the caller can repair the other habit the
* model has: stopping without the closing quote. See `extractJsonObject`.
*/
const escapeRawControlChars = (json: string): string => {
interface EscapeResult {
text: string;
/** True when the input ran out mid-string — the model never closed its quote. */
unterminated: boolean;
/** Braces opened outside a string and never closed. */
openBraces: number;
}
const escapeRawControlChars = (json: string): EscapeResult => {
let out = '';
let inString = false;
let escaped = false;
let openBraces = 0;
for (const char of json) {
if (!inString) {
if (char === '"') inString = true;
else if (char === '{') openBraces += 1;
else if (char === '}') openBraces -= 1;
out += char;
continue;
}
@@ -134,7 +149,7 @@ const escapeRawControlChars = (json: string): string => {
else out += char;
}
return out;
return { text: out, unterminated: inString, openBraces: Math.max(openBraces, 0) };
};
/** Strips think-tags, code fences, comments and trailing commas, then isolates the JSON object. */
@@ -151,7 +166,31 @@ const extractJsonObject = (text: string): string | null => {
const first = cleaned.indexOf('{');
const last = cleaned.lastIndexOf('}');
if (first === -1 || last === -1 || last <= first) return null;
return escapeRawControlChars(cleaned.slice(first, last + 1));
const {
text: escaped,
unterminated,
openBraces,
} = escapeRawControlChars(cleaned.slice(first, last + 1));
if (!unterminated) return escaped;
/*
* The model stopped without closing its last string.
*
* Observed verbatim from qwen2.5:7b on the counting card — a complete,
* well-written PENYEBAB/TINDAKAN answer whose final line ended
* "...riwayat penyakit]" and went straight to the closing brace, no quote in
* between. `JSON.parse` rejects that, so the whole answer was discarded and
* the card showed "Format respons LM Studio tidak valid" after minutes of
* model time. No prompt wording makes a 7B model reliable about this, so it
* is repaired here, the same way the missing newline escapes already are.
*
* The braces that followed the missing quote were swallowed into the string,
* so they are trimmed off before the quote is put back and the structure is
* closed for real.
*/
const trimmed = escaped.replace(/(?:\\[nrt]|[}\s])+$/, '');
return `${trimmed}"${'}'.repeat(Math.max(openBraces, 1))}`;
};
export const parseAiResult = (text: string): AiResult | null => {