File size: 8,935 Bytes
87b004b
 
 
 
 
 
77c855f
 
 
 
 
 
 
87b004b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81836d8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87b004b
 
 
 
 
 
 
 
 
 
 
 
 
 
81836d8
 
 
87b004b
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# Pythonise Exercice — v2

App Flask à deux modes :
- **Pythonisation** d'exercices PyxiScience MyST (valeurs fixes → randomisées) ;
- **Déclinaisons** (2026-07) : génération de versions **QCM** (MCQ) et/ou
  **QAT** (FGQ, champs libres ordonnés) d'un exercice, randomisées elles aussi.
  `:originalExerciseId:` systématique (= `:id:` du QST source, vide sinon) ;
  distracteurs MCQ en **grille miroir** (variations symétriques — la bonne
  réponse ne se devine jamais par la forme ; règle enseignante 2026-07-02).

Un bouton **⏹ Arrêter** (tous modes) annule le job en cours : arrêt coopératif
au prochain point d'étape (`POST /api/jobs/<id>/cancel`), résultats partiels
conservés et téléchargeables.

Pipeline async : analyse LLM + notions + RAG (parallèles, **partagés entre QCM
et QAT**) → génération par paires → audit LLM → filets déterministes → **porte
harnais** (100 graines + contrôles étendus déclinaisons : unicité des options
MCQ sur toutes les graines, 1 seule bonne réponse, « None » en dernier, arité
FGQ, `:solution:` JSON) ; verdict exposé dans l'UI.

## Architecture (refonte 2026-06)

```
run.py                      Lancement : python run.py   (ou python -m app)
app/
├── __init__.py             create_app() + logging + .env
├── config.py               TOUTE la config : modèles, PYTHON_FENCE_BACKTICKS=4,
│                           ANALYSIS_MODEL_IDX, RAG_TOP_K, USE_REASONING,
│                           DEFAULT_LANG, JOB_TTL, chemins, prix
├── server.py               routes Flask (/, /health, /api/models, /api/jobs)
├── keys.py                 chargement des clés API (.env)
├── pipeline/
│   ├── orchestrator.py     chef d'orchestre (1 exercice)
│   ├── analyze.py          analyse + notions + RAG en PARALLÈLE
│   ├── generate.py         découpage + génération par paires
│   ├── audit.py            audit LLM ≤2 itérations + filet de sécurité patches
│   ├── postprocess.py      filets déterministes (auto-lift {{}}, ${}, fences 4,
│   │                       renommage camelCase, décimales/langue, dédoublonnage)
│   ├── solutions.py        substitution des solutions validées (règle 8.1)
│   ├── translate.py        langue cible fr/en/both (prose seule, Python masqué)
│   ├── prompts.py          tous les prompts (v1 archivée dans knowledge/)
│   └── fewshots.py         sélection d'un exemple canonique par type détecté
├── rag/                    functions.py (FAISS) · formatter.py · notions.py ·
│                           catalogue.py (catalogue curé injecté par domaine)
├── llm/                    client.py (OpenRouter + reasoning opt.) · cost.py
├── validation/             harness.py (porte qualité) · sandbox.py (exec sandboxée)
├── knowledge/              pythonisation_rules.md · rules_digest.py ·
│                           fewshots/ (exemples VERTS) · prompts_v1_archive.md ·
│                           functions_catalogue.md (catalogue curé des helpers)
├── corpus/                 5 fichiers de fonctions PyxiScience (RAG)
└── web/templates/index.html  UI (batch, langue, verdict harnais, coût)
data/                       notions.xlsx · faiss_cache/
tests/smoke.py              smoke tests hors-ligne (LLM mocké)
```

## Installation (WSL Ubuntu)

```bash
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt        # faiss-cpu + sentence-transformers ≈ 1.5 Go
cp .env.example .env                   # renseigner OPENROUTER_API_KEY / OPENAI_API_KEY
python run.py                          # http://127.0.0.1:5000
```

Smoke tests (sans réseau) : `.venv/bin/python tests/smoke.py`
Validation d'une sortie : `python ../.claude/skills/validation-harness/harness.py <fichier>.md --seeds 300`

### Endpoints

| Méthode | URL              | Description                                            |
|---------|------------------|--------------------------------------------------------|
| GET     | `/`              | UI web                                                 |
| GET     | `/health`        | sanity check                                           |
| GET     | `/api/models`    | roster des modèles + défauts                           |
| POST    | `/api/jobs`      | démarre un job (1 fichier OU batch) → `{job_id}`       |
| GET     | `/api/jobs/<id>` | suivi + résultats par fichier (harnais, coût, langue)  |
| GET     | `/api/jobs/<id>/download` | ZIP de toutes les sorties `.md` + `_recapitulatif.md` |

POST body :
```json
{
  "files": [{"filename": "exo.md", "content": "<MyST>"}, ...],
  "lang": "fr | en | both",
  "level": "",
  "model_idx": 1,
  "mode": "pythonise | declinaisons",
  "types": {"qcm": true, "qat": true}
}
```
(`mode` absent ⇒ `pythonise`, rétro-compatible ; `types` requis en mode
`declinaisons`, au moins un `true` — les deux cochés ⇒ 2 fichiers par source,
nommés `<source>_QCM.md` / `<source>_QAT.md`, avec UNE seule analyse partagée.
Rétro-compat : `{"content": "...", "filename": "..."}` accepté pour 1 fichier.)

Résultat par fichier : `exercise`, `analysis`, `notions`, `audit_patches`,
`warnings`, **`harness` {ok, seeds, summary}**, **`lang` {source, target, action}**,
**`cost` {usd, eur, requests}**, `duration_s`.

**Téléchargement** (UI, panneau Résultat) : bouton **« Télécharger .md »**
(sortie pythonisée du fichier affiché, suffixe `_pythonise.md` pour ne pas
écraser la source) ; en **batch**, bouton **« Tout (.zip) »** → ZIP de toutes
les sorties + un `_recapitulatif.md` (verdict harnais / warnings / coût par fichier).

## Politique de modèles & banc d'essai (2026-07)

**4 politiques** (`policy` dans `POST /api/jobs`, sélecteur dans l'UI) :
`auto` (défaut — pré-classifieur de difficulté, départ sur l'échelle par coût
croissant, escalade d'un échelon à chaque échec harnais, plafonnée `best`) ·
`best` (qualité max) · `cheap` (le moins cher qui tient `SEUIL_VERT=0.90`) ·
`manual` (IDs explicites par rôle : `models {generate, audit, mecanique}`).
Rôles : `generate` (+réparation), `audit`, `mecanique` (analyse, traduction,
substitutions). Télémétrie de l'échelon gagnant dans `result.policy_telemetry`.
**`claude-fable-5` est retiré de toutes les listes (choix volontaire).**

**Clés API** : tout fonctionne avec la seule `OPENROUTER_API_KEY` (route de
repli universelle). Clés directes optionnelles par fournisseur :
`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `XAI_API_KEY`,
`DEEPSEEK_API_KEY`, `MOONSHOT_API_KEY`, `ZAI_API_KEY`, `MINIMAX_API_KEY`,
`MISTRAL_API_KEY` — un modèle sans aucune clé est « non testé », jamais bloquant.

**Banc d'essai** :
```bash
python -m bench run                    # rôles/modèles dispo, corpus échantillonné
python -m bench run --roles generate --models claude-sonnet-5,deepseek-v4-pro
python -m bench run --exos all --types both --seuil 0.92
python -m bench run --dry-run          # plomberie mockée (CI, aucune clé)
python -m bench consolidate            # fusionne les runs partiels/parallèles
```
Sorties : `bench/results/<ts>.json|.csv|_rapport.md` + mise à jour de
`app/models/recommended.json` (protégé par `manual_override: true`).
Un run filtré `--models` n'écrit jamais recommended.json ; `consolidate`
fusionne tous les résultats (la cellule la plus récente gagne, restreinte
à l'échantillon courant du rôle) puis réécrit la recommandation globale.
Plusieurs bancs peuvent tourner en parallèle (un processus = un compteur
de coûts isolé) ; ne pas lancer d'autre job LLM dans le même processus.
Corpus : `bench/corpus/*.md` (ajouter un exercice = y déposer un `.md`).
Prix : `app/models/prices.json`**à re-vérifier avant prod, ça change
chaque semaine**.

## Sécurité

🚨 **Les clés API dans `.env` étaient en clair dans le zip d'origine** —
considère-les comme exposées et **rotate-les** :
- OpenRouter : https://openrouter.ai/keys
- OpenAI : https://platform.openai.com/api-keys

La sandbox (`app/validation/sandbox.py`) exécute le code généré par le LLM
avec builtins restreints, imports whitelistés et timeout (mono-poste : le
modèle de menace est l'accident LLM, pas un adversaire).

## Notes

- Premier lancement : reconstruit le cache FAISS si absent (≈ 30 s).
- Modèles (IDs vérifiés sur OpenRouter 2026-07-02) : Opus 4.8, **Sonnet 5
  (défaut)**, Haiku 4.5, Gemini 2.5 Pro, GPT-5.4 — catalogue complet (16
  modèles) dans `app/models/catalog.py`. Fable 5 retiré volontairement.
- L'étape d'analyse suit le modèle choisi par l'utilisateur
  (`ANALYSIS_MODEL_IDX=None` dans config.py pour ce comportement).
- Mode batch : fichiers traités séquentiellement ; un échec n'arrête pas les
  autres ; récapitulatif VERT/ROUGE/erreurs + coût total.