183 lines
7.3 KiB
Python
183 lines
7.3 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
# -*- coding: utf-8 -*-
|
||
|
|
"""
|
||
|
|
patch_facilitator_c1.py — Chantier C1 (Encoder structuré)
|
||
|
|
=========================================================
|
||
|
|
Patch strict de facilitator_v9.py :
|
||
|
|
P1. Config : ajoute ENCODER_MODE (.env, défaut 'agent' — rien ne change
|
||
|
|
tant que ENCODER_MODE=schema n'est pas posé).
|
||
|
|
P2. Insère run_encoder_schema() avant la section FREE DESIGNER
|
||
|
|
(même contrat de retour que run_encoder, fallback agent intégré).
|
||
|
|
P3. Dispatch dans run_full_pipeline_pass (pipeline + révisions).
|
||
|
|
P4. Dispatch dans le flux standard (fallback agent en mode express :
|
||
|
|
le plan Narrator n'y est pas annoté SLIDE N — layout).
|
||
|
|
|
||
|
|
Usage (dossier du pipeline, single-line) :
|
||
|
|
python3 patch_facilitator_c1.py
|
||
|
|
Vérifie chaque ancre (présence + unicité), écrit facilitator_v9.py.bak,
|
||
|
|
compile le résultat. Idempotent : refuse de patcher deux fois.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import py_compile
|
||
|
|
import shutil
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
TARGET = Path("facilitator_v9.py")
|
||
|
|
|
||
|
|
FUNC = '''
|
||
|
|
def run_encoder_schema(plan: str, layouts: dict, proj: "Project"):
|
||
|
|
"""Encoder structuré (chantier C1) : chat/completions + json_schema
|
||
|
|
strict Mistral, slide par slide. Même contrat de retour que
|
||
|
|
run_encoder : (yaml_str, data, path, attempts) — attempts = nb de
|
||
|
|
slides en échec. Fallback automatique sur l'Encoder agent si le
|
||
|
|
module manque ou si aucune slide n'est encodée."""
|
||
|
|
section("ÉTAPE 3 — THE ENCODER (structured outputs)")
|
||
|
|
try:
|
||
|
|
import encoder_schema as enc
|
||
|
|
except ImportError as e:
|
||
|
|
warn(f"encoder_schema.py indisponible ({e}) — bascule mode agent.")
|
||
|
|
return run_encoder(AgentSession(ENCODER_ID), plan, layouts, proj)
|
||
|
|
try:
|
||
|
|
import schemas as sch
|
||
|
|
for issue in (sch.verify_against_layouts(layouts) if layouts else []):
|
||
|
|
warn(f"Schéma vs layouts_v2 : {issue}")
|
||
|
|
except ImportError:
|
||
|
|
warn("schemas.py absent — vérification de cohérence sautée.")
|
||
|
|
info(f"Encodage slide par slide ({enc.DEFAULT_MODEL}, temp 0)...")
|
||
|
|
data, usage, errors = enc.encode_plan(plan, API_KEY, progress=info)
|
||
|
|
nb = len(data.get("slides", []))
|
||
|
|
ok(f"{nb} slides encodées — tokens : {usage.get('total_tokens', 0)} "
|
||
|
|
f"(prompt {usage.get('prompt_tokens', 0)} / "
|
||
|
|
f"completion {usage.get('completion_tokens', 0)})")
|
||
|
|
for e in errors:
|
||
|
|
warn(e)
|
||
|
|
if not nb:
|
||
|
|
warn("Aucune slide encodée — bascule mode agent.")
|
||
|
|
return run_encoder(AgentSession(ENCODER_ID), plan, layouts, proj)
|
||
|
|
yaml_str = enc.to_yaml(data)
|
||
|
|
is_valid, message, _ = validate_yaml(yaml_str, layouts)
|
||
|
|
if is_valid:
|
||
|
|
ok(f"YAML valide : {message}")
|
||
|
|
else:
|
||
|
|
warn(f"Validation : {message}")
|
||
|
|
if errors:
|
||
|
|
print("\\n [1] Continuer sans les slides en échec")
|
||
|
|
print(" [2] Basculer sur l'Encoder agent (deck complet)")
|
||
|
|
print(" [0] Abandonner")
|
||
|
|
choix = ask("Votre choix :")
|
||
|
|
if choix == "2":
|
||
|
|
return run_encoder(AgentSession(ENCODER_ID), plan, layouts, proj)
|
||
|
|
if choix == "0":
|
||
|
|
return None, None, None, len(errors)
|
||
|
|
path = save_text(yaml_str, proj.out("input", "yaml"))
|
||
|
|
return yaml_str, data, path, len(errors)
|
||
|
|
|
||
|
|
'''
|
||
|
|
|
||
|
|
PATCHES = [
|
||
|
|
# P1 — config
|
||
|
|
(
|
||
|
|
'ENCODER_ID = os.getenv("ENCODER_AGENT_ID")\n',
|
||
|
|
'ENCODER_ID = os.getenv("ENCODER_AGENT_ID")\n'
|
||
|
|
'ENCODER_MODE = os.getenv("ENCODER_MODE", "agent")'
|
||
|
|
' # agent | schema (C1)\n',
|
||
|
|
),
|
||
|
|
# P3 — dispatch pipeline pass (révisions comprises)
|
||
|
|
(
|
||
|
|
' encoder = AgentSession(ENCODER_ID)\n'
|
||
|
|
' yaml_str, yaml_data, yaml_path, tries = run_encoder(\n'
|
||
|
|
' encoder, plan, layouts, proj, scope_encoder)\n',
|
||
|
|
' if ENCODER_MODE == "schema":\n'
|
||
|
|
' yaml_str, yaml_data, yaml_path, tries = '
|
||
|
|
'run_encoder_schema(\n'
|
||
|
|
' plan, layouts, proj)\n'
|
||
|
|
' else:\n'
|
||
|
|
' encoder = AgentSession(ENCODER_ID)\n'
|
||
|
|
' yaml_str, yaml_data, yaml_path, tries = run_encoder(\n'
|
||
|
|
' encoder, plan, layouts, proj, scope_encoder)\n',
|
||
|
|
),
|
||
|
|
# P4 — dispatch flux standard (express → agent)
|
||
|
|
(
|
||
|
|
' encoder = AgentSession(ENCODER_ID)\n'
|
||
|
|
' yaml_str, yaml_data, yaml_path, tries = run_encoder(\n'
|
||
|
|
' encoder, plan, layouts, proj)\n'
|
||
|
|
' manifest.step("encoder", tentatives_correction=tries)\n'
|
||
|
|
' if yaml_str is None:\n'
|
||
|
|
' if express:\n',
|
||
|
|
' if ENCODER_MODE == "schema" and not express:\n'
|
||
|
|
' yaml_str, yaml_data, yaml_path, tries = '
|
||
|
|
'run_encoder_schema(\n'
|
||
|
|
' plan, layouts, proj)\n'
|
||
|
|
' else:\n'
|
||
|
|
' if ENCODER_MODE == "schema" and express:\n'
|
||
|
|
' info("Mode express → Encoder agent '
|
||
|
|
'(plan non annoté).")\n'
|
||
|
|
' encoder = AgentSession(ENCODER_ID)\n'
|
||
|
|
' yaml_str, yaml_data, yaml_path, tries = run_encoder(\n'
|
||
|
|
' encoder, plan, layouts, proj)\n'
|
||
|
|
' manifest.step("encoder", tentatives_correction=tries)\n'
|
||
|
|
' if yaml_str is None:\n'
|
||
|
|
' if express:\n',
|
||
|
|
),
|
||
|
|
]
|
||
|
|
|
||
|
|
MARKER = "run_encoder_schema"
|
||
|
|
ANCHOR_SECTION = "# FLUX LIBRE — THE FREE DESIGNER"
|
||
|
|
|
||
|
|
|
||
|
|
def fail(msg):
|
||
|
|
print(" ! %s" % msg)
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
if not TARGET.exists():
|
||
|
|
fail("%s introuvable — lancer depuis le dossier du pipeline."
|
||
|
|
% TARGET)
|
||
|
|
for dep in ("schemas.py", "encoder_schema.py"):
|
||
|
|
if not Path(dep).exists():
|
||
|
|
fail("%s manquant à côté du facilitator." % dep)
|
||
|
|
content = TARGET.read_text(encoding="utf-8")
|
||
|
|
if MARKER in content:
|
||
|
|
fail("Déjà patché (run_encoder_schema présent) — rien à faire.")
|
||
|
|
|
||
|
|
# Vérification de toutes les ancres AVANT toute écriture
|
||
|
|
for i, (old, _) in enumerate(PATCHES, 1):
|
||
|
|
n = content.count(old)
|
||
|
|
if n == 0:
|
||
|
|
fail("Ancre du patch %d introuvable — facilitator inattendu."
|
||
|
|
% i)
|
||
|
|
if n > 1:
|
||
|
|
fail("Ancre du patch %d non unique (%d occurrences)." % (i, n))
|
||
|
|
if content.count(ANCHOR_SECTION) != 1:
|
||
|
|
fail("Ancre de section FREE DESIGNER introuvable ou non unique.")
|
||
|
|
|
||
|
|
shutil.copy2(TARGET, TARGET.with_suffix(".py.bak"))
|
||
|
|
print(" + Sauvegarde : %s.bak" % TARGET)
|
||
|
|
|
||
|
|
for old, new in PATCHES:
|
||
|
|
content = content.replace(old, new)
|
||
|
|
|
||
|
|
# P2 — insertion de la fonction avant le séparateur de la section
|
||
|
|
lines = content.split("\n")
|
||
|
|
idx = next(i for i, l in enumerate(lines) if ANCHOR_SECTION in l)
|
||
|
|
ins = idx - 1 if lines[idx - 1].startswith("# ───") else idx
|
||
|
|
lines[ins:ins] = FUNC.split("\n")
|
||
|
|
content = "\n".join(lines)
|
||
|
|
|
||
|
|
TARGET.write_text(content, encoding="utf-8")
|
||
|
|
print(" + 4 patchs appliqués.")
|
||
|
|
try:
|
||
|
|
py_compile.compile(str(TARGET), doraise=True)
|
||
|
|
print(" + Compilation OK.")
|
||
|
|
except py_compile.PyCompileError as e:
|
||
|
|
shutil.copy2(TARGET.with_suffix(".py.bak"), TARGET)
|
||
|
|
fail("Erreur de compilation — fichier restauré :\n%s" % e)
|
||
|
|
print("\n Activer : ajouter ENCODER_MODE=schema dans .env")
|
||
|
|
print(" Retour arrière : ENCODER_MODE=agent (ou supprimer la ligne).")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|