n8n-nodes-docx-filler 2.1.0 → 2.3.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) {
@@ -530,6 +565,122 @@ async function getDocumentBuffer(context, itemIndex, inputType, inputValue, item
530
565
  validateDocxBuffer(buffer, source);
531
566
  return buffer;
532
567
  }
568
+ /**
569
+ * Génère un rapport de vérification basique (sans LLM)
570
+ */
571
+ function generateBasicVerificationReport(sourceData, templateDocType, filledFields, fillPositions) {
572
+ const fieldDetails = [];
573
+ const warnings = [];
574
+ const suggestions = [];
575
+ // Analyser les champs remplis
576
+ for (const field of sourceData.fields) {
577
+ const wasFilled = filledFields.some(f => f.includes(field.fieldType));
578
+ fieldDetails.push({
579
+ fieldType: field.fieldType,
580
+ label: field.label,
581
+ filledValue: field.value,
582
+ confidence: field.confidence >= 0.9 ? 'high' : field.confidence >= 0.7 ? 'medium' : 'low',
583
+ doubt: field.confidence < 0.7 ? 'Confiance faible dans l\'extraction' : null,
584
+ wasEmpty: !wasFilled,
585
+ });
586
+ }
587
+ // Identifier les champs non remplis
588
+ const filledTypes = new Set(sourceData.fields.map(f => f.fieldType));
589
+ const expectedFields = ['nom_commercial', 'siret', 'adresse', 'email', 'telephone'];
590
+ const unfilledFields = expectedFields.filter(f => !filledTypes.has(f));
591
+ if (unfilledFields.length > 0) {
592
+ warnings.push(`Champs attendus non trouvés: ${unfilledFields.join(', ')}`);
593
+ }
594
+ // Vérifier la compatibilité
595
+ const compatiblePairs = [
596
+ ['DC1', 'DC1'], ['DC1', 'DC2'], ['DC2', 'DC1'], ['DC2', 'DC2'],
597
+ ['AE', 'AE'], ['AE', 'DC1'], ['AE', 'DC2'],
598
+ ['DC1', 'AE'], ['DC2', 'AE'],
599
+ ];
600
+ const isCompatible = compatiblePairs.some(([s, t]) => sourceData.documentType.includes(s) && templateDocType.includes(t)) || sourceData.documentType === 'unknown' || templateDocType === 'unknown';
601
+ if (!isCompatible) {
602
+ warnings.push(`Types de documents potentiellement incompatibles: ${sourceData.documentType} → ${templateDocType}`);
603
+ }
604
+ // Suggestions
605
+ if (sourceData.fields.length < 3) {
606
+ suggestions.push('Peu de champs extraits. Vérifiez que le document source contient bien les données entreprise.');
607
+ }
608
+ if (fillPositions.length > sourceData.fields.length) {
609
+ suggestions.push('Le template contient plus de champs que le source. Certains resteront vides.');
610
+ }
611
+ const highConfCount = fieldDetails.filter(f => f.confidence === 'high').length;
612
+ const medConfCount = fieldDetails.filter(f => f.confidence === 'medium').length;
613
+ const lowConfCount = fieldDetails.filter(f => f.confidence === 'low').length;
614
+ return {
615
+ documentsCompatible: isCompatible,
616
+ compatibilityScore: isCompatible ? (sourceData.fields.length > 3 ? 85 : 60) : 30,
617
+ compatibilityReason: isCompatible
618
+ ? 'Documents compatibles pour le mapping des données entreprise'
619
+ : 'Types de documents potentiellement incompatibles',
620
+ sourceDocType: sourceData.documentType,
621
+ templateDocType,
622
+ fieldsAnalysis: {
623
+ total: fillPositions.length,
624
+ filled: filledFields.length,
625
+ unfilled: fillPositions.length - filledFields.length,
626
+ highConfidence: highConfCount,
627
+ mediumConfidence: medConfCount,
628
+ lowConfidence: lowConfCount,
629
+ },
630
+ fieldDetails,
631
+ checkboxesAnalysis: {
632
+ total: sourceData.checkboxes.length,
633
+ checked: sourceData.checkboxes.filter(c => c.isChecked).length,
634
+ unchecked: sourceData.checkboxes.filter(c => !c.isChecked).length,
635
+ },
636
+ warnings,
637
+ suggestions,
638
+ };
639
+ }
640
+ /**
641
+ * Effectue une vérification avec le LLM
642
+ */
643
+ async function performLLMVerification(llm, sourceText, templateText, sourceDocType, templateDocType, mappedFields) {
644
+ var _a, _b, _c, _d, _e, _f;
645
+ const prompt = VERIFICATION_PROMPT
646
+ .replace('{source_type}', sourceDocType)
647
+ .replace('{template_type}', templateDocType)
648
+ .replace('{source_text}', sourceText.substring(0, 3000))
649
+ .replace('{template_text}', templateText.substring(0, 3000))
650
+ .replace('{mapped_fields}', JSON.stringify(mappedFields, null, 2));
651
+ try {
652
+ const response = await llm.invoke(prompt);
653
+ const responseText = typeof response === 'string'
654
+ ? response
655
+ : response.content || response.text || JSON.stringify(response);
656
+ const jsonMatch = responseText.match(/\{[\s\S]*\}/);
657
+ if (jsonMatch) {
658
+ const parsed = JSON.parse(jsonMatch[0]);
659
+ return {
660
+ documentsCompatible: (_a = parsed.documentsCompatible) !== null && _a !== void 0 ? _a : true,
661
+ compatibilityScore: (_b = parsed.compatibilityScore) !== null && _b !== void 0 ? _b : 70,
662
+ compatibilityReason: (_c = parsed.compatibilityReason) !== null && _c !== void 0 ? _c : 'Analyse IA effectuée',
663
+ warnings: (_d = parsed.warnings) !== null && _d !== void 0 ? _d : [],
664
+ suggestions: (_e = parsed.suggestions) !== null && _e !== void 0 ? _e : [],
665
+ fieldDetails: ((_f = parsed.fieldVerifications) !== null && _f !== void 0 ? _f : []).map((fv) => ({
666
+ fieldType: fv.fieldType,
667
+ label: fv.label,
668
+ filledValue: fv.filledValue,
669
+ confidence: fv.confidence || 'medium',
670
+ doubt: fv.doubt,
671
+ wasEmpty: false,
672
+ })),
673
+ };
674
+ }
675
+ }
676
+ catch (error) {
677
+ // En cas d'erreur, retourner un rapport partiel
678
+ return {
679
+ warnings: [`Erreur lors de la vérification IA: ${error.message}`],
680
+ };
681
+ }
682
+ return {};
683
+ }
533
684
  /**
534
685
  * Convertit le document en texte structuré pour le LLM
535
686
  */
@@ -609,6 +760,68 @@ RÈGLES:
609
760
  1. Fais le mapping SÉMANTIQUE (Raison sociale = Dénomination = Nom commercial)
610
761
  2. Utilise les [EMPTY] du template comme positions cibles
611
762
  3. Le templateIndex doit correspondre à un paragraphe [EMPTY] qui suit un [LABEL]`;
763
+ const VERIFICATION_PROMPT = `Tu es un expert en vérification de documents administratifs français (DC1, DC2, AE, ATTRI1).
764
+
765
+ CONTEXTE:
766
+ - Document SOURCE (contient les données de l'entreprise): {source_type}
767
+ - Document TEMPLATE (formulaire à remplir): {template_type}
768
+
769
+ DOCUMENT SOURCE (avec données):
770
+ """
771
+ {source_text}
772
+ """
773
+
774
+ DOCUMENT TEMPLATE (à remplir):
775
+ """
776
+ {template_text}
777
+ """
778
+
779
+ DONNÉES EXTRAITES ET MAPPÉES:
780
+ {mapped_fields}
781
+
782
+ ANALYSE DEMANDÉE:
783
+ 1. Les deux documents sont-ils compatibles pour un mapping de données entreprise ?
784
+ - Un DC1/DC2/AE rempli peut servir de source pour un autre DC1/DC2/AE vide
785
+ - Les données d'entreprise (SIRET, adresse, nom, etc.) sont transférables entre formulaires similaires
786
+
787
+ 2. Pour chaque champ mappé, évalue:
788
+ - La CONFIANCE du mapping (high/medium/low)
789
+ - Un éventuel DOUTE si le mapping semble incorrect
790
+
791
+ 3. Identifie:
792
+ - Les champs qui DEVRAIENT être remplis mais ne le sont pas
793
+ - Les WARNINGS (incohérences potentielles)
794
+ - Les SUGGESTIONS d'amélioration
795
+
796
+ IMPORTANT - Distingue bien:
797
+ - Données de l'ACHETEUR (nom du marché, références, dates limites) = NE PAS mapper
798
+ - Données du CANDIDAT (entreprise: SIRET, adresse, nom commercial, etc.) = À mapper
799
+
800
+ Retourne UNIQUEMENT un JSON valide:
801
+ {
802
+ "documentsCompatible": true,
803
+ "compatibilityScore": 85,
804
+ "compatibilityReason": "Les deux documents sont des formulaires de marchés publics partageant les mêmes champs entreprise",
805
+ "fieldVerifications": [
806
+ {
807
+ "fieldType": "siret",
808
+ "label": "Numéro SIRET",
809
+ "filledValue": "89198692900018",
810
+ "confidence": "high",
811
+ "doubt": null
812
+ },
813
+ {
814
+ "fieldType": "adresse",
815
+ "label": "Adresse postale",
816
+ "filledValue": "13 rue exemple",
817
+ "confidence": "medium",
818
+ "doubt": "L'adresse semble incomplète (code postal manquant)"
819
+ }
820
+ ],
821
+ "unfilledFields": ["capital", "code_naf"],
822
+ "warnings": ["Le SIRET source ne correspond pas au format attendu"],
823
+ "suggestions": ["Vérifier manuellement le champ TVA intracommunautaire"]
824
+ }`;
612
825
  // ============================================================================
613
826
  // Main Node Class
614
827
  // ============================================================================
@@ -738,6 +951,14 @@ class DocxFillerAI {
738
951
  displayOptions: { show: { operation: ['fill'] } },
739
952
  description: 'Utiliser le LLM connecté pour un mapping plus intelligent (plus lent)',
740
953
  },
954
+ {
955
+ displayName: 'AI Verification',
956
+ name: 'enableVerification',
957
+ type: 'boolean',
958
+ default: true,
959
+ displayOptions: { show: { operation: ['fill'] } },
960
+ description: 'Activer la vérification IA pour valider le mapping et détecter les doutes',
961
+ },
741
962
  // Extract
742
963
  {
743
964
  displayName: 'Document Input Type',
@@ -789,6 +1010,7 @@ class DocxFillerAI {
789
1010
  const templateValue = this.getNodeParameter('templateDocument', i, '');
790
1011
  const outputProperty = this.getNodeParameter('outputProperty', i);
791
1012
  const useLLM = this.getNodeParameter('useLLM', i);
1013
+ const enableVerification = this.getNodeParameter('enableVerification', i);
792
1014
  // Charger les documents
793
1015
  const sourceBuffer = await getDocumentBuffer(this, i, sourceInputType, sourceValue, items, 'source');
794
1016
  const templateBuffer = await getDocumentBuffer(this, i, templateInputType, templateValue, items, 'template');
@@ -854,22 +1076,87 @@ class DocxFillerAI {
854
1076
  type: 'nodebuffer',
855
1077
  compression: 'DEFLATE',
856
1078
  });
1079
+ // Détecter le type du template (document de sortie)
1080
+ const templateDocType = detectDocumentType(templateParagraphs);
1081
+ const finalDocType = templateDocType !== 'unknown' ? templateDocType : sourceData.documentType;
1082
+ // Générer le rapport de vérification
1083
+ const fillPositionsForReport = findFillPositions(templateParagraphs);
1084
+ let verificationReport = generateBasicVerificationReport(sourceData, finalDocType, filledFields, fillPositionsForReport);
1085
+ // Si vérification IA activée et LLM disponible, enrichir le rapport
1086
+ if (enableVerification && llm) {
1087
+ const sourceStructuredText = docxToStructuredText(sourceParagraphs);
1088
+ const templateStructuredText = docxToStructuredText(templateParagraphs);
1089
+ const llmVerification = await performLLMVerification(llm, sourceStructuredText, templateStructuredText, sourceData.documentType, finalDocType, filledFields);
1090
+ // Fusionner les résultats LLM avec le rapport basique
1091
+ if (llmVerification.documentsCompatible !== undefined) {
1092
+ verificationReport.documentsCompatible = llmVerification.documentsCompatible;
1093
+ }
1094
+ if (llmVerification.compatibilityScore !== undefined) {
1095
+ verificationReport.compatibilityScore = llmVerification.compatibilityScore;
1096
+ }
1097
+ if (llmVerification.compatibilityReason) {
1098
+ verificationReport.compatibilityReason = llmVerification.compatibilityReason;
1099
+ }
1100
+ if (llmVerification.warnings && llmVerification.warnings.length > 0) {
1101
+ verificationReport.warnings = [...verificationReport.warnings, ...llmVerification.warnings];
1102
+ }
1103
+ if (llmVerification.suggestions && llmVerification.suggestions.length > 0) {
1104
+ verificationReport.suggestions = [...verificationReport.suggestions, ...llmVerification.suggestions];
1105
+ }
1106
+ if (llmVerification.fieldDetails && llmVerification.fieldDetails.length > 0) {
1107
+ // Enrichir avec les détails LLM (doutes, confiance)
1108
+ for (const llmField of llmVerification.fieldDetails) {
1109
+ const existingField = verificationReport.fieldDetails.find(f => f.fieldType === llmField.fieldType);
1110
+ if (existingField && llmField.doubt) {
1111
+ existingField.doubt = llmField.doubt;
1112
+ existingField.confidence = llmField.confidence;
1113
+ }
1114
+ }
1115
+ }
1116
+ }
857
1117
  // Générer le nom de fichier
858
1118
  const date = new Date().toISOString().split('T')[0];
859
1119
  const companyName = (sourceData.companyName || 'Document').replace(/[^a-zA-Z0-9]/g, '_');
860
- const outputFilename = `${companyName}_${sourceData.documentType}_${date}.docx`;
1120
+ const outputFilename = `${companyName}_${finalDocType}_${date}.docx`;
861
1121
  const binaryData = await this.helpers.prepareBinaryData(outputBuffer, outputFilename, DOCX_MIME_TYPE);
1122
+ // Extraire les champs avec doutes pour un accès facile
1123
+ const fieldsWithDoubts = verificationReport.fieldDetails
1124
+ .filter(f => f.doubt !== null)
1125
+ .map(f => ({ field: f.fieldType, label: f.label, doubt: f.doubt }));
862
1126
  returnData.push({
863
1127
  json: {
864
1128
  success: true,
865
1129
  filename: outputFilename,
866
- documentType: sourceData.documentType,
1130
+ documentType: finalDocType,
1131
+ sourceDocumentType: sourceData.documentType,
867
1132
  companyName: sourceData.companyName,
868
- filledFields,
869
- modifiedCheckboxes,
870
- extractedFieldsCount: sourceData.fields.length,
1133
+ // Résumé du remplissage
1134
+ summary: {
1135
+ filledFields: filledFields.length,
1136
+ unfilledFields: verificationReport.fieldsAnalysis.unfilled,
1137
+ modifiedCheckboxes,
1138
+ extractedFieldsCount: sourceData.fields.length,
1139
+ compatibilityScore: verificationReport.compatibilityScore,
1140
+ },
1141
+ // Détails des champs remplis
1142
+ filledFieldsList: filledFields,
1143
+ // Rapport de vérification complet
1144
+ verification: {
1145
+ documentsCompatible: verificationReport.documentsCompatible,
1146
+ compatibilityScore: verificationReport.compatibilityScore,
1147
+ compatibilityReason: verificationReport.compatibilityReason,
1148
+ fieldsAnalysis: verificationReport.fieldsAnalysis,
1149
+ checkboxesAnalysis: verificationReport.checkboxesAnalysis,
1150
+ fieldsWithDoubts,
1151
+ warnings: verificationReport.warnings,
1152
+ suggestions: verificationReport.suggestions,
1153
+ },
1154
+ // Détails complets des champs (pour debug)
1155
+ fieldDetails: verificationReport.fieldDetails,
1156
+ // Métadonnées
871
1157
  usedLLM: useLLM && !!llm,
872
- message: `Rempli: ${filledFields.length} champs, ${modifiedCheckboxes} checkboxes`,
1158
+ usedVerification: enableVerification && !!llm,
1159
+ message: `Rempli: ${filledFields.length} champs, ${modifiedCheckboxes} checkboxes. Score: ${verificationReport.compatibilityScore}%`,
873
1160
  },
874
1161
  binary: {
875
1162
  [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.3.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",