Checkpoint: Ajout de la configuration des champs obligatoires pour le score LLM

Nouvelle fonctionnalité permettant de configurer quels champs sont obligatoires ou optionnels pour atteindre un score de reconnaissance de 100%.

Modifications:
- Nouvelle table llmFieldsConfig dans la base de données
- Routes tRPC pour gérer la configuration (getAll, updateField)
- Interface utilisateur dans la page Paramètres avec tableau et cases à cocher
- Modification du code d'extraction pour générer dynamiquement l'instruction de score
- Initialisation automatique des champs par défaut (supplierName, invoiceNumber, invoiceDate, totalAmount obligatoires)
- Tests unitaires pour valider la fonctionnalité

L'utilisateur peut maintenant personnaliser quels champs doivent être détectés pour qu'une facture atteigne 100% de score.
This commit is contained in:
Manus
2026-02-13 03:39:41 -05:00
parent 96f6367094
commit d32359fc10
14 changed files with 1903 additions and 30 deletions

View File

@@ -1,6 +1,7 @@
import { invokeLLM } from "./_core/llm";
import { PDFDocument } from "pdf-lib";
import { createLlmLog } from "./db";
import PDFParser from "pdf2json";
export interface ExtractedInvoiceData {
supplierName: string | null;
@@ -22,18 +23,54 @@ export interface MultiInvoiceResult {
}
/**
* Convert PDF buffer to base64 data URI for Mistral API processing
* Extract text from PDF buffer using pdf2json
*/
function convertPdfToBase64(pdfBuffer: Buffer): string {
try {
const base64Pdf = pdfBuffer.toString("base64");
const dataUri = `data:application/pdf;base64,${base64Pdf}`;
console.log("[Mistral] PDF converted to base64, size:", Math.round(base64Pdf.length / 1024), "KB");
return dataUri;
} catch (error) {
console.error("Error converting PDF to base64:", error);
throw new Error("Failed to convert PDF to base64");
}
async function extractTextFromPdf(pdfBuffer: Buffer): Promise<string> {
return new Promise((resolve, reject) => {
const pdfParser = new (PDFParser as any)(null, 1);
pdfParser.on("pdfParser_dataError", (errData: any) => {
console.error("Error parsing PDF:", errData.parserError);
reject(new Error("Failed to parse PDF"));
});
pdfParser.on("pdfParser_dataReady", (pdfData: any) => {
try {
let text = "";
// Extract text from all pages
if (pdfData.Pages) {
for (const page of pdfData.Pages) {
if (page.Texts) {
for (const textItem of page.Texts) {
if (textItem.R) {
for (const run of textItem.R) {
if (run.T) {
try {
text += decodeURIComponent(run.T) + " ";
} catch (e) {
// If decodeURIComponent fails, use the raw text
text += run.T + " ";
}
}
}
}
}
text += "\n";
}
}
}
console.log("[PDF] Text extracted, length:", text.length, "characters");
resolve(text);
} catch (error) {
console.error("Error extracting text from PDF data:", error);
reject(new Error("Failed to extract text from PDF"));
}
});
pdfParser.parseBuffer(pdfBuffer);
});
}
/**
@@ -106,13 +143,30 @@ export async function extractInvoicesWithMistral(
subscription?: string | null;
}
): Promise<MultiInvoiceResult> {
// Load user's field configuration
const { getLlmFieldsConfigByUser } = await import("./db");
const fieldsConfig = await getLlmFieldsConfigByUser(userId);
// Build quality score instruction based on required fields
const requiredFields = fieldsConfig.filter(f => f.isRequired === 1);
const optionalFields = fieldsConfig.filter(f => f.isRequired === 0);
let qualityScoreInstruction = "- qualityScore: Score de qualité de l'extraction de 0 à 100";
if (requiredFields.length > 0) {
const requiredFieldNames = requiredFields.map(f => f.displayName).join(", ");
qualityScoreInstruction += ` (100 = tous les champs obligatoires trouvés: ${requiredFieldNames})`;
}
if (optionalFields.length > 0) {
const optionalFieldNames = optionalFields.map(f => f.displayName).join(", ");
qualityScoreInstruction += `. Champs optionnels (n'affectent pas le score): ${optionalFieldNames}`;
}
const startTime = Date.now();
try {
console.log("[Mistral] Starting invoice extraction...");
// Convert PDF to base64
const pdfDataUri = convertPdfToBase64(pdfBuffer);
// Extract text from PDF
const pdfText = await extractTextFromPdf(pdfBuffer);
// Get PDF page count
const pdfDoc = await PDFDocument.load(pdfBuffer);
@@ -153,7 +207,7 @@ Pour chaque facture trouvée, extrais les informations suivantes:
- orderNumber: Numéro de commande client (si présent)
- totalAmount: Montant total TTC (nombre décimal)
- pageRange: Plage de pages de cette facture (ex: "1-2" ou "5")
- qualityScore: Score de qualité de l'extraction de 0 à 100 (100 = toutes les informations trouvées et claires)
${qualityScoreInstruction}
- extractedText: Texte complet extrait de la facture (tout le texte visible sur les pages de cette facture)
${subscriptionInstruction}${keywordsHint}
@@ -179,15 +233,13 @@ Réponds UNIQUEMENT avec un objet JSON valide au format suivant:
Si une information n'est pas trouvée, utilise null. Ne retourne AUCUN texte en dehors du JSON.`;
// Call Mistral LLM with PDF
// Call Mistral LLM with extracted text
const fullPrompt = `${prompt}\n\nTexte extrait du PDF:\n${pdfText}`;
const response = await invokeLLM({
messages: [
{
role: "user",
content: [
{ type: "text", text: prompt },
{ type: "file_url", file_url: { url: pdfDataUri, mime_type: "application/pdf" } },
],
content: fullPrompt,
},
],
});