Refactor rewrite prompt and update tests for narrative generation
- Updated the rewrite prompt to remove markdown elements such as titles, bullet points, and code fences, ensuring a cleaner narrative output. - Enhanced the GenerationPipeline tests to verify that code fences and chapter titles are stripped before manuscript promotion. - Adjusted NextLots tests to include new functionality for command runners and added tests for deduplication of headings and timeout handling. - Introduced new documentation files detailing project context and execution plans for March 2026, outlining current project status and objectives. - Added operational memory documents to summarize project state and decisions for resuming work on the ai-novel-engine.
This commit is contained in:
+16
@@ -1,4 +1,20 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.eggs/
|
||||
*.egg
|
||||
automation/reports/
|
||||
automation/state/
|
||||
.env
|
||||
.venv/
|
||||
venv/
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
brouillons/*/meta.json
|
||||
|
||||
@@ -20,7 +20,9 @@ v2 — développement en cours (open-source)
|
||||
## Suivi
|
||||
- backlog actif: [`TODO_ACTIVE.md`](./TODO_ACTIVE.md)
|
||||
- etat livre: [`TODO_IMPLEMENTE.md`](./TODO_IMPLEMENTE.md)
|
||||
- ordre d'execution recommande: [`docs/EXECUTION_PLAN_2026-03-08.md`](./docs/EXECUTION_PLAN_2026-03-08.md)
|
||||
- contexte courant: [`docs/CONTEXTE_PROJET_2026-03-14.md`](./docs/CONTEXTE_PROJET_2026-03-14.md)
|
||||
- memoire de reprise: [`docs/MEMOIRE_REPRISE_2026-03-14.md`](./docs/MEMOIRE_REPRISE_2026-03-14.md)
|
||||
- ordre d'execution recommande: [`docs/EXECUTION_PLAN_2026-03-14.md`](./docs/EXECUTION_PLAN_2026-03-14.md)
|
||||
- runbook local: [`docs/runbooks/LOCAL_GENERATION.md`](./docs/runbooks/LOCAL_GENERATION.md)
|
||||
- comparatif modeles local: [`docs/MODEL_COMPARISON_2026-03-08.md`](./docs/MODEL_COMPARISON_2026-03-08.md)
|
||||
|
||||
@@ -34,7 +36,10 @@ python3 scripts/run_next_lots.py --lot full
|
||||
|
||||
Points clés:
|
||||
- le manifeste versionné est `automation/next_lots.toml`
|
||||
- `paths.ollama_runtime = "native"` garde le preflight Ollama natif avant smoke
|
||||
- `paths.ollama_runtime = "openai_compatible"` saute ce preflight et envoie `ollama:*` vers `paths.ollama_openai_base_url`
|
||||
- le driver réutilise les smokes existants au lieu de dupliquer le pipeline
|
||||
- `tracking_sync` consolide maintenant les derniers verdicts connus par modele a partir de `automation/reports/*/run.json`
|
||||
- les opérations sensibles restent semi-autos: en cas de switch Apple ou de restart runtime, le cycle prépare les commandes exactes puis s'arrête avec un état de reprise
|
||||
- reprise:
|
||||
|
||||
@@ -85,13 +90,18 @@ Notes :
|
||||
- `ANE_REPAIR_FALLBACK_MODEL` permet de forcer le modele du second passage `repair`
|
||||
- le pipeline narratif reste entierement dans `ai-novel-engine`
|
||||
- `mascarade` sert uniquement de runtime local et de couche OpenAI-compatible
|
||||
- au 13 mars 2026 au soir, `:8100` et `:8201` sont remontes, et `http://127.0.0.1:11434/api/tags` repond de nouveau
|
||||
- le vrai blocage restant n'est plus un service eteint mais `ollama` natif 0.17.7, qui echoue encore en generation sur `qwen2.5:7b` et `qwen2.5:1.5b` avec une erreur Metal
|
||||
- le rerun Apple comparable `automation/reports/apple_rerun_7oY51o` reste utile comme incident historique: il a ete bloque a `gate` sur `too_short` + `truncated_ending`, puis a casse sur l'ancien fallback `repair` vers Ollama
|
||||
- le rerun comparable `automation/reports/apple_rerun_preset_20260313T223555Z` est `accepted` le 13 mars 2026 avec `323` mots et `repair_attempts=0`; la reference Apple locale est donc reconfirmee
|
||||
- dernier cycle complet termine au 9 mars 2026 :
|
||||
- `apple-coreml:qwen3.5-4b-onnx-q4f16` est `accepted` de bout en bout sous garde-fou
|
||||
- `ollama:qwen2.5:7b` atteint `gate`, exerce `repair` en live, puis finit `quality_blocked` sur `outline_like`
|
||||
- `apple-coreml:stateful-mistral7b-instruct-int4-coreml` reste `preflight_only`
|
||||
- les baselines `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` sont en rerun automatise separe; ils ne sont plus la reference locale courante
|
||||
- `apple-coreml:stateful-mistral7b-instruct-int4-coreml` est sorti du chemin critique; il reste archive comme piste experimentale
|
||||
- les baselines `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` ont ete rejouees et finissent actuellement `quality_blocked` sur `truncated_ending`
|
||||
- le smoke et `status` exposent maintenant `gate_v1.json`, `quality_blockers`, `failed_stage`, `repair_attempts` et `repair_models`
|
||||
- le runtime Apple local ne sert qu'un `model_id` a la fois; un fallback `repair` vers un autre modele Apple exige donc un switch de service entre runs
|
||||
- par defaut, le second passage `repair` reste maintenant sur le meme provider; `ANE_REPAIR_FALLBACK_MODEL` sert seulement a forcer un switch explicite
|
||||
|
||||
Smoke test local rapide :
|
||||
|
||||
@@ -104,38 +114,13 @@ Smoke test local rapide :
|
||||
|
||||
Le script cree un workspace temporaire, ecrit une intention de test, lance la vraie CLI publique, fait un warm-up automatique pour `apple-coreml`, puis affiche un resume humain des artefacts et du `meta.json`. En mode `apple-coreml`, il applique par defaut un timeout plus large (`ANE_TIMEOUT=900`) et des budgets de smoke plus courts pour eviter de faire exploser la latence locale. Pour les reruns qualitatifs de reference, fixer explicitement `--timeout 300` et des budgets `ANE_MAX_TOKENS_*` communs. Utiliser `--workspace`, `--chapter`, `--intention`, `--timeout`, `--approve` ou `--reject` si besoin.
|
||||
|
||||
## Etat auto-synchronise
|
||||
## Etat auto-synchronise
|
||||
<!-- AUTO-SYNC:ANE-README:START -->
|
||||
- dernier cycle automatise: 2026-03-09T06:53:02+00:00
|
||||
- reference locale actuelle: aucun accepted, meilleur diagnostic: apple-coreml:qwen2.5-0.5b-instruct-onnx
|
||||
- prochain lot utile: Analyser les runs ayant atteint gate/repair puis resserrer la reference locale autour des meilleurs candidats.
|
||||
- dernier cycle automatise: 2026-03-14T14:03:06+00:00
|
||||
- reference locale actuelle: apple-coreml:qwen3.5-4b-onnx-q4f16
|
||||
- prochain lot utile: Reference locale reconfirmee; retablir le runtime des modeles provider_failed puis reprendre rewrite/repair sur les modeles bloques a gate.
|
||||
- lancer un cycle: `python3 scripts/run_next_lots.py --lot full`
|
||||
- checkpoint manuel en attente: Le runtime Apple sert `qwen2.5-0.5b-instruct-onnx` au lieu de `stateful-mistral7b-instruct-int4-coreml`.
|
||||
<!-- AUTO-SYNC:ANE-README:END -->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- CHANTIER:AUDIT START -->
|
||||
## Audit & Execution Plan (2026-03-10)
|
||||
|
||||
|
||||
+42
-60
@@ -1,74 +1,56 @@
|
||||
# TODO actif - AI Novel Engine
|
||||
|
||||
Source de verite des taches restantes pour `ai-novel-engine`.
|
||||
Backlog courant pour reprendre `ai-novel-engine` sans melanger le reste avec les lots deja livres.
|
||||
|
||||
Regle:
|
||||
- cocher ici ce qui est fait puis deplacer le lot livre vers `TODO_IMPLEMENTE.md`
|
||||
- ne suivre ici que le travail restant ou les blocages encore ouverts
|
||||
- garder les dependances `mascarade` explicites
|
||||
References:
|
||||
|
||||
## Deja implemente
|
||||
- [x] P0 Pipeline chapitre `intention -> structure -> draft -> critique -> rewrite -> validation -> memoire`
|
||||
- [x] P0 Normalisation de chapitre avec identifiant canonique `chapitre_XX` et detection des collisions legacy
|
||||
- [x] P0 Provider OpenAI-compatible et branchement local via `mascarade`
|
||||
- [x] P0 Budgets par etape (`ANE_MAX_TOKENS_*`)
|
||||
- [x] P0 Parsing JSON tolerant pour les sorties locales imparfaites
|
||||
- [x] P0 Second passage de reessai pour `critique` et `memory` si le JSON reste invalide apres parsing tolerant
|
||||
- [x] P0 Garde-fou manuscrit dur avant promotion (`gate_v1.json`, heuristiques locales, verdict `quality_blocked`)
|
||||
- [x] P0 Boucle `repair` automatique entre `gate` et `quality_blocked`, avec artefacts `repair_vN.md`, fallback modele et preservation de `draft_v2.md`
|
||||
- [x] P0 Smoke script local `scripts/smoke_local_generation.sh`
|
||||
- [x] P0 Timeout provider remonte maintenant en `ProviderError` et marque correctement `failed_stage` dans `meta.json`
|
||||
- [x] P0 Warm-up Apple du smoke remonte maintenant une erreur lisible au lieu d'une stacktrace brute
|
||||
- [x] P0 Prompts `draft_v1` et `rewrite_v1` durcis pour imposer une prose continue sans titres ni puces
|
||||
- [x] P1 Flags CLI non interactifs `--approve` et `--reject`
|
||||
- [x] P1 `status` enrichi avec les chapitres en echec, en attente et bloques par garde-fou
|
||||
- [x] P1 Resume de smoke humain a partir du `meta.json`
|
||||
- [x] P1 Contrat cross-repo et recovery documentes via les runbooks
|
||||
- [x] P2 `docs/vision.md`, `docs/roadmap.md` et le runbook local ne sont plus des placeholders
|
||||
- [x] P0 Revalidation sous garde-fou de `ollama:qwen2.5:1.5b` -> `quality_blocked`
|
||||
- [x] P0 Revalidation sous garde-fou de `apple-coreml:qwen2.5-0.5b-instruct-onnx` -> `quality_blocked`
|
||||
- [x] P0 Revalidation sous garde-fou de `apple-coreml:qwen3.5-4b-onnx-q4f16` -> `provider_failed` en `rewrite`
|
||||
- [x] P0 Revalidation sous garde-fou de `ollama:qwen2.5:7b` -> `provider_failed` par timeout en `draft`
|
||||
- [x] P0 Comparatif local re-ecrit pour le protocole avec garde-fou dans `docs/MODEL_COMPARISON_2026-03-08.md`
|
||||
- [x] P0 Revalidation reelle sous protocole `gate + repair` borne a `300s` par requete:
|
||||
- `ollama:qwen2.5:1.5b` -> `failed` en `structure`
|
||||
- `apple-coreml:qwen2.5-0.5b-instruct-onnx` -> `failed` en `rewrite`
|
||||
- `apple-coreml:qwen3.5-4b-onnx-q4f16` -> `failed` en `rewrite`
|
||||
- `ollama:qwen2.5:7b` -> `failed` en `rewrite`
|
||||
- contexte courant: [`docs/CONTEXTE_PROJET_2026-03-14.md`](./docs/CONTEXTE_PROJET_2026-03-14.md)
|
||||
- memoire de reprise: [`docs/MEMOIRE_REPRISE_2026-03-14.md`](./docs/MEMOIRE_REPRISE_2026-03-14.md)
|
||||
- plan courant: [`docs/EXECUTION_PLAN_2026-03-14.md`](./docs/EXECUTION_PLAN_2026-03-14.md)
|
||||
- lots livres: [`TODO_IMPLEMENTE.md`](./TODO_IMPLEMENTE.md)
|
||||
|
||||
## Memoire de reprise
|
||||
|
||||
- reference locale reconfirmee: `apple-coreml:qwen3.5-4b-onnx-q4f16` sur `20260309T055457Z` puis `apple_rerun_preset_20260313T223555Z`
|
||||
- meilleur candidat alternatif: `qwen2.5:7b`, mais il faut d'abord lui rendre un runtime stable
|
||||
- baselines vitesse a ce jour:
|
||||
- `apple-coreml:qwen2.5-0.5b-instruct-onnx` -> `quality_blocked`
|
||||
- `ollama:qwen2.5:1.5b` -> `provider_failed`
|
||||
- faits live du 14 mars:
|
||||
- Apple warm-up direct sur `:8100` en `2.76s`
|
||||
- requete prose Apple representative en `39.99s`
|
||||
- `llama-server` a charge le blob `qwen2.5:1.5b` et a repondu en `0.31s`
|
||||
- etat automatise clos; `stateful-mistral7b-instruct-int4-coreml` est hors chemin critique
|
||||
- le suivi `tracking_sync` consolide maintenant les derniers resultats connus par modele au lieu de n'utiliser qu'un lot partiel recent
|
||||
|
||||
## Actif
|
||||
- [ ] P0 Terminer le lot `baselines`, puis relancer `tracking_sync` sur un etat complet incluant `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b`
|
||||
- [ ] P0 Faire passer `ollama:qwen2.5:7b` de `quality_blocked` a `accepted` en supprimant le residu `outline_like` apres `repair`
|
||||
- [ ] P0 Faire terminer au moins un cycle `python3 scripts/run_next_lots.py --lot full` jusqu'a `tracking_sync` sans checkpoint manuel autre qu'un switch Apple explicite
|
||||
- [ ] P1 Requalifier `apple-coreml:qwen3.5-4b-onnx-q4f16` comme reference Apple stable sur plusieurs cycles, pas sur un seul run accepte
|
||||
- [ ] P1 Rendre la strategie de fallback `repair` consciente des modeles reellement servis: le runtime Apple n'expose qu'un `model_id` a la fois
|
||||
- [ ] P1 Garder l'installation/staging Apple de `qwen2.5-0.5b-instruct-onnx`, `qwen3.5-4b-onnx-q4f16` et `stateful-mistral7b-instruct-int4-coreml` comme prerequis explicite
|
||||
|
||||
- [ ] P0 Industrialiser un chemin `llama.cpp` / `llama-server` reusable pour `qwen2.5:1.5b`
|
||||
- [ ] P0 Tester le blob local `qwen2.5:7b` via le meme chemin alternatif
|
||||
- [ ] P1 Rejouer `priority_models` puis `baselines` des que le backend alternatif est branche
|
||||
- [ ] P1 Reprendre `rewrite` / `repair` uniquement sur les blockers qui survivent apres stabilisation runtime
|
||||
|
||||
## Bloque
|
||||
- [ ] P1 `ollama:qwen2.5:7b` atteint maintenant `gate` et exerce `repair`, mais reste bloque sur `outline_like` apres deux passes
|
||||
- [ ] P1 Le lot `baselines` exige encore un switch Apple explicite avant de pouvoir finir `apple-coreml:qwen2.5-0.5b-instruct-onnx`
|
||||
- [ ] P1 `ollama:qwen2.5:1.5b` reste lent et reste a requalifier une fois `baselines` repris jusqu'au bout
|
||||
- [ ] P1 `apple-coreml:stateful-mistral7b-instruct-int4-coreml` reste preflight-only sur cette machine: preflight froid `:8100` OK en 128 s, preflight chaud OK en 63 s, mais le smoke ANE est reste bloque a `structure` pendant plus de 8 minutes avec les budgets de smoke
|
||||
- [ ] P1 Le host `ollama` natif 0.17.7 sur cette machine echoue sur `qwen2.5:1.5b` avec un crash Metal; la validation ANE reelle passe par un service Docker CPU expose sur `127.0.0.1:11435` et route via `mascarade`
|
||||
- [ ] P1 Le runtime Apple local n'expose qu'un seul `model_id` a la fois; un fallback `repair` vers un autre modele Apple exige donc un switch de service entre deux runs, pas au milieu d'un smoke
|
||||
|
||||
- [ ] P0 `ollama` natif 0.17.7 sur macOS 26.3.1 / Apple M5 echoue encore en generation sur `qwen2.5:7b` et `qwen2.5:1.5b` avec une erreur Metal / `HTTP 500`
|
||||
- [ ] P1 Le runtime Apple local ne sert qu'un seul `model_id` a la fois; tout switch Apple reste une action manuelle ou semi-auto
|
||||
- [ ] P1 `mascarade` route aujourd'hui `ollama:*` vers `/api/chat`, alors que `llama-server` expose surtout `/v1/chat/completions`; un pont runtime ou provider reste a creer
|
||||
|
||||
## Prochain ordre
|
||||
- [ ] P0 Finir `python3 scripts/run_next_lots.py --lot baselines`, puis laisser `tracking_sync` recalculer les verdicts complets
|
||||
- [ ] P0 Tuner `rewrite_v1` et la passe `repair` pour eliminer `outline_like` sur `ollama:qwen2.5:7b`
|
||||
- [ ] P1 Rejouer ensuite `python3 scripts/run_next_lots.py --lot priority_models` pour verifier la stabilite de `apple-coreml:qwen3.5-4b-onnx-q4f16` et le sort de `ollama:qwen2.5:7b`
|
||||
- [ ] P1 Garder `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` comme baselines vitesse ou regressions tant qu'ils n'ont pas de verdict comparable au protocole courant
|
||||
- [ ] P1 Verifier avant tout rerun Apple que `qwen2.5-0.5b-instruct-onnx`, `qwen3.5-4b-onnx-q4f16` et `stateful-mistral7b-instruct-int4-coreml` sont bien installes/stages et que le bon `model_id` est charge sur `:8201`
|
||||
|
||||
## Auto-sync
|
||||
- [ ] P0 Choisir le mode d'integration `llama.cpp`: adapter provider Mascarade ou shim local
|
||||
- [ ] P0 Valider `qwen2.5:1.5b` de bout en bout via ce nouveau chemin
|
||||
- [ ] P1 Etendre le meme chemin a `qwen2.5:7b`
|
||||
- [ ] P1 Relancer `python3 scripts/run_next_lots.py --lot priority_models`
|
||||
- [ ] P1 Relancer ensuite `python3 scripts/run_next_lots.py --lot baselines`
|
||||
- [ ] P1 Garder `automation/reports/apple_rerun_preset_20260313T223555Z` comme rerun de reference pour les comparaisons Apple futures
|
||||
|
||||
## Auto-sync
|
||||
<!-- AUTO-SYNC:ANE-TODO-ACTIVE:START -->
|
||||
- dernier cycle automatique: 2026-03-09T06:53:02+00:00
|
||||
- modeles accepted: aucun
|
||||
- modeles ayant atteint gate: apple-coreml:qwen2.5-0.5b-instruct-onnx, ollama:qwen2.5:1.5b
|
||||
- quality_blocked: apple-coreml:qwen2.5-0.5b-instruct-onnx, ollama:qwen2.5:1.5b
|
||||
- provider_failed: aucun
|
||||
- prochain lot recommande: Analyser les runs ayant atteint gate/repair puis resserrer la reference locale autour des meilleurs candidats.
|
||||
- checkpoint manuel en attente: Le runtime Apple sert `qwen2.5-0.5b-instruct-onnx` au lieu de `stateful-mistral7b-instruct-int4-coreml`.
|
||||
- commande preparee: `bash scripts/prepare_runtime_step.sh --apple-model stateful-mistral7b-instruct-int4-coreml --resume-state /Users/electron/Documents/Projets_Creatifs/ai-novel-engine/automation/state/next_lots_state.json --ane-script /Users/electron/Documents/Projets_Creatifs/ai-novel-engine/scripts/run_next_lots.py`
|
||||
- reprise: `python3 scripts/run_next_lots.py --resume /Users/electron/Documents/Projets_Creatifs/ai-novel-engine/automation/state/next_lots_state.json`
|
||||
- dernier cycle automatique: 2026-03-14T14:03:06+00:00
|
||||
- modeles accepted: apple-coreml:qwen3.5-4b-onnx-q4f16
|
||||
- modeles ayant atteint gate: apple-coreml:qwen3.5-4b-onnx-q4f16, apple-coreml:qwen2.5-0.5b-instruct-onnx
|
||||
- quality_blocked: apple-coreml:qwen2.5-0.5b-instruct-onnx
|
||||
- provider_failed: ollama:qwen2.5:7b, ollama:qwen2.5:1.5b
|
||||
- prochain lot recommande: Reference locale reconfirmee; retablir le runtime des modeles provider_failed puis reprendre rewrite/repair sur les modeles bloques a gate.
|
||||
<!-- AUTO-SYNC:ANE-TODO-ACTIVE:END -->
|
||||
|
||||
+10
-2
@@ -91,6 +91,15 @@ Regle:
|
||||
- [x] `ollama:qwen2.5:7b` atteint `gate`, exerce `repair` en live sur deux passes, puis finit `quality_blocked` avec `outline_like`
|
||||
- [x] Le comparatif local, les TODOs, les README et les runbooks disposent maintenant d'un premier resultat `accepted` sous protocole courant
|
||||
|
||||
### Lot livre - 13 mars 2026 (reprise et durcissement prose)
|
||||
- [x] `tracking_sync` consolide maintenant les derniers verdicts connus par modele depuis `automation/reports/*/run.json`
|
||||
- [x] Normalisation legere des sorties prose pour retirer les code fences et titres `# Chapitre` avant garde-fou
|
||||
- [x] Heuristique `outline_like` renforcee pour mieux detecter headings, listes, titres et labels structurants
|
||||
- [x] Prompts `draft_v1`, `rewrite_v1`, `repair_v1` et `gate_v1` resserres contre `outline_like` et `truncated_ending`
|
||||
- [x] Le rerun comparable `automation/reports/apple_rerun_preset_20260313T223555Z` passe `accepted` sans `repair` et reconfirme la reference Apple locale
|
||||
- [x] Etat automatise `baselines` clos proprement; `stateful-mistral7b-instruct-int4-coreml` sort du chemin critique
|
||||
- [x] Suite unitaire etendue a 43 tests verts
|
||||
|
||||
## Actif
|
||||
- [x] Aucun suivi actif ici. Voir `TODO_ACTIVE.md`.
|
||||
|
||||
@@ -100,11 +109,10 @@ Regle:
|
||||
## Prochain ordre
|
||||
- [x] Mettre a jour ce fichier uniquement quand un nouveau lot est reellement termine.
|
||||
|
||||
## Auto-sync
|
||||
## Auto-sync
|
||||
<!-- AUTO-SYNC:ANE-TODO-DONE:START -->
|
||||
- orchestrateur `scripts/run_next_lots.py` disponible
|
||||
- manifeste `automation/next_lots.toml` charge
|
||||
- derniers fichiers de suivi synchronisables via marqueurs `AUTO-SYNC`
|
||||
- dernier cycle automatise observe: 2026-03-09T06:53:02+00:00
|
||||
- dernier cycle automatise observe: 2026-03-14T14:03:06+00:00
|
||||
<!-- AUTO-SYNC:ANE-TODO-DONE:END -->
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
mascarade_repo = "/Users/electron/mascarade"
|
||||
core_base_url = "http://127.0.0.1:8100"
|
||||
apple_runtime_url = "http://127.0.0.1:8201"
|
||||
ollama_tags_url = "http://127.0.0.1:11435/api/tags"
|
||||
ollama_tags_url = "http://127.0.0.1:11434/api/tags"
|
||||
# `native` garde le preflight Ollama `/api/chat`.
|
||||
# `openai_compatible` saute ce preflight et envoie `ollama:*` vers l'URL ci-dessous.
|
||||
ollama_runtime = "native"
|
||||
ollama_openai_base_url = "http://127.0.0.1:8100"
|
||||
apple_model_ready_timeout_seconds = 30
|
||||
apple_model_poll_interval_seconds = 2
|
||||
|
||||
@@ -28,7 +32,6 @@ ANE_REPAIR_MAX_PASSES = "2"
|
||||
apple_models = [
|
||||
"qwen2.5-0.5b-instruct-onnx",
|
||||
"qwen3.5-4b-onnx-q4f16",
|
||||
"stateful-mistral7b-instruct-int4-coreml",
|
||||
]
|
||||
ollama_models = [
|
||||
"qwen2.5:7b",
|
||||
@@ -48,14 +51,12 @@ models = [
|
||||
]
|
||||
|
||||
[lots.preflight_only]
|
||||
models = [
|
||||
"apple-coreml:stateful-mistral7b-instruct-int4-coreml",
|
||||
]
|
||||
models = []
|
||||
|
||||
[tracking.ane]
|
||||
todo_active = "TODO_ACTIVE.md"
|
||||
todo_done = "TODO_IMPLEMENTE.md"
|
||||
plan = "docs/EXECUTION_PLAN_2026-03-08.md"
|
||||
plan = "docs/EXECUTION_PLAN_2026-03-14.md"
|
||||
comparison = "docs/MODEL_COMPARISON_2026-03-08.md"
|
||||
readme = "README.md"
|
||||
runbook = "docs/runbooks/LOCAL_GENERATION.md"
|
||||
|
||||
+5
-1
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
@@ -109,6 +110,9 @@ def cmd_intention_create(root: Path, chapter_value: str | None = None, input_fun
|
||||
intentions_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
raw_chapter = chapter_value or input_func("Numéro du chapitre (ex: 08) : ").strip()
|
||||
if not re.match(r'^[a-zA-Z0-9_]+$', raw_chapter):
|
||||
print("Numéro de chapitre invalide (alphanumériques et _ uniquement).")
|
||||
return 1
|
||||
chapter = ChapterId.parse(raw_chapter)
|
||||
|
||||
path = resolve_chapter_file(intentions_dir, chapter)
|
||||
@@ -116,7 +120,7 @@ def cmd_intention_create(root: Path, chapter_value: str | None = None, input_fun
|
||||
print(f"Une intention existe déjà : {path}")
|
||||
return 1
|
||||
|
||||
print("\nDécris l’intention (finir par Ctrl+D / Ctrl+Z):\n")
|
||||
print("\nDécris l'intention (finir par Ctrl+D / Ctrl+Z):\n")
|
||||
lines = []
|
||||
try:
|
||||
while True:
|
||||
|
||||
+88
-14
@@ -287,6 +287,55 @@ class GenerationPipeline:
|
||||
return text
|
||||
return f"{text[:limit].rstrip()}\n[...]"
|
||||
|
||||
def _normalize_generated_prose(self, text: str) -> str:
|
||||
payload = text.strip()
|
||||
if not payload:
|
||||
return ""
|
||||
|
||||
if payload.startswith("```"):
|
||||
lines = payload.splitlines()
|
||||
if len(lines) >= 3 and lines[-1].strip() == "```":
|
||||
payload = "\n".join(lines[1:-1]).strip()
|
||||
|
||||
cleaned_lines: list[str] = []
|
||||
for raw_line in payload.splitlines():
|
||||
stripped = raw_line.strip()
|
||||
if not stripped:
|
||||
cleaned_lines.append("")
|
||||
continue
|
||||
if stripped in {"```", "```markdown", "```md"}:
|
||||
continue
|
||||
if stripped == "---":
|
||||
continue
|
||||
if re.match(r"^#\s+(chapitre|chapter)\b", stripped, flags=re.IGNORECASE):
|
||||
continue
|
||||
cleaned_lines.append(raw_line)
|
||||
|
||||
normalized = "\n".join(cleaned_lines).strip()
|
||||
return f"{normalized}\n" if normalized else ""
|
||||
|
||||
def _repair_focus(self, gate_report: ManuscriptGateReport) -> str:
|
||||
blockers = set(gate_report.all_blockers())
|
||||
focus: list[str] = []
|
||||
if "outline_like" in blockers:
|
||||
focus.append(
|
||||
"- priorite absolue: supprimer toute ecriture qui resume ou annonce la scene; montrer l'action, la perception, la decision et la consequence au lieu de les lister"
|
||||
)
|
||||
focus.append(
|
||||
"- chaque paragraphe doit faire avancer concretement la situation; eviter les intitulés, les titres de chapitre et les transitions meta"
|
||||
)
|
||||
if "truncated_ending" in blockers:
|
||||
focus.append(
|
||||
"- la derniere scene doit se fermer sur une decision nette et sa consequence immediate, dans une phrase pleinement terminee"
|
||||
)
|
||||
if "too_short" in blockers:
|
||||
focus.append(
|
||||
"- viser au moins 4 paragraphes utiles pour obtenir une scene complete, pas un resume raccourci"
|
||||
)
|
||||
if not focus:
|
||||
focus.append("- conserver une prose continue, concrete et entierement narrative")
|
||||
return "\n".join(focus)
|
||||
|
||||
def _generate_structure(
|
||||
self,
|
||||
provider: GenerationProvider,
|
||||
@@ -332,10 +381,10 @@ class GenerationPipeline:
|
||||
"Génération du brouillon initial en cours.",
|
||||
)
|
||||
response = provider.generate(GenerationRequest(stage="draft", prompt=prompt, temperature=0.4))
|
||||
draft = response.content.strip()
|
||||
draft = self._normalize_generated_prose(response.content)
|
||||
if not draft:
|
||||
raise ProviderError("Le provider a renvoyé un brouillon vide.")
|
||||
return f"{draft}\n"
|
||||
return draft
|
||||
|
||||
def _generate_control_report(
|
||||
self,
|
||||
@@ -392,10 +441,10 @@ class GenerationPipeline:
|
||||
"Réécriture guidée par la critique en cours.",
|
||||
)
|
||||
response = provider.generate(GenerationRequest(stage="rewrite", prompt=prompt, temperature=0.3))
|
||||
draft = response.content.strip()
|
||||
draft = self._normalize_generated_prose(response.content)
|
||||
if not draft:
|
||||
raise ProviderError("Le provider a renvoyé une réécriture vide.")
|
||||
return f"{draft}\n"
|
||||
return draft
|
||||
|
||||
def _repair_until_ready(
|
||||
self,
|
||||
@@ -471,6 +520,7 @@ class GenerationPipeline:
|
||||
gate_json=gate_report.to_dict(),
|
||||
repair_attempt=attempt,
|
||||
repair_model=repair_model or "",
|
||||
repair_focus=self._repair_focus(gate_report),
|
||||
story_context=context.story_context,
|
||||
)
|
||||
model_label = repair_model or self._provider_model_name(provider) or "provider_courant"
|
||||
@@ -481,10 +531,10 @@ class GenerationPipeline:
|
||||
f"Réparation prose v{attempt} en cours avec {model_label}.",
|
||||
)
|
||||
response = provider.generate(GenerationRequest(stage="repair", prompt=prompt, temperature=0.2))
|
||||
repaired = response.content.strip()
|
||||
repaired = self._normalize_generated_prose(response.content)
|
||||
if not repaired:
|
||||
raise ProviderError("Le provider a renvoyé une réparation vide.")
|
||||
return f"{repaired}\n"
|
||||
return repaired
|
||||
|
||||
def _generate_manuscript_gate_report(
|
||||
self,
|
||||
@@ -563,6 +613,8 @@ class GenerationPipeline:
|
||||
|
||||
override = os.environ.get("ANE_REPAIR_FALLBACK_MODEL", "").strip()
|
||||
candidate = override or self._default_repair_fallback_model(base_model) or base_model
|
||||
if not override and self._model_provider_name(candidate) != self._model_provider_name(base_model):
|
||||
candidate = base_model
|
||||
if self._is_cross_apple_runtime_switch(base_model, candidate):
|
||||
raise ProviderError(
|
||||
"ANE_REPAIR_FALLBACK_MODEL ne peut pas viser un autre modèle apple-coreml pendant un même smoke. "
|
||||
@@ -587,6 +639,13 @@ class GenerationPipeline:
|
||||
return False
|
||||
return base_model.startswith("apple-coreml:") and candidate.startswith("apple-coreml:")
|
||||
|
||||
def _model_provider_name(self, model: str | None) -> str | None:
|
||||
if not model or ":" not in model:
|
||||
return None
|
||||
provider, _ = model.split(":", 1)
|
||||
provider = provider.strip()
|
||||
return provider or None
|
||||
|
||||
def _heuristic_gate_report(self, draft_v2: str) -> ManuscriptGateReport | None:
|
||||
blockers: list[str] = []
|
||||
recommendations: list[str] = []
|
||||
@@ -630,16 +689,31 @@ class GenerationPipeline:
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
if stripped.startswith("## "):
|
||||
detected_markers.add("heading_level_2")
|
||||
if stripped.startswith("### "):
|
||||
detected_markers.add("heading_level_3")
|
||||
if stripped.startswith("- "):
|
||||
detected_markers.add("bullet_list")
|
||||
lowered = stripped.lower()
|
||||
if "**objectif**" in lowered or "**conflit**" in lowered or "**sortie**" in lowered:
|
||||
if stripped.startswith("```"):
|
||||
detected_markers.add("fenced_block")
|
||||
if re.match(r"^#{1,6}\s", stripped):
|
||||
detected_markers.add("heading")
|
||||
if stripped == "---":
|
||||
detected_markers.add("horizontal_rule")
|
||||
if stripped.startswith(("- ", "* ")):
|
||||
detected_markers.add("bullet_list")
|
||||
if re.match(r"^\d+[.)]\s", stripped):
|
||||
detected_markers.add("numbered_list")
|
||||
if (
|
||||
"**objectif**" in lowered
|
||||
or "**conflit**" in lowered
|
||||
or "**sortie**" in lowered
|
||||
or "objectif:" in lowered
|
||||
or "conflit:" in lowered
|
||||
or "sortie:" in lowered
|
||||
):
|
||||
detected_markers.add("scene_fields")
|
||||
if "scène" in lowered or "scene" in lowered:
|
||||
if re.match(r"^#{0,6}\s*(objectif dramatique|tension|scènes?|scenes?)\b", lowered):
|
||||
detected_markers.add("structure_label")
|
||||
if re.match(r"^#{0,6}\s*chapitre\b", lowered):
|
||||
detected_markers.add("chapter_title")
|
||||
if "scène" in lowered or "scene" in lowered or "— titre" in lowered:
|
||||
detected_markers.add("scene_heading")
|
||||
if len(detected_markers) >= 2:
|
||||
return True
|
||||
|
||||
@@ -4,7 +4,9 @@ from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, replace
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import socket
|
||||
import time
|
||||
from typing import Mapping
|
||||
from urllib import error, request
|
||||
|
||||
@@ -146,19 +148,39 @@ class OpenAICompatibleProvider(GenerationProvider):
|
||||
method="POST",
|
||||
)
|
||||
|
||||
_RETRYABLE_HTTP_CODES = {429, 500, 502, 503}
|
||||
_MAX_RETRIES = 3
|
||||
_BASE_DELAY = 1.0
|
||||
_MAX_DELAY = 10.0
|
||||
|
||||
last_exc: Exception | None = None
|
||||
for attempt in range(_MAX_RETRIES):
|
||||
try:
|
||||
with request.urlopen(http_request, timeout=self.config.timeout) as response:
|
||||
raw_payload = json.loads(response.read().decode("utf-8"))
|
||||
break
|
||||
except error.HTTPError as exc:
|
||||
if exc.code in _RETRYABLE_HTTP_CODES and attempt < _MAX_RETRIES - 1:
|
||||
last_exc = exc
|
||||
delay = min(_BASE_DELAY * (2 ** attempt), _MAX_DELAY)
|
||||
delay += random.uniform(0, delay * 0.25)
|
||||
time.sleep(delay)
|
||||
continue
|
||||
details = exc.read().decode("utf-8", errors="replace")
|
||||
raise ProviderError(
|
||||
f"Le provider a répondu avec HTTP {exc.code} pendant l'étape '{prompt_request.stage}': {details}"
|
||||
) from exc
|
||||
except error.URLError as exc:
|
||||
except (error.URLError, TimeoutError, socket.timeout) as exc:
|
||||
if attempt < _MAX_RETRIES - 1:
|
||||
last_exc = exc
|
||||
delay = min(_BASE_DELAY * (2 ** attempt), _MAX_DELAY)
|
||||
delay += random.uniform(0, delay * 0.25)
|
||||
time.sleep(delay)
|
||||
continue
|
||||
if isinstance(exc, error.URLError):
|
||||
raise ProviderError(
|
||||
f"Impossible de joindre le provider pendant l'étape '{prompt_request.stage}': {exc.reason}"
|
||||
) from exc
|
||||
except (TimeoutError, socket.timeout) as exc:
|
||||
raise ProviderError(
|
||||
f"Timeout du provider pendant l'étape '{prompt_request.stage}' après {self.config.timeout:.0f}s."
|
||||
) from exc
|
||||
@@ -166,6 +188,10 @@ class OpenAICompatibleProvider(GenerationProvider):
|
||||
raise ProviderError(
|
||||
f"Réponse non JSON du provider pendant l'étape '{prompt_request.stage}'."
|
||||
) from exc
|
||||
else:
|
||||
raise ProviderError(
|
||||
f"Le provider a échoué après {_MAX_RETRIES} tentatives pendant l'étape '{prompt_request.stage}'."
|
||||
) from last_exc
|
||||
|
||||
try:
|
||||
choice = raw_payload["choices"][0]
|
||||
|
||||
Binary file not shown.
+253
-12
@@ -3,9 +3,11 @@ from __future__ import annotations
|
||||
import argparse
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import tomllib
|
||||
@@ -55,6 +57,8 @@ class Manifest:
|
||||
core_base_url: str
|
||||
apple_runtime_url: str
|
||||
ollama_tags_url: str
|
||||
ollama_runtime: str
|
||||
ollama_openai_base_url: str
|
||||
apple_model_ready_timeout_seconds: float
|
||||
apple_model_poll_interval_seconds: float
|
||||
smoke_chapter: str
|
||||
@@ -77,6 +81,11 @@ class Manifest:
|
||||
tracking = payload["tracking"]
|
||||
lots = payload["lots"]
|
||||
ensure_models = payload["ensure_models"]
|
||||
ollama_runtime = str(paths.get("ollama_runtime", "native")).strip() or "native"
|
||||
if ollama_runtime not in {"native", "openai_compatible"}:
|
||||
raise NextLotsError(
|
||||
"paths.ollama_runtime doit valoir 'native' ou 'openai_compatible'."
|
||||
)
|
||||
|
||||
mascarade_repo = Path(paths["mascarade_repo"]).expanduser()
|
||||
return cls(
|
||||
@@ -98,6 +107,10 @@ class Manifest:
|
||||
core_base_url=str(paths["core_base_url"]).rstrip("/"),
|
||||
apple_runtime_url=str(paths["apple_runtime_url"]).rstrip("/"),
|
||||
ollama_tags_url=str(paths["ollama_tags_url"]).rstrip("/"),
|
||||
ollama_runtime=ollama_runtime,
|
||||
ollama_openai_base_url=str(
|
||||
paths.get("ollama_openai_base_url", paths["core_base_url"])
|
||||
).rstrip("/"),
|
||||
apple_model_ready_timeout_seconds=float(paths.get("apple_model_ready_timeout_seconds", 30)),
|
||||
apple_model_poll_interval_seconds=float(paths.get("apple_model_poll_interval_seconds", 2)),
|
||||
smoke_chapter=str(smoke["chapter"]),
|
||||
@@ -122,7 +135,7 @@ class CommandResult:
|
||||
duration_seconds: float
|
||||
|
||||
|
||||
CommandRunner = Callable[[list[str], Path, dict[str, str] | None], CommandResult]
|
||||
CommandRunner = Callable[..., CommandResult]
|
||||
JsonFetcher = Callable[[str, float], Any]
|
||||
|
||||
|
||||
@@ -217,11 +230,17 @@ def _timestamp() -> str:
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
||||
|
||||
|
||||
def _default_command_runner(args: list[str], cwd: Path, env: dict[str, str] | None = None) -> CommandResult:
|
||||
def _default_command_runner(
|
||||
args: list[str],
|
||||
cwd: Path,
|
||||
env: dict[str, str] | None = None,
|
||||
timeout_seconds: float | None = None,
|
||||
) -> CommandResult:
|
||||
merged_env = os.environ.copy()
|
||||
if env:
|
||||
merged_env.update(env)
|
||||
started = time.monotonic()
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
args,
|
||||
cwd=str(cwd),
|
||||
@@ -229,6 +248,19 @@ def _default_command_runner(args: list[str], cwd: Path, env: dict[str, str] | No
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=timeout_seconds,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
stdout = exc.stdout if isinstance(exc.stdout, str) else (exc.stdout.decode("utf-8", errors="replace") if exc.stdout else "")
|
||||
stderr = exc.stderr if isinstance(exc.stderr, str) else (exc.stderr.decode("utf-8", errors="replace") if exc.stderr else "")
|
||||
detail = f"Timed out after {timeout_seconds:.1f}s." if timeout_seconds is not None else "Timed out."
|
||||
stderr = f"{stderr}\n{detail}".strip()
|
||||
return CommandResult(
|
||||
args=args,
|
||||
returncode=124,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
duration_seconds=time.monotonic() - started,
|
||||
)
|
||||
return CommandResult(
|
||||
args=args,
|
||||
@@ -267,9 +299,69 @@ def replace_auto_section(path: Path, marker_name: str, heading: str, body: str)
|
||||
else:
|
||||
suffix = "\n" if text.endswith("\n") else "\n\n"
|
||||
new_text = f"{text}{suffix}{section}"
|
||||
repeated_heading_pattern = rf"(?:{re.escape(heading)}\n){{2,}}"
|
||||
new_text = re.sub(repeated_heading_pattern, f"{heading}\n", new_text)
|
||||
path.write_text(new_text, encoding="utf-8")
|
||||
|
||||
|
||||
def _safe_timestamp(value: str) -> datetime:
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value)
|
||||
except ValueError:
|
||||
return datetime.fromtimestamp(0, tz=timezone.utc)
|
||||
if parsed.tzinfo is None:
|
||||
parsed = parsed.replace(tzinfo=timezone.utc)
|
||||
return parsed.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def _load_report_history(reports_root: Path) -> list[RunState]:
|
||||
history: list[RunState] = []
|
||||
if not reports_root.exists():
|
||||
return history
|
||||
for run_path in sorted(reports_root.glob("*/run.json")):
|
||||
try:
|
||||
history.append(RunState.load(run_path))
|
||||
except (OSError, json.JSONDecodeError, TypeError, ValueError):
|
||||
continue
|
||||
history.sort(key=lambda item: (_safe_timestamp(item.updated_at), item.report_dir))
|
||||
return history
|
||||
|
||||
|
||||
def _result_sort_key(result: ModelRunResult) -> tuple[int, str, str]:
|
||||
category_order = {
|
||||
"priority_models": 0,
|
||||
"baselines": 1,
|
||||
"preflight_only": 2,
|
||||
"runtime_preflight": 3,
|
||||
}
|
||||
provider = result.model.split(":", 1)[0]
|
||||
return (category_order.get(result.category, 9), provider, result.model)
|
||||
|
||||
|
||||
def _consolidated_tracking_results(state: RunState, reports_root: Path) -> list[ModelRunResult]:
|
||||
latest_by_model: dict[str, tuple[tuple[datetime, int], ModelRunResult]] = {}
|
||||
sequence = 0
|
||||
for snapshot in [*_load_report_history(reports_root), state]:
|
||||
stamp = _safe_timestamp(snapshot.updated_at)
|
||||
for result in snapshot.typed_results():
|
||||
candidate_key = (stamp, sequence)
|
||||
current = latest_by_model.get(result.model)
|
||||
if current is None or candidate_key >= current[0]:
|
||||
latest_by_model[result.model] = (candidate_key, result)
|
||||
sequence += 1
|
||||
return sorted((payload[1] for payload in latest_by_model.values()), key=_result_sort_key)
|
||||
|
||||
|
||||
def _accepted_history_counts(state: RunState, reports_root: Path) -> dict[str, int]:
|
||||
counts: dict[str, int] = {}
|
||||
for snapshot in [*_load_report_history(reports_root), state]:
|
||||
for result in snapshot.typed_results():
|
||||
if result.classification != "accepted":
|
||||
continue
|
||||
counts[result.model] = counts.get(result.model, 0) + 1
|
||||
return counts
|
||||
|
||||
|
||||
class NextLotsRunner:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -281,6 +373,7 @@ class NextLotsRunner:
|
||||
self.manifest = manifest
|
||||
self.command_runner = command_runner
|
||||
self.json_fetcher = json_fetcher
|
||||
self._command_runner_supports_timeout = len(inspect.signature(command_runner).parameters) >= 4
|
||||
|
||||
def run(
|
||||
self,
|
||||
@@ -381,7 +474,7 @@ class NextLotsRunner:
|
||||
state.notes.append("Dry-run: ensure_models non exécuté.")
|
||||
return
|
||||
args = ["bash", "scripts/ensure_apple_models.sh"]
|
||||
result = self.command_runner(args, self.manifest.tracking.mascarade_repo)
|
||||
result = self._invoke_command(args, self.manifest.tracking.mascarade_repo, timeout_seconds=900)
|
||||
log_path = Path(state.report_dir) / "ensure_models.log"
|
||||
log_path.write_text(_command_log(result), encoding="utf-8")
|
||||
if result.returncode != 0:
|
||||
@@ -471,7 +564,7 @@ class NextLotsRunner:
|
||||
)
|
||||
|
||||
def _build_manual_action(self, state: RunState, *, args: list[str], reason: str) -> dict[str, Any]:
|
||||
result = self.command_runner(args, self.manifest.tracking.mascarade_repo)
|
||||
result = self._invoke_command(args, self.manifest.tracking.mascarade_repo, timeout_seconds=300)
|
||||
log_path = Path(state.report_dir) / f"manual_action_{len(state.results):02d}.log"
|
||||
log_path.write_text(_command_log(result), encoding="utf-8")
|
||||
return {
|
||||
@@ -514,20 +607,123 @@ class NextLotsRunner:
|
||||
return last_seen
|
||||
return last_seen
|
||||
|
||||
def _ollama_base_url(self) -> str:
|
||||
tags_url = self.manifest.ollama_tags_url.rstrip("/")
|
||||
suffix = "/api/tags"
|
||||
if tags_url.endswith(suffix):
|
||||
return tags_url[: -len(suffix)]
|
||||
return tags_url
|
||||
|
||||
def _openai_base_url_for_model(self, model: str) -> str:
|
||||
if model.startswith("ollama:") and self.manifest.ollama_runtime == "openai_compatible":
|
||||
return self.manifest.ollama_openai_base_url
|
||||
return self.manifest.core_base_url
|
||||
|
||||
def _should_run_ollama_native_preflight(self, model: str) -> bool:
|
||||
return model.startswith("ollama:") and self.manifest.ollama_runtime == "native"
|
||||
|
||||
def _run_ollama_native_preflight(self, model: str) -> CommandResult:
|
||||
timeout_seconds = min(45.0, float(self._timeout_for_model(f"ollama:{model}")))
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": "Respond with exactly: ollama native preflight ok"}],
|
||||
"stream": False,
|
||||
"options": {
|
||||
"temperature": 0,
|
||||
"num_predict": 16,
|
||||
},
|
||||
}
|
||||
body = json.dumps(payload).encode("utf-8")
|
||||
started = time.monotonic()
|
||||
try:
|
||||
req = request.Request(
|
||||
f"{self._ollama_base_url()}/api/chat",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with request.urlopen(req, timeout=timeout_seconds) as response:
|
||||
raw_payload = response.read().decode("utf-8")
|
||||
except error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", errors="replace")
|
||||
return CommandResult(
|
||||
args=["ollama-native-preflight", model],
|
||||
returncode=1,
|
||||
stdout="",
|
||||
stderr=f"HTTP {exc.code} {exc.reason}\n{detail}".strip(),
|
||||
duration_seconds=time.monotonic() - started,
|
||||
)
|
||||
except Exception as exc:
|
||||
return CommandResult(
|
||||
args=["ollama-native-preflight", model],
|
||||
returncode=1,
|
||||
stdout="",
|
||||
stderr=f"{type(exc).__name__}: {exc}",
|
||||
duration_seconds=time.monotonic() - started,
|
||||
)
|
||||
try:
|
||||
parsed = json.loads(raw_payload)
|
||||
except json.JSONDecodeError:
|
||||
parsed = {"raw": raw_payload}
|
||||
preview = {
|
||||
"model": parsed.get("model"),
|
||||
"content": (parsed.get("message") or {}).get("content", ""),
|
||||
"done_reason": parsed.get("done_reason"),
|
||||
}
|
||||
return CommandResult(
|
||||
args=["ollama-native-preflight", model],
|
||||
returncode=0,
|
||||
stdout=json.dumps(preview, ensure_ascii=False, indent=2),
|
||||
stderr="",
|
||||
duration_seconds=time.monotonic() - started,
|
||||
)
|
||||
|
||||
def _invoke_command(
|
||||
self,
|
||||
args: list[str],
|
||||
cwd: Path,
|
||||
*,
|
||||
env: dict[str, str] | None = None,
|
||||
timeout_seconds: float | None = None,
|
||||
) -> CommandResult:
|
||||
if self._command_runner_supports_timeout:
|
||||
return self.command_runner(args, cwd, env, timeout_seconds)
|
||||
return self.command_runner(args, cwd, env)
|
||||
|
||||
def _run_model(self, model: str, *, category: str, preflight_only: bool, report_dir: Path) -> ModelRunResult:
|
||||
result = ModelRunResult(model=model, category=category, apple_model_active=self._current_apple_model())
|
||||
model_slug = _slugify(model)
|
||||
openai_base_url = self._openai_base_url_for_model(model)
|
||||
if self._should_run_ollama_native_preflight(model):
|
||||
native_preflight = self._run_ollama_native_preflight(model.split(":", 1)[1])
|
||||
if native_preflight.returncode != 0:
|
||||
result.preflight_duration_seconds = native_preflight.duration_seconds
|
||||
native_log = report_dir / f"{model_slug}_ollama_native_preflight.log"
|
||||
native_log.write_text(_command_log(native_preflight), encoding="utf-8")
|
||||
result.preflight_log = str(native_log)
|
||||
result.preflight_ok = False
|
||||
result.classification = "provider_failed"
|
||||
result.status = "ollama_runtime_unhealthy"
|
||||
result.notes.append("Le preflight Ollama natif a échoué.")
|
||||
hint = _runtime_error_hint(native_preflight.stderr)
|
||||
if hint:
|
||||
result.notes.append(hint)
|
||||
return result
|
||||
preflight_args = [
|
||||
"bash",
|
||||
"scripts/smoke_openai_compat_ane.sh",
|
||||
"--url",
|
||||
self.manifest.core_base_url,
|
||||
openai_base_url,
|
||||
"--model",
|
||||
model,
|
||||
"--timeout",
|
||||
str(self._timeout_for_model(model)),
|
||||
]
|
||||
preflight = self.command_runner(preflight_args, self.manifest.tracking.mascarade_repo)
|
||||
preflight = self._invoke_command(
|
||||
preflight_args,
|
||||
self.manifest.tracking.mascarade_repo,
|
||||
timeout_seconds=float(self._timeout_for_model(model) + 30),
|
||||
)
|
||||
result.preflight_duration_seconds = preflight.duration_seconds
|
||||
preflight_log = report_dir / f"{model_slug}_preflight.log"
|
||||
preflight_log.write_text(_command_log(preflight), encoding="utf-8")
|
||||
@@ -550,7 +746,7 @@ class NextLotsRunner:
|
||||
"bash",
|
||||
"scripts/smoke_local_generation.sh",
|
||||
"--base-url",
|
||||
self.manifest.core_base_url,
|
||||
openai_base_url,
|
||||
"--model",
|
||||
model,
|
||||
"--chapter",
|
||||
@@ -563,7 +759,12 @@ class NextLotsRunner:
|
||||
self.manifest.smoke_intention,
|
||||
"--approve",
|
||||
]
|
||||
smoke = self.command_runner(smoke_args, self.manifest.repo_root, env=self.manifest.preset_env)
|
||||
smoke = self._invoke_command(
|
||||
smoke_args,
|
||||
self.manifest.repo_root,
|
||||
env=self.manifest.preset_env,
|
||||
timeout_seconds=float(self.manifest.smoke_timeout_seconds + 60),
|
||||
)
|
||||
result.smoke_attempted = True
|
||||
result.smoke_duration_seconds = smoke.duration_seconds
|
||||
smoke_log = report_dir / f"{model_slug}_smoke.log"
|
||||
@@ -614,11 +815,22 @@ class NextLotsRunner:
|
||||
if dry_run:
|
||||
self._write_report_summary(state)
|
||||
return
|
||||
typed_results = state.typed_results()
|
||||
typed_results = _consolidated_tracking_results(
|
||||
state,
|
||||
self.manifest.repo_root / "automation" / "reports",
|
||||
)
|
||||
accepted_counts = _accepted_history_counts(
|
||||
state,
|
||||
self.manifest.repo_root / "automation" / "reports",
|
||||
)
|
||||
project_state = ProjectState(self.manifest.repo_root).summary()
|
||||
summary = _build_summary(state, typed_results)
|
||||
comparison = _render_comparison_markdown(state, typed_results)
|
||||
active_next = _compute_next_lot_recommendation(typed_results, self.manifest.next_code_lot)
|
||||
active_next = _compute_next_lot_recommendation(
|
||||
typed_results,
|
||||
self.manifest.next_code_lot,
|
||||
accepted_counts=accepted_counts,
|
||||
)
|
||||
|
||||
replace_auto_section(
|
||||
self.manifest.tracking.ane_todo_active,
|
||||
@@ -702,6 +914,15 @@ def _optional_string(value: object) -> str | None:
|
||||
return text or None
|
||||
|
||||
|
||||
def _runtime_error_hint(stderr: str) -> str | None:
|
||||
for raw_line in stderr.splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
return line[:240]
|
||||
return None
|
||||
|
||||
|
||||
def _slugify(value: str) -> str:
|
||||
return "".join(char if char.isalnum() else "_" for char in value).strip("_").lower()
|
||||
|
||||
@@ -732,9 +953,29 @@ def _build_summary(state: RunState, results: list[ModelRunResult]) -> dict[str,
|
||||
}
|
||||
|
||||
|
||||
def _compute_next_lot_recommendation(results: list[ModelRunResult], fallback: str) -> str:
|
||||
def _compute_next_lot_recommendation(
|
||||
results: list[ModelRunResult],
|
||||
fallback: str,
|
||||
*,
|
||||
accepted_counts: dict[str, int] | None = None,
|
||||
) -> str:
|
||||
accepted_counts = accepted_counts or {}
|
||||
provider_failed_models = [item.model for item in results if item.classification == "provider_failed"]
|
||||
has_quality_blocked = any(item.classification == "quality_blocked" for item in results)
|
||||
if accepted_counts.get("apple-coreml:qwen3.5-4b-onnx-q4f16", 0) >= 2:
|
||||
if provider_failed_models:
|
||||
if has_quality_blocked:
|
||||
return "Reference locale reconfirmee; retablir le runtime des modeles provider_failed puis reprendre rewrite/repair sur les modeles bloques a gate."
|
||||
return "Reference locale reconfirmee; retablir le runtime des modeles provider_failed avant de poursuivre."
|
||||
if any(item.classification == "quality_blocked" for item in results):
|
||||
return "Reference locale reconfirmee; resserrer rewrite/repair sur les modeles deja bloques a gate."
|
||||
return "Reference locale reconfirmee; garder les autres modeles en regression."
|
||||
if any(item.classification == "accepted" for item in results):
|
||||
return "Rejouer uniquement les baselines vitesse puis figer la référence locale dans les README/runbooks."
|
||||
if provider_failed_models:
|
||||
return "Confirmer la reference accepted puis retablir le runtime des modeles provider_failed."
|
||||
if any(item.classification == "quality_blocked" for item in results):
|
||||
return "Confirmer la reference accepted puis resserrer rewrite/repair sur les modeles deja bloques a gate."
|
||||
return "Figer la reference locale dans les README/runbooks et garder les autres modeles en regression."
|
||||
if any(item.reached_gate() for item in results):
|
||||
return "Analyser les runs ayant atteint gate/repair puis resserrer la reference locale autour des meilleurs candidats."
|
||||
return fallback
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# Contexte projet - 14 mars 2026
|
||||
|
||||
Document court pour comprendre rapidement ou en est `ai-novel-engine` aujourd'hui.
|
||||
|
||||
## Ce que fait le projet
|
||||
|
||||
`ai-novel-engine` est un moteur de redaction longue qui garde l'auteur dans la boucle.
|
||||
|
||||
Le pipeline reste stable:
|
||||
|
||||
`intention -> structure -> draft -> critique -> rewrite -> gate -> validation -> memoire`
|
||||
|
||||
Le coeur produit n'est pas un "chat", mais une chaine de production narrative avec:
|
||||
|
||||
- intention obligatoire avant toute generation
|
||||
- garde-fou dur avant promotion manuscrit
|
||||
- boucle `repair`
|
||||
- memoire externe par chapitre
|
||||
|
||||
## Architecture locale
|
||||
|
||||
Sur cette machine:
|
||||
|
||||
- `ai-novel-engine` parle un provider OpenAI-compatible
|
||||
- `mascarade-core` expose `http://127.0.0.1:8100`
|
||||
- le runtime Apple local expose `http://127.0.0.1:8201`
|
||||
- Ollama natif expose `http://127.0.0.1:11434`
|
||||
|
||||
Le routage actuel repose surtout sur le prefixe de modele:
|
||||
|
||||
- `apple-coreml:*`
|
||||
- `ollama:*`
|
||||
|
||||
## Etat produit confirme
|
||||
|
||||
- la reference locale reste `apple-coreml:qwen3.5-4b-onnx-q4f16`
|
||||
- le comparatif versionne montre aujourd'hui:
|
||||
- `apple-coreml:qwen3.5-4b-onnx-q4f16` -> `accepted`
|
||||
- `apple-coreml:qwen2.5-0.5b-instruct-onnx` -> `quality_blocked`
|
||||
- `ollama:qwen2.5:7b` -> `provider_failed`
|
||||
- `ollama:qwen2.5:1.5b` -> `provider_failed`
|
||||
- `python3 -m unittest discover -s tests -v` passe avec `48` tests
|
||||
|
||||
## Faits live du 14 mars 2026
|
||||
|
||||
### Apple
|
||||
|
||||
- warm-up direct sur `apple-coreml:qwen3.5-4b-onnx-q4f16` via `:8100/v1/chat/completions`: `2.76s`
|
||||
- requete prose plus representative (`max_tokens=96`) sur le meme modele: `39.99s`
|
||||
- conclusion: le runtime Apple n'est pas mort; il est lent mais exploitable
|
||||
|
||||
### Ollama natif
|
||||
|
||||
- `ollama` 0.17.7 sur macOS 26.3.1 / Apple M5 echoue encore en generation sur `qwen2.5:7b` et `qwen2.5:1.5b`
|
||||
- les deux reports cibles du 14 mars tombent en `provider_failed` / `ollama_runtime_unhealthy`
|
||||
- le symptome utile cote projet est `HTTP 500 Internal Server Error`
|
||||
|
||||
### Contournement valide
|
||||
|
||||
- `llama-server` local sait charger directement le blob GGUF Ollama de `qwen2.5:1.5b`
|
||||
- un serveur temporaire sur `127.0.0.1:8082` a repondu en `0.31s`
|
||||
- implication: le blocage n'est pas le modele lui-meme, mais le runtime Ollama natif
|
||||
|
||||
## Ce qui est vraiment bloque
|
||||
|
||||
Le projet n'est plus bloque par le pipeline narratif.
|
||||
|
||||
Le vrai blocage est maintenant:
|
||||
|
||||
- comment servir `qwen2.5:1.5b` et `qwen2.5:7b` via un backend local stable
|
||||
- sans perdre le routage `provider:model` attendu par `mascarade` et `ai-novel-engine`
|
||||
|
||||
## Hypothese de travail la plus rentable
|
||||
|
||||
La piste la plus prometteuse n'est plus "reparer les prompts" mais:
|
||||
|
||||
1. brancher un chemin `llama.cpp` / `llama-server` reutilisable pour les blobs `qwen2.5`
|
||||
2. rerun `priority_models` et `baselines`
|
||||
3. reprendre `rewrite` / `repair` seulement sur les blockers qui survivent apres stabilisation runtime
|
||||
|
||||
## Fichiers a ouvrir en premier
|
||||
|
||||
- `docs/MEMOIRE_REPRISE_2026-03-14.md`
|
||||
- `docs/EXECUTION_PLAN_2026-03-14.md`
|
||||
- `TODO_ACTIVE.md`
|
||||
- `docs/MODEL_COMPARISON_2026-03-08.md`
|
||||
- `automation/reports/20260314T085946Z/SUMMARY.md`
|
||||
- `automation/reports/20260314T100648Z/SUMMARY.md`
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# Plan d'execution - 13 mars 2026
|
||||
|
||||
Plan de reprise apres revue des rapports du 9 mars 2026 et remise a plat du suivi.
|
||||
|
||||
References:
|
||||
|
||||
- memoire de reprise: [`MEMOIRE_REPRISE_2026-03-13.md`](./MEMOIRE_REPRISE_2026-03-13.md)
|
||||
- backlog actif: [`../TODO_ACTIVE.md`](../TODO_ACTIVE.md)
|
||||
- lots livres: [`../TODO_IMPLEMENTE.md`](../TODO_IMPLEMENTE.md)
|
||||
- comparatif courant: [`MODEL_COMPARISON_2026-03-08.md`](./MODEL_COMPARISON_2026-03-08.md)
|
||||
|
||||
Pilotage:
|
||||
|
||||
- lancer un cycle complet: `python3 scripts/run_next_lots.py --lot full`
|
||||
- resynchroniser seulement les docs: `python3 scripts/run_next_lots.py --lot tracking_sync --report-only`
|
||||
|
||||
## Lot 1 - Garder le chemin critique ferme et rerunnable
|
||||
|
||||
### Etat constate
|
||||
|
||||
- le `state` courant est clos et synchronise
|
||||
- `stateful-mistral7b-instruct-int4-coreml` est retire du lot critique
|
||||
- le runtime minimal est revenu: `:8100`, `:8201` et `:11434/api/tags` repondent
|
||||
- `mascarade-core` et `mascarade-api` ont ete recrees avec le bon `OLLAMA_BASE_URL`
|
||||
- le blocage live restant est maintenant plus precis: `ollama` natif 0.17.7 plante en generation sur `qwen2.5:7b` et `qwen2.5:1.5b` a cause du backend Metal
|
||||
|
||||
### Objectif
|
||||
|
||||
- garder `full` focalise sur les modeles utiles
|
||||
- garder Apple et le core en etat, puis rebrancher un chemin Ollama CPU avant les vrais reruns qualite
|
||||
|
||||
### Done quand
|
||||
|
||||
- `automation/state/next_lots_state.json` reste sans checkpoint ambigu
|
||||
- les endpoints Apple et core repondent de nouveau
|
||||
- un chemin Ollama CPU redevient executable pour les smokes comparatifs
|
||||
|
||||
### Risque principal
|
||||
|
||||
- un runtime partiellement revenu peut masquer un blocage plus fin du backend Ollama si on s'arrete au simple `api/tags`
|
||||
|
||||
## Lot 2 - Corriger les epreuves qualite, pas ajouter de complexite
|
||||
|
||||
### Etat constate
|
||||
|
||||
- `ollama:qwen2.5:7b` atteint `gate` mais reste bloque sur `outline_like`
|
||||
- `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` finissent sur `truncated_ending`
|
||||
- la reference Apple 4B a deja prouve qu'un cycle `accepted` est possible
|
||||
- le rerun comparable `automation/reports/apple_rerun_7oY51o` n'a pas reconfirme cette reference: `gate` bloque sur `too_short` + `truncated_ending`, puis `repair` echoue a cause de l'ancien fallback pipeline vers Ollama en `HTTP 502`
|
||||
- le rerun comparable `automation/reports/apple_rerun_preset_20260313T223555Z` a depuis fini `accepted` sans `repair`; la reference Apple locale est reconfirmee
|
||||
|
||||
### Objectif
|
||||
|
||||
- resserrer `rewrite_v1`, `memory_v1` si besoin, et surtout la boucle `repair` sur les deux blocages reels:
|
||||
- prose trop proche d'un plan
|
||||
- fin de texte tronquee ou suspendue
|
||||
|
||||
### Done quand
|
||||
|
||||
- `ollama:qwen2.5:7b` finit au moins une fois `accepted`
|
||||
- au moins une baseline sort de `truncated_ending` ou est officiellement releguee a simple test de regression
|
||||
|
||||
### Risque principal
|
||||
|
||||
- augmenter les budgets sans clarifier les consignes risque d'allonger les runs sans faire tomber les blockers
|
||||
|
||||
## Lot 3 - Reference locale reconfirmee
|
||||
|
||||
### Etat constate
|
||||
|
||||
- `apple-coreml:qwen3.5-4b-onnx-q4f16` a maintenant deux runs comparables `accepted`
|
||||
- le rerun `automation/reports/apple_rerun_preset_20260313T223555Z` est passe de bout en bout avec `repair_attempts=0`
|
||||
|
||||
### Objectif
|
||||
|
||||
- figer la reference Apple locale et sortir ce sujet du flux d'incertitude; les reruns Apple restent maintenant isoles d'un fallback Ollama par defaut
|
||||
|
||||
### Done quand
|
||||
|
||||
- deux runs comparables au meme preset passent jusqu'a `accepted`
|
||||
- la doc peut nommer une reference locale sans ambiguite
|
||||
|
||||
### Risque principal
|
||||
|
||||
- laisser la doc ou le suivi parler d'une reference "a confirmer" alors que le point est deja tranche
|
||||
|
||||
## Lot 4 - Garder une memoire projet exploitable
|
||||
|
||||
### Objectif
|
||||
|
||||
- faire en sorte que README, TODO, plan et runbook racontent la meme chose
|
||||
- ne plus perdre les bons resultats lorsqu'un lot partiel plus recent tourne apres un lot complet
|
||||
|
||||
### Done quand
|
||||
|
||||
- `tracking_sync` consolide les derniers verdicts connus par modele
|
||||
- les documents de suivi renvoient tous vers ce plan du 13 mars 2026
|
||||
|
||||
### Risque principal
|
||||
|
||||
- si les docs versionnees ne pointent pas vers le bon plan, la reprise devient plus couteuse que le code
|
||||
|
||||
## Auto-sync
|
||||
<!-- AUTO-SYNC:ANE-PLAN:START -->
|
||||
- dernier verdict automatise: 2026-03-14T10:06:55+00:00
|
||||
- accepted: apple-coreml:qwen3.5-4b-onnx-q4f16
|
||||
- gate atteint: apple-coreml:qwen3.5-4b-onnx-q4f16, apple-coreml:qwen2.5-0.5b-instruct-onnx
|
||||
- prochain lot calcule: Reference locale reconfirmee; retablir le runtime des modeles provider_failed puis reprendre rewrite/repair sur les modeles bloques a gate.
|
||||
<!-- AUTO-SYNC:ANE-PLAN:END -->
|
||||
@@ -0,0 +1,76 @@
|
||||
# Plan d'execution - 14 mars 2026
|
||||
|
||||
Plan de reprise base sur l'etat reel du 14 mars 2026.
|
||||
|
||||
References:
|
||||
|
||||
- contexte: [`CONTEXTE_PROJET_2026-03-14.md`](./CONTEXTE_PROJET_2026-03-14.md)
|
||||
- memoire: [`MEMOIRE_REPRISE_2026-03-14.md`](./MEMOIRE_REPRISE_2026-03-14.md)
|
||||
- backlog actif: [`../TODO_ACTIVE.md`](../TODO_ACTIVE.md)
|
||||
- comparatif courant: [`MODEL_COMPARISON_2026-03-08.md`](./MODEL_COMPARISON_2026-03-08.md)
|
||||
|
||||
## Lot 1 - Industrialiser le contournement `llama.cpp`
|
||||
|
||||
### Etat constate
|
||||
|
||||
- `ollama:qwen2.5:7b` et `ollama:qwen2.5:1.5b` echouent encore en runtime natif
|
||||
- `llama-server` sait deja charger un blob Ollama local et repondre vite
|
||||
- le blob `qwen2.5:1.5b` a deja repondu via `llama-server`
|
||||
|
||||
### Objectif
|
||||
|
||||
- disposer d'un chemin local stable pour les modeles `qwen2.5` sans dependre du runtime Ollama natif
|
||||
|
||||
### Done quand
|
||||
|
||||
- un backend `llama.cpp` est branchable de facon reproductible pour `qwen2.5:1.5b`
|
||||
- le routage `provider:model` reste pilotable depuis `mascarade`
|
||||
|
||||
## Lot 2 - Etendre au 7B
|
||||
|
||||
### Etat constate
|
||||
|
||||
- le 7B reste le meilleur candidat alternatif cote qualite
|
||||
- son blocage courant est d'abord runtime, pas prompt
|
||||
|
||||
### Objectif
|
||||
|
||||
- verifier si le meme chemin `llama.cpp` peut servir `qwen2.5:7b`
|
||||
|
||||
### Done quand
|
||||
|
||||
- un preflight et un smoke simple du 7B passent via le backend alternatif
|
||||
|
||||
## Lot 3 - Rejouer les lots utiles
|
||||
|
||||
### Objectif
|
||||
|
||||
- rerun `priority_models` puis `baselines` sur un runtime local stable
|
||||
|
||||
### Done quand
|
||||
|
||||
- les deux modeles `qwen2.5` ne sont plus `provider_failed`
|
||||
- le comparatif reflete enfin des verdicts qualite plutot que des erreurs de runtime
|
||||
|
||||
## Lot 4 - Revenir aux vrais blockers qualite
|
||||
|
||||
### Objectif
|
||||
|
||||
- reprendre `rewrite` / `repair` seulement sur les blockers qui subsistent apres stabilisation runtime
|
||||
|
||||
### Cible
|
||||
|
||||
- `outline_like` pour le meilleur candidat alternatif
|
||||
- `truncated_ending` pour la baseline Apple encore bloquee
|
||||
|
||||
## Risque a eviter
|
||||
|
||||
Ne pas re-rentrer dans une boucle de tuning prompts tant que les reruns `qwen2.5` passent par un runtime non fiable.
|
||||
|
||||
## Auto-sync
|
||||
<!-- AUTO-SYNC:ANE-PLAN:START -->
|
||||
- dernier verdict automatise: 2026-03-14T14:03:06+00:00
|
||||
- accepted: apple-coreml:qwen3.5-4b-onnx-q4f16
|
||||
- gate atteint: apple-coreml:qwen3.5-4b-onnx-q4f16, apple-coreml:qwen2.5-0.5b-instruct-onnx
|
||||
- prochain lot calcule: Reference locale reconfirmee; retablir le runtime des modeles provider_failed puis reprendre rewrite/repair sur les modeles bloques a gate.
|
||||
<!-- AUTO-SYNC:ANE-PLAN:END -->
|
||||
@@ -0,0 +1,67 @@
|
||||
# Memoire de reprise - 13 mars 2026
|
||||
|
||||
Memoire operationnelle courte pour reprendre `ai-novel-engine` sans relire tout le repo.
|
||||
|
||||
## Etat confirme
|
||||
|
||||
- la suite locale est saine: `python3 -m unittest discover -s tests -v` passe avec `44` tests
|
||||
- le pipeline auteur reste le meme: `intention -> structure -> draft -> critique -> rewrite -> gate -> validation -> memoire`
|
||||
- le lot `priority_models` du `2026-03-09T05:54:57Z` a donne:
|
||||
- `apple-coreml:qwen3.5-4b-onnx-q4f16` -> `accepted`
|
||||
- `ollama:qwen2.5:7b` -> `quality_blocked` avec residu `outline_like`
|
||||
- le rerun comparable `automation/reports/apple_rerun_preset_20260313T223555Z` a fini `accepted` le 13 mars 2026 avec `repair_attempts=0` et un `draft_v2` de `323` mots
|
||||
- le lot `baselines` du `2026-03-09T06:35:12Z` a donne:
|
||||
- `apple-coreml:qwen2.5-0.5b-instruct-onnx` -> `quality_blocked` avec `truncated_ending`
|
||||
- `ollama:qwen2.5:1.5b` -> `quality_blocked` avec `truncated_ending`
|
||||
- l'etat automatise a ete cloture proprement le `2026-03-13T14:15:56+00:00`
|
||||
- `stateful-mistral7b-instruct-int4-coreml` est sorti du chemin critique; il ne bloque plus `full`
|
||||
- etat runtime relu le 13 mars 2026 au soir:
|
||||
- `http://127.0.0.1:8100/health` repond de nouveau
|
||||
- `http://127.0.0.1:8201/health` et `http://127.0.0.1:8201/models` repondent de nouveau
|
||||
- `http://127.0.0.1:11434/api/tags` repond de nouveau
|
||||
- `mascarade-core` et `mascarade-api` ont ete recrees avec `OLLAMA_BASE_URL=http://host.docker.internal:11434`
|
||||
- le vrai blocage live restant n'est plus un port mort mais `ollama` natif 0.17.7, qui echoue encore en generation sur `qwen2.5:7b` et `qwen2.5:1.5b` avec une erreur Metal
|
||||
- le rerun Apple comparable `automation/reports/apple_rerun_7oY51o` n'a pas reconfirme la reference: il a atteint `gate`, a ete bloque par `too_short` + `truncated_ending`, puis a echoue a `repair` a cause de l'ancien fallback pipeline vers l'Ollama natif
|
||||
|
||||
## Contradiction resolue
|
||||
|
||||
Le repo avait deux verites concurrentes:
|
||||
|
||||
- les rapports `priority_models` montraient un vrai modele `accepted`
|
||||
- les sections `AUTO-SYNC` les plus recentes perdaient cette information des qu'un lot partiel plus recent ecrasait le `state` courant
|
||||
|
||||
Le suivi a ete corrige pour consolider les derniers resultats connus par modele depuis `automation/reports/*/run.json`. Le prochain `tracking_sync` ne doit plus oublier la reference `apple-coreml:qwen3.5-4b-onnx-q4f16`.
|
||||
|
||||
## Decisions de reprise
|
||||
|
||||
- fixer `apple-coreml:qwen3.5-4b-onnx-q4f16` comme reference locale reconfirmee sur deux runs comparables (`20260309T055457Z` puis `apple_rerun_preset_20260313T223555Z`)
|
||||
- traiter `ollama:qwen2.5:7b` comme meilleur candidat alternatif a faire sortir de `outline_like`
|
||||
- garder `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` comme baselines vitesse et regression, pas comme references qualite
|
||||
- garder `stateful-mistral7b-instruct-int4-coreml` hors chemin critique tant qu'un besoin produit explicite ne justifie pas sa reintroduction
|
||||
|
||||
## Fichiers a regarder en premier
|
||||
|
||||
- `automation/state/next_lots_state.json`
|
||||
- `automation/reports/20260309T055457Z/SUMMARY.md`
|
||||
- `automation/reports/20260309T063512Z/SUMMARY.md`
|
||||
- `automation/reports/apple_rerun_preset_20260313T223555Z/brouillons/chapitres/chapitre_02/`
|
||||
- `automation/reports/20260309T055457Z/workspaces/ollama_qwen2_5_7b/brouillons/chapitres/chapitre_02/`
|
||||
- `automation/reports/20260309T063512Z/workspaces/apple_coreml_qwen2_5_0_5b_instruct_onnx/brouillons/chapitres/chapitre_02/`
|
||||
- `automation/reports/20260309T063512Z/workspaces/ollama_qwen2_5_1_5b/brouillons/chapitres/chapitre_02/`
|
||||
|
||||
## Commandes utiles
|
||||
|
||||
```bash
|
||||
python3 -m unittest discover -s tests -v
|
||||
python3 scripts/run_next_lots.py --lot tracking_sync --report-only
|
||||
python3 scripts/run_next_lots.py --lot priority_models
|
||||
```
|
||||
|
||||
## Prochaine hypothese de travail
|
||||
|
||||
Le prochain gain utile ne viendra pas d'un nouveau backend mais d'un meilleur controle de `rewrite` et `repair`:
|
||||
|
||||
- `ollama:qwen2.5:7b` doit perdre le residu planifie `outline_like`
|
||||
- les petites baselines doivent finir proprement sans `truncated_ending`
|
||||
- la reference Apple 4B est reconfirmee; la priorite live se deplace maintenant vers `ollama:qwen2.5:7b`
|
||||
- la priorite live n'est plus de rallumer `:8100` et `:8201`, mais de retablir un chemin Ollama CPU stable; le rerun Apple ne bascule plus par defaut vers Ollama pendant `repair`
|
||||
@@ -0,0 +1,57 @@
|
||||
# Memoire de reprise - 14 mars 2026
|
||||
|
||||
Memoire operationnelle pour reprendre `ai-novel-engine` sans refaire toute l'enquete runtime.
|
||||
|
||||
## Etat confirme
|
||||
|
||||
- la suite locale passe avec `48` tests
|
||||
- la reference locale reste `apple-coreml:qwen3.5-4b-onnx-q4f16`
|
||||
- `apple-coreml:qwen2.5-0.5b-instruct-onnx` reste `quality_blocked` sur un blocage de fin / garde-fou
|
||||
- `ollama:qwen2.5:7b` est maintenant a lire comme un blocage runtime, pas comme un simple blocage qualite
|
||||
- `ollama:qwen2.5:1.5b` est dans le meme etat runtime que le 7b
|
||||
|
||||
## Reports du 14 mars 2026 a retenir
|
||||
|
||||
- `automation/reports/20260314T085946Z`:
|
||||
- `ollama:qwen2.5:7b` -> `provider_failed`
|
||||
- `status=ollama_runtime_unhealthy`
|
||||
- note utile: `HTTP 500 Internal Server Error`
|
||||
- `automation/reports/20260314T100648Z`:
|
||||
- `ollama:qwen2.5:1.5b` -> `provider_failed`
|
||||
- meme symptome utile: `HTTP 500 Internal Server Error`
|
||||
|
||||
## Mesures live utiles
|
||||
|
||||
- Apple warm-up direct sur `:8100`: `2.76s`
|
||||
- Apple requete prose representative (`96` tokens): `39.99s`
|
||||
- `llama-server` temporaire sur le blob `qwen2.5:1.5b`: reponse en `0.31s`
|
||||
|
||||
## Ce que cela veut dire
|
||||
|
||||
- Apple n'est pas le sujet critique du jour
|
||||
- Ollama natif 0.17.7 reste le composant qui casse
|
||||
- les blobs GGUF `qwen2.5` presents dans `~/.ollama/models/blobs/` sont sains et reutilisables via `llama.cpp`
|
||||
|
||||
## Decision de reprise
|
||||
|
||||
- ne plus perdre du temps a tuner `rewrite` / `repair` tant que les reruns `qwen2.5` passent par un runtime instable
|
||||
- prioriser un contournement `llama-server` / `llama.cpp` avant tout nouveau lot comparatif
|
||||
- garder `apple-coreml:qwen3.5-4b-onnx-q4f16` comme reference stable
|
||||
|
||||
## Fichiers et chemins utiles
|
||||
|
||||
- blob `qwen2.5:1.5b`: `/Users/electron/.ollama/models/blobs/sha256-183715c435899236895da3869489cc30ac241476b4971a20285b1a462818a5b4`
|
||||
- blob `qwen2.5:7b`: `/Users/electron/.ollama/models/blobs/sha256-2bada8a7450677000f678be90653b85d364de7db25eb5ea54136ada5f3933730`
|
||||
- report `7b`: `automation/reports/20260314T085946Z/`
|
||||
- report `1.5b`: `automation/reports/20260314T100648Z/`
|
||||
- comparatif courant: `docs/MODEL_COMPARISON_2026-03-08.md`
|
||||
|
||||
## Prochaine action utile
|
||||
|
||||
Rendre le contournement `llama.cpp` reusable cote runtime, puis rejouer:
|
||||
|
||||
```bash
|
||||
python3 scripts/run_next_lots.py --lot priority_models
|
||||
python3 scripts/run_next_lots.py --lot baselines
|
||||
```
|
||||
|
||||
@@ -21,13 +21,14 @@ Comparatif realise avec le protocole courant:
|
||||
Contexte machine:
|
||||
|
||||
- `ai-novel-engine` pointe vers `mascarade` sur `http://127.0.0.1:8100`
|
||||
- `ollama` est route vers un service Docker CPU expose sur `127.0.0.1:11435`
|
||||
- le host `ollama` natif 0.17.7 reste bloque par un crash Metal sur cette machine
|
||||
- `ai-novel-engine` resynchronise maintenant les tags Ollama via `127.0.0.1:11434`
|
||||
- le host `ollama` natif 0.17.7 repond a `api/tags`, mais reste bloque en generation par un crash Metal sur cette machine
|
||||
- pour rejouer les lots comparatifs Ollama, il faut rebrancher un chemin CPU stable cote Mascarade
|
||||
- le runtime Apple local n'expose qu'un seul `model_id` a la fois sur `:8201`
|
||||
- dernier cycle complet termine au 9 mars 2026:
|
||||
- `apple-coreml:qwen3.5-4b-onnx-q4f16` est `accepted`
|
||||
- `ollama:qwen2.5:7b` atteint `gate`, exerce `repair` puis finit `quality_blocked`
|
||||
- le lot `baselines` est relance separement pour les petits modeles
|
||||
- le lot `baselines` s'est termine separement avec deux `quality_blocked` sur `truncated_ending`
|
||||
|
||||
## Resultats
|
||||
|
||||
@@ -35,14 +36,14 @@ Contexte machine:
|
||||
|---|---|---|---|---|---|---:|---|---|---|
|
||||
| `apple-coreml:qwen3.5-4b-onnx-q4f16` | `apple-coreml` | OK | oui | `accepted` | `memory` | `711s` | meilleure nuance narrative du lot | critique exploitable, gate vert | reference ANE locale actuelle |
|
||||
| `ollama:qwen2.5:7b` | `ollama` | OK | oui | `quality_blocked` | `gate` | `825s` | correcte, plus sobre que l'Apple 4B | critique exploitable, mais le texte reste trop proche d'un plan | meilleur candidat Ollama, encore bloque |
|
||||
| `apple-coreml:qwen2.5-0.5b-instruct-onnx` | `apple-coreml` | OK | rerun en cours | n/a | n/a | n/a | baseline vitesse a requalifier | n/a | en attente de verdict courant |
|
||||
| `ollama:qwen2.5:1.5b` | `ollama` | OK | rerun en cours | n/a | n/a | n/a | baseline vitesse a requalifier | n/a | en attente de verdict courant |
|
||||
| `apple-coreml:qwen2.5-0.5b-instruct-onnx` | `apple-coreml` | OK | oui | `quality_blocked` | `gate` | `801s` | baseline vitesse Apple encore trop fragile pour la reference | `repair` actif, mais fin tronquee | baseline regression utile, pas reference qualite |
|
||||
| `ollama:qwen2.5:1.5b` | `ollama` | OK | oui | `quality_blocked` | `gate` | `224s` | baseline vitesse correcte mais trop courte ou suspendue | `repair` actif, mais fin tronquee | temoin regression utile, pas candidat qualite |
|
||||
|
||||
Point legacy hors protocole courant:
|
||||
|
||||
| Modele | Backend | Preflight | Smoke complet | Statut final |
|
||||
|---|---|---|---|---|
|
||||
| `apple-coreml:stateful-mistral7b-instruct-int4-coreml` | `apple-coreml` | OK | bloque > `8 min` a `structure` | `preflight_only` |
|
||||
| `apple-coreml:stateful-mistral7b-instruct-int4-coreml` | `apple-coreml` | OK | bloque > `8 min` a `structure` | `archive hors chemin critique` |
|
||||
|
||||
## Lecture rapide
|
||||
|
||||
@@ -59,11 +60,11 @@ Point legacy hors protocole courant:
|
||||
- c'est le meilleur candidat Ollama actuel, mais il lui manque encore une prose plus continue
|
||||
|
||||
### `apple-coreml:qwen2.5-0.5b-instruct-onnx`
|
||||
- rerun baseline en cours via le lot `baselines`
|
||||
- reste utile comme candidat vitesse Apple, pas comme reference qualite tant qu'un verdict courant n'est pas resynchronise
|
||||
- atteint `gate`, exerce `repair`, puis reste bloque sur `truncated_ending`
|
||||
- reste utile comme candidat vitesse Apple, pas comme reference qualite
|
||||
|
||||
### `ollama:qwen2.5:1.5b`
|
||||
- rerun baseline en cours via le lot `baselines`
|
||||
- atteint `gate`, exerce `repair`, puis reste bloque sur `truncated_ending`
|
||||
- reste un temoin de regression plus qu'un candidat qualite
|
||||
|
||||
## Verdicts
|
||||
@@ -80,23 +81,24 @@ Le cycle `priority_models` atteint enfin un objectif produit minimal:
|
||||
|
||||
- la boucle `repair` est implementée, testee et visible dans `status` / `meta.json`
|
||||
- `repair` a maintenant une validation live sur `ollama:qwen2.5:7b`
|
||||
- un premier modele est `accepted` sous protocole courant: `apple-coreml:qwen3.5-4b-onnx-q4f16`
|
||||
- la reference Apple locale est reconfirmee sur deux runs comparables: `apple-coreml:qwen3.5-4b-onnx-q4f16`
|
||||
- le prochain enjeu n'est plus de trouver un premier succes, mais de finir les baselines et de sortir `ollama:qwen2.5:7b` de `outline_like`
|
||||
|
||||
Le prochain lot logique n'est plus "ajouter un garde-fou", mais:
|
||||
|
||||
1. finir le lot `baselines`
|
||||
2. confirmer `apple-coreml:qwen3.5-4b-onnx-q4f16` sur rerun
|
||||
1. garder `automation/reports/apple_rerun_preset_20260313T223555Z` comme rerun Apple de reference pour les comparaisons futures
|
||||
2. retablir un chemin Ollama CPU puis rejouer `priority_models` pour `ollama:qwen2.5:7b`
|
||||
3. regler `rewrite` et `repair` pour faire tomber `outline_like` sur `ollama:qwen2.5:7b`
|
||||
4. ne garder `qwen2.5-0.5b` et `qwen2.5:1.5b` que comme baselines vitesse
|
||||
4. ne garder `qwen2.5-0.5b` et `qwen2.5:1.5b` que comme baselines vitesse ou regression
|
||||
|
||||
## Auto-sync
|
||||
## Auto-sync
|
||||
<!-- AUTO-SYNC:ANE-COMPARISON:START -->
|
||||
- dernier cycle automatise: 2026-03-09T06:53:02+00:00
|
||||
- dernier cycle automatise: 2026-03-14T14:03:06+00:00
|
||||
|
||||
| Modele | Categorie | Preflight | Smoke | Classification | Failed stage | Gate | Repairs | Notes |
|
||||
|---|---|---|---|---|---|---|---:|---|
|
||||
| apple-coreml:qwen3.5-4b-onnx-q4f16 | priority_models | OK | oui | accepted | | oui | 0 | Smoke manuel comparable rejoue avec les budgets preset du manifeste. |
|
||||
| ollama:qwen2.5:7b | priority_models | KO | non | provider_failed | | non | 0 | Le preflight Ollama natif a échoué.; HTTP 500 Internal Server Error |
|
||||
| apple-coreml:qwen2.5-0.5b-instruct-onnx | baselines | OK | oui | quality_blocked | gate | oui | 2 | |
|
||||
| ollama:qwen2.5:1.5b | baselines | OK | oui | quality_blocked | gate | oui | 2 | |
|
||||
| ollama:qwen2.5:1.5b | baselines | KO | non | provider_failed | | non | 0 | Le preflight Ollama natif a échoué.; HTTP 500 Internal Server Error |
|
||||
<!-- AUTO-SYNC:ANE-COMPARISON:END -->
|
||||
|
||||
+18
-15
@@ -1,28 +1,31 @@
|
||||
# Roadmap v2
|
||||
|
||||
Roadmap courte et concrete, alignee sur l'etat reel du repo.
|
||||
Roadmap courte et concrete, alignee sur l'etat reel du repo au 13 mars 2026.
|
||||
|
||||
## Priorite 1 - Passer au moins un modele jusqu'a `gate`
|
||||
## Priorite 1 - Stabiliser la reprise
|
||||
|
||||
- compacter `rewrite` pour qu'au moins un modele atteigne `gate`
|
||||
- conserver la boucle `repair` et le garde-fou comme blocages durs
|
||||
- viser d'abord `apple-coreml:qwen3.5-4b-onnx-q4f16` et `ollama:qwen2.5:7b`
|
||||
- garder `automation/state/next_lots_state.json` sans checkpoint ambigu
|
||||
- garder `:8100` et `:8201` operationnels, puis retablir un vrai chemin Ollama CPU avant de rejouer `priority_models`
|
||||
- garder `apple-coreml:qwen3.5-4b-onnx-q4f16` comme reference provisoire tant qu'un rerun comparable ne la contredit pas
|
||||
- traiter le rerun `automation/reports/apple_rerun_7oY51o` comme une alerte de stabilite: la reference Apple n'est pas encore reconfirmee
|
||||
- ne plus laisser les docs perdre un resultat `accepted` lorsqu'un lot partiel plus recent tourne ensuite
|
||||
|
||||
## Priorite 2 - Requalifier les modeles plus lourds
|
||||
## Priorite 2 - Faire tomber les blockers reels
|
||||
|
||||
- garder `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` comme baselines vitesse
|
||||
- rejouer `qwen3.5:9b` seulement si `qwen2.5:7b` termine un smoke complet
|
||||
- maintenir les modeles toujours explicites dans les smokes et la doc
|
||||
- tenir compte du fait que le runtime Apple local ne sert qu'un `model_id` a la fois
|
||||
- sortir `ollama:qwen2.5:7b` de `outline_like`
|
||||
- faire disparaitre `truncated_ending` sur au moins une baseline
|
||||
- limiter les changements a `rewrite`, `repair` et leurs budgets tant qu'aucun autre blocker n'apparait
|
||||
|
||||
## Priorite 3 - Exploitation locale et docs
|
||||
## Priorite 3 - Resserer la matrice locale
|
||||
|
||||
- runbook local ANE centre sur `rewrite`, `gate_v1.json`, `repair_vN.md` et `quality_blocked`
|
||||
- runbook Apple local cote `mascarade` aligne sur les statuts reels
|
||||
- README et suivi croises pointent vers `EXECUTION_PLAN_2026-03-08.md`
|
||||
- garder `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` comme baselines vitesse ou regression
|
||||
- garder `stateful-mistral7b-instruct-int4-coreml` hors chemin critique tant qu'un besoin produit n'impose pas son retour
|
||||
- maintenir les modeles et le runtime Apple explicites dans chaque smoke et chaque doc
|
||||
|
||||
## Source de verite
|
||||
|
||||
- contexte courant: [`CONTEXTE_PROJET_2026-03-14.md`](./CONTEXTE_PROJET_2026-03-14.md)
|
||||
- memoire de reprise: [`MEMOIRE_REPRISE_2026-03-14.md`](./MEMOIRE_REPRISE_2026-03-14.md)
|
||||
- backlog actif: [`../TODO_ACTIVE.md`](../TODO_ACTIVE.md)
|
||||
- etat livre: [`../TODO_IMPLEMENTE.md`](../TODO_IMPLEMENTE.md)
|
||||
- ordre d'execution: [`EXECUTION_PLAN_2026-03-08.md`](./EXECUTION_PLAN_2026-03-08.md)
|
||||
- ordre d'execution: [`EXECUTION_PLAN_2026-03-14.md`](./EXECUTION_PLAN_2026-03-14.md)
|
||||
|
||||
@@ -42,8 +42,10 @@ Le driver:
|
||||
- dernier cycle complet termine au 9 mars 2026:
|
||||
- `apple-coreml:qwen3.5-4b-onnx-q4f16` est `accepted` de bout en bout sous garde-fou
|
||||
- `ollama:qwen2.5:7b` atteint `gate`, exerce `repair` en live, puis finit `quality_blocked` sur `outline_like`
|
||||
- `apple-coreml:stateful-mistral7b-instruct-int4-coreml` reste `preflight_only`
|
||||
- le lot `baselines` pour `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` est rejoue separement
|
||||
- `apple-coreml:stateful-mistral7b-instruct-int4-coreml` est retire du chemin critique et garde comme reference experimentale
|
||||
- rerun comparable du 13 mars 2026:
|
||||
- `automation/reports/apple_rerun_preset_20260313T223555Z` est `accepted` sans `repair` et reconfirme la reference Apple locale
|
||||
- le lot `baselines` pour `apple-coreml:qwen2.5-0.5b-instruct-onnx` et `ollama:qwen2.5:1.5b` a ete rejoue separement; les deux modeles restent `quality_blocked` sur `truncated_ending`
|
||||
- le runtime Apple local ne sert qu'un seul `model_id` a la fois
|
||||
- le fallback `repair` n'essaie plus de changer de modele `apple-coreml` en plein smoke; tout switch Apple reste une action runtime explicite
|
||||
|
||||
@@ -73,8 +75,8 @@ Notes:
|
||||
- `ANE_MAX_TOKENS_GATE` permet de regler le budget du garde-fou LLM
|
||||
- `ANE_MAX_TOKENS_REPAIR` et `ANE_REPAIR_MAX_PASSES` reglent la boucle `repair`
|
||||
- `apple-coreml:qwen2.5-0.5b-instruct-onnx` reste le candidat vitesse Apple a requalifier en baseline
|
||||
- `apple-coreml:qwen3.5-4b-onnx-q4f16` est la reference Apple locale actuelle
|
||||
- `apple-coreml:stateful-mistral7b-instruct-int4-coreml` reste preflight-only sur cette machine: il repond, mais le smoke ANE est reste bloque a `structure` pendant plus de 8 minutes
|
||||
- `apple-coreml:qwen3.5-4b-onnx-q4f16` est la reference Apple locale actuelle, reconfirmee par `apple_rerun_preset_20260313T223555Z`
|
||||
- `apple-coreml:stateful-mistral7b-instruct-int4-coreml` reste archive hors lot utile sur cette machine: il repond, mais le smoke ANE est reste bloque a `structure` pendant plus de 8 minutes
|
||||
|
||||
## Smoke Ollama
|
||||
|
||||
@@ -87,7 +89,7 @@ bash /Users/electron/mascarade/scripts/smoke_openai_compat_ane.sh \
|
||||
```
|
||||
|
||||
Le provider `ollama` doit apparaitre dans `providers` et le modele cible doit etre deja installe.
|
||||
Sur cette machine, le meilleur candidat Ollama courant est `ollama:qwen2.5:7b`; `qwen2.5:1.5b` reste une baseline a rerun.
|
||||
Sur cette machine, le meilleur candidat Ollama courant est `ollama:qwen2.5:7b`; `qwen2.5:1.5b` reste une baseline de regression, pas un candidat qualite.
|
||||
|
||||
Smoke chapitre complet:
|
||||
|
||||
@@ -131,12 +133,10 @@ Champs utiles:
|
||||
- `provider.base_url`
|
||||
- `provider.model`
|
||||
|
||||
## Etat auto-synchronise
|
||||
## Etat auto-synchronise
|
||||
<!-- AUTO-SYNC:ANE-RUNBOOK:START -->
|
||||
- dernier cycle automatise: 2026-03-09T06:53:02+00:00
|
||||
- dernier cycle automatise: 2026-03-14T14:03:06+00:00
|
||||
- chapitre courant detecte: chapitre_01
|
||||
- reference locale actuelle: aucun accepted, meilleur diagnostic: apple-coreml:qwen2.5-0.5b-instruct-onnx
|
||||
- prochain lot utile: Analyser les runs ayant atteint gate/repair puis resserrer la reference locale autour des meilleurs candidats.
|
||||
- reprise attendue apres action manuelle: /Users/electron/Documents/Projets_Creatifs/ai-novel-engine/automation/state/next_lots_state.json
|
||||
- reference locale actuelle: apple-coreml:qwen3.5-4b-onnx-q4f16
|
||||
- prochain lot utile: Reference locale reconfirmee; retablir le runtime des modeles provider_failed puis reprendre rewrite/repair sur les modeles bloques a gate.
|
||||
<!-- AUTO-SYNC:ANE-RUNBOOK:END -->
|
||||
|
||||
@@ -16,11 +16,12 @@ Consignes:
|
||||
- répondre uniquement avec le chapitre en Markdown
|
||||
- produire uniquement de la prose narrative continue, sous forme de paragraphes
|
||||
- ne jamais recopier la structure sous forme de plan
|
||||
- interdit: titres Markdown (`#`, `##`, `###`), listes a puces, numerotations, labels `objectif`, `conflit`, `sortie`, section `Tension`, section `Scènes`, code fences
|
||||
- interdit: titres Markdown (`#`, `##`, `###`), listes a puces, numerotations, labels `objectif`, `conflit`, `sortie`, section `Tension`, section `Scènes`, code fences, titre `Chapitre XX`
|
||||
- garder une voix cohérente
|
||||
- matérialiser la tension annoncée
|
||||
- ouvrir directement dans l'action ou dans la scene, sans preambule meta
|
||||
- transformer chaque beat de structure en action, perception, decision, consequence et, si utile, dialogue
|
||||
- ne pas commenter la structure ni annoncer les scenes; les jouer directement
|
||||
- finir sur une vraie phrase complete avec une ponctuation finale
|
||||
- viser un chapitre bref mais complet, d'au moins 3 paragraphes substantiels
|
||||
- ne pas ajouter d'explication hors texte narratif
|
||||
|
||||
@@ -4,6 +4,8 @@ Ne mets aucun texte avant ou après le JSON.
|
||||
Ne mets aucun bloc Markdown.
|
||||
Le but est de decider si ce texte peut etre promu dans le manuscrit.
|
||||
Bloque si le texte ressemble encore a un plan, s'il semble coupe avant sa fin, ou s'il manque une vraie continuite narrative.
|
||||
Ne bloque pas `outline_like` si le texte reste en prose continue, sans titres, sans puces et sans labels visibles, meme s'il est simple ou sobre.
|
||||
Utilise `outline_like` seulement s'il reste des traces visibles de plan, de checklist, de resume de scene, d'intitules structurants ou d'ecriture meta.
|
||||
Limite-toi a 1 phrase de resume, 4 blockers max et 4 recommandations max.
|
||||
|
||||
Chapitre cible: $chapter_slug
|
||||
|
||||
@@ -23,10 +23,14 @@ $draft_markdown
|
||||
Consignes impératives:
|
||||
- répondre uniquement avec la nouvelle version du chapitre en Markdown
|
||||
- produire uniquement de la prose narrative continue en paragraphes
|
||||
- ne garder aucun titre, aucune puce, aucune numérotation, aucun label de plan visible
|
||||
- ne garder aucun titre, aucune puce, aucune numérotation, aucun code fence, aucun titre `Chapitre XX`, aucun label de plan visible
|
||||
- supprimer completement les mots `objectif`, `conflit`, `sortie`, `Scène`, `scene`, `Tension` s'ils apparaissent comme labels ou sous-titres
|
||||
- transformer toute structure, note ou checklist en scene(s) jouee(s) avec actions, perceptions et consequences
|
||||
- conserver l'intention et les informations utiles deja presentes
|
||||
- allonger si besoin pour obtenir une scene complete et continue
|
||||
- montrer la scene au lieu de la resumer depuis l'exterieur
|
||||
- chaque paragraphe doit produire une action, une reaction ou une consequence concrete
|
||||
- finir le dernier paragraphe sur une decision nette suivie d'une consequence immediate
|
||||
$repair_focus
|
||||
- finir obligatoirement sur une vraie phrase complete avec une ponctuation finale
|
||||
- ne rien ajouter avant ou apres le chapitre
|
||||
|
||||
@@ -18,10 +18,11 @@ $critique_json
|
||||
Consignes de réécriture:
|
||||
- répondre uniquement avec la version réécrite du chapitre en Markdown
|
||||
- produire uniquement de la prose narrative continue, sous forme de paragraphes
|
||||
- supprimer completement tout titre, toute puce, toute numérotation et tout label de plan
|
||||
- supprimer completement tout titre, toute puce, toute numérotation, tout code fence et tout label de plan
|
||||
- si le brouillon ressemble a une structure ou a des notes, le convertir integralement en scene(s) racontee(s)
|
||||
- ne jamais garder les termes `objectif`, `conflit`, `sortie`, `Tension`, `Scène` comme titres ou labels visibles
|
||||
- ne jamais garder les termes `objectif`, `conflit`, `sortie`, `Tension`, `Scène`, `Chapitre` comme titres ou labels visibles
|
||||
- conserver l'intention, mais augmenter la continuite dramatique d'une scene a l'autre
|
||||
- materialiser les decisions et leurs consequences
|
||||
- ne pas resumer la scene depuis l'exterieur; montrer les gestes, les perceptions, les hesitations et les consequences dans le fil du texte
|
||||
- finir sur une vraie phrase complete avec une ponctuation finale
|
||||
- ne rien ajouter avant ou apres le chapitre
|
||||
|
||||
@@ -313,6 +313,44 @@ class GenerationPipelineTests(unittest.TestCase):
|
||||
self.assertEqual(meta["artifacts"]["repair_latest"], str(self.root / "brouillons" / "chapitres" / "chapitre_01" / "repair_v1.md"))
|
||||
self.assertEqual(meta["draft_final"], str(self.root / "brouillons" / "chapitres" / "chapitre_01" / "repair_v1.md"))
|
||||
|
||||
def test_rewrite_strips_code_fences_and_chapter_title_before_promotion(self):
|
||||
rewritten = f"```markdown\n# Chapitre 01\n\n{self._narrative_text().strip()}\n```"
|
||||
provider = MockGenerationProvider(
|
||||
{
|
||||
"structure": "# Structure — chapitre_01\n\n## Objectif dramatique\nPoser une menace.\n",
|
||||
"draft": "# Chapitre 01\n\nUn premier jet tendu.\n",
|
||||
"critique": {
|
||||
"summary": "Le brouillon doit etre reraconte proprement.",
|
||||
"rewrite_required": True,
|
||||
"deviations": ["Le texte garde un habillage markdown inutile."],
|
||||
"recommendations": ["Supprimer les marqueurs de presentation."],
|
||||
},
|
||||
"rewrite": rewritten,
|
||||
"gate": {
|
||||
"ready_for_manuscript": True,
|
||||
"summary": "Le chapitre peut etre promu.",
|
||||
"blockers": [],
|
||||
"recommendations": [],
|
||||
"heuristic_blockers": [],
|
||||
},
|
||||
"memory": {
|
||||
"summary": "Le chapitre garde sa tension sans habillage markdown.",
|
||||
"characters": [{"name": "Ariane", "description": "Traverse la scene sans filtre meta."}],
|
||||
"locations": [{"name": "Port-Vieux", "description": "Reste nocturne et menaçant."}],
|
||||
"timeline_events": [{"event": "Ariane avance sans retour en arriere.", "order_hint": "nuit"}],
|
||||
},
|
||||
}
|
||||
)
|
||||
pipeline = GenerationPipeline(self.root, provider=provider)
|
||||
|
||||
outcome = pipeline.generate_chapter("01", approval_callback=lambda _report, _path: True)
|
||||
|
||||
self.assertTrue(outcome.accepted)
|
||||
manuscript_text = outcome.manuscript_path.read_text(encoding="utf-8")
|
||||
self.assertNotIn("```", manuscript_text)
|
||||
self.assertNotIn("# Chapitre 01", manuscript_text)
|
||||
self.assertTrue(manuscript_text.startswith("Ariane longe le quai"))
|
||||
|
||||
def test_truncated_ending_triggers_repair_before_promotion(self):
|
||||
provider = MockGenerationProvider(
|
||||
{
|
||||
@@ -919,7 +957,7 @@ class ProviderConfigTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(
|
||||
pipeline._repair_model_for_attempt(apple_provider, 2),
|
||||
"ollama:qwen2.5:7b",
|
||||
"apple-coreml:qwen2.5-0.5b-instruct-onnx",
|
||||
)
|
||||
|
||||
def test_repair_fallback_override_env_wins(self):
|
||||
|
||||
+349
-1
@@ -13,6 +13,7 @@ from core.next_lots import (
|
||||
ModelRunResult,
|
||||
NextLotsRunner,
|
||||
RunState,
|
||||
_default_command_runner,
|
||||
replace_auto_section,
|
||||
)
|
||||
|
||||
@@ -49,7 +50,7 @@ class NextLotsTests(unittest.TestCase):
|
||||
f"mascarade_repo = \"{self.mascarade}\"\n"
|
||||
"core_base_url = \"http://127.0.0.1:8100\"\n"
|
||||
"apple_runtime_url = \"http://127.0.0.1:8201\"\n"
|
||||
"ollama_tags_url = \"http://127.0.0.1:11435/api/tags\"\n\n"
|
||||
"ollama_tags_url = \"http://127.0.0.1:11434/api/tags\"\n\n"
|
||||
"apple_model_ready_timeout_seconds = 0\n"
|
||||
"apple_model_poll_interval_seconds = 0.01\n\n"
|
||||
"[smoke]\n"
|
||||
@@ -97,6 +98,8 @@ class NextLotsTests(unittest.TestCase):
|
||||
self.assertEqual(manifest.tracking.mascarade_repo, self.mascarade)
|
||||
self.assertEqual(manifest.tracking.ane_todo_active, self.root / "TODO_ACTIVE.md")
|
||||
self.assertEqual(manifest.apple_model_ready_timeout_seconds, 0)
|
||||
self.assertEqual(manifest.ollama_runtime, "native")
|
||||
self.assertEqual(manifest.ollama_openai_base_url, "http://127.0.0.1:8100")
|
||||
|
||||
def test_replace_auto_section_only_replaces_managed_block(self) -> None:
|
||||
path = self.root / "TODO_ACTIVE.md"
|
||||
@@ -119,6 +122,33 @@ class NextLotsTests(unittest.TestCase):
|
||||
self.assertIn("- nouveau", rendered)
|
||||
self.assertNotIn("ancien", rendered)
|
||||
|
||||
def test_replace_auto_section_deduplicates_repeated_heading(self) -> None:
|
||||
path = self.root / "README.md"
|
||||
path.write_text(
|
||||
"## Etat auto-synchronise\n"
|
||||
"## Etat auto-synchronise\n"
|
||||
"<!-- AUTO-SYNC:ANE-README:START -->\n"
|
||||
"ancien\n"
|
||||
"<!-- AUTO-SYNC:ANE-README:END -->\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
replace_auto_section(path, "ANE-README", "## Etat auto-synchronise", "- propre")
|
||||
rendered = path.read_text(encoding="utf-8")
|
||||
|
||||
self.assertEqual(rendered.count("## Etat auto-synchronise\n"), 1)
|
||||
self.assertIn("- propre", rendered)
|
||||
|
||||
def test_default_command_runner_returns_timeout_result(self) -> None:
|
||||
result = _default_command_runner(
|
||||
["python3", "-c", "import time; time.sleep(1)"],
|
||||
self.root,
|
||||
timeout_seconds=0.1,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 124)
|
||||
self.assertIn("Timed out after 0.1s.", result.stderr)
|
||||
|
||||
def test_runner_creates_checkpoint_when_apple_model_differs(self) -> None:
|
||||
manifest = Manifest.load(self.root, self.manifest_path)
|
||||
prepare_calls: list[list[str]] = []
|
||||
@@ -228,6 +258,13 @@ class NextLotsTests(unittest.TestCase):
|
||||
command_runner=command_runner,
|
||||
json_fetcher=lambda url, timeout: {"status": "ok"} if url.endswith("/health") else ["qwen3.5-4b-onnx-q4f16"],
|
||||
)
|
||||
runner._run_ollama_native_preflight = lambda model: CommandResult( # type: ignore[method-assign]
|
||||
args=["ollama-native-preflight", model],
|
||||
returncode=0,
|
||||
stdout='{"content": "ollama native preflight ok"}',
|
||||
stderr="",
|
||||
duration_seconds=0.1,
|
||||
)
|
||||
report_dir = self.root / "automation" / "reports" / "test"
|
||||
report_dir.mkdir(parents=True, exist_ok=True)
|
||||
result = runner._run_model("ollama:qwen2.5:7b", category="priority_models", preflight_only=False, report_dir=report_dir)
|
||||
@@ -236,6 +273,99 @@ class NextLotsTests(unittest.TestCase):
|
||||
self.assertEqual(result.repair_attempts, 1)
|
||||
self.assertIn("gate", result.completed_stages)
|
||||
|
||||
def test_run_model_short_circuits_when_ollama_native_preflight_fails(self) -> None:
|
||||
manifest = Manifest.load(self.root, self.manifest_path)
|
||||
command_calls: list[list[str]] = []
|
||||
|
||||
def command_runner(args: list[str], cwd: Path, env=None) -> CommandResult:
|
||||
command_calls.append(args)
|
||||
raise AssertionError("OpenAI-compatible preflight should not run when native Ollama preflight fails")
|
||||
|
||||
runner = NextLotsRunner(
|
||||
manifest,
|
||||
command_runner=command_runner,
|
||||
json_fetcher=lambda url, timeout: {"status": "ok"} if url.endswith("/health") else ["qwen3.5-4b-onnx-q4f16"],
|
||||
)
|
||||
runner._run_ollama_native_preflight = lambda model: CommandResult( # type: ignore[method-assign]
|
||||
args=["ollama-native-preflight", model],
|
||||
returncode=1,
|
||||
stdout="",
|
||||
stderr="HTTP 500 Internal Server Error",
|
||||
duration_seconds=0.3,
|
||||
)
|
||||
|
||||
report_dir = self.root / "automation" / "reports" / "native_fail"
|
||||
report_dir.mkdir(parents=True, exist_ok=True)
|
||||
result = runner._run_model("ollama:qwen2.5:7b", category="priority_models", preflight_only=False, report_dir=report_dir)
|
||||
|
||||
self.assertEqual(command_calls, [])
|
||||
self.assertEqual(result.classification, "provider_failed")
|
||||
self.assertEqual(result.status, "ollama_runtime_unhealthy")
|
||||
self.assertFalse(result.preflight_ok)
|
||||
self.assertIn("Le preflight Ollama natif a échoué.", result.notes)
|
||||
self.assertIn("HTTP 500 Internal Server Error", result.notes)
|
||||
self.assertIsNotNone(result.preflight_log)
|
||||
self.assertTrue(Path(result.preflight_log).exists())
|
||||
|
||||
def test_run_model_can_use_openai_compatible_ollama_runtime(self) -> None:
|
||||
manifest = Manifest.load(self.root, self.manifest_path)
|
||||
manifest = Manifest(
|
||||
**{
|
||||
**manifest.__dict__,
|
||||
"ollama_runtime": "openai_compatible",
|
||||
"ollama_openai_base_url": "http://127.0.0.1:9100",
|
||||
}
|
||||
)
|
||||
chapter = ChapterId.parse("02")
|
||||
command_calls: list[list[str]] = []
|
||||
|
||||
def command_runner(args: list[str], cwd: Path, env=None) -> CommandResult:
|
||||
command_calls.append(args)
|
||||
if "smoke_openai_compat_ane.sh" in " ".join(args):
|
||||
self.assertEqual(args[args.index("--url") + 1], "http://127.0.0.1:9100")
|
||||
return CommandResult(args=args, returncode=0, stdout="ok", stderr="", duration_seconds=0.2)
|
||||
if "smoke_local_generation.sh" in " ".join(args):
|
||||
self.assertEqual(args[args.index("--base-url") + 1], "http://127.0.0.1:9100")
|
||||
workspace = Path(args[args.index("--workspace") + 1])
|
||||
meta_path = workspace / "brouillons" / "chapitres" / chapter.slug / "meta.json"
|
||||
meta_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
meta_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"status": "accepted",
|
||||
"accepted": True,
|
||||
"completed_stages": ["structure", "draft", "critique", "rewrite", "gate", "memory"],
|
||||
"artifacts": {
|
||||
"draft_v2": str(meta_path.parent / "draft_v2.md"),
|
||||
"gate_v1": str(meta_path.parent / "gate_v1.json"),
|
||||
"manuscript": str(workspace / "manuscrit" / chapter.filename),
|
||||
},
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return CommandResult(args=args, returncode=0, stdout="smoke ok", stderr="", duration_seconds=1.1)
|
||||
raise AssertionError(args)
|
||||
|
||||
runner = NextLotsRunner(
|
||||
manifest,
|
||||
command_runner=command_runner,
|
||||
json_fetcher=lambda url, timeout: {"status": "ok"} if url.endswith("/health") else ["qwen3.5-4b-onnx-q4f16"],
|
||||
)
|
||||
runner._run_ollama_native_preflight = lambda model: (_ for _ in ()).throw( # type: ignore[method-assign]
|
||||
AssertionError("Le preflight Ollama natif ne doit pas être appelé en mode openai_compatible")
|
||||
)
|
||||
|
||||
report_dir = self.root / "automation" / "reports" / "openai_runtime"
|
||||
report_dir.mkdir(parents=True, exist_ok=True)
|
||||
result = runner._run_model("ollama:qwen2.5:7b", category="priority_models", preflight_only=False, report_dir=report_dir)
|
||||
|
||||
self.assertEqual(result.classification, "accepted")
|
||||
self.assertEqual(len(command_calls), 2)
|
||||
|
||||
def test_tracking_sync_updates_docs_with_auto_sync_sections(self) -> None:
|
||||
manifest = Manifest.load(self.root, self.manifest_path)
|
||||
runner = NextLotsRunner(
|
||||
@@ -270,6 +400,224 @@ class NextLotsTests(unittest.TestCase):
|
||||
self.assertIn("Compacter rewrite", (self.root / "docs" / "EXECUTION_PLAN_2026-03-08.md").read_text(encoding="utf-8"))
|
||||
self.assertIn("ollama:qwen2.5:7b", (self.root / "docs" / "MODEL_COMPARISON_2026-03-08.md").read_text(encoding="utf-8"))
|
||||
|
||||
def test_tracking_sync_consolidates_latest_results_across_reports(self) -> None:
|
||||
manifest = Manifest.load(self.root, self.manifest_path)
|
||||
runner = NextLotsRunner(
|
||||
manifest,
|
||||
command_runner=lambda args, cwd, env=None: CommandResult(args=args, returncode=0, stdout="", stderr="", duration_seconds=0.0),
|
||||
json_fetcher=lambda url, timeout: {"status": "ok"},
|
||||
)
|
||||
previous_state = RunState.new(
|
||||
manifest,
|
||||
lot="priority_models",
|
||||
report_dir=self.root / "automation" / "reports" / "20260309T055457Z",
|
||||
state_path=self.root / "automation" / "state" / "next_lots_state.json",
|
||||
steps=[{"type": "models", "name": "priority_models", "models": manifest.priority_models, "preflight_only": False}],
|
||||
)
|
||||
previous_state.updated_at = "2026-03-09T06:20:33+00:00"
|
||||
previous_state.results = [
|
||||
asdict(
|
||||
ModelRunResult(
|
||||
model="apple-coreml:qwen3.5-4b-onnx-q4f16",
|
||||
category="priority_models",
|
||||
classification="accepted",
|
||||
preflight_ok=True,
|
||||
smoke_attempted=True,
|
||||
status="accepted",
|
||||
accepted=True,
|
||||
completed_stages=["structure", "draft", "critique", "rewrite", "gate", "memory"],
|
||||
)
|
||||
)
|
||||
]
|
||||
previous_report_dir = Path(previous_state.report_dir)
|
||||
previous_report_dir.mkdir(parents=True, exist_ok=True)
|
||||
(previous_report_dir / "run.json").write_text(json.dumps(previous_state.__dict__, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
current_state = RunState.new(
|
||||
manifest,
|
||||
lot="tracking_sync",
|
||||
report_dir=self.root / "automation" / "reports" / "20260309T063512Z",
|
||||
state_path=self.root / "automation" / "state" / "next_lots_state.json",
|
||||
steps=[{"type": "tracking_sync"}],
|
||||
)
|
||||
current_state.updated_at = "2026-03-09T06:53:02+00:00"
|
||||
current_state.results = [
|
||||
asdict(
|
||||
ModelRunResult(
|
||||
model="apple-coreml:qwen2.5-0.5b-instruct-onnx",
|
||||
category="baselines",
|
||||
classification="quality_blocked",
|
||||
preflight_ok=True,
|
||||
smoke_attempted=True,
|
||||
status="quality_blocked",
|
||||
failed_stage="gate",
|
||||
quality_blockers=["truncated_ending"],
|
||||
completed_stages=["structure", "draft", "critique", "rewrite", "gate", "repair"],
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
runner._sync_tracking(current_state, dry_run=False)
|
||||
|
||||
readme = (self.root / "README.md").read_text(encoding="utf-8")
|
||||
comparison = (self.root / "docs" / "MODEL_COMPARISON_2026-03-08.md").read_text(encoding="utf-8")
|
||||
todo_active = (self.root / "TODO_ACTIVE.md").read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn("reference locale actuelle: apple-coreml:qwen3.5-4b-onnx-q4f16", readme)
|
||||
self.assertIn("apple-coreml:qwen3.5-4b-onnx-q4f16", comparison)
|
||||
self.assertIn("apple-coreml:qwen2.5-0.5b-instruct-onnx", comparison)
|
||||
self.assertIn("Confirmer la reference accepted puis resserrer rewrite/repair", todo_active)
|
||||
|
||||
def test_tracking_sync_marks_reference_reconfirmed_after_two_accepted_runs(self) -> None:
|
||||
manifest = Manifest.load(self.root, self.manifest_path)
|
||||
runner = NextLotsRunner(
|
||||
manifest,
|
||||
command_runner=lambda args, cwd, env=None: CommandResult(args=args, returncode=0, stdout="", stderr="", duration_seconds=0.0),
|
||||
json_fetcher=lambda url, timeout: {"status": "ok"},
|
||||
)
|
||||
|
||||
first_state = RunState.new(
|
||||
manifest,
|
||||
lot="priority_models",
|
||||
report_dir=self.root / "automation" / "reports" / "20260309T055457Z",
|
||||
state_path=self.root / "automation" / "state" / "next_lots_state.json",
|
||||
steps=[{"type": "models", "name": "priority_models", "models": manifest.priority_models, "preflight_only": False}],
|
||||
)
|
||||
first_state.updated_at = "2026-03-09T06:20:33+00:00"
|
||||
first_state.results = [
|
||||
asdict(
|
||||
ModelRunResult(
|
||||
model="apple-coreml:qwen3.5-4b-onnx-q4f16",
|
||||
category="priority_models",
|
||||
classification="accepted",
|
||||
preflight_ok=True,
|
||||
smoke_attempted=True,
|
||||
status="accepted",
|
||||
accepted=True,
|
||||
completed_stages=["structure", "draft", "critique", "rewrite", "gate", "memory"],
|
||||
)
|
||||
)
|
||||
]
|
||||
first_report_dir = Path(first_state.report_dir)
|
||||
first_report_dir.mkdir(parents=True, exist_ok=True)
|
||||
(first_report_dir / "run.json").write_text(json.dumps(first_state.__dict__, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
second_state = RunState.new(
|
||||
manifest,
|
||||
lot="priority_models",
|
||||
report_dir=self.root / "automation" / "reports" / "20260313T225017Z",
|
||||
state_path=self.root / "automation" / "state" / "next_lots_state.json",
|
||||
steps=[{"type": "models", "name": "priority_models", "models": ["apple-coreml:qwen3.5-4b-onnx-q4f16"], "preflight_only": False}],
|
||||
)
|
||||
second_state.updated_at = "2026-03-13T22:50:17+00:00"
|
||||
second_state.results = [
|
||||
asdict(
|
||||
ModelRunResult(
|
||||
model="apple-coreml:qwen3.5-4b-onnx-q4f16",
|
||||
category="priority_models",
|
||||
classification="accepted",
|
||||
preflight_ok=True,
|
||||
smoke_attempted=True,
|
||||
status="accepted",
|
||||
accepted=True,
|
||||
completed_stages=["structure", "draft", "critique", "rewrite", "gate", "memory"],
|
||||
)
|
||||
)
|
||||
]
|
||||
second_report_dir = Path(second_state.report_dir)
|
||||
second_report_dir.mkdir(parents=True, exist_ok=True)
|
||||
(second_report_dir / "run.json").write_text(json.dumps(second_state.__dict__, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
current_state = RunState.new(
|
||||
manifest,
|
||||
lot="tracking_sync",
|
||||
report_dir=self.root / "automation" / "reports" / "20260313T230000Z",
|
||||
state_path=self.root / "automation" / "state" / "next_lots_state.json",
|
||||
steps=[{"type": "tracking_sync"}],
|
||||
)
|
||||
current_state.updated_at = "2026-03-13T23:00:00+00:00"
|
||||
current_state.results = [
|
||||
asdict(
|
||||
ModelRunResult(
|
||||
model="ollama:qwen2.5:7b",
|
||||
category="priority_models",
|
||||
classification="quality_blocked",
|
||||
preflight_ok=True,
|
||||
smoke_attempted=True,
|
||||
status="quality_blocked",
|
||||
failed_stage="gate",
|
||||
quality_blockers=["outline_like"],
|
||||
completed_stages=["structure", "draft", "critique", "rewrite", "gate", "repair"],
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
runner._sync_tracking(current_state, dry_run=False)
|
||||
|
||||
todo_active = (self.root / "TODO_ACTIVE.md").read_text(encoding="utf-8")
|
||||
self.assertIn("Reference locale reconfirmee; resserrer rewrite/repair", todo_active)
|
||||
|
||||
def test_tracking_sync_prioritizes_runtime_fix_when_reference_reconfirmed_but_provider_failed(self) -> None:
|
||||
manifest = Manifest.load(self.root, self.manifest_path)
|
||||
runner = NextLotsRunner(
|
||||
manifest,
|
||||
command_runner=lambda args, cwd, env=None: CommandResult(args=args, returncode=0, stdout="", stderr="", duration_seconds=0.0),
|
||||
json_fetcher=lambda url, timeout: {"status": "ok"},
|
||||
)
|
||||
|
||||
for stamp in ("20260309T055457Z", "20260313T225017Z"):
|
||||
accepted_state = RunState.new(
|
||||
manifest,
|
||||
lot="priority_models",
|
||||
report_dir=self.root / "automation" / "reports" / stamp,
|
||||
state_path=self.root / "automation" / "state" / "next_lots_state.json",
|
||||
steps=[{"type": "models", "name": "priority_models", "models": ["apple-coreml:qwen3.5-4b-onnx-q4f16"], "preflight_only": False}],
|
||||
)
|
||||
accepted_state.updated_at = "2026-03-13T22:50:17+00:00"
|
||||
accepted_state.results = [
|
||||
asdict(
|
||||
ModelRunResult(
|
||||
model="apple-coreml:qwen3.5-4b-onnx-q4f16",
|
||||
category="priority_models",
|
||||
classification="accepted",
|
||||
preflight_ok=True,
|
||||
smoke_attempted=True,
|
||||
status="accepted",
|
||||
accepted=True,
|
||||
completed_stages=["structure", "draft", "critique", "rewrite", "gate", "memory"],
|
||||
)
|
||||
)
|
||||
]
|
||||
report_dir = Path(accepted_state.report_dir)
|
||||
report_dir.mkdir(parents=True, exist_ok=True)
|
||||
(report_dir / "run.json").write_text(json.dumps(accepted_state.__dict__, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
current_state = RunState.new(
|
||||
manifest,
|
||||
lot="tracking_sync",
|
||||
report_dir=self.root / "automation" / "reports" / "20260314T000000Z",
|
||||
state_path=self.root / "automation" / "state" / "next_lots_state.json",
|
||||
steps=[{"type": "tracking_sync"}],
|
||||
)
|
||||
current_state.updated_at = "2026-03-14T00:00:00+00:00"
|
||||
current_state.results = [
|
||||
asdict(
|
||||
ModelRunResult(
|
||||
model="ollama:qwen2.5:7b",
|
||||
category="priority_models",
|
||||
classification="provider_failed",
|
||||
preflight_ok=False,
|
||||
status="ollama_runtime_unhealthy",
|
||||
notes=["Le preflight Ollama natif a échoué."],
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
runner._sync_tracking(current_state, dry_run=False)
|
||||
|
||||
todo_active = (self.root / "TODO_ACTIVE.md").read_text(encoding="utf-8")
|
||||
self.assertIn("retablir le runtime des modeles provider_failed", todo_active)
|
||||
|
||||
|
||||
def asdict(result: ModelRunResult) -> dict[str, object]:
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user