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:
@@ -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,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user