1 parent
333b1cf370
commit
01b3178bc6
7 files changed
+333
-23
No files matched your search
@@ -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
@@ -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 => {
|
||||
|
||||
Reference in new issue
Block a user