n8n-nodes-docx-filler 2.1.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -197,6 +197,32 @@ function validateFieldValue(fieldType, value) {
197
197
  const cleanValue = value.trim();
198
198
  return field.valuePatterns.some(pattern => pattern.test(cleanValue));
199
199
  }
200
+ /**
201
+ * Détecte le type de document à partir des paragraphes
202
+ */
203
+ function detectDocumentType(paragraphs) {
204
+ for (const p of paragraphs) {
205
+ const text = p.textNormalized;
206
+ // AE - Acte d'Engagement (vérifier en premier car plus spécifique)
207
+ if (text.includes('acte d\'engagement') || text.includes('acte d engagement') ||
208
+ text.includes('engagement du titulaire')) {
209
+ return 'AE';
210
+ }
211
+ // DC1 - Lettre de candidature
212
+ if (text.includes('dc1') || text.includes('lettre de candidature')) {
213
+ return 'DC1';
214
+ }
215
+ // DC2 - Déclaration du candidat
216
+ if (text.includes('dc2') || text.includes('déclaration du candidat individuel')) {
217
+ return 'DC2';
218
+ }
219
+ // ATTRI1 - Attribution
220
+ if (text.includes('attri1')) {
221
+ return 'ATTRI1';
222
+ }
223
+ }
224
+ return 'unknown';
225
+ }
200
226
  /**
201
227
  * Extrait les données d'un document source
202
228
  */
@@ -209,12 +235,21 @@ function extractSourceData(paragraphs) {
209
235
  for (let i = 0; i < paragraphs.length; i++) {
210
236
  const p = paragraphs[i];
211
237
  const text = p.text.trim();
212
- // Détecter le type de document
213
- if (p.textNormalized.includes('dc1') || p.textNormalized.includes('lettre de candidature')) {
214
- documentType = 'DC1';
215
- }
216
- else if (p.textNormalized.includes('dc2') || p.textNormalized.includes('déclaration du candidat')) {
217
- documentType = 'DC2';
238
+ // Détecter le type de document (priorité au premier match explicite)
239
+ if (documentType === 'unknown') {
240
+ if (p.textNormalized.includes('acte d\'engagement') || p.textNormalized.includes('acte d engagement') ||
241
+ (p.textNormalized.includes('ae') && p.textNormalized.includes('formulaire'))) {
242
+ documentType = 'AE';
243
+ }
244
+ else if (p.textNormalized.includes('dc1') || p.textNormalized.includes('lettre de candidature')) {
245
+ documentType = 'DC1';
246
+ }
247
+ else if (p.textNormalized.includes('dc2') || p.textNormalized.includes('déclaration du candidat')) {
248
+ documentType = 'DC2';
249
+ }
250
+ else if (p.textNormalized.includes('attri1') || p.textNormalized.includes('attribution')) {
251
+ documentType = 'ATTRI1';
252
+ }
218
253
  }
219
254
  // Extraire les checkboxes
220
255
  if (p.hasCheckbox) {
@@ -271,21 +306,61 @@ function extractSourceData(paragraphs) {
271
306
  }
272
307
  return { documentType, companyName, fields, checkboxes };
273
308
  }
309
+ /**
310
+ * Vérifie si un texte est une note de bas de page ou un titre de section (faux positif)
311
+ */
312
+ function isFalsePositive(text) {
313
+ const trimmed = text.trim();
314
+ const normalized = normalize(text);
315
+ // Notes de bas de page
316
+ if (/^\(\*+\)/.test(trimmed))
317
+ return true;
318
+ if (/^\*+\s/.test(trimmed))
319
+ return true;
320
+ // Titres de section (A -, B -, C -, etc.)
321
+ if (/^[A-Z]\s*[-–—]\s/.test(trimmed))
322
+ return true;
323
+ // Textes explicatifs trop longs (généralement des instructions)
324
+ if (trimmed.length > 150 && !trimmed.includes(':'))
325
+ return true;
326
+ // Références aux membres du groupement (zone spéciale, pas candidat individuel)
327
+ if (normalized.includes('membres du groupement') && normalized.includes('***'))
328
+ return true;
329
+ if (normalized.includes('groupement conjoint'))
330
+ return true;
331
+ if (normalized.includes('groupement solidaire'))
332
+ return true;
333
+ // Textes qui commencent par des références de notes
334
+ if (/^\(\*/.test(trimmed) || /^à défaut/.test(normalized))
335
+ return true;
336
+ return false;
337
+ }
274
338
  /**
275
339
  * Trouve les positions à remplir dans un template
340
+ * IMPORTANT: Chaque type de champ n'est rempli qu'UNE SEULE FOIS (premier match)
276
341
  */
277
342
  function findFillPositions(paragraphs) {
278
343
  const positions = [];
279
344
  const usedFillIndices = new Set();
345
+ const usedFieldTypes = new Set(); // NOUVEAU: éviter les doublons par type
280
346
  for (let i = 0; i < paragraphs.length; i++) {
281
347
  const p = paragraphs[i];
348
+ // Ignorer les faux positifs (notes, titres, etc.)
349
+ if (isFalsePositive(p.text))
350
+ continue;
282
351
  const fieldType = detectFieldType(p.text);
352
+ // NOUVEAU: Si ce type de champ a déjà été trouvé, passer au suivant
353
+ if (fieldType && usedFieldTypes.has(fieldType))
354
+ continue;
283
355
  if (fieldType) {
284
356
  // Chercher le prochain paragraphe vide pour y insérer la valeur
285
357
  for (let j = i + 1; j < Math.min(i + 6, paragraphs.length); j++) {
286
358
  if (usedFillIndices.has(j))
287
359
  continue;
288
360
  const nextP = paragraphs[j];
361
+ // Ignorer les faux positifs comme position de remplissage
362
+ if (isFalsePositive(nextP.text))
363
+ continue;
289
364
  // Position vide = remplissable
290
365
  if (nextP.isEmpty) {
291
366
  positions.push({
@@ -296,6 +371,7 @@ function findFillPositions(paragraphs) {
296
371
  paragraph: nextP,
297
372
  });
298
373
  usedFillIndices.add(j);
374
+ usedFieldTypes.add(fieldType); // NOUVEAU: marquer comme utilisé
299
375
  break;
300
376
  }
301
377
  // Si on trouve un autre label, arrêter
@@ -530,6 +606,139 @@ async function getDocumentBuffer(context, itemIndex, inputType, inputValue, item
530
606
  validateDocxBuffer(buffer, source);
531
607
  return buffer;
532
608
  }
609
+ /**
610
+ * Génère un rapport de vérification basique (sans LLM)
611
+ */
612
+ function generateBasicVerificationReport(sourceData, templateDocType, filledFields, fillPositions) {
613
+ const fieldDetails = [];
614
+ const warnings = [];
615
+ const suggestions = [];
616
+ // Analyser les champs remplis
617
+ for (const field of sourceData.fields) {
618
+ const wasFilled = filledFields.some(f => f.includes(field.fieldType));
619
+ fieldDetails.push({
620
+ fieldType: field.fieldType,
621
+ label: field.label,
622
+ filledValue: field.value,
623
+ confidence: field.confidence >= 0.9 ? 'high' : field.confidence >= 0.7 ? 'medium' : 'low',
624
+ doubt: field.confidence < 0.7 ? 'Confiance faible dans l\'extraction' : null,
625
+ wasEmpty: !wasFilled,
626
+ });
627
+ }
628
+ // Identifier les champs non remplis
629
+ const filledTypes = new Set(sourceData.fields.map(f => f.fieldType));
630
+ const expectedFields = ['nom_commercial', 'siret', 'adresse', 'email', 'telephone'];
631
+ const unfilledFields = expectedFields.filter(f => !filledTypes.has(f));
632
+ if (unfilledFields.length > 0) {
633
+ warnings.push(`Champs attendus non trouvés: ${unfilledFields.join(', ')}`);
634
+ }
635
+ // Vérifier la compatibilité
636
+ const compatiblePairs = [
637
+ ['DC1', 'DC1'], ['DC1', 'DC2'], ['DC2', 'DC1'], ['DC2', 'DC2'],
638
+ ['AE', 'AE'], ['AE', 'DC1'], ['AE', 'DC2'],
639
+ ['DC1', 'AE'], ['DC2', 'AE'],
640
+ ];
641
+ const isCompatible = compatiblePairs.some(([s, t]) => sourceData.documentType.includes(s) && templateDocType.includes(t)) || sourceData.documentType === 'unknown' || templateDocType === 'unknown';
642
+ if (!isCompatible) {
643
+ warnings.push(`Types de documents potentiellement incompatibles: ${sourceData.documentType} → ${templateDocType}`);
644
+ }
645
+ // Suggestions
646
+ if (sourceData.fields.length < 3) {
647
+ suggestions.push('Peu de champs extraits. Vérifiez que le document source contient bien les données entreprise.');
648
+ }
649
+ if (fillPositions.length > sourceData.fields.length) {
650
+ suggestions.push('Le template contient plus de champs que le source. Certains resteront vides.');
651
+ }
652
+ const highConfCount = fieldDetails.filter(f => f.confidence === 'high').length;
653
+ const medConfCount = fieldDetails.filter(f => f.confidence === 'medium').length;
654
+ const lowConfCount = fieldDetails.filter(f => f.confidence === 'low').length;
655
+ return {
656
+ documentsCompatible: isCompatible,
657
+ compatibilityScore: isCompatible ? (sourceData.fields.length > 3 ? 85 : 60) : 30,
658
+ compatibilityReason: isCompatible
659
+ ? 'Documents compatibles pour le mapping des données entreprise'
660
+ : 'Types de documents potentiellement incompatibles',
661
+ sourceDocType: sourceData.documentType,
662
+ templateDocType,
663
+ fieldsAnalysis: {
664
+ total: fillPositions.length,
665
+ filled: filledFields.length,
666
+ unfilled: fillPositions.length - filledFields.length,
667
+ highConfidence: highConfCount,
668
+ mediumConfidence: medConfCount,
669
+ lowConfidence: lowConfCount,
670
+ },
671
+ fieldDetails,
672
+ checkboxesAnalysis: {
673
+ total: sourceData.checkboxes.length,
674
+ checked: sourceData.checkboxes.filter(c => c.isChecked).length,
675
+ unchecked: sourceData.checkboxes.filter(c => !c.isChecked).length,
676
+ },
677
+ warnings,
678
+ suggestions,
679
+ };
680
+ }
681
+ /**
682
+ * Effectue une vérification avec le LLM
683
+ */
684
+ async function performLLMVerification(llm, sourceText, templateText, sourceDocType, templateDocType, mappedFields) {
685
+ var _a, _b, _c, _d, _e, _f;
686
+ const prompt = VERIFICATION_PROMPT
687
+ .replace('{source_type}', sourceDocType)
688
+ .replace('{template_type}', templateDocType)
689
+ .replace('{source_text}', sourceText.substring(0, 3000))
690
+ .replace('{template_text}', templateText.substring(0, 3000))
691
+ .replace('{mapped_fields}', JSON.stringify(mappedFields, null, 2));
692
+ try {
693
+ const response = await llm.invoke(prompt);
694
+ let responseText;
695
+ if (typeof response === 'string') {
696
+ responseText = response;
697
+ }
698
+ else if (response && typeof response.content === 'string') {
699
+ responseText = response.content;
700
+ }
701
+ else if (response && typeof response.text === 'string') {
702
+ responseText = response.text;
703
+ }
704
+ else if (response && Array.isArray(response.content)) {
705
+ // Format OpenAI/Anthropic avec content array
706
+ responseText = response.content
707
+ .filter((c) => c.type === 'text')
708
+ .map((c) => c.text)
709
+ .join('');
710
+ }
711
+ else {
712
+ responseText = JSON.stringify(response);
713
+ }
714
+ const jsonMatch = responseText.match(/\{[\s\S]*\}/);
715
+ if (jsonMatch) {
716
+ const parsed = JSON.parse(jsonMatch[0]);
717
+ return {
718
+ documentsCompatible: (_a = parsed.documentsCompatible) !== null && _a !== void 0 ? _a : true,
719
+ compatibilityScore: (_b = parsed.compatibilityScore) !== null && _b !== void 0 ? _b : 70,
720
+ compatibilityReason: (_c = parsed.compatibilityReason) !== null && _c !== void 0 ? _c : 'Analyse IA effectuée',
721
+ warnings: (_d = parsed.warnings) !== null && _d !== void 0 ? _d : [],
722
+ suggestions: (_e = parsed.suggestions) !== null && _e !== void 0 ? _e : [],
723
+ fieldDetails: ((_f = parsed.fieldVerifications) !== null && _f !== void 0 ? _f : []).map((fv) => ({
724
+ fieldType: fv.fieldType,
725
+ label: fv.label,
726
+ filledValue: fv.filledValue,
727
+ confidence: fv.confidence || 'medium',
728
+ doubt: fv.doubt,
729
+ wasEmpty: false,
730
+ })),
731
+ };
732
+ }
733
+ }
734
+ catch (error) {
735
+ // En cas d'erreur, retourner un rapport partiel
736
+ return {
737
+ warnings: [`Erreur lors de la vérification IA: ${error.message}`],
738
+ };
739
+ }
740
+ return {};
741
+ }
533
742
  /**
534
743
  * Convertit le document en texte structuré pour le LLM
535
744
  */
@@ -587,28 +796,102 @@ IMPORTANT:
587
796
  - Extrait TOUTES les valeurs remplies (SIRET, adresse, email, téléphone, etc.)
588
797
  - Pour les checkboxes, isChecked=true si ☒☑▣, false si ☐□▢
589
798
  - paragraphIndex = numéro entre crochets au début de chaque ligne`;
590
- const FILL_PROMPT = `Tu dois mapper les données source vers les positions du template.
799
+ const FILL_PROMPT = `Tu es un expert en remplissage de formulaires administratifs français (DC1, DC2, AE).
591
800
 
592
- DONNÉES SOURCE:
801
+ OBJECTIF: Mapper INTELLIGEMMENT les données entreprise du document SOURCE vers le TEMPLATE vide.
802
+
803
+ DONNÉES ENTREPRISE EXTRAITES DU SOURCE:
593
804
  {source_data}
594
805
 
595
- TEMPLATE (positions vides à remplir):
806
+ TEMPLATE À REMPLIR (paragraphes indexés):
596
807
  {template_text}
597
808
 
598
- Retourne UNIQUEMENT un JSON:
809
+ RÈGLES CRITIQUES:
810
+ 1. NE MAPPER QUE les données ENTREPRISE (candidat): nom commercial, SIRET, adresse, email, téléphone, TVA, etc.
811
+ 2. NE PAS MAPPER les données ACHETEUR: nom du marché, objet, références, dates limites, montants du marché
812
+ 3. CHAQUE champ ne doit être rempli qu'UNE SEULE FOIS (pas de doublons!)
813
+ 4. NE PAS remplir les zones qui sont:
814
+ - Des notes de bas de page (commençant par (*), (**), etc.)
815
+ - Des titres de section (A -, B -, C -, F -, etc.)
816
+ - Des instructions ou explications
817
+ 5. Ne remplir QUE les [EMPTY] qui suivent directement un [LABEL] correspondant à un champ entreprise
818
+
819
+ Retourne UNIQUEMENT un JSON valide:
599
820
  {
600
821
  "mappings": [
601
- {"sourceField": "nom_commercial", "templateIndex": 34, "value": "ROKODO.IO"}
822
+ {"fieldType": "nom_commercial", "templateIndex": 34, "value": "ROKODO.IO", "confidence": "high"},
823
+ {"fieldType": "siret", "templateIndex": 56, "value": "89198692900018", "confidence": "high"}
602
824
  ],
603
825
  "checkboxMappings": [
604
- {"sourceSignature": "candidat pme", "templateIndex": 45, "shouldBeChecked": true}
826
+ {"templateIndex": 45, "shouldBeChecked": true, "reason": "PME confirmé"}
827
+ ],
828
+ "skippedFields": [
829
+ {"templateIndex": 78, "reason": "Note de bas de page, pas un champ à remplir"},
830
+ {"templateIndex": 90, "reason": "Titre de section F"}
605
831
  ]
606
- }
832
+ }`;
833
+ const VERIFICATION_PROMPT = `Tu es un expert en vérification de documents administratifs français (DC1, DC2, AE, ATTRI1).
834
+
835
+ CONTEXTE:
836
+ - Document SOURCE (contient les données de l'entreprise): {source_type}
837
+ - Document TEMPLATE (formulaire à remplir): {template_type}
607
838
 
608
- RÈGLES:
609
- 1. Fais le mapping SÉMANTIQUE (Raison sociale = Dénomination = Nom commercial)
610
- 2. Utilise les [EMPTY] du template comme positions cibles
611
- 3. Le templateIndex doit correspondre à un paragraphe [EMPTY] qui suit un [LABEL]`;
839
+ DOCUMENT SOURCE (avec données):
840
+ """
841
+ {source_text}
842
+ """
843
+
844
+ DOCUMENT TEMPLATE (à remplir):
845
+ """
846
+ {template_text}
847
+ """
848
+
849
+ DONNÉES EXTRAITES ET MAPPÉES:
850
+ {mapped_fields}
851
+
852
+ ANALYSE DEMANDÉE:
853
+ 1. Les deux documents sont-ils compatibles pour un mapping de données entreprise ?
854
+ - Un DC1/DC2/AE rempli peut servir de source pour un autre DC1/DC2/AE vide
855
+ - Les données d'entreprise (SIRET, adresse, nom, etc.) sont transférables entre formulaires similaires
856
+
857
+ 2. Pour chaque champ mappé, évalue:
858
+ - La CONFIANCE du mapping (high/medium/low)
859
+ - Un éventuel DOUTE si le mapping semble incorrect
860
+
861
+ 3. Identifie:
862
+ - Les champs qui DEVRAIENT être remplis mais ne le sont pas
863
+ - Les WARNINGS (incohérences potentielles)
864
+ - Les SUGGESTIONS d'amélioration
865
+
866
+ IMPORTANT - Distingue bien:
867
+ - Données de l'ACHETEUR (nom du marché, références, dates limites) = NE PAS mapper
868
+ - Données du CANDIDAT (entreprise: SIRET, adresse, nom commercial, etc.) = À mapper
869
+
870
+ Retourne UNIQUEMENT un JSON valide:
871
+ {
872
+ "documentsCompatible": true,
873
+ "compatibilityScore": 85,
874
+ "compatibilityReason": "Les deux documents sont des formulaires de marchés publics partageant les mêmes champs entreprise",
875
+ "fieldVerifications": [
876
+ {
877
+ "fieldType": "siret",
878
+ "label": "Numéro SIRET",
879
+ "filledValue": "89198692900018",
880
+ "confidence": "high",
881
+ "doubt": null
882
+ },
883
+ {
884
+ "fieldType": "adresse",
885
+ "label": "Adresse postale",
886
+ "filledValue": "13 rue exemple",
887
+ "confidence": "medium",
888
+ "doubt": "L'adresse semble incomplète (code postal manquant)"
889
+ }
890
+ ],
891
+ "unfilledFields": ["capital", "code_naf"],
892
+ "warnings": ["Le SIRET source ne correspond pas au format attendu"],
893
+ "suggestions": ["Vérifier manuellement le champ TVA intracommunautaire"]
894
+ }`;
612
895
  // ============================================================================
613
896
  // Main Node Class
614
897
  // ============================================================================
@@ -738,6 +1021,14 @@ class DocxFillerAI {
738
1021
  displayOptions: { show: { operation: ['fill'] } },
739
1022
  description: 'Utiliser le LLM connecté pour un mapping plus intelligent (plus lent)',
740
1023
  },
1024
+ {
1025
+ displayName: 'AI Verification',
1026
+ name: 'enableVerification',
1027
+ type: 'boolean',
1028
+ default: true,
1029
+ displayOptions: { show: { operation: ['fill'] } },
1030
+ description: 'Activer la vérification IA pour valider le mapping et détecter les doutes',
1031
+ },
741
1032
  // Extract
742
1033
  {
743
1034
  displayName: 'Document Input Type',
@@ -789,6 +1080,7 @@ class DocxFillerAI {
789
1080
  const templateValue = this.getNodeParameter('templateDocument', i, '');
790
1081
  const outputProperty = this.getNodeParameter('outputProperty', i);
791
1082
  const useLLM = this.getNodeParameter('useLLM', i);
1083
+ const enableVerification = this.getNodeParameter('enableVerification', i);
792
1084
  // Charger les documents
793
1085
  const sourceBuffer = await getDocumentBuffer(this, i, sourceInputType, sourceValue, items, 'source');
794
1086
  const templateBuffer = await getDocumentBuffer(this, i, templateInputType, templateValue, items, 'template');
@@ -825,19 +1117,68 @@ class DocxFillerAI {
825
1117
  // Fallback au mode sans LLM
826
1118
  sourceData = extractSourceData(sourceParagraphs);
827
1119
  }
828
- // Mapping avec LLM (pour amélioration future)
1120
+ // Mapping avec LLM - UTILISER les résultats de l'IA
829
1121
  const fillPrompt = FILL_PROMPT
830
1122
  .replace('{source_data}', JSON.stringify(sourceData, null, 2))
831
1123
  .replace('{template_text}', templateStructured);
832
- // Note: LLM mapping response peut être utilisé pour un mapping plus intelligent
833
- // Pour l'instant, on utilise la logique standard de mapping par patterns
834
- await llm.invoke(fillPrompt);
835
- // Utiliser la logique standard de mapping
836
- const fillPositions = findFillPositions(templateParagraphs);
837
- const result = fillTemplateXml(templateXml, templateParagraphs, sourceData, fillPositions);
838
- templateXml = result.xml;
839
- filledFields = result.filledFields;
840
- modifiedCheckboxes = result.modifiedCheckboxes;
1124
+ const fillResponse = await llm.invoke(fillPrompt);
1125
+ let llmMappings = [];
1126
+ // Parser la réponse du LLM
1127
+ try {
1128
+ let fillText;
1129
+ if (typeof fillResponse === 'string') {
1130
+ fillText = fillResponse;
1131
+ }
1132
+ else if (fillResponse && typeof fillResponse.content === 'string') {
1133
+ fillText = fillResponse.content;
1134
+ }
1135
+ else if (fillResponse && typeof fillResponse.text === 'string') {
1136
+ fillText = fillResponse.text;
1137
+ }
1138
+ else if (fillResponse && Array.isArray(fillResponse.content)) {
1139
+ fillText = fillResponse.content
1140
+ .filter((c) => c.type === 'text')
1141
+ .map((c) => c.text)
1142
+ .join('');
1143
+ }
1144
+ else {
1145
+ fillText = JSON.stringify(fillResponse);
1146
+ }
1147
+ const jsonMatch = fillText.match(/\{[\s\S]*\}/);
1148
+ if (jsonMatch) {
1149
+ const parsed = JSON.parse(jsonMatch[0]);
1150
+ llmMappings = parsed.mappings || [];
1151
+ }
1152
+ }
1153
+ catch {
1154
+ // Fallback si parsing échoue
1155
+ llmMappings = [];
1156
+ }
1157
+ // Si l'IA a fourni des mappings valides, les utiliser
1158
+ if (llmMappings.length > 0) {
1159
+ // Utiliser les mappings de l'IA
1160
+ const usedIndices = new Set();
1161
+ for (const mapping of llmMappings) {
1162
+ if (usedIndices.has(mapping.templateIndex))
1163
+ continue;
1164
+ const targetParagraph = templateParagraphs.find(p => p.index === mapping.templateIndex);
1165
+ if (!targetParagraph)
1166
+ continue;
1167
+ const oldP = targetParagraph.fullMatch;
1168
+ const newP = oldP.replace(/(<w:p[^>]*>)([\s\S]*?)(<\/w:p>)/, `$1<w:r><w:t>${escapeXml(mapping.value)}</w:t></w:r>$3`);
1169
+ templateXml = templateXml.replace(oldP, newP);
1170
+ filledFields.push(`${mapping.fieldType}: ${mapping.value}`);
1171
+ usedIndices.add(mapping.templateIndex);
1172
+ }
1173
+ }
1174
+ else {
1175
+ // Fallback: utiliser la logique standard
1176
+ const fillPositions = findFillPositions(templateParagraphs);
1177
+ const result = fillTemplateXml(templateXml, templateParagraphs, sourceData, fillPositions);
1178
+ templateXml = result.xml;
1179
+ filledFields = result.filledFields;
1180
+ modifiedCheckboxes = result.modifiedCheckboxes;
1181
+ }
841
1182
  }
842
1183
  else {
843
1184
  // Mode sans LLM: extraction et mapping par patterns
@@ -854,22 +1195,87 @@ class DocxFillerAI {
854
1195
  type: 'nodebuffer',
855
1196
  compression: 'DEFLATE',
856
1197
  });
1198
+ // Détecter le type du template (document de sortie)
1199
+ const templateDocType = detectDocumentType(templateParagraphs);
1200
+ const finalDocType = templateDocType !== 'unknown' ? templateDocType : sourceData.documentType;
1201
+ // Générer le rapport de vérification
1202
+ const fillPositionsForReport = findFillPositions(templateParagraphs);
1203
+ let verificationReport = generateBasicVerificationReport(sourceData, finalDocType, filledFields, fillPositionsForReport);
1204
+ // Si vérification IA activée et LLM disponible, enrichir le rapport
1205
+ if (enableVerification && llm) {
1206
+ const sourceStructuredText = docxToStructuredText(sourceParagraphs);
1207
+ const templateStructuredText = docxToStructuredText(templateParagraphs);
1208
+ const llmVerification = await performLLMVerification(llm, sourceStructuredText, templateStructuredText, sourceData.documentType, finalDocType, filledFields);
1209
+ // Fusionner les résultats LLM avec le rapport basique
1210
+ if (llmVerification.documentsCompatible !== undefined) {
1211
+ verificationReport.documentsCompatible = llmVerification.documentsCompatible;
1212
+ }
1213
+ if (llmVerification.compatibilityScore !== undefined) {
1214
+ verificationReport.compatibilityScore = llmVerification.compatibilityScore;
1215
+ }
1216
+ if (llmVerification.compatibilityReason) {
1217
+ verificationReport.compatibilityReason = llmVerification.compatibilityReason;
1218
+ }
1219
+ if (llmVerification.warnings && llmVerification.warnings.length > 0) {
1220
+ verificationReport.warnings = [...verificationReport.warnings, ...llmVerification.warnings];
1221
+ }
1222
+ if (llmVerification.suggestions && llmVerification.suggestions.length > 0) {
1223
+ verificationReport.suggestions = [...verificationReport.suggestions, ...llmVerification.suggestions];
1224
+ }
1225
+ if (llmVerification.fieldDetails && llmVerification.fieldDetails.length > 0) {
1226
+ // Enrichir avec les détails LLM (doutes, confiance)
1227
+ for (const llmField of llmVerification.fieldDetails) {
1228
+ const existingField = verificationReport.fieldDetails.find(f => f.fieldType === llmField.fieldType);
1229
+ if (existingField && llmField.doubt) {
1230
+ existingField.doubt = llmField.doubt;
1231
+ existingField.confidence = llmField.confidence;
1232
+ }
1233
+ }
1234
+ }
1235
+ }
857
1236
  // Générer le nom de fichier
858
1237
  const date = new Date().toISOString().split('T')[0];
859
1238
  const companyName = (sourceData.companyName || 'Document').replace(/[^a-zA-Z0-9]/g, '_');
860
- const outputFilename = `${companyName}_${sourceData.documentType}_${date}.docx`;
1239
+ const outputFilename = `${companyName}_${finalDocType}_${date}.docx`;
861
1240
  const binaryData = await this.helpers.prepareBinaryData(outputBuffer, outputFilename, DOCX_MIME_TYPE);
1241
+ // Extraire les champs avec doutes pour un accès facile
1242
+ const fieldsWithDoubts = verificationReport.fieldDetails
1243
+ .filter(f => f.doubt !== null)
1244
+ .map(f => ({ field: f.fieldType, label: f.label, doubt: f.doubt }));
862
1245
  returnData.push({
863
1246
  json: {
864
1247
  success: true,
865
1248
  filename: outputFilename,
866
- documentType: sourceData.documentType,
1249
+ documentType: finalDocType,
1250
+ sourceDocumentType: sourceData.documentType,
867
1251
  companyName: sourceData.companyName,
868
- filledFields,
869
- modifiedCheckboxes,
870
- extractedFieldsCount: sourceData.fields.length,
1252
+ // Résumé du remplissage
1253
+ summary: {
1254
+ filledFields: filledFields.length,
1255
+ unfilledFields: verificationReport.fieldsAnalysis.unfilled,
1256
+ modifiedCheckboxes,
1257
+ extractedFieldsCount: sourceData.fields.length,
1258
+ compatibilityScore: verificationReport.compatibilityScore,
1259
+ },
1260
+ // Détails des champs remplis
1261
+ filledFieldsList: filledFields,
1262
+ // Rapport de vérification complet
1263
+ verification: {
1264
+ documentsCompatible: verificationReport.documentsCompatible,
1265
+ compatibilityScore: verificationReport.compatibilityScore,
1266
+ compatibilityReason: verificationReport.compatibilityReason,
1267
+ fieldsAnalysis: verificationReport.fieldsAnalysis,
1268
+ checkboxesAnalysis: verificationReport.checkboxesAnalysis,
1269
+ fieldsWithDoubts,
1270
+ warnings: verificationReport.warnings,
1271
+ suggestions: verificationReport.suggestions,
1272
+ },
1273
+ // Détails complets des champs (pour debug)
1274
+ fieldDetails: verificationReport.fieldDetails,
1275
+ // Métadonnées
871
1276
  usedLLM: useLLM && !!llm,
872
- message: `Rempli: ${filledFields.length} champs, ${modifiedCheckboxes} checkboxes`,
1277
+ usedVerification: enableVerification && !!llm,
1278
+ message: `Rempli: ${filledFields.length} champs, ${modifiedCheckboxes} checkboxes. Score: ${verificationReport.compatibilityScore}%`,
873
1279
  },
874
1280
  binary: {
875
1281
  [outputProperty]: binaryData,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-docx-filler",
3
- "version": "2.1.0",
3
+ "version": "2.4.0",
4
4
  "description": "n8n node to automatically fill DOCX documents (French DC1, DC2, AE forms) using AI for semantic understanding and field mapping.",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",