4NK_IA_back/services/host_api/routes/notary_documents.py
root bf2c0901f4 feat: Organisation des scripts et amélioration de l'installation
- 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
2025-09-11 00:41:57 +02:00

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"
)