fix: accepter les réponses IA avec contrôles OCR
This commit is contained in:
29
server/invoiceExtractor.test.ts
Normal file
29
server/invoiceExtractor.test.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { cleanJsonResponse } from "./invoiceExtractor";
|
||||||
|
|
||||||
|
describe("cleanJsonResponse", () => {
|
||||||
|
it("supprime les caractères de contrôle bruts dans une chaîne JSON LLM", () => {
|
||||||
|
const rawResponse = `{
|
||||||
|
"pageCount": 1,
|
||||||
|
"invoiceCount": 1,
|
||||||
|
"invoices": [{
|
||||||
|
"supplierName": "Manus AI",
|
||||||
|
"invoiceNumber": "J6LNGDAL-0021",
|
||||||
|
"extractedText": "Ligne valide\u0000suite du texte"
|
||||||
|
}]
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const parsed = JSON.parse(cleanJsonResponse(rawResponse));
|
||||||
|
|
||||||
|
expect(parsed.invoices[0].invoiceNumber).toBe("J6LNGDAL-0021");
|
||||||
|
expect(parsed.invoices[0].extractedText).toBe("Ligne validesuite du texte");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("préserve les retours à la ligne échappés dans les chaînes JSON", () => {
|
||||||
|
const rawResponse = '{"invoices":[{"extractedText":"Ligne 1\nLigne 2"}] }';
|
||||||
|
|
||||||
|
const parsed = JSON.parse(cleanJsonResponse(rawResponse));
|
||||||
|
|
||||||
|
expect(parsed.invoices[0].extractedText).toBe("Ligne 1\nLigne 2");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -75,9 +75,54 @@ async function extractTextFromPdf(pdfBuffer: Buffer): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean JSON response from LLM by removing markdown code blocks
|
* Supprime ou échappe les caractères de contrôle bruts uniquement à
|
||||||
|
* l'intérieur des chaînes JSON. Certains moteurs retournent ponctuellement
|
||||||
|
* un caractère NUL provenant du texte OCR : JSON.parse le refuse alors que
|
||||||
|
* le reste de la réponse est exploitable.
|
||||||
*/
|
*/
|
||||||
function cleanJsonResponse(content: string): string {
|
function normalizeControlCharactersInJsonStrings(content: string): string {
|
||||||
|
let normalized = "";
|
||||||
|
let inString = false;
|
||||||
|
let escaped = false;
|
||||||
|
|
||||||
|
for (const character of content) {
|
||||||
|
if (escaped) {
|
||||||
|
normalized += character;
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === "\\") {
|
||||||
|
normalized += character;
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character === '"') {
|
||||||
|
inString = !inString;
|
||||||
|
normalized += character;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inString && character.charCodeAt(0) < 0x20) {
|
||||||
|
if (character === "\n") normalized += "\\n";
|
||||||
|
else if (character === "\r") normalized += "\\r";
|
||||||
|
else if (character === "\t") normalized += "\\t";
|
||||||
|
// Les autres contrôles (notamment NUL) n'ont pas de valeur métier.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized += character;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean JSON response from LLM by removing markdown code blocks.
|
||||||
|
* Exportée pour couvrir les réponses LLM mal formées en régression.
|
||||||
|
*/
|
||||||
|
export function cleanJsonResponse(content: string): string {
|
||||||
let cleaned = content.trim();
|
let cleaned = content.trim();
|
||||||
|
|
||||||
// Remove markdown code blocks
|
// Remove markdown code blocks
|
||||||
@@ -123,7 +168,7 @@ function cleanJsonResponse(content: string): string {
|
|||||||
cleaned = cleaned.substring(startIdx, endIdx + 1);
|
cleaned = cleaned.substring(startIdx, endIdx + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleaned;
|
return normalizeControlCharactersInJsonStrings(cleaned);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user