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 (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
799
|
+
const FILL_PROMPT = `Tu es un expert en remplissage de formulaires administratifs français (DC1, DC2, AE).
|
|
591
800
|
|
|
592
|
-
|
|
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
|
|
806
|
+
TEMPLATE À REMPLIR (paragraphes indexés):
|
|
596
807
|
{template_text}
|
|
597
808
|
|
|
598
|
-
|
|
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
|
-
{"
|
|
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
|
-
{"
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
|
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
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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}_${
|
|
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:
|
|
1249
|
+
documentType: finalDocType,
|
|
1250
|
+
sourceDocumentType: sourceData.documentType,
|
|
867
1251
|
companyName: sourceData.companyName,
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|