n8n-nodes-docx-filler 2.3.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.
|
@@ -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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
|
799
|
+
const FILL_PROMPT = `Tu es un expert en remplissage de formulaires administratifs français (DC1, DC2, AE).
|
|
742
800
|
|
|
743
|
-
|
|
801
|
+
OBJECTIF: Mapper INTELLIGEMMENT les données entreprise du document SOURCE vers le TEMPLATE vide.
|
|
802
|
+
|
|
803
|
+
DONNÉES ENTREPRISE EXTRAITES DU SOURCE:
|
|
744
804
|
{source_data}
|
|
745
805
|
|
|
746
|
-
TEMPLATE
|
|
806
|
+
TEMPLATE À REMPLIR (paragraphes indexés):
|
|
747
807
|
{template_text}
|
|
748
808
|
|
|
749
|
-
|
|
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
|
-
{"
|
|
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
|
-
{"
|
|
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,68 @@ class DocxFillerAI {
|
|
|
1047
1117
|
// Fallback au mode sans LLM
|
|
1048
1118
|
sourceData = extractSourceData(sourceParagraphs);
|
|
1049
1119
|
}
|
|
1050
|
-
// Mapping avec LLM
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
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
|
+
}
|
|
1063
1182
|
}
|
|
1064
1183
|
else {
|
|
1065
1184
|
// 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
|
+
"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",
|