feat(extraction): handler extract_dossier + pont worker→DB VWB mutualisé (brique 3)
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>
This commit is contained in:
106
agent_v0/server_v1/vwb_db.py
Normal file
106
agent_v0/server_v1/vwb_db.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user