
- Création du répertoire scripts/ avec tous les scripts d'installation et de test - Scripts d'installation automatique (install.sh, quick-start.sh) - Scripts de maintenance complète (maintenance.sh) - Scripts de test (test-installation.sh, test-api.sh, test-services.sh, test-integration.sh) - Amélioration du Dockerfile avec healthchecks et sécurité - Mise à jour du docker-compose.yml avec healthchecks et dépendances - Makefile étendu avec nouvelles commandes - Documentation complète mise à jour - Fichier de configuration d'exemple (env.example) - app.py corrigé et fonctionnel
445 lines
16 KiB
Python
445 lines
16 KiB
Python
"""
|
|
Routes pour le traitement des documents notariaux
|
|
"""
|
|
from fastapi import APIRouter, UploadFile, File, Form, HTTPException, Depends, BackgroundTasks
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel, Field
|
|
from typing import Optional, List, Dict, Any
|
|
import uuid
|
|
import time
|
|
import logging
|
|
from enum import Enum
|
|
|
|
from domain.models import DocumentStatus, DocumentType
|
|
from tasks.notary_tasks import process_notary_document
|
|
from utils.external_apis import ExternalAPIManager
|
|
from utils.llm_client import LLMClient
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
class DocumentTypeEnum(str, Enum):
|
|
"""Types de documents notariaux supportés"""
|
|
ACTE_VENTE = "acte_vente"
|
|
ACTE_DONATION = "acte_donation"
|
|
ACTE_SUCCESSION = "acte_succession"
|
|
CNI = "cni"
|
|
CONTRAT = "contrat"
|
|
AUTRE = "autre"
|
|
|
|
class ProcessingRequest(BaseModel):
|
|
"""Modèle pour une demande de traitement"""
|
|
id_dossier: str = Field(..., description="Identifiant du dossier")
|
|
etude_id: str = Field(..., description="Identifiant de l'étude")
|
|
utilisateur_id: str = Field(..., description="Identifiant de l'utilisateur")
|
|
source: str = Field(default="upload", description="Source du document")
|
|
type_document_attendu: Optional[DocumentTypeEnum] = Field(None, description="Type de document attendu")
|
|
|
|
class ProcessingResponse(BaseModel):
|
|
"""Réponse de traitement"""
|
|
document_id: str
|
|
status: str
|
|
message: str
|
|
estimated_processing_time: Optional[int] = None
|
|
|
|
class DocumentAnalysis(BaseModel):
|
|
"""Analyse complète d'un document"""
|
|
document_id: str
|
|
type_detecte: DocumentTypeEnum
|
|
confiance_classification: float
|
|
texte_extrait: str
|
|
entites_extraites: Dict[str, Any]
|
|
verifications_externes: Dict[str, Any]
|
|
score_vraisemblance: float
|
|
avis_synthese: str
|
|
recommandations: List[str]
|
|
timestamp_analyse: str
|
|
|
|
@router.post("/notary/upload", response_model=ProcessingResponse)
|
|
async def upload_notary_document(
|
|
background_tasks: BackgroundTasks,
|
|
file: UploadFile = File(..., description="Document à traiter"),
|
|
id_dossier: str = Form(..., description="Identifiant du dossier"),
|
|
etude_id: str = Form(..., description="Identifiant de l'étude"),
|
|
utilisateur_id: str = Form(..., description="Identifiant de l'utilisateur"),
|
|
source: str = Form(default="upload", description="Source du document"),
|
|
type_document_attendu: Optional[str] = Form(None, description="Type de document attendu")
|
|
):
|
|
"""
|
|
Upload et traitement d'un document notarial
|
|
|
|
Supporte les formats : PDF, JPEG, PNG, TIFF, HEIC
|
|
"""
|
|
# Validation du type de fichier
|
|
allowed_types = {
|
|
"application/pdf": "PDF",
|
|
"image/jpeg": "JPEG",
|
|
"image/png": "PNG",
|
|
"image/tiff": "TIFF",
|
|
"image/heic": "HEIC"
|
|
}
|
|
|
|
if file.content_type not in allowed_types:
|
|
raise HTTPException(
|
|
status_code=415,
|
|
detail=f"Type de fichier non supporté. Types acceptés: {', '.join(allowed_types.keys())}"
|
|
)
|
|
|
|
# Génération d'un ID unique pour le document
|
|
document_id = str(uuid.uuid4())
|
|
|
|
# Validation du type de document attendu
|
|
type_attendu = None
|
|
if type_document_attendu:
|
|
try:
|
|
type_attendu = DocumentTypeEnum(type_document_attendu)
|
|
except ValueError:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Type de document invalide. Types supportés: {[t.value for t in DocumentTypeEnum]}"
|
|
)
|
|
|
|
# Création de la demande de traitement
|
|
request_data = ProcessingRequest(
|
|
id_dossier=id_dossier,
|
|
etude_id=etude_id,
|
|
utilisateur_id=utilisateur_id,
|
|
source=source,
|
|
type_document_attendu=type_attendu
|
|
)
|
|
|
|
try:
|
|
# Enregistrement du document et lancement du traitement
|
|
background_tasks.add_task(
|
|
process_notary_document,
|
|
document_id=document_id,
|
|
file=file,
|
|
request_data=request_data
|
|
)
|
|
|
|
logger.info(f"Document {document_id} mis en file de traitement")
|
|
|
|
return ProcessingResponse(
|
|
document_id=document_id,
|
|
status="queued",
|
|
message="Document mis en file de traitement",
|
|
estimated_processing_time=120 # 2 minutes estimées
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de l'upload du document {document_id}: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Erreur lors du traitement du document"
|
|
)
|
|
|
|
@router.get("/notary/document/{document_id}/status")
|
|
async def get_document_status(document_id: str):
|
|
"""
|
|
Récupération du statut de traitement d'un document
|
|
"""
|
|
try:
|
|
# TODO: Récupérer le statut depuis la base de données
|
|
# Pour l'instant, simulation
|
|
return {
|
|
"document_id": document_id,
|
|
"status": "processing",
|
|
"progress": 45,
|
|
"current_step": "extraction_entites",
|
|
"estimated_completion": time.time() + 60
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la récupération du statut {document_id}: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Erreur lors de la récupération du statut"
|
|
)
|
|
|
|
@router.get("/notary/document/{document_id}/analysis", response_model=DocumentAnalysis)
|
|
async def get_document_analysis(document_id: str):
|
|
"""
|
|
Récupération de l'analyse complète d'un document
|
|
"""
|
|
try:
|
|
# TODO: Récupérer l'analyse depuis la base de données
|
|
# Pour l'instant, simulation d'une analyse complète
|
|
return DocumentAnalysis(
|
|
document_id=document_id,
|
|
type_detecte=DocumentTypeEnum.ACTE_VENTE,
|
|
confiance_classification=0.95,
|
|
texte_extrait="Texte extrait du document...",
|
|
entites_extraites={
|
|
"identites": [
|
|
{"nom": "DUPONT", "prenom": "Jean", "type": "vendeur"},
|
|
{"nom": "MARTIN", "prenom": "Marie", "type": "acheteur"}
|
|
],
|
|
"adresses": [
|
|
{"adresse": "123 rue de la Paix, 75001 Paris", "type": "bien_vendu"}
|
|
],
|
|
"biens": [
|
|
{"description": "Appartement 3 pièces", "surface": "75m²", "prix": "250000€"}
|
|
]
|
|
},
|
|
verifications_externes={
|
|
"cadastre": {"status": "verified", "details": "Parcelle 1234 confirmée"},
|
|
"georisques": {"status": "checked", "risques": ["retrait_gonflement_argiles"]},
|
|
"bodacc": {"status": "checked", "result": "aucune_annonce"}
|
|
},
|
|
score_vraisemblance=0.92,
|
|
avis_synthese="Document cohérent et vraisemblable. Vérifications externes positives.",
|
|
recommandations=[
|
|
"Vérifier l'identité des parties avec pièces d'identité",
|
|
"Contrôler la conformité du prix au marché local"
|
|
],
|
|
timestamp_analyse=time.strftime("%Y-%m-%d %H:%M:%S")
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la récupération de l'analyse {document_id}: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Erreur lors de la récupération de l'analyse"
|
|
)
|
|
|
|
@router.post("/notary/document/{document_id}/reprocess")
|
|
async def reprocess_document(
|
|
document_id: str,
|
|
background_tasks: BackgroundTasks,
|
|
force_reclassification: bool = False,
|
|
force_reverification: bool = False
|
|
):
|
|
"""
|
|
Retraitement d'un document avec options
|
|
"""
|
|
try:
|
|
# TODO: Implémenter le retraitement
|
|
background_tasks.add_task(
|
|
process_notary_document,
|
|
document_id=document_id,
|
|
reprocess=True,
|
|
force_reclassification=force_reclassification,
|
|
force_reverification=force_reverification
|
|
)
|
|
|
|
return {
|
|
"document_id": document_id,
|
|
"status": "reprocessing_queued",
|
|
"message": "Document mis en file de retraitement"
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors du retraitement {document_id}: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Erreur lors du retraitement"
|
|
)
|
|
|
|
@router.get("/notary/documents")
|
|
async def list_documents(
|
|
etude_id: Optional[str] = None,
|
|
id_dossier: Optional[str] = None,
|
|
status: Optional[str] = None,
|
|
limit: int = 50,
|
|
offset: int = 0
|
|
):
|
|
"""
|
|
Liste des documents avec filtres
|
|
"""
|
|
try:
|
|
# TODO: Implémenter la récupération depuis la base de données
|
|
return {
|
|
"documents": [],
|
|
"total": 0,
|
|
"limit": limit,
|
|
"offset": offset
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la récupération des documents: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Erreur lors de la récupération des documents"
|
|
)
|
|
|
|
@router.get("/notary/stats")
|
|
async def get_processing_stats():
|
|
"""
|
|
Statistiques de traitement
|
|
"""
|
|
try:
|
|
# TODO: Implémenter les statistiques réelles
|
|
return {
|
|
"documents_traites": 1250,
|
|
"documents_en_cours": 15,
|
|
"taux_reussite": 0.98,
|
|
"temps_moyen_traitement": 95,
|
|
"types_documents": {
|
|
"acte_vente": 450,
|
|
"acte_donation": 200,
|
|
"acte_succession": 300,
|
|
"cni": 150,
|
|
"contrat": 100,
|
|
"autre": 50
|
|
}
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la récupération des statistiques: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Erreur lors de la récupération des statistiques"
|
|
)
|
|
|
|
@router.get("/documents/{document_id}/extract")
|
|
async def extract_document_data(document_id: str):
|
|
"""
|
|
Extraction des données du document avec IA locale
|
|
"""
|
|
try:
|
|
# TODO: Implémenter l'extraction réelle avec IA locale
|
|
return {
|
|
"documentId": document_id,
|
|
"text": "Texte extrait du document avec IA locale...",
|
|
"language": "fr",
|
|
"documentType": "Acte de vente",
|
|
"identities": [
|
|
{
|
|
"id": "person-1",
|
|
"type": "person",
|
|
"firstName": "Jean",
|
|
"lastName": "Dupont",
|
|
"birthDate": "1980-05-15",
|
|
"nationality": "Française",
|
|
"confidence": 0.95
|
|
},
|
|
{
|
|
"id": "person-2",
|
|
"type": "person",
|
|
"firstName": "Marie",
|
|
"lastName": "Martin",
|
|
"birthDate": "1985-03-22",
|
|
"nationality": "Française",
|
|
"confidence": 0.92
|
|
}
|
|
],
|
|
"addresses": [
|
|
{
|
|
"street": "123 Rue de la Paix",
|
|
"city": "Paris",
|
|
"postalCode": "75001",
|
|
"country": "France"
|
|
}
|
|
],
|
|
"properties": [
|
|
{
|
|
"id": "prop-1",
|
|
"type": "apartment",
|
|
"address": {
|
|
"street": "123 Rue de la Paix",
|
|
"city": "Paris",
|
|
"postalCode": "75001",
|
|
"country": "France"
|
|
},
|
|
"surface": 75,
|
|
"cadastralReference": "1234567890AB",
|
|
"value": 250000
|
|
}
|
|
],
|
|
"contracts": [
|
|
{
|
|
"id": "contract-1",
|
|
"type": "sale",
|
|
"parties": [],
|
|
"amount": 250000,
|
|
"date": "2024-01-15",
|
|
"clauses": ["Clause de garantie", "Clause de condition suspensive"]
|
|
}
|
|
],
|
|
"signatures": ["Jean Dupont", "Marie Martin"],
|
|
"confidence": 0.85
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de l'extraction {document_id}: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Erreur lors de l'extraction"
|
|
)
|
|
|
|
@router.get("/documents/{document_id}/analyze")
|
|
async def analyze_document_data(document_id: str):
|
|
"""
|
|
Analyse du document avec IA locale
|
|
"""
|
|
try:
|
|
# TODO: Implémenter l'analyse réelle avec IA locale
|
|
return {
|
|
"documentId": document_id,
|
|
"documentType": "Acte de vente",
|
|
"isCNI": False,
|
|
"credibilityScore": 0.88,
|
|
"summary": "Document analysé avec succès par l'IA locale. Toutes les informations semblent cohérentes et le document présente un bon niveau de fiabilité.",
|
|
"recommendations": [
|
|
"Vérifier l'identité des parties auprès des autorités compétentes",
|
|
"Contrôler la validité des documents cadastraux",
|
|
"S'assurer de la conformité des clauses contractuelles"
|
|
]
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de l'analyse {document_id}: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Erreur lors de l'analyse"
|
|
)
|
|
|
|
@router.get("/documents/{document_id}/context")
|
|
async def get_document_context_data(document_id: str):
|
|
"""
|
|
Données contextuelles du document
|
|
"""
|
|
try:
|
|
# TODO: Implémenter les vérifications contextuelles réelles
|
|
return {
|
|
"documentId": document_id,
|
|
"cadastreData": {"status": "disponible", "reference": "1234567890AB"},
|
|
"georisquesData": {"status": "aucun risque identifié"},
|
|
"geofoncierData": {"status": "données disponibles"},
|
|
"bodaccData": {"status": "aucune procédure en cours"},
|
|
"infogreffeData": {"status": "entreprise en règle"},
|
|
"lastUpdated": time.strftime("%Y-%m-%d %H:%M:%S")
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la récupération du contexte {document_id}: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Erreur lors de la récupération du contexte"
|
|
)
|
|
|
|
@router.get("/documents/{document_id}/conseil")
|
|
async def get_document_conseil_data(document_id: str):
|
|
"""
|
|
Conseil LLM local pour le document
|
|
"""
|
|
try:
|
|
# TODO: Implémenter le conseil LLM local réel
|
|
return {
|
|
"documentId": document_id,
|
|
"analysis": "Ce document présente toutes les caractéristiques d'un acte notarial standard analysé par l'IA locale. Les informations sont cohérentes et les parties semblent légitimes. Aucun élément suspect n'a été détecté.",
|
|
"recommendations": [
|
|
"Procéder à la vérification d'identité des parties",
|
|
"Contrôler la validité des documents fournis",
|
|
"S'assurer de la conformité réglementaire"
|
|
],
|
|
"risks": [
|
|
"Risque faible : Vérification d'identité recommandée",
|
|
"Risque moyen : Contrôle cadastral nécessaire"
|
|
],
|
|
"nextSteps": [
|
|
"Collecter les pièces d'identité des parties",
|
|
"Vérifier les documents cadastraux",
|
|
"Préparer l'acte final"
|
|
],
|
|
"generatedAt": time.strftime("%Y-%m-%d %H:%M:%S")
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la génération du conseil {document_id}: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Erreur lors de la génération du conseil"
|
|
)
|