Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/eval.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: eval

# Non-régression de la qualité d'extraction contre le vrai modèle.
#
# Les runners GitHub n'ont pas de GPU : ce workflow tourne sur un runner
# auto-hébergé enregistré sur le DGX Spark avec le label `dgx`, et vise le
# backend déployé sur cette machine. Lancement manuel (onglet Actions →
# eval → Run workflow), typiquement sur la branche d'une PR qui touche aux
# prompts, au modèle, au découpage ou aux templates.
#
# La référence (eval/baseline.json) se met à jour par PR, après validation :
# python eval/regression.py --api http://localhost:8080 --api-key $KEY --update-baseline

on:
workflow_dispatch:
inputs:
api:
description: URL du backend à évaluer
default: http://localhost:8080
forms:
description: Formulaires (séparés par des espaces, vide = les 21)
default: ""

jobs:
regression:
runs-on: [self-hosted, dgx]
timeout-minutes: 180
steps:
- uses: actions/checkout@v4
# truth.py n'importe que la partie pure de l'orchestrateur (core/fields, core/provenance).
- run: python3 -m pip install --user "httpx>=0.27.0,<1.0"
- name: Dossiers de test contre le backend
env:
API_KEY: ${{ secrets.DOCTORFILL_API_KEY }}
API_URL: ${{ inputs.api }}
FORMS: ${{ inputs.forms }}
run: |
python3 eval/regression.py --api "$API_URL" --api-key "$API_KEY" \
${FORMS:+--forms $FORMS} --report report.md --json results.json
- if: always()
run: cat report.md >> "$GITHUB_STEP_SUMMARY" || true
- if: always()
uses: actions/upload-artifact@v4
with:
name: eval-report
path: |
report.md
results.json
73 changes: 73 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: tests

# À déclarer comme « required status checks » sur main (Settings → Branches) :
# lint, orchestrator, marker-ocr, frontend. Sans cette règle, un merge reste
# possible malgré une CI rouge.

on:
push:
branches: [main, dev]
pull_request:

concurrency:
group: tests-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install "ruff==0.16.9"
# Erreurs seulement (noms indéfinis, imports inutiles, syntaxe) : pas de style imposé.
- run: ruff check --select F,E9 services eval

orchestrator:
runs-on: ubuntu-latest
defaults:
run:
working-directory: services/orchestrator
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
# Celle de l'image nvcr.io/nvidia/pytorch de l'orchestrateur.
python-version: "3.12"
cache: pip
cache-dependency-path: services/orchestrator/requirements*.txt
- run: pip install -r requirements-dev.txt
# Sans GPU ni service : OCR, TEI et vLLM sont simulés. Couvre les 21
# formulaires du catalogue et les 21 dossiers de eval/dossiers/.
- run: python -m pytest tests -q --cov=. --cov-report=term:skip-covered --cov-fail-under=70

marker-ocr:
runs-on: ubuntu-latest
defaults:
run:
working-directory: services/marker_ocr
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
# textlayer.py seul : marker et ses modèles ne sont pas nécessaires.
- run: pip install "pypdfium2>=4.30.0,<6.0" "pikepdf>=9.0.0,<11.0" "pytest>=8.0,<10.0"
- run: python -m pytest tests -q

frontend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: frontend/package-lock.json
- run: npm ci
- run: npm run build
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,17 @@ services/orchestrator/uploads/
services/orchestrator/results/

# ── eval & logs
eval
# Seuls les sorties et les documents générés sont ignorés : une règle `eval`
# entière rendait inertes les scripts et dossiers de test versionnés, et faisait
# échouer tout `git add` sur eux.
eval/results/
eval/test_docs/
services/logs/*.md

# ── tests
.pytest_cache
.coverage

# ── Formulaires medForms récupérés par tools/gen_catalog.py ──
# Les PDF vierges appartiennent à medForms et pèsent ~2 Mo pièce : on versionne
# la liste (tools/catalog_fr.txt) et les templates dérivés, pas les binaires.
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,20 @@ doctorfill/
└── docs/wiki/ # Documentation wiki
```

## Tests

```bash
cd services/orchestrator
pip install -r requirements-dev.txt
python -m pytest tests # sans GPU : OCR, TEI et vLLM sont simulés
```

Exécutés en CI à chaque PR (`.github/workflows/tests.yml`) : les 21 formulaires
et les 21 dossiers de `eval/dossiers/` de bout en bout. La qualité d'extraction
du vrai modèle se contrôle avec `eval/regression.py` contre une référence.
Détails, et réglage de GitHub pour bloquer un merge rouge :
[docs/wiki/Tests.md](docs/wiki/Tests.md).

## Évaluation RAG

```bash
Expand All @@ -116,7 +130,10 @@ python generate_docs.py --count 60 --noise 10
python run_eval.py --api http://localhost:8080 --api-key <KEY>
```

Précision actuelle : **84.6%** sur Form_AVS (26 champs) — voir [#21](https://github.com/doctorfill-dev/DoctorFill/issues/21) pour le plan d'amélioration.
Non-régression sur les 21 dossiers (vérité terrain dérivée des scénarios) :
`python eval/regression.py --api http://localhost:8080 --api-key <KEY>` — voir [docs/wiki/Tests.md](docs/wiki/Tests.md).

Dernière mesure connue, avant septembre 2026 : **84.6%** sur Form_AVS (26 champs) — voir [#21](https://github.com/doctorfill-dev/DoctorFill/issues/21) pour le plan d'amélioration.

## Roadmap

Expand Down
1 change: 1 addition & 0 deletions docs/wiki/Home.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Bienvenue sur le wiki de **DoctorFill**, l'application d'auto-remplissage de for
| [Déploiement Frontend (Cloudflare)](Deploiement-Frontend-Cloudflare) | Déploiement du frontend sur Cloudflare Pages + Tunnel |
| [Pipeline RAG](Pipeline-RAG) | Fonctionnement du pipeline RAG (OCR → Embedding → Retrieval → LLM) |
| [Templates](Templates) | Documentation des templates de formulaires (AVS, Cardio, LAA) |
| [Tests](Tests) | Tests en CI et non-régression de la qualité d'extraction |

## Stack technique

Expand Down
172 changes: 106 additions & 66 deletions docs/wiki/Pipeline-RAG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,96 +2,136 @@

## Vue d'ensemble

Le pipeline RAG (Retrieval-Augmented Generation) est le coeur de DoctorFill. Il extrait automatiquement les informations des documents médicaux pour remplir les formulaires.
Le pipeline extrait les informations des documents médicaux d'un dossier pour
remplir un formulaire medForms (XFA statique hybride, couche AcroForm comprise).
Il est piloté par l'orchestrateur (`services/orchestrator/app.py`) et partagé
par le traitement initial et le re-run (`_extract_fields`, `_fill_pdf`).

## Étapes du pipeline
```
PDF ──► OCR (texte natif, sinon marker) ──► découpage + embeddings (ChromaDB)
└► synthèse médicale (LLM)
│
champs du template ──► questions univoques ──► lots ──► LLM (JSON contraint)
│
normalisation + provenance ◄────────────┘
│
remplissage XFA + AcroForm
```

### 1. OCR — Extraction de texte
## Étapes

**Service** : Marker OCR (`:8082`)
### 1. OCR

- Chaque PDF est envoyé au service Marker OCR
- Sortie : Markdown structuré (headings, tableaux, listes)
- Parallélisation : `asyncio.Semaphore(5)` pour limiter les appels GPU concurrents
- Timeout : 120s par document
**Service** : `marker_ocr` (`:8082`). La couche texte native est lue d'abord
(`textlayer.py`, quelques ms/page) ; l'OCR marker n'est lancé que pour les pages
scannées. Résultats mis en cache par empreinte SHA-256.

### 2. Chunking — Découpage du texte
Un document illisible **n'interrompt plus le job** : il est écarté et signalé
dans `warnings` (`/status`). Reprises proportionnées à la cause : service
injoignable → 5 tentatives espacées ; délai dépassé ou 5xx → une seule reprise ;
4xx → aucune. Si aucun document n'est lisible, le job échoue avec un message
explicite.

**Stratégie** : Semi-sémantique
### 2. Découpage et embeddings

1. **Découpage par headings Markdown** : chaque section `#`, `##`, `###` crée un chunk
2. **Fallback** : si un chunk dépasse 400 mots, il est découpé avec 50 mots de chevauchement
`extraction.chunk_document` — sections Markdown fusionnées si trop courtes
(< 40 mots), découpées si trop longues (300 mots, chevauchement 50).
**Chaque extrait porte sa source** : `[Source : 03_consultations.pdf — page 2 — Médecin traitant]`.
Embeddings `BAAI/bge-m3` par lots de 64 (TEI `:8081`, reprises sur erreur
transitoire), stockés dans une collection ChromaDB éphémère par job (réutilisée
par le chat et le re-run).

### 3. Embedding — Vectorisation
### 3. Synthèse médicale

**Service** : TEI (`:8081`) — Modèle `BAAI/bge-m3`
`medical_synthesis.py` — JSON structuré (patient, diagnostics, incapacités,
traitements, médecins, dates clés). Directe si le dossier tient dans la fenêtre,
hiérarchique sinon (résumé par document, découpé si besoin, puis fusion).
Replis : directe → hiérarchique ; fusion LLM → fusion déterministe. En cas
d'échec complet, l'extraction continue sur les seuls documents.

- Les chunks sont envoyés par batch de 64 pour éviter les timeouts
- Les vecteurs sont stockés dans une collection ChromaDB éphémère (en mémoire)
- Timeout : 120s par batch
### 4. Préparation des champs

### 4. Retrieval — Recherche sémantique
`core/fields.py` :

Pour chaque champ du formulaire :
- **Champs soumis** : question rédigée, ni `computed` ni `preset` d'éditeur.
Un `preset` qui n'est que l'**état vierge** du champ (case « /Off », question
oui/non sur « non », compteur à 0) reste à remplir.
- **Questions univoques** : les occurrences d'une structure répétée reçoivent
leur contexte — « (Contexte : période d'incapacité de travail n°2 sur 4 — de la
plus ancienne à la plus récente ; vide s'il n'y en a pas de n°2) ».
- **Lots** : par section, jusqu'à 8 champs ; les occurrences d'une même
structure restent dans le même lot (jusqu'à 32) pour être réparties sans doublon.

1. **Embedding de la question** : la question du template est vectorisée
2. **Recherche ChromaDB** : `n_results = min(20, nombre_total_de_chunks)`
3. **Reranking** : les 20 résultats sont re-classés par le modèle `BAAI/bge-reranker-v2-m3`
4. **Sélection** : les 7 meilleurs chunks sont conservés
### 5. Contexte du prompt

### 5. Extraction LLM
Deux modes, choisis par job selon un budget en tokens (`extraction.py`) :

**Service** : vLLM (`:8000`) — Modèle `Qwen/Qwen2.5-14B-Instruct-AWQ`
| Mode | Quand | Contexte |
|------|-------|----------|
| Dossier intégral | dossier ≤ `FULL_CONTEXT_MAX_TOKENS` (16k) et place dans la fenêtre | texte complet de tous les documents, identique pour tous les lots (préfixe mis en cache par vLLM) |
| Extraits | au-delà | recherche vectorielle (30 candidats) → rerank `bge-reranker-v2-m3` (top 8) → extraits **entrelacés par rang** entre les champs du lot, dans `RAG_CONTEXT_TOKENS` (9k) |

- Les 7 chunks sélectionnés sont envoyés comme contexte
- Le LLM extrait la valeur selon des règles strictes (prompt système dédié)
- Température : 0.05 (quasi-déterministe)
- Format de réponse : `{"value": "...", "source_quote": "..."}`
La synthèse est transmise **en entier** à chaque lot, avant les documents.

### 6. Remplissage PDF
### 6. Extraction LLM

- Les valeurs extraites sont injectées dans le XML/XFA du formulaire PDF
- Le PDF rempli est disponible en téléchargement
**Service** : vLLM (`:8000`) — `Qwen/Qwen2.5-14B-Instruct-AWQ`, fenêtre 32k.

## Prompt système
- Réponse contrainte par **schéma JSON** (`response_format: json_schema`) : tous
les IDs sont présents, les listes de choix sont des `enum`. Repli automatique
sur `json_object` si le serveur refuse le schéma.
- Température 0, `max_tokens` 4096, délai 600 s.
- Reprises sur les seules erreurs transitoires (réseau, délai, 5xx, 429).
- Fenêtre dépassée → contexte réduit de moitié (3 fois au plus) ;
réponse tronquée → lot scindé ; IDs absents → redemandés une fois.

```
Tu es un assistant spécialisé dans l'extraction de données depuis des documents médicaux.
On te fournit des EXTRAITS de rapports et une QUESTION.

RÈGLES STRICTES :
1. Cherche la réponse dans les extraits fournis.
2. Extrais la valeur EXACTE telle qu'elle apparaît dans le texte.
3. Ne réponds "Inconnu" que si l'information est RÉELLEMENT ABSENTE.
4. Réponds UNIQUEMENT en JSON valide.
5. Pour les dates, conserve le format original du document.
6. Pour les noms/prénoms, conserve la casse originale.
```
### 7. Normalisation et provenance

## Optimisations réalisées
- Valeurs : « non mentionné », « N/A », « ... », exemples de format → vide ;
dates → `JJ.MM.AAAA` ; oui/non canoniques ; nombres seuls ; sexe `M`/`F`.
La réponse brute reste dans `raw_value`.
- Provenance (`core/provenance.py`) : la valeur est-elle retrouvable dans les
documents, et où ? Verdict `verified` / `attested` / `inferred` / `unverified`
/ `not_checkable`, avec document, page et extrait. Pour une réponse choisie
(oui/non, option de liste), seule la citation compte.

| Paramètre | Avant | Après | Impact |
|-----------|-------|-------|--------|
| MAX_FILES | 10 | 100 | Support de gros dossiers |
| n_results | 5 | min(20, total) | Meilleure couverture |
| reranked | top 3 | top 7 | Plus de contexte au LLM |
| Embedding | 1 appel | Batch de 64 | Pas de timeout |
| OCR | Séquentiel | Parallèle (sem=5) | ~2x plus rapide |
| Température | 0.1 | 0.05 | Réponses plus stables |
| Timeout embed/rerank | 60s | 120s | Pas de timeout sur gros corpus |
### 8. Remplissage PDF

## Métriques actuelles
`core/fields.collect_form_values` construit les valeurs XFA et AcroForm :
extraits, champs calculés, puis destinataire dérivé du canton.
Cases à cocher : état déclaré par le formulaire (`On`/`Off` ou `1`/`0`).
Listes : libellé choisi → valeur d'export, et libellé affiché dans l'apparence.

| Métrique | Valeur |
|----------|--------|
| Précision AVS (26 champs) | 84.6% (22/26) |
| Temps de traitement (70 docs) | ~90s |
| Temps de traitement (100 docs) | <5 min (estimé) |
## Réglages

| Variable | Défaut | Rôle |
|----------|--------|------|
| `LLM_MAX_MODEL_LEN` | 32768 | À aligner sur `--max-model-len` de vLLM |
| `FULL_CONTEXT_MAX_TOKENS` | 16000 | Seuil du mode dossier intégral |
| `RAG_CONTEXT_TOKENS` | 9000 | Budget d'extraits par lot |
| `MAX_TOKENS_EXTRACT` | 4096 | Génération par lot |
| `LLM_TIMEOUT` / `SYNTHESIS_TIMEOUT` | 600 / 900 s | Délais LLM |
| `MAX_BATCH_SIZE` / `MAX_FAMILY_SIZE` | 8 / 32 | Taille des lots |
| `RETRIEVAL_CANDIDATES` / `RETRIEVAL_TOP_K` | 30 / 8 | Retrieval |
| `KEEP_DEBUG_LOGS` | false | Conserver les journaux de debug au-delà du job |

## Problèmes connus
## Tests

1. **Contamination par bruit** : les documents d'autres patients polluent le retrieval
2. **Confusion d'entités** : patient/médecin/spécialiste mal distingués
3. **Retrieval miss** : certains champs ne trouvent pas leur chunk pertinent
```bash
cd services/orchestrator
pip install -r requirements-dev.txt
python -m pytest tests
```

Sans GPU ni service : OCR, TEI et vLLM sont simulés. Les 21 formulaires et les
21 dossiers d'évaluation sont vérifiés de bout en bout à chaque PR — voir
[Tests](Tests.md).

## Métriques

| Métrique | Valeur |
|----------|--------|
| Précision AVS (26 champs, avant les changements de septembre 2026) | 84.6% (22/26) |

→ Voir issue #21 pour le plan d'amélioration.
À remesurer avec `eval/regression.py` (21 dossiers, 324 champs de vérité
terrain) et `eval/check_grounding.py` — voir [Tests](Tests.md).
Loading
Loading