feat(extraction): modèle DB dossier patient extrait (Job/Table/Field)

ExtractionJob -> ExtractedTable -> ExtractedField (SQLAlchemy, cascade), avec
preuve par cellule (bbox + confidence) réutilisant la sémantique VWBEvidence,
et statut dossier needs_review|complete. Brique 2 de la verticale extraction.
Documenté : ce canal conserve les données patient EN CLAIR (≠ canal
apprentissage anonymisé) — aucune anonymisation ne doit cibler ces colonnes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-06-29 12:47:03 +02:00
parent 5ed5ae2d4b
commit 9883cad012
2 changed files with 188 additions and 0 deletions

View File

@@ -321,6 +321,70 @@ class ExecutionStep(db.Model):
}
# ---------------------------------------------------------------------------
# Extraction — « dossier patient extrait » (brique 2)
#
# ⚠️ CANAL EXTRACTION ≠ canal apprentissage. Ces tables conservent les
# VRAIES données patient (patient_ref, ExtractedField.value) : c'est le but,
# constituer le dossier. Elles NE doivent PAS être anonymisées/tokenisées
# (à l'inverse du canal apprentissage, cf. pii_sanitizer). Aucun appel
# d'assainissement PII ne doit cibler ces colonnes.
#
# Sémantique de preuve réutilisée de contracts/evidence.py (VWBEvidence) :
# screenshot_ref ≈ screenshot, screen_bbox/bbox ≈ highlight_box, confidence
# ≈ confidence_score, created_at ≈ timestamp.
# ---------------------------------------------------------------------------
class ExtractionJob(db.Model):
"""Dossier patient extrait — racine d'une session d'extraction."""
__tablename__ = 'extraction_jobs'
id = db.Column(db.String(64), primary_key=True)
patient_ref = db.Column(db.String(255), nullable=True) # donnée patient EN CLAIR (volontaire)
source_session_id = db.Column(db.String(64), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# status: 'needs_review' (revue humaine requise) | 'complete' (validé)
status = db.Column(db.String(32), default='needs_review')
tables = db.relationship('ExtractedTable', backref='job', lazy='dynamic',
cascade='all, delete-orphan')
def __repr__(self):
return f'<ExtractionJob {self.id}: {self.status}>'
class ExtractedTable(db.Model):
"""Tableau extrait d'un écran (preuve : screenshot_ref + screen_bbox)."""
__tablename__ = 'extracted_tables'
id = db.Column(db.String(64), primary_key=True)
job_id = db.Column(db.String(64), db.ForeignKey('extraction_jobs.id'), nullable=False)
screen_bbox = db.Column(db.JSON, nullable=True) # {x, y, width, height}
screenshot_ref = db.Column(db.String(512), nullable=True)
fields = db.relationship('ExtractedField', backref='table', lazy='dynamic',
cascade='all, delete-orphan')
def __repr__(self):
return f'<ExtractedTable {self.id}>'
class ExtractedField(db.Model):
"""Cellule extraite (donnée patient EN CLAIR) + preuve bbox/confidence."""
__tablename__ = 'extracted_fields'
id = db.Column(db.String(64), primary_key=True)
table_id = db.Column(db.String(64), db.ForeignKey('extracted_tables.id'), nullable=False)
row = db.Column(db.Integer, nullable=True)
col = db.Column(db.Integer, nullable=True)
value = db.Column(db.Text, nullable=True) # valeur patient EN CLAIR (volontaire)
bbox = db.Column(db.JSON, nullable=True) # {x, y, width, height}
confidence = db.Column(db.Float, nullable=True)
def __repr__(self):
return f'<ExtractedField {self.id}: r{self.row}c{self.col}>'
# Session active (en mémoire, pas en DB)
class SessionState:
"""État de la session utilisateur (en mémoire)"""