4NK_IA_front/backend/entityExtraction.js
4NK IA aad52027c1 ci: docker_tag=dev-test
- Alignement backend: seules 4 entités retournées (persons, companies, addresses, contractual)
- Version API mise à jour à 1.0.1 dans /api/health
- Interface onglets d entités: Personnes, Adresses, Entreprises, Contractuel
- Correction erreurs TypeScript pour build stricte
- Tests et documentation mis à jour
- CHANGELOG.md mis à jour avec version 1.1.1
2025-09-18 20:07:08 +00:00

385 lines
12 KiB
JavaScript

/**
* Extraction d'entités métier spécialisées pour les actes notariés
* Biens immobiliers, clauses contractuelles, signatures, héritiers, etc.
*/
const fs = require('fs')
const path = require('path')
/**
* Extraction des biens immobiliers
* @param {string} text - Texte à analyser
* @returns {Array} Liste des biens immobiliers
*/
function extractBiensImmobiliers(text) {
const biens = []
// Patterns pour les biens immobiliers
const patterns = [
// Maison, appartement, terrain
/(maison|appartement|terrain|villa|studio|loft|duplex|triplex|pavillon|chalet|château|manoir|hôtel particulier|immeuble|bâtiment|construction|édifice)\s+(?:situé[e]?\s+)?(?:au\s+)?(?:n°\s*)?(\d+[a-z]?)?\s*(?:rue|avenue|boulevard|place|chemin|route|impasse|allée|square|quai|cours|passage)\s+([^,]+)/gi,
// Adresse complète
/(?:situé[e]?\s+)?(?:au\s+)?(?:n°\s*)?(\d+[a-z]?)\s*(?:rue|avenue|boulevard|place|chemin|route|impasse|allée|square|quai|cours|passage)\s+([^,]+),\s*(\d{5})\s+([^,]+)/gi,
// Surface et caractéristiques
/(?:d'une\s+)?(?:surface\s+de\s+)?(\d+(?:\.\d+)?)\s*(?:m²|m2|mètres?\s+carrés?)/gi,
// Nombre de pièces
/(?:composé[e]?\s+de\s+)?(\d+)\s*(?:pièces?|chambres?|salles?)/gi,
// Type de bien
/(?:un\s+)?(maison|appartement|terrain|villa|studio|loft|duplex|triplex|pavillon|chalet|château|manoir|hôtel particulier|immeuble|bâtiment|construction|édifice)/gi
]
// Extraction des adresses
const adresseMatches = text.match(patterns[1]) || []
for (const match of adresseMatches) {
const parts = match.match(/(\d+[a-z]?)\s*(?:rue|avenue|boulevard|place|chemin|route|impasse|allée|square|quai|cours|passage)\s+([^,]+),\s*(\d{5})\s+([^,]+)/i)
if (parts) {
biens.push({
type: 'bien_immobilier',
adresse: {
numero: parts[1],
rue: parts[2].trim(),
codePostal: parts[3],
ville: parts[4].trim()
},
surface: null,
pieces: null,
description: match.trim()
})
}
}
// Extraction des surfaces
const surfaceMatches = text.match(patterns[2]) || []
for (const match of surfaceMatches) {
const surface = parseFloat(match.match(/(\d+(?:\.\d+)?)/)[1])
if (biens.length > 0) {
biens[biens.length - 1].surface = surface
}
}
// Extraction du nombre de pièces
const piecesMatches = text.match(patterns[3]) || []
for (const match of piecesMatches) {
const pieces = parseInt(match.match(/(\d+)/)[1])
if (biens.length > 0) {
biens[biens.length - 1].pieces = pieces
}
}
return biens
}
/**
* Extraction des clauses contractuelles
* @param {string} text - Texte à analyser
* @returns {Array} Liste des clauses
*/
function extractClauses(text) {
const clauses = []
// Patterns pour les clauses
const patterns = [
// Clauses de prix
/(?:prix|montant|somme)\s+(?:de\s+)?(?:vente\s+)?(?:fixé[e]?\s+à\s+)?(\d+(?:\s+\d+)*(?:\.\d+)?)\s*(?:euros?|€|EUR)/gi,
// Clauses suspensives
/(?:clause\s+)?(?:suspensive|condition)\s+(?:de\s+)?([^.]{10,100})/gi,
// Clauses de garantie
/(?:garantie|garanties?)\s+(?:de\s+)?([^.]{10,100})/gi,
// Clauses de délai
/(?:délai|échéance|terme)\s+(?:de\s+)?(\d+)\s*(?:jours?|mois|années?)/gi,
// Clauses de résolution
/(?:résolution|annulation)\s+(?:du\s+)?(?:contrat|acte)\s+(?:en\s+cas\s+de\s+)?([^.]{10,100})/gi
]
// Extraction des prix
const prixMatches = text.match(patterns[0]) || []
for (const match of prixMatches) {
const prix = match.match(/(\d+(?:\s+\d+)*(?:\.\d+)?)/)[1].replace(/\s+/g, '')
clauses.push({
type: 'prix',
valeur: parseFloat(prix),
devise: 'EUR',
description: match.trim()
})
}
// Extraction des clauses suspensives
const suspensivesMatches = text.match(patterns[1]) || []
for (const match of suspensivesMatches) {
clauses.push({
type: 'clause_suspensive',
description: match.trim(),
condition: match.replace(/(?:clause\s+)?(?:suspensive|condition)\s+(?:de\s+)?/i, '').trim()
})
}
// Extraction des garanties
const garantiesMatches = text.match(patterns[2]) || []
for (const match of garantiesMatches) {
clauses.push({
type: 'garantie',
description: match.trim(),
garantie: match.replace(/(?:garantie|garanties?)\s+(?:de\s+)?/i, '').trim()
})
}
return clauses
}
/**
* Extraction des signatures
* @param {string} text - Texte à analyser
* @returns {Array} Liste des signatures
*/
function extractSignatures(text) {
const signatures = []
// Patterns pour les signatures
const patterns = [
// Signature simple
/(?:signé|signature)\s+(?:par\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)/gi,
// Signature avec date
/(?:signé|signature)\s+(?:par\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:le\s+)?(\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4})/gi,
// Signature avec lieu
/(?:signé|signature)\s+(?:par\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:à\s+)?([A-Z][a-z]+)/gi,
// Signature notariale
/(?:notaire|maître)\s+([A-Z][a-z]+\s+[A-Z][a-z]+)/gi
]
// Extraction des signatures simples
const signatureMatches = text.match(patterns[0]) || []
for (const match of signatureMatches) {
const nom = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)/)[1]
signatures.push({
type: 'signature',
nom: nom,
date: null,
lieu: null,
description: match.trim()
})
}
// Extraction des signatures avec date
const signatureDateMatches = text.match(patterns[1]) || []
for (const match of signatureDateMatches) {
const parts = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:le\s+)?(\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4})/)
if (parts) {
signatures.push({
type: 'signature',
nom: parts[1],
date: parts[2],
lieu: null,
description: match.trim()
})
}
}
// Extraction des signatures notariales
const notaireMatches = text.match(patterns[3]) || []
for (const match of notaireMatches) {
const nom = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)/)[1]
signatures.push({
type: 'signature_notariale',
nom: nom,
date: null,
lieu: null,
description: match.trim()
})
}
return signatures
}
/**
* Extraction des héritiers
* @param {string} text - Texte à analyser
* @returns {Array} Liste des héritiers
*/
function extractHeritiers(text) {
const heritiers = []
// Patterns pour les héritiers
const patterns = [
// Héritier simple
/(?:héritier|héritière|successeur|successeure|bénéficiaire)\s+(?:de\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)/gi,
// Héritier avec degré de parenté
/(?:fils|fille|père|mère|frère|sœur|époux|épouse|mari|femme|conjoint|conjointe|enfant|parent|grand-père|grand-mère|oncle|tante|neveu|nièce|cousin|cousine)\s+(?:de\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)/gi,
// Héritier avec part
/([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:hérite|bénéficie)\s+(?:de\s+)?(?:la\s+)?(?:part\s+de\s+)?(\d+(?:\/\d+)?|tout|totalité)/gi
]
// Extraction des héritiers simples
const heritierMatches = text.match(patterns[0]) || []
for (const match of heritierMatches) {
const nom = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)/)[1]
heritiers.push({
type: 'heritier',
nom: nom,
parente: null,
part: null,
description: match.trim()
})
}
// Extraction des héritiers avec parenté
const parenteMatches = text.match(patterns[1]) || []
for (const match of parenteMatches) {
const parts = match.match(/(fils|fille|père|mère|frère|sœur|époux|épouse|mari|femme|conjoint|conjointe|enfant|parent|grand-père|grand-mère|oncle|tante|neveu|nièce|cousin|cousine)\s+(?:de\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)/i)
if (parts) {
heritiers.push({
type: 'heritier',
nom: parts[2],
parente: parts[1],
part: null,
description: match.trim()
})
}
}
return heritiers
}
/**
* Extraction des vendeurs et acheteurs
* @param {string} text - Texte à analyser
* @returns {Object} Vendeurs et acheteurs
*/
function extractVendeursAcheteurs(text) {
const result = {
vendeurs: [],
acheteurs: []
}
// Patterns pour vendeurs et acheteurs
const patterns = [
// Vendeur
/(?:vendeur|vendeuse|vendant|vendant)\s+(?:M\.|Mme|Mademoiselle|Monsieur|Madame)?\s*([A-Z][a-z]+\s+[A-Z][a-z]+)/gi,
// Acheteur
/(?:acheteur|acheteuse|achetant|achetant)\s+(?:M\.|Mme|Mademoiselle|Monsieur|Madame)?\s*([A-Z][a-z]+\s+[A-Z][a-z]+)/gi,
// Vente entre
/(?:vente\s+)?(?:entre\s+)?(?:M\.|Mme|Mademoiselle|Monsieur|Madame)?\s*([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:et\s+)?(?:M\.|Mme|Mademoiselle|Monsieur|Madame)?\s*([A-Z][a-z]+\s+[A-Z][a-z]+)/gi
]
// Extraction des vendeurs
const vendeurMatches = text.match(patterns[0]) || []
for (const match of vendeurMatches) {
const nom = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)/)[1]
result.vendeurs.push({
type: 'vendeur',
nom: nom,
description: match.trim()
})
}
// Extraction des acheteurs
const acheteurMatches = text.match(patterns[1]) || []
for (const match of acheteurMatches) {
const nom = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)/)[1]
result.acheteurs.push({
type: 'acheteur',
nom: nom,
description: match.trim()
})
}
// Extraction des ventes entre
const venteMatches = text.match(patterns[2]) || []
for (const match of venteMatches) {
const parts = match.match(/([A-Z][a-z]+\s+[A-Z][a-z]+)\s+(?:et\s+)?([A-Z][a-z]+\s+[A-Z][a-z]+)/)
if (parts) {
result.vendeurs.push({
type: 'vendeur',
nom: parts[1],
description: match.trim()
})
result.acheteurs.push({
type: 'acheteur',
nom: parts[2],
description: match.trim()
})
}
}
return result
}
/**
* Classification du type de document
* @param {string} text - Texte à analyser
* @returns {string} Type de document
*/
function classifyDocumentType(text) {
const textLower = text.toLowerCase()
// Types de documents notariés
const types = {
'acte_vente': ['vente', 'achat', 'acquisition', 'cession', 'transfert'],
'acte_succession': ['succession', 'héritage', 'héritier', 'défunt', 'décès'],
'acte_donation': ['donation', 'donner', 'donné', 'donateur', 'donataire'],
'acte_mariage': ['mariage', 'époux', 'épouse', 'conjoint', 'conjointe'],
'acte_divorce': ['divorce', 'séparation', 'liquidation'],
'acte_pacs': ['pacs', 'pacte civil', 'solidarité'],
'acte_testament': ['testament', 'testateur', 'testatrice', 'legs', 'léguer'],
'acte_promesse': ['promesse', 'compromis', 'avant-contrat'],
'acte_authentification': ['authentification', 'authentifier', 'certifier'],
'acte_pouvoir': ['pouvoir', 'procuration', 'mandat'],
'acte_societe': ['société', 'entreprise', 'sarl', 'sas', 'eurl', 'snc']
}
// Calcul des scores
const scores = {}
for (const [type, keywords] of Object.entries(types)) {
scores[type] = keywords.reduce((score, keyword) => {
const matches = (textLower.match(new RegExp(keyword, 'g')) || []).length
return score + matches
}, 0)
}
// Retourner le type avec le score le plus élevé
const bestType = Object.entries(scores).reduce((a, b) => scores[a[0]] > scores[b[0]] ? a : b)
return bestType[1] > 0 ? bestType[0] : 'document_inconnu'
}
/**
* Extraction complète des entités métier
* @param {string} text - Texte à analyser
* @returns {Object} Toutes les entités extraites
*/
function extractMetierEntities(text) {
return {
biensImmobiliers: extractBiensImmobiliers(text),
clauses: extractClauses(text),
signatures: extractSignatures(text),
heritiers: extractHeritiers(text),
vendeursAcheteurs: extractVendeursAcheteurs(text),
documentType: classifyDocumentType(text),
timestamp: new Date().toISOString()
}
}
module.exports = {
extractBiensImmobiliers,
extractClauses,
extractSignatures,
extractHeritiers,
extractVendeursAcheteurs,
classifyDocumentType,
extractMetierEntities
}