# 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//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 .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/` | suivi + résultats par fichier (harnais, coût, langue) | | GET | `/api/jobs//download` | ZIP de toutes les sorties `.md` + `_recapitulatif.md` | POST body : ```json { "files": [{"filename": "exo.md", "content": ""}, ...], "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 `_QCM.md` / `_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/.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.