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();
|
||||
|
||||
// Remove markdown code blocks
|
||||
@@ -123,7 +168,7 @@ function cleanJsonResponse(content: string): string {
|
||||
cleaned = cleaned.substring(startIdx, endIdx + 1);
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
return normalizeControlCharactersInJsonStrings(cleaned);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user