n8n-nodes-docx-filler 2.3.0 → 2.4.1

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.
@@ -306,21 +306,61 @@ function extractSourceData(paragraphs) {
306
306
  }
307
307
  return { documentType, companyName, fields, checkboxes };
308
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
+ }
309
338
  /**
310
339
  * Trouve les positions à remplir dans un template
340
+ * IMPORTANT: Chaque type de champ n'est rempli qu'UNE SEULE FOIS (premier match)
311
341
  */
312
342
  function findFillPositions(paragraphs) {
313
343
  const positions = [];
314
344
  const usedFillIndices = new Set();
345
+ const usedFieldTypes = new Set(); // NOUVEAU: éviter les doublons par type
315
346
  for (let i = 0; i < paragraphs.length; i++) {
316
347
  const p = paragraphs[i];
348
+ // Ignorer les faux positifs (notes, titres, etc.)
349
+ if (isFalsePositive(p.text))
350
+ continue;
317
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;
318
355
  if (fieldType) {
319
356
  // Chercher le prochain paragraphe vide pour y insérer la valeur
320
357
  for (let j = i + 1; j < Math.min(i + 6, paragraphs.length); j++) {
321
358
  if (usedFillIndices.has(j))
322
359
  continue;
323
360
  const nextP = paragraphs[j];
361
+ // Ignorer les faux positifs comme position de remplissage
362
+ if (isFalsePositive(nextP.text))
363
+ continue;
324
364
  // Position vide = remplissable
325
365
  if (nextP.isEmpty) {
326
366
  positions.push({
@@ -331,6 +371,7 @@ function findFillPositions(paragraphs) {
331
371
  paragraph: nextP,
332
372
  });
333
373
  usedFillIndices.add(j);
374
+ usedFieldTypes.add(fieldType); // NOUVEAU: marquer comme utilisé
334
375
  break;
335
376
  }
336
377
  // Si on trouve un autre label, arrêter
@@ -650,9 +691,26 @@ async function performLLMVerification(llm, sourceText, templateText, sourceDocTy
650
691
  .replace('{mapped_fields}', JSON.stringify(mappedFields, null, 2));
651
692
  try {
652
693
  const response = await llm.invoke(prompt);
653
- const responseText = typeof response === 'string'
654
- ? response
655
- : response.content || response.text || JSON.stringify(response);
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
+ }
656
714
  const jsonMatch = responseText.match(/\{[\s\S]*\}/);
657
715
  if (jsonMatch) {
658
716
  const parsed = JSON.parse(jsonMatch[0]);
@@ -738,28 +796,40 @@ IMPORTANT:
738
796
  - Extrait TOUTES les valeurs remplies (SIRET, adresse, email, téléphone, etc.)
739
797
  - Pour les checkboxes, isChecked=true si ☒☑▣, false si ☐□▢
740
798
  - paragraphIndex = numéro entre crochets au début de chaque ligne`;
741
- 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).
800
+
801
+ OBJECTIF: Mapper INTELLIGEMMENT les données entreprise du document SOURCE vers le TEMPLATE vide.
742
802
 
743
- DONNÉES SOURCE:
803
+ DONNÉES ENTREPRISE EXTRAITES DU SOURCE:
744
804
  {source_data}
745
805
 
746
- TEMPLATE (positions vides à remplir):
806
+ TEMPLATE À REMPLIR (paragraphes indexés):
747
807
  {template_text}
748
808
 
749
- 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:
750
820
  {
751
821
  "mappings": [
752
- {"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"}
753
824
  ],
754
825
  "checkboxMappings": [
755
- {"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"}
756
831
  ]
757
- }
758
-
759
- RÈGLES:
760
- 1. Fais le mapping SÉMANTIQUE (Raison sociale = Dénomination = Nom commercial)
761
- 2. Utilise les [EMPTY] du template comme positions cibles
762
- 3. Le templateIndex doit correspondre à un paragraphe [EMPTY] qui suit un [LABEL]`;
832
+ }`;
763
833
  const VERIFICATION_PROMPT = `Tu es un expert en vérification de documents administratifs français (DC1, DC2, AE, ATTRI1).
764
834
 
765
835
  CONTEXTE:
@@ -1047,19 +1117,80 @@ class DocxFillerAI {
1047
1117
  // Fallback au mode sans LLM
1048
1118
  sourceData = extractSourceData(sourceParagraphs);
1049
1119
  }
1050
- // Mapping avec LLM (pour amélioration future)
1120
+ // Mapping avec LLM - UTILISER les résultats de l'IA
1051
1121
  const fillPrompt = FILL_PROMPT
1052
1122
  .replace('{source_data}', JSON.stringify(sourceData, null, 2))
1053
1123
  .replace('{template_text}', templateStructured);
1054
- // Note: LLM mapping response peut être utilisé pour un mapping plus intelligent
1055
- // Pour l'instant, on utilise la logique standard de mapping par patterns
1056
- await llm.invoke(fillPrompt);
1057
- // Utiliser la logique standard de mapping
1058
- const fillPositions = findFillPositions(templateParagraphs);
1059
- const result = fillTemplateXml(templateXml, templateParagraphs, sourceData, fillPositions);
1060
- templateXml = result.xml;
1061
- filledFields = result.filledFields;
1062
- 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
+ // Collecter les modifications avec leurs positions
1160
+ const modifications = [];
1161
+ const usedIndices = new Set();
1162
+ for (const mapping of llmMappings) {
1163
+ if (usedIndices.has(mapping.templateIndex))
1164
+ continue;
1165
+ const targetParagraph = templateParagraphs.find(p => p.index === mapping.templateIndex);
1166
+ if (!targetParagraph)
1167
+ continue;
1168
+ const oldP = targetParagraph.fullMatch;
1169
+ const newP = oldP.replace(/(<w:p[^>]*>)([\s\S]*?)(<\/w:p>)/, `$1<w:r><w:t>${escapeXml(mapping.value)}</w:t></w:r>$3`);
1170
+ modifications.push({
1171
+ start: targetParagraph.start,
1172
+ end: targetParagraph.end,
1173
+ newContent: newP,
1174
+ fieldType: mapping.fieldType,
1175
+ value: mapping.value,
1176
+ });
1177
+ usedIndices.add(mapping.templateIndex);
1178
+ }
1179
+ // Appliquer les modifications en ordre INVERSE (pour éviter les décalages de position)
1180
+ modifications.sort((a, b) => b.start - a.start);
1181
+ for (const mod of modifications) {
1182
+ templateXml = templateXml.slice(0, mod.start) + mod.newContent + templateXml.slice(mod.end);
1183
+ filledFields.push(`${mod.fieldType}: ${mod.value}`);
1184
+ }
1185
+ }
1186
+ else {
1187
+ // Fallback: utiliser la logique standard
1188
+ const fillPositions = findFillPositions(templateParagraphs);
1189
+ const result = fillTemplateXml(templateXml, templateParagraphs, sourceData, fillPositions);
1190
+ templateXml = result.xml;
1191
+ filledFields = result.filledFields;
1192
+ modifiedCheckboxes = result.modifiedCheckboxes;
1193
+ }
1063
1194
  }
1064
1195
  else {
1065
1196
  // Mode sans LLM: extraction et mapping par patterns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-docx-filler",
3
- "version": "2.3.0",
3
+ "version": "2.4.1",
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",