vwb_db.py : couplage worker→DB VWB lazy (app Flask sur instance/workflows.db) mutualisé (R1 + extraction), + persist_extracted_dossier (grille → Job/Table/Field). replay_engine.py : handler _handle_extract_dossier_action — lit le screenshot, extrait une grille structurée, gate qualité conservatrice (complete|needs_review), persiste avec preuve (screenshot_ref/bbox/confidence). N'échoue JAMAIS le replay. Données patient EN CLAIR (canal extraction, non anonymisé). Réserve : dispatch runtime (api_stream.py) non encore branché — étape suivante, à coordonner. Brique 3/4 de la verticale extraction dossier patient. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
107 lines
3.6 KiB
Python
107 lines
3.6 KiB
Python
"""Couplage worker → DB VWB (mutualisé) + persistance « dossier patient extrait ».
|
|
|
|
Le worker/serveur streaming est un process distinct du backend VWB : il n'a
|
|
pas d'app Flask en mémoire. Ce module fournit :
|
|
|
|
- ``vwb_app_context()`` : un app-context Flask lazy (singleton module) lié au
|
|
fichier SQLite VWB ``visual_workflow_builder/backend/instance/workflows.db``,
|
|
avec ``db.init_app`` (db de ``db.models``). Réutilisable par tout module
|
|
serveur qui doit écrire dans la DB VWB (R1, extraction métier, …).
|
|
|
|
- ``persist_extracted_dossier(...)`` : depuis une grille OCR
|
|
(``List[List[cell]]``), crée ExtractionJob → ExtractedTable → ExtractedField
|
|
et commit. Suppose un app-context actif (comme le pont R1 existant).
|
|
|
|
⚠️ CANAL EXTRACTION = données patient EN CLAIR (volontaire) : aucune
|
|
tokenisation/assainissement PII ici (cf. note dans db/models.py).
|
|
"""
|
|
|
|
import sys
|
|
import uuid
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
# Ajout du backend VWB au sys.path à l'import → rend ``db.models`` importable
|
|
# (couplage worker→DB VWB mutualisé ; identique au pattern stream_processor).
|
|
_VWB_BACKEND = Path(__file__).resolve().parents[2] / "visual_workflow_builder" / "backend"
|
|
if str(_VWB_BACKEND) not in sys.path:
|
|
sys.path.insert(0, str(_VWB_BACKEND))
|
|
|
|
# App Flask lazy (singleton module) — un seul db.init_app pour tout le process.
|
|
_vwb_app = None
|
|
|
|
|
|
@contextmanager
|
|
def vwb_app_context():
|
|
"""App-context Flask VWB (lazy singleton) sur instance/workflows.db.
|
|
|
|
À utiliser via ``with vwb_app_context(): ...`` autour des appels qui
|
|
nécessitent ``db.session`` (ex. persist_extracted_dossier).
|
|
"""
|
|
global _vwb_app
|
|
if _vwb_app is None:
|
|
from flask import Flask
|
|
from db.models import db
|
|
|
|
db_path = _VWB_BACKEND / "instance" / "workflows.db"
|
|
app = Flask("worker_vwb")
|
|
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{db_path}"
|
|
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
|
db.init_app(app)
|
|
_vwb_app = app
|
|
with _vwb_app.app_context():
|
|
yield
|
|
|
|
|
|
def persist_extracted_dossier(
|
|
grid: List[List[Dict[str, Any]]],
|
|
*,
|
|
patient_ref: Optional[str],
|
|
source_session_id: Optional[str],
|
|
screenshot_ref: Optional[str],
|
|
screen_bbox: Optional[Dict[str, Any]],
|
|
status: str,
|
|
) -> str:
|
|
"""Persiste un « dossier patient extrait » et retourne le job_id.
|
|
|
|
Crée 1 ExtractionJob → 1 ExtractedTable → N ExtractedField (une par
|
|
cellule de la grille), puis commit. Suppose un app-context VWB actif
|
|
(fourni par ``vwb_app_context()`` ou par l'appelant, comme le pont R1).
|
|
|
|
⚠️ ``patient_ref`` et ``cell["text"]`` sont stockés EN CLAIR (volontaire) :
|
|
le but est de constituer le dossier, pas d'anonymiser.
|
|
"""
|
|
from db.models import db, ExtractionJob, ExtractedTable, ExtractedField
|
|
|
|
job = ExtractionJob(
|
|
id=uuid.uuid4().hex,
|
|
patient_ref=patient_ref,
|
|
source_session_id=source_session_id,
|
|
status=status,
|
|
)
|
|
db.session.add(job)
|
|
|
|
table = ExtractedTable(
|
|
id=uuid.uuid4().hex,
|
|
job_id=job.id,
|
|
screen_bbox=screen_bbox,
|
|
screenshot_ref=screenshot_ref,
|
|
)
|
|
db.session.add(table)
|
|
|
|
for row in grid or []:
|
|
for cell in row or []:
|
|
db.session.add(ExtractedField(
|
|
id=uuid.uuid4().hex,
|
|
table_id=table.id,
|
|
row=cell.get("row"),
|
|
col=cell.get("col"),
|
|
value=cell.get("text"),
|
|
bbox=cell.get("bbox"),
|
|
confidence=cell.get("confidence"),
|
|
))
|
|
|
|
db.session.commit()
|
|
return job.id
|