Phase 6: train a second LoRA for real Penpot UI design capability #5
+251
-251
File diff suppressed because one or more lines are too long
+97
-97
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+843
-843
File diff suppressed because one or more lines are too long
@@ -93,12 +93,20 @@ PORTION_OF_GROUP = {
|
|||||||
"B5": "diseno", "B6": "diseno", "B7": "diseno", "B8": "diseno",
|
"B5": "diseno", "B6": "diseno", "B7": "diseno", "B8": "diseno",
|
||||||
"C": "api_preguntar",
|
"C": "api_preguntar",
|
||||||
"D": "api_recuperacion",
|
"D": "api_recuperacion",
|
||||||
|
# Brief ambiguo: el usuario da UN adjetivo y ninguna paleta, y el modelo tiene que elegirla,
|
||||||
|
# justificarla y declararla. Es el painpoint principal del usuario y la contramedida directa
|
||||||
|
# a la cuarta causa raiz (la instruccion del servidor de no elegir colores propios).
|
||||||
|
# Va como porcion PROPIA y no dentro de 'diseno' para garantizarle un piso: diluida entre los
|
||||||
|
# 76 seeds de diseno quedaria a merced de la proporcion, y esta clase es justamente la que no
|
||||||
|
# puede quedar corta.
|
||||||
|
"E": "diseno_brief_ambiguo",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Los targets son sobre la mezcla de 1000; las proporciones son las mismas que documenta el
|
# Los targets son sobre la mezcla de 1000; las proporciones son las mismas que documenta el
|
||||||
# docstring (33.3 / 6.7 / 4.4 / 2.2 y 53.3 de replay).
|
# docstring (33.3 / 6.7 / 4.4 / 2.2 y 53.3 de replay).
|
||||||
PORTION_TARGETS = {
|
PORTION_TARGETS = {
|
||||||
"diseno": 333,
|
"diseno": 253,
|
||||||
|
"diseno_brief_ambiguo": 80,
|
||||||
"api_forma_correcta": 67,
|
"api_forma_correcta": 67,
|
||||||
"api_recuperacion": 44,
|
"api_recuperacion": 44,
|
||||||
"api_preguntar": 23,
|
"api_preguntar": 23,
|
||||||
|
|||||||
@@ -75,6 +75,21 @@ DISJOINT_AGAINST = [
|
|||||||
]
|
]
|
||||||
SHINGLE_N = 6
|
SHINGLE_N = 6
|
||||||
|
|
||||||
|
# Rubros que usan los prompts de la puerta. Ningun seed puede compartirlos.
|
||||||
|
#
|
||||||
|
# Los shingles de 6-gramas NO atrapan esto: "necesito la home de una escuela de musica para
|
||||||
|
# chicos" (puerta) y "estoy redisenando el sitio de la escuela de musica" (seed) no comparten
|
||||||
|
# ningun 6-grama literal y sin embargo son el MISMO negocio. Entrenar sobre el dominio que
|
||||||
|
# despues se evalua infla el resultado sin que se note en ninguna metrica -- es la misma fuga
|
||||||
|
# que la de las palabras, un nivel mas arriba. Los sinonimos y las parafrasis del mismo rubro
|
||||||
|
# solo se atrapan nombrando el rubro.
|
||||||
|
RUBROS_DE_LA_PUERTA = [
|
||||||
|
"pizzeria", "pizzería",
|
||||||
|
"escuela de musica", "escuela de música",
|
||||||
|
"notaria", "notaría",
|
||||||
|
"trattoria",
|
||||||
|
]
|
||||||
|
|
||||||
# Reintento de transporte contra el MCP de Penpot. El servidor corta la conexion a mitad de
|
# Reintento de transporte contra el MCP de Penpot. El servidor corta la conexion a mitad de
|
||||||
# llamada de forma intermitente; ya se habia visto en la puerta 4 de la Fase 4.
|
# llamada de forma intermitente; ya se habia visto en la puerta 4 de la Fase 4.
|
||||||
MCP_MAX_RETRIES = int(os.environ.get("PENPOT_MCP_RETRIES", "4"))
|
MCP_MAX_RETRIES = int(os.environ.get("PENPOT_MCP_RETRIES", "4"))
|
||||||
@@ -481,7 +496,11 @@ if (seed) {
|
|||||||
return {
|
return {
|
||||||
pageId: page.id,
|
pageId: page.id,
|
||||||
pageName: page.name,
|
pageName: page.name,
|
||||||
rootId: penpot.root.id,
|
// NO se devuelve penpot.root.id. `penpot.root` es el root de la pagina ACTIVA, y despues de
|
||||||
|
// `createPage` + `openPage` puede no haberse actualizado todavia: en la primera corrida con
|
||||||
|
// borrado de paginas activado devolvio "" en el segundo prompt. Ademas todas las paginas
|
||||||
|
// comparten el mismo id de root, asi que ese valor nunca sirvio para identificar nada. El
|
||||||
|
// pageId es lo unico unico y estable, y es lo que usa la auditoria.
|
||||||
seedBoardId: seedBoardId
|
seedBoardId: seedBoardId
|
||||||
};
|
};
|
||||||
"""
|
"""
|
||||||
@@ -846,6 +865,39 @@ def corpus_shingles(paths):
|
|||||||
return acumulado
|
return acumulado
|
||||||
|
|
||||||
|
|
||||||
|
def check_rubros(prompts):
|
||||||
|
"""Ningun seed puede compartir el RUBRO de un prompt de la puerta. Ver RUBROS_DE_LA_PUERTA."""
|
||||||
|
import unicodedata as _ud
|
||||||
|
|
||||||
|
def plano(t):
|
||||||
|
t = _ud.normalize("NFKD", (t or "").lower())
|
||||||
|
return "".join(c for c in t if not _ud.combining(c))
|
||||||
|
|
||||||
|
rubros = sorted({plano(r) for r in RUBROS_DE_LA_PUERTA})
|
||||||
|
problemas = []
|
||||||
|
seeds_path = REPO_ROOT / "data" / "raw" / "seeds" / "penpot.jsonl"
|
||||||
|
if not seeds_path.exists():
|
||||||
|
print("[WARN] no hay corpus de seeds: se saltea el chequeo de rubros")
|
||||||
|
return
|
||||||
|
with open(seeds_path, encoding="utf-8") as f:
|
||||||
|
for lineno, linea in enumerate(f, start=1):
|
||||||
|
linea = linea.strip()
|
||||||
|
if not linea:
|
||||||
|
continue
|
||||||
|
texto = plano(linea)
|
||||||
|
for rubro in rubros:
|
||||||
|
if rubro in texto:
|
||||||
|
problemas.append(f"seed {lineno}: usa el rubro {rubro!r} de la puerta 5")
|
||||||
|
break
|
||||||
|
if problemas:
|
||||||
|
raise Gate5Error(
|
||||||
|
"hay seeds que comparten RUBRO con los prompts de la puerta 5 (fuga de dominio: los "
|
||||||
|
"shingles no la ven porque no hace falta compartir palabras para compartir negocio):"
|
||||||
|
"\n - " + "\n - ".join(problemas[:20])
|
||||||
|
)
|
||||||
|
print(f"[OK] ningun seed usa los rubros de la puerta ({', '.join(rubros)})")
|
||||||
|
|
||||||
|
|
||||||
def check_disjunto(prompts):
|
def check_disjunto(prompts):
|
||||||
corpus = corpus_shingles(DISJOINT_AGAINST)
|
corpus = corpus_shingles(DISJOINT_AGAINST)
|
||||||
if not corpus:
|
if not corpus:
|
||||||
@@ -1246,9 +1298,9 @@ def run_prompt(cfg, mcp, tools, system_prompt, fila, png_dir):
|
|||||||
print(f"\n--- {prompt_id} ({fila.get('dificultad', '?')}) -> pagina {page_name}")
|
print(f"\n--- {prompt_id} ({fila.get('dificultad', '?')}) -> pagina {page_name}")
|
||||||
setup = mcp.execute_json(build_setup_js(page_name, fila.get("seed_board")),
|
setup = mcp.execute_json(build_setup_js(page_name, fila.get("seed_board")),
|
||||||
f"{prompt_id}: setup de la pagina")
|
f"{prompt_id}: setup de la pagina")
|
||||||
root_id = setup.get("rootId")
|
page_id = setup.get("pageId")
|
||||||
if not root_id:
|
if not page_id:
|
||||||
raise Gate5Error(f"{prompt_id}: el setup no devolvio rootId: {setup}")
|
raise Gate5Error(f"{prompt_id}: el setup no devolvio pageId: {setup}")
|
||||||
|
|
||||||
contexto = [
|
contexto = [
|
||||||
f"Trabajás en la página «{page_name}», que ya está creada y abierta "
|
f"Trabajás en la página «{page_name}», que ya está creada y abierta "
|
||||||
@@ -1355,15 +1407,22 @@ def run_prompt(cfg, mcp, tools, system_prompt, fila, png_dir):
|
|||||||
|
|
||||||
# --- auditoria inyectada por la puerta ------------------------------------------------
|
# --- auditoria inyectada por la puerta ------------------------------------------------
|
||||||
try:
|
try:
|
||||||
audit = mcp.execute_json(build_audit_js(setup["pageId"]), f"{prompt_id}: auditoria")
|
audit = mcp.execute_json(build_audit_js(page_id), f"{prompt_id}: auditoria")
|
||||||
except Gate5Error as e:
|
except Gate5Error as e:
|
||||||
print(f"[WARN] {prompt_id}: la auditoria fallo ({e})")
|
print(f"[WARN] {prompt_id}: la auditoria fallo ({e})")
|
||||||
audit = {"auditError": str(e)}
|
audit = {"auditError": str(e)}
|
||||||
|
|
||||||
# --- PNG de la puerta (nunca reemplaza al export del modelo, se suma) -----------------
|
# --- PNG de la puerta (nunca reemplaza al export del modelo, se suma) -----------------
|
||||||
png_path = None
|
png_path = None
|
||||||
shape_a_exportar = audit.get("mainBoardId") or root_id
|
# Sin mainBoardId no hay nada util que exportar: el root de la pagina no se puede exportar
|
||||||
|
# por id (todas las paginas comparten el mismo), asi que se omite el PNG en vez de exportar
|
||||||
|
# el arbol equivocado.
|
||||||
|
shape_a_exportar = audit.get("mainBoardId")
|
||||||
|
if not shape_a_exportar:
|
||||||
|
print(f"[WARN] {prompt_id}: la auditoria no encontro un board principal; no hay PNG")
|
||||||
try:
|
try:
|
||||||
|
if not shape_a_exportar:
|
||||||
|
raise Gate5Error("sin board principal que exportar")
|
||||||
_texto, binarios, _ = mcp.call_tool(
|
_texto, binarios, _ = mcp.call_tool(
|
||||||
"export_shape", {"shapeId": shape_a_exportar, "format": "png", "mode": "shape"})
|
"export_shape", {"shapeId": shape_a_exportar, "format": "png", "mode": "shape"})
|
||||||
if binarios:
|
if binarios:
|
||||||
@@ -1385,7 +1444,7 @@ def run_prompt(cfg, mcp, tools, system_prompt, fila, png_dir):
|
|||||||
# Ademas le deja el archivo limpio al usuario.
|
# Ademas le deja el archivo limpio al usuario.
|
||||||
if LIMPIAR_PAGINAS and png_path is not None:
|
if LIMPIAR_PAGINAS and png_path is not None:
|
||||||
try:
|
try:
|
||||||
info = mcp.execute_json(build_delete_page_js(setup["pageId"]),
|
info = mcp.execute_json(build_delete_page_js(page_id),
|
||||||
f"{prompt_id}: limpieza de la pagina")
|
f"{prompt_id}: limpieza de la pagina")
|
||||||
if info.get("borrada"):
|
if info.get("borrada"):
|
||||||
print(f"[CLEAN] {info['nombre']}: {info['shapesBorrados']} shape(s) borrados "
|
print(f"[CLEAN] {info['nombre']}: {info['shapesBorrados']} shape(s) borrados "
|
||||||
@@ -1422,7 +1481,7 @@ def run_prompt(cfg, mcp, tools, system_prompt, fila, png_dir):
|
|||||||
"prompt": fila["prompt"],
|
"prompt": fila["prompt"],
|
||||||
"pageId": setup.get("pageId"),
|
"pageId": setup.get("pageId"),
|
||||||
"pageName": page_name,
|
"pageName": page_name,
|
||||||
"rootId": root_id,
|
"pageId_auditado": page_id,
|
||||||
"seedBoardId": setup.get("seedBoardId"),
|
"seedBoardId": setup.get("seedBoardId"),
|
||||||
"turnos_usados": len(turnos),
|
"turnos_usados": len(turnos),
|
||||||
"finish_reason": finish_reason,
|
"finish_reason": finish_reason,
|
||||||
@@ -1643,6 +1702,7 @@ def run_full(cfg, tools, system_prompt):
|
|||||||
prompts = load_prompts()
|
prompts = load_prompts()
|
||||||
todos_los_prompts = list(prompts)
|
todos_los_prompts = list(prompts)
|
||||||
check_disjunto(prompts)
|
check_disjunto(prompts)
|
||||||
|
check_rubros(prompts)
|
||||||
|
|
||||||
# Re-medir un subconjunto sin quemar los 10. Existe porque el MCP de Penpot se cae: cuando
|
# Re-medir un subconjunto sin quemar los 10. Existe porque el MCP de Penpot se cae: cuando
|
||||||
# una corrida pierde 5 prompts por un corte de transporte, repetir los 10 desperdicia media
|
# una corrida pierde 5 prompts por un corte de transporte, repetir los 10 desperdicia media
|
||||||
@@ -1772,11 +1832,27 @@ def run_full(cfg, tools, system_prompt):
|
|||||||
sin_medir = sorted(set(pendientes) |
|
sin_medir = sorted(set(pendientes) |
|
||||||
{r["id"] for r in fusionar(resultados) if r.get("score") is None})
|
{r["id"] for r in fusionar(resultados) if r.get("score") is None})
|
||||||
if sin_medir:
|
if sin_medir:
|
||||||
print(f"\n[PLUGIN DEGRADADO] {len(sin_medir)} prompt(s) no se pudieron medir. El plugin "
|
# NO culpar al plugin por defecto. Un mensaje que manda al usuario a recargar el navegador
|
||||||
f"de Penpot responde pero se degrada tras 5 o 6 prompts pesados.")
|
# cuando la causa es un bug de la puerta cuesta tiempo de las dos partes, y ya paso una
|
||||||
print(f" Pedile al usuario que RECARGUE el plugin en el navegador y retoma con:")
|
# vez: el setup fallo por depender de `penpot.root` y el script igual imprimio "plugin
|
||||||
|
# degradado". Se clasifica por la evidencia del error, no por el hecho de que falte algo.
|
||||||
|
errores = " ".join(str(r.get("error", "")) for r in fusionar(resultados)
|
||||||
|
if r.get("score") is None)
|
||||||
|
parece_plugin = bool(re.search(
|
||||||
|
r"timed out after \d+ seconds|No Penpot plugin instances|ConnectionError|"
|
||||||
|
r"error de transporte", errores, re.I))
|
||||||
|
print(f"\n[INCOMPLETO] {len(sin_medir)} prompt(s) no se pudieron medir.")
|
||||||
|
if parece_plugin:
|
||||||
|
print(" Causa probable: el plugin de Penpot. Los errores son timeouts de tarea o "
|
||||||
|
"cortes de transporte.")
|
||||||
|
print(" Pedile al usuario que RECARGUE el plugin en el navegador y retoma con:")
|
||||||
|
else:
|
||||||
|
print(" Causa probable: un bug de la PUERTA, no del plugin -- los errores no son "
|
||||||
|
"timeouts ni cortes de transporte. Revisar el mensaje de error antes de "
|
||||||
|
"pedirle nada al usuario.")
|
||||||
|
print(" Cuando este arreglado, retoma con:")
|
||||||
print(f" GATE5_ONLY={','.join(sin_medir)}")
|
print(f" GATE5_ONLY={','.join(sin_medir)}")
|
||||||
return EXIT_PLUGIN_DEGRADADO
|
return EXIT_PLUGIN_DEGRADADO if parece_plugin else 1
|
||||||
print(f" resultados en {salida.relative_to(REPO_ROOT)}")
|
print(f" resultados en {salida.relative_to(REPO_ROOT)}")
|
||||||
print(f" PNGs en {png_dir.relative_to(REPO_ROOT)}/ (la pagina de cada prompt se vacia tras "
|
print(f" PNGs en {png_dir.relative_to(REPO_ROOT)}/ (la pagina de cada prompt se vacia tras "
|
||||||
f"exportar su PNG; solo se conserva la de un prompt cuyo PNG haya fallado)")
|
f"exportar su PNG; solo se conserva la de un prompt cuyo PNG haya fallado)")
|
||||||
|
|||||||
Reference in New Issue
Block a user