Phase 6.3: fix the augmentation, close the holdout leak, stop rewarding invented parameters
Dataset build (05, 06): perturb_value is gone. It rewrote only tool_calls.arguments and left the tool results and the final answer saying something else, which is how data/train.jsonl ended up with 30 self-contradictory examples where the call says issue_number 82 and the answer says issue #77. Variation now comes from hand-written meta.paraphrases, or from meta.variation applied atomically across every field of the example at once. Nothing is substituted unless the seed declares it: guessing which number in a string is safe to change is what produced the contradictions in the first place. Prefix injection survives only as a fallback and only where the verb form can actually be conjugated, and there is a hard assert that no user turn matches the broken "Necesito que ¿Podés..." shape that 68 v1 prompts had. The penpot bucket is exempt from substitution entirely, since its payloads are code. Also asserts the bucket cannot collapse (verified: the old seeds give 320 rows from 83 unique trajectories and the build now fails) and scans for forbidden API patterns by importing them from the linter, so there is one source of truth. 06 now actually exits 1 on over-length rows. It printed [FILTERED], incremented a counter, and left the row in the file, which 10_train.py then trained on since it has no max_seq_length and batch 1. Gate 2 (32): reject any argument key absent from the schema, as its own failure category. It only checked required fields, so an invented scale or filePath passed - the gate was actively rewarding the exact behaviour this phase removes. Verified: export_shape with scale=2 now fails as unknown_argument, while a valid call still passes. Holdout (31, 35): rebalanced to penpot 60 / 35 each, added 20 real design templates, and replaced the full-string equality check with 6-gram shingles. Measured: a light paraphrase of a train.jsonl prompt scores 43% overlap and now fails the build, where the old check let it through at "not equal". Value pools are asserted disjoint from the corpus. The "2x resolution" template stays, relabelled as an invented-argument probe now that gate 2 can detect one; the createBoolean template stays because the API is real and the new B2 seeds teach it. Also dedupes: the old holdout had 15 duplicate prompts out of 200, i.e. 15 wasted measurements. Note: rebalancing the holdout means the 192/200 gate 2 baseline from phase 5 no longer applies to it, so that baseline has to be re-measured against production on the new file before it can be compared to. Gate 3 (33): 11 content checklists for the non-obvious conventions of the other MCPs - GFM table separators in Docmost, the update_page staleness retry, commit message shape, never merging the PR, dict-not-XML tool arguments. That is the most likely regression no gate currently covers. Mix builder: added the anti-collapse guard, so 420 new-portion rows that are really 96 trajectories repeated cannot pass unnoticed.
This commit is contained in:
+371
-110
@@ -1,12 +1,37 @@
|
||||
"""Fase 4 -- genera data/holdout_prompts.jsonl: ~200 prompts held-out para la Puerta 2
|
||||
(validez de tool-calls), cubriendo los 5 MCPs, sin overlap con train.jsonl/eval.jsonl.
|
||||
"""Fase 4 (rebalanceado en Fase 6) -- genera data/holdout_prompts.jsonl: ~200 prompts
|
||||
held-out para la Puerta 2 (validez de tool-calls), cubriendo los 5 MCPs, sin overlap con
|
||||
train.jsonl/eval.jsonl.
|
||||
|
||||
Cada linea: {"prompt": "...", "mcp": "penpot|gitea|github-personal|docmost|atlassian",
|
||||
"tools": [...schema real del MCP...]}. Corre localmente, no requiere GPU.
|
||||
"expect": "...", "tools": [...schema real del MCP...]}. Corre localmente, no requiere GPU.
|
||||
|
||||
Variacion deterministica (random.seed(43), semilla distinta de la de 05_build_dataset.py
|
||||
para no reusar la misma secuencia) sobre plantillas por MCP -- nunca copia textual de un
|
||||
ejemplo de train/eval (se verifica al final contra el texto normalizado de ambos archivos).
|
||||
para no reusar la misma secuencia) sobre plantillas por MCP.
|
||||
|
||||
QUE CAMBIO EN LA FASE 6
|
||||
-----------------------
|
||||
1. **Rebalanceo a penpot=60 / 35 por cada uno de los otros cuatro MCPs.** La Fase 6 solo
|
||||
agrega capacidad de diseno en Penpot: ahi es donde hace falta resolucion de medicion.
|
||||
2. **PENPOT_DESIGN_TEMPLATES**: prompts de diseno real (landing, card, navbar, dashboard,
|
||||
pantalla mobile, precios, galeria, formulario...), con dominios, medidas y estilos
|
||||
variados. Son la materia prima del holdout de la puerta 5.
|
||||
3. **Chequeo de shingles de 6-gramas** contra train.jsonl/eval.jsonl. Antes solo se
|
||||
comparaba igualdad de string completo, que cualquier parafrasis esquiva: el holdout
|
||||
podia estar contaminado sin que nada lo notara. Ahora un solapamiento de shingles por
|
||||
encima de MAX_SHINGLE_OVERLAP hace fallar la build.
|
||||
4. **Pools de valores disjuntos**: los dominios, colores, medidas, repos y nombres que usa
|
||||
el holdout no aparecen en los seeds ni en train/eval, y se asierta.
|
||||
5. **Campo `expect`** por prompt, para que las puertas puedan clasificar. En particular:
|
||||
- El template de "exportar a 2x de resolucion" NO estaba roto: es una **sonda de
|
||||
invencion de parametros** (`scale` no existe en el schema de `export_shape`). Se queda,
|
||||
reetiquetado como `no-invented-args`, y ahora la puerta 2 efectivamente lo detecta
|
||||
(ver el cambio de `validate_tool_call` en 32_gate2_toolcalls.py).
|
||||
- El template de `createBoolean` tampoco estaba roto: `createBoolean(boolType, shapes)`
|
||||
es API real y verificada (ver data/schemas/PENPOT_API_VERIFIED.md). El bug era testear
|
||||
algo que ningun seed ensenaba; los seeds del grupo B2 de la Fase 6 lo ensenan.
|
||||
|
||||
Las funciones de shingles se exportan a proposito: scripts/35_build_penpot_design_holdout.py
|
||||
las importa para usar EXACTAMENTE el mismo criterio de contaminacion.
|
||||
"""
|
||||
import json
|
||||
import random
|
||||
@@ -14,6 +39,7 @@ from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
SCHEMAS_DIR = REPO_ROOT / "data" / "schemas"
|
||||
SEEDS_DIR = REPO_ROOT / "data" / "raw" / "seeds"
|
||||
TRAIN_PATH = REPO_ROOT / "data" / "train.jsonl"
|
||||
EVAL_PATH = REPO_ROOT / "data" / "eval.jsonl"
|
||||
OUT_PATH = REPO_ROOT / "data" / "holdout_prompts.jsonl"
|
||||
@@ -21,102 +47,55 @@ OUT_PATH = REPO_ROOT / "data" / "holdout_prompts.jsonl"
|
||||
SEED = 43
|
||||
TARGET_TOTAL = 200
|
||||
|
||||
# Fase 6: el grueso de la medicion se mueve a penpot, que es la unica capacidad que el
|
||||
# LoRA #2 toca. 60 + 4*35 = 200, el mismo total que en las Fases 4-5.
|
||||
MCP_TARGETS = {
|
||||
"penpot": 40,
|
||||
"gitea": 40,
|
||||
"github-personal": 40,
|
||||
"docmost": 40,
|
||||
"atlassian": 40,
|
||||
"penpot": 60,
|
||||
"gitea": 35,
|
||||
"github-personal": 35,
|
||||
"docmost": 35,
|
||||
"atlassian": 35,
|
||||
}
|
||||
|
||||
PENPOT_TEMPLATES = [
|
||||
"Crea un rectangulo de {w}x{h} en el board '{board}' con color {color}.",
|
||||
"Necesito un texto que diga '{text}' dentro del board '{board}', alineado a la izquierda.",
|
||||
"Cambia el fill del shape '{shape}' a {color} y verifica que se aplico.",
|
||||
"Agrega un board nuevo llamado '{board}' de {w}x{h} px en la pagina actual.",
|
||||
"Exporta el shape '{shape}' como PNG a 2x de resolucion.",
|
||||
"Lista los shapes del board '{board}' y decime cuales son grupos.",
|
||||
"Mueve el shape '{shape}' a la posicion x={w}, y={h} dentro de '{board}'.",
|
||||
"Crea un boolean de union entre '{shape}' y otro rectangulo superpuesto.",
|
||||
]
|
||||
# --- deteccion de contaminacion ----------------------------------------------------
|
||||
|
||||
GITEA_TEMPLATES = [
|
||||
"Crea una rama llamada '{branch}' en el repo '{repo}' desde main.",
|
||||
"Abri un issue en '{repo}' titulado '{text}' con la label 'bug'.",
|
||||
"Lista los pull requests abiertos del repo '{repo}'.",
|
||||
"Crea un release '{branch}' en '{repo}' con las notas '{text}'.",
|
||||
"Busca commits recientes en '{repo}' que mencionen '{text}'.",
|
||||
"Agrega un comentario '{text}' al issue numero {num} de '{repo}'.",
|
||||
"Revisa el estado de los actions/workflows del repo '{repo}'.",
|
||||
"Mergea (solo si el usuario lo pide explicitamente) el PR numero {num} de '{repo}'.",
|
||||
]
|
||||
|
||||
GITHUB_TEMPLATES = [
|
||||
"Crea un pull request en '{repo}' desde la rama '{branch}' hacia main, titulo '{text}'.",
|
||||
"Lista los issues abiertos de '{repo}' con label 'enhancement'.",
|
||||
"Agrega un comentario '{text}' al PR numero {num} de '{repo}'.",
|
||||
"Busca en '{repo}' el codigo que define la funcion '{text}'.",
|
||||
"Crea un issue en '{repo}' titulado '{text}' asignado a mi usuario.",
|
||||
"Revisa los commits recientes de la rama '{branch}' en '{repo}'.",
|
||||
"Lista los releases publicados de '{repo}'.",
|
||||
"Solicita una review de Copilot para el PR numero {num} de '{repo}'.",
|
||||
]
|
||||
|
||||
DOCMOST_TEMPLATES = [
|
||||
"Crea una pagina llamada '{text}' en el space '{repo}' con una tabla de {num} columnas.",
|
||||
"Busca en Docmost paginas que mencionen '{text}'.",
|
||||
"Actualiza la pagina '{text}' agregando una fila mas a la tabla existente.",
|
||||
"Comenta '{text}' en la pagina con id conocido del space '{repo}'.",
|
||||
"Lista las paginas del space '{repo}' ordenadas por actualizacion reciente.",
|
||||
"Crea una subpagina '{text}' bajo la pagina principal del space '{repo}'.",
|
||||
"Revisa si hay comentarios nuevos en el space '{repo}' desde ayer.",
|
||||
"Mueve la pagina '{text}' a otro parent dentro del space '{repo}'.",
|
||||
]
|
||||
|
||||
ATLASSIAN_TEMPLATES = [
|
||||
"Crea un issue de Jira en el proyecto '{repo}' titulado '{text}', tipo Bug.",
|
||||
"Busca issues de Jira asignados a mi usuario con JQL en el proyecto '{repo}'.",
|
||||
"Agrega un comentario '{text}' al issue {repo}-{num} de Jira.",
|
||||
"Transiciona el issue {repo}-{num} a 'In Progress'.",
|
||||
"Crea una pagina de Confluence '{text}' en el espacio '{repo}'.",
|
||||
"Busca en Confluence paginas del espacio '{repo}' que mencionen '{text}'.",
|
||||
"Agrega un worklog de 1h al issue {repo}-{num} con el comentario '{text}'.",
|
||||
"Lista los tipos de issue disponibles en el proyecto '{repo}' de Jira.",
|
||||
]
|
||||
|
||||
MCP_TEMPLATES = {
|
||||
"penpot": PENPOT_TEMPLATES,
|
||||
"gitea": GITEA_TEMPLATES,
|
||||
"github-personal": GITHUB_TEMPLATES,
|
||||
"docmost": DOCMOST_TEMPLATES,
|
||||
"atlassian": ATLASSIAN_TEMPLATES,
|
||||
}
|
||||
|
||||
WORDS = [
|
||||
"dashboard", "login", "checkout", "onboarding", "reporte semanal", "bugfix urgente",
|
||||
"modal de confirmacion", "flujo de pago", "endpoint de usuarios", "cache de sesion",
|
||||
"migracion de datos", "notificaciones push", "tema oscuro", "landing page",
|
||||
"formulario de contacto", "panel de admin", "integracion con Stripe", "tabla de precios",
|
||||
"footer del sitio", "header responsive",
|
||||
]
|
||||
REPOS = ["backend-core", "frontend-app", "infra-tools", "data-pipeline", "mobile-client"]
|
||||
BRANCHES = ["feature/nueva-vista", "fix/timeout-api", "chore/deps", "hotfix/prod", "feature/dark-mode"]
|
||||
BOARDS = ["Dashboard Principal", "Mobile Screens", "Design System", "Landing v2", "Checkout Flow"]
|
||||
SHAPES = ["boton-cta", "card-producto", "icono-menu", "titulo-principal", "footer-logo"]
|
||||
COLORS = ["#1a73e8", "#e8710a", "#188038", "#d93025", "#9334e6", "#12b5cb"]
|
||||
|
||||
|
||||
def load_tools(mcp_name):
|
||||
return json.loads((SCHEMAS_DIR / f"{mcp_name}.json").read_text(encoding="utf-8"))
|
||||
SHINGLE_N = 6
|
||||
# Fraccion maxima de shingles de 6-gramas de un prompt held-out que puede aparecer en
|
||||
# train/eval. Un prompt genuinamente nuevo comparte a lo sumo giros sueltos ("en el repo
|
||||
# de", "de la pagina con"); una parafrasis de un ejemplo de training comparte la mayoria.
|
||||
MAX_SHINGLE_OVERLAP = 0.34
|
||||
|
||||
|
||||
def normalize(text):
|
||||
return " ".join(text.lower().split())
|
||||
|
||||
|
||||
def load_existing_texts():
|
||||
texts = set()
|
||||
for path in (TRAIN_PATH, EVAL_PATH):
|
||||
def shingles(text, n=SHINGLE_N):
|
||||
"""Conjunto de n-gramas de palabras del texto normalizado.
|
||||
|
||||
Si el texto tiene menos de n palabras se devuelve el texto entero como unico shingle:
|
||||
para prompts muy cortos la igualdad exacta es el unico criterio sensato.
|
||||
"""
|
||||
tokens = normalize(text).split()
|
||||
if not tokens:
|
||||
return set()
|
||||
if len(tokens) < n:
|
||||
return {" ".join(tokens)}
|
||||
return {" ".join(tokens[i:i + n]) for i in range(len(tokens) - n + 1)}
|
||||
|
||||
|
||||
def shingle_overlap(text, reference_shingles, n=SHINGLE_N):
|
||||
"""Fraccion de shingles del texto que ya existen en el corpus de referencia."""
|
||||
own = shingles(text, n)
|
||||
if not own:
|
||||
return 0.0
|
||||
return len(own & reference_shingles) / len(own)
|
||||
|
||||
|
||||
def iter_user_texts(paths):
|
||||
for path in paths:
|
||||
if not Path(path).exists():
|
||||
continue
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
@@ -125,18 +104,265 @@ def load_existing_texts():
|
||||
example = json.loads(line)
|
||||
for msg in example.get("messages", []):
|
||||
if msg.get("role") == "user" and isinstance(msg.get("content"), str):
|
||||
texts.add(normalize(msg["content"]))
|
||||
return texts
|
||||
yield msg["content"]
|
||||
|
||||
|
||||
def build_prompts(rng, mcp_name, count):
|
||||
templates = MCP_TEMPLATES[mcp_name]
|
||||
prompts = []
|
||||
for i in range(count):
|
||||
template = templates[i % len(templates)]
|
||||
text = template.format(
|
||||
w=rng.choice([80, 120, 200, 320, 480]),
|
||||
h=rng.choice([40, 60, 100, 240, 360]),
|
||||
def load_reference_corpus(paths=(TRAIN_PATH, EVAL_PATH)):
|
||||
"""Devuelve (textos_normalizados, shingles) de los turnos de usuario de train/eval."""
|
||||
texts = set()
|
||||
ref_shingles = set()
|
||||
for content in iter_user_texts(paths):
|
||||
texts.add(normalize(content))
|
||||
ref_shingles |= shingles(content)
|
||||
return texts, ref_shingles
|
||||
|
||||
|
||||
def assert_no_contamination(prompt, existing_texts, reference_shingles, origen=""):
|
||||
"""Hard-fail si el prompt es copia literal o parafrasis cercana de train/eval."""
|
||||
if normalize(prompt) in existing_texts:
|
||||
raise AssertionError(f"prompt held-out{origen} colisiona literal con train/eval: {prompt!r}")
|
||||
overlap = shingle_overlap(prompt, reference_shingles)
|
||||
if overlap > MAX_SHINGLE_OVERLAP:
|
||||
raise AssertionError(
|
||||
f"prompt held-out{origen} solapa {overlap:.0%} de sus shingles de {SHINGLE_N}-gramas "
|
||||
f"con train/eval (maximo {MAX_SHINGLE_OVERLAP:.0%}): {prompt!r}"
|
||||
)
|
||||
return overlap
|
||||
|
||||
|
||||
# --- pools de valores ---------------------------------------------------------------
|
||||
#
|
||||
# Pools que usaban los seeds y el holdout anterior a la Fase 6. Se conservan SOLO para
|
||||
# asertar que los pools nuevos no reusan ninguno de sus valores: compartir el vocabulario
|
||||
# de valores con el corpus de training es una fuga silenciosa que ninguna comparacion de
|
||||
# strings completos detecta.
|
||||
POOLS_PRE_FASE6 = {
|
||||
"words": [
|
||||
"dashboard", "login", "checkout", "onboarding", "reporte semanal", "bugfix urgente",
|
||||
"modal de confirmacion", "flujo de pago", "endpoint de usuarios", "cache de sesion",
|
||||
"migracion de datos", "notificaciones push", "tema oscuro", "landing page",
|
||||
"formulario de contacto", "panel de admin", "integracion con Stripe", "tabla de precios",
|
||||
"footer del sitio", "header responsive",
|
||||
],
|
||||
"repos": ["backend-core", "frontend-app", "infra-tools", "data-pipeline", "mobile-client"],
|
||||
"branches": [
|
||||
"feature/nueva-vista", "fix/timeout-api", "chore/deps", "hotfix/prod", "feature/dark-mode",
|
||||
],
|
||||
"boards": [
|
||||
"Dashboard Principal", "Mobile Screens", "Design System", "Landing v2", "Checkout Flow",
|
||||
],
|
||||
"shapes": ["boton-cta", "card-producto", "icono-menu", "titulo-principal", "footer-logo"],
|
||||
"colors": ["#1a73e8", "#e8710a", "#188038", "#d93025", "#9334e6", "#12b5cb"],
|
||||
"widths": [80, 120, 200, 320, 480],
|
||||
"heights": [40, 60, 100, 240, 360],
|
||||
}
|
||||
|
||||
# Pools del holdout. Disjuntos de los de arriba y ausentes del corpus de seeds/train/eval.
|
||||
WORDS = [
|
||||
"buscador con filtros", "historial de pedidos", "widget de clima", "lector de codigos QR",
|
||||
"linea de tiempo", "mapa de sucursales", "editor de firmas", "panel de alertas",
|
||||
"carrusel de novedades", "calendario de turnos", "resumen de consumo", "selector de idioma",
|
||||
]
|
||||
REPOS = [
|
||||
"servicio-facturacion", "gateway-eventos", "cliente-escritorio", "motor-reportes",
|
||||
"sincronizador-offline", "portal-proveedores", "indexador-catalogo", "bus-notificaciones",
|
||||
"consola-soporte",
|
||||
]
|
||||
BRANCHES = [
|
||||
"feature/panel-metricas", "fix/reintentos-webhook", "chore/actualizar-lockfile",
|
||||
"hotfix/zona-horaria", "feature/modo-lectura",
|
||||
]
|
||||
BOARDS = [
|
||||
"Catalogo Impreso", "Pantallas Kiosco", "Sistema Tipografico", "Portada Editorial",
|
||||
"Flujo de Reserva",
|
||||
]
|
||||
SHAPES = ["chip-etiqueta", "banner-promo", "avatar-usuario", "separador-seccion", "badge-descuento"]
|
||||
COLORS = ["#0e5f8a", "#c2410c", "#5b21b6", "#9d174d", "#166534", "#7e22ce", "#0d9488", "#831843"]
|
||||
WIDTHS = [640, 720, 880, 1120, 1360]
|
||||
HEIGHTS = [72, 96, 180, 420, 640]
|
||||
|
||||
# Dominios de diseno: ninguno aparece en los seeds (los seeds usan pizzeria, SaaS, clinica,
|
||||
# inmobiliaria, e-commerce...). La variedad de dominio es lo que impide que el modelo
|
||||
# memorice una composicion y la repita.
|
||||
DOMINIOS = [
|
||||
"una tostaduria de cafe de especialidad",
|
||||
"un estudio de tatuajes",
|
||||
"una veterinaria felina",
|
||||
"una escuela de buceo",
|
||||
"una libreria de comics",
|
||||
"un vivero de plantas de interior",
|
||||
"una fabrica de bicicletas de bambu",
|
||||
"un observatorio astronomico",
|
||||
"una heladeria artesanal",
|
||||
"un taller de ceramica",
|
||||
"un club de ajedrez",
|
||||
"una panaderia sin gluten",
|
||||
"un refugio de montana",
|
||||
"una tienda de instrumentos de viento",
|
||||
"una clinica de fisioterapia deportiva",
|
||||
"un festival de cine documental",
|
||||
]
|
||||
ESTILOS = [
|
||||
"minimalista y luminoso",
|
||||
"editorial con mucho contraste",
|
||||
"retro setentoso",
|
||||
"brutalista",
|
||||
"pastel y suave",
|
||||
"oscuro con acentos neon",
|
||||
]
|
||||
|
||||
# --- plantillas ---------------------------------------------------------------------
|
||||
#
|
||||
# Cada plantilla es (texto, expect). Valores de `expect`:
|
||||
# tool-call-valido : caso normal, se espera una tool_call bien formada.
|
||||
# no-invented-args : sonda -- el pedido tienta a inventar un argumento que el schema no
|
||||
# declara (scale, filePath). La puerta 2 lo cuenta como
|
||||
# unknown_argument.
|
||||
# api-verificada : el pedido requiere un miembro de API real pero poco frecuente
|
||||
# (createBoolean); mide si el modelo lo conoce en vez de improvisar.
|
||||
# diseno-penpot : pedido de diseno real; alimenta ademas la puerta 5.
|
||||
|
||||
PENPOT_TEMPLATES = [
|
||||
("Crea un rectangulo de {w}x{h} en el board '{board}' con color {color}.", "tool-call-valido"),
|
||||
("Necesito un texto que diga '{text}' dentro del board '{board}', alineado a la izquierda.", "tool-call-valido"),
|
||||
("Cambia el fill del shape '{shape}' a {color} y verifica que se aplico.", "tool-call-valido"),
|
||||
("Agrega un board nuevo llamado '{board}' de {w}x{h} px en la pagina actual.", "tool-call-valido"),
|
||||
("Exporta el shape '{shape}' como PNG a 2x de resolucion.", "no-invented-args"),
|
||||
("Exporta el shape '{shape}' como PNG y dejalo guardado en /tmp/{shape}.png.", "no-invented-args"),
|
||||
("Lista los shapes del board '{board}' y decime cuales son grupos.", "tool-call-valido"),
|
||||
("Mueve el shape '{shape}' a la posicion x={w}, y={h} dentro de '{board}'.", "tool-call-valido"),
|
||||
("Crea un boolean de union entre '{shape}' y otro rectangulo superpuesto.", "api-verificada"),
|
||||
("Recorta el shape '{shape}' contra un circulo con una operacion booleana de interseccion.", "api-verificada"),
|
||||
]
|
||||
|
||||
PENPOT_DESIGN_TEMPLATES = [
|
||||
("Armame una landing completa de {dominio}, {w} px de ancho, con hero, tres beneficios y footer. Estilo {estilo}, color principal {color}.", "diseno-penpot"),
|
||||
("Necesito una card de producto para {dominio}: foto arriba, titulo, precio y boton, de {w}x{h} y con esquinas redondeadas.", "diseno-penpot"),
|
||||
("Hazme un navbar de {w}x{h} para el sitio de {dominio}, con logo a la izquierda y cuatro enlaces a la derecha sobre fondo {color}.", "diseno-penpot"),
|
||||
("Hazme un dashboard para {dominio} con barra lateral, cuatro tarjetas de metricas y una tabla debajo, {w} de ancho.", "diseno-penpot"),
|
||||
("Pantalla mobile de {w}x{h} para la app de {dominio}: onboarding de tres pasos con ilustracion y boton primario {color}.", "diseno-penpot"),
|
||||
("Seccion de precios con tres planes para {dominio}, el del medio destacado, paleta {estilo}.", "diseno-penpot"),
|
||||
("Galeria de seis fotos de {dominio} en grid de tres columnas, con titulo y bajada arriba.", "diseno-penpot"),
|
||||
("Formulario de contacto de {dominio}: cuatro campos, un area de texto y boton de enviar, {w} de ancho.", "diseno-penpot"),
|
||||
("Hero de {w}x{h} para {dominio} con foto de fondo, un velo oscuro encima y titular en blanco.", "diseno-penpot"),
|
||||
("Hazme el encabezado y el pie de {dominio} con la misma paleta {estilo} y acento {color}.", "diseno-penpot"),
|
||||
("Pantalla de pago de {dominio}: resumen de compra a la derecha, datos de tarjeta a la izquierda, {w} de ancho.", "diseno-penpot"),
|
||||
("Armame un cuadro comparativo de {num} columnas para {dominio}, encabezado en {color} y filas alternadas.", "diseno-penpot"),
|
||||
("Necesito la escala tipografica de {dominio} en un board de {w}x{h}: seis tamanos del display al pie de foto.", "diseno-penpot"),
|
||||
("Defini la paleta de {dominio} en la biblioteca del archivo y aplicala a un boton primario y a uno secundario.", "diseno-penpot"),
|
||||
("Pantalla de perfil de {dominio} de {w}x{h}, con avatar, datos de la cuenta y actividad reciente.", "diseno-penpot"),
|
||||
("El board '{board}' quedo lleno de cajas grises sin nada de texto; convertilo en un diseno real de {dominio} usando {color}.", "diseno-penpot"),
|
||||
("Tres testimonios de clientes de {dominio} en tarjetas con foto, cita y nombre, {w} de ancho.", "diseno-penpot"),
|
||||
("Un cotizador simple para {dominio} de {w}x{h}: dos selectores, un deslizador y el resultado en grande.", "diseno-penpot"),
|
||||
("Pantalla de resultados de busqueda de {dominio}: filtros a la izquierda y ocho resultados en dos columnas, {w} de ancho.", "diseno-penpot"),
|
||||
("Banner promocional de {w}x{h} para {dominio}, estilo {estilo}, con un descuento bien visible y un boton.", "diseno-penpot"),
|
||||
]
|
||||
|
||||
GITEA_TEMPLATES = [
|
||||
("Crea una rama llamada '{branch}' en el repo '{repo}' desde main.", "tool-call-valido"),
|
||||
("Abri un issue en '{repo}' titulado '{text}' con la label 'bug'.", "tool-call-valido"),
|
||||
("Decime que pull requests siguen abiertos hoy en '{repo}'.", "tool-call-valido"),
|
||||
("Crea un release '{branch}' en '{repo}' con las notas '{text}'.", "tool-call-valido"),
|
||||
("Busca commits recientes en '{repo}' que mencionen '{text}'.", "tool-call-valido"),
|
||||
("Agrega un comentario '{text}' al issue numero {num} de '{repo}'.", "tool-call-valido"),
|
||||
("Revisa el estado de los actions/workflows del repo '{repo}'.", "tool-call-valido"),
|
||||
("Mergea (solo si el usuario lo pide explicitamente) el PR numero {num} de '{repo}'.", "tool-call-valido"),
|
||||
]
|
||||
|
||||
GITHUB_TEMPLATES = [
|
||||
("Crea un pull request en '{repo}' desde la rama '{branch}' hacia main, titulo '{text}'.", "tool-call-valido"),
|
||||
("Lista los issues abiertos de '{repo}' con label 'enhancement'.", "tool-call-valido"),
|
||||
("Agrega un comentario '{text}' al PR numero {num} de '{repo}'.", "tool-call-valido"),
|
||||
("Busca en '{repo}' el codigo que define la funcion '{text}'.", "tool-call-valido"),
|
||||
("Crea un issue en '{repo}' titulado '{text}' asignado a mi usuario.", "tool-call-valido"),
|
||||
("Revisa los commits recientes de la rama '{branch}' en '{repo}'.", "tool-call-valido"),
|
||||
("Lista los releases publicados de '{repo}'.", "tool-call-valido"),
|
||||
("Solicita una review de Copilot para el PR numero {num} de '{repo}'.", "tool-call-valido"),
|
||||
]
|
||||
|
||||
DOCMOST_TEMPLATES = [
|
||||
("Crea una pagina llamada '{text}' en el space '{repo}' con una tabla de {num} columnas.", "tool-call-valido"),
|
||||
("Busca en Docmost paginas que mencionen '{text}'.", "tool-call-valido"),
|
||||
("Actualiza la pagina '{text}' agregando una fila mas a la tabla existente.", "tool-call-valido"),
|
||||
("Comenta '{text}' en la pagina con id conocido del space '{repo}'.", "tool-call-valido"),
|
||||
("Lista las paginas del space '{repo}' ordenadas por actualizacion reciente.", "tool-call-valido"),
|
||||
("Crea una subpagina '{text}' bajo la pagina principal del space '{repo}'.", "tool-call-valido"),
|
||||
("Fijate si quedo algun comentario sin responder en '{repo}' de esta semana.", "tool-call-valido"),
|
||||
("Mueve la pagina '{text}' a otro parent dentro del space '{repo}'.", "tool-call-valido"),
|
||||
]
|
||||
|
||||
ATLASSIAN_TEMPLATES = [
|
||||
("Crea un issue de Jira en el proyecto '{repo}' titulado '{text}', tipo Bug.", "tool-call-valido"),
|
||||
("Busca issues de Jira asignados a mi usuario con JQL en el proyecto '{repo}'.", "tool-call-valido"),
|
||||
("Agrega un comentario '{text}' al issue {repo}-{num} de Jira.", "tool-call-valido"),
|
||||
("Transiciona el issue {repo}-{num} a 'In Progress'.", "tool-call-valido"),
|
||||
("Crea una pagina de Confluence '{text}' en el espacio '{repo}'.", "tool-call-valido"),
|
||||
("Busca en Confluence paginas del espacio '{repo}' que mencionen '{text}'.", "tool-call-valido"),
|
||||
("Agrega un worklog de 1h al issue {repo}-{num} con el comentario '{text}'.", "tool-call-valido"),
|
||||
("Lista los tipos de issue disponibles en el proyecto '{repo}' de Jira.", "tool-call-valido"),
|
||||
]
|
||||
|
||||
MCP_TEMPLATES = {
|
||||
"penpot": PENPOT_TEMPLATES + PENPOT_DESIGN_TEMPLATES,
|
||||
"gitea": GITEA_TEMPLATES,
|
||||
"github-personal": GITHUB_TEMPLATES,
|
||||
"docmost": DOCMOST_TEMPLATES,
|
||||
"atlassian": ATLASSIAN_TEMPLATES,
|
||||
}
|
||||
|
||||
|
||||
def load_tools(mcp_name):
|
||||
return json.loads((SCHEMAS_DIR / f"{mcp_name}.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def load_corpus_text():
|
||||
"""Texto normalizado de todos los seeds + train + eval, para el chequeo de pools."""
|
||||
chunks = []
|
||||
for path in sorted(SEEDS_DIR.glob("*.jsonl")) + sorted((SEEDS_DIR / "_parts").glob("*.jsonl")):
|
||||
chunks.append(path.read_text(encoding="utf-8"))
|
||||
for path in (TRAIN_PATH, EVAL_PATH):
|
||||
if path.exists():
|
||||
chunks.append(path.read_text(encoding="utf-8"))
|
||||
return normalize(" ".join(chunks))
|
||||
|
||||
|
||||
def assert_pools_disjuntos(corpus_text):
|
||||
"""Los pools del holdout no comparten valores con los del corpus de training.
|
||||
|
||||
Dos chequeos complementarios: (a) ningun valor de texto del holdout aparece en el
|
||||
corpus de seeds/train/eval; (b) los pools numericos son disjuntos de los que usaba el
|
||||
holdout anterior a la Fase 6, que si se solapaban con los seeds.
|
||||
"""
|
||||
pools_texto = {
|
||||
"words": WORDS, "repos": REPOS, "branches": BRANCHES, "boards": BOARDS,
|
||||
"shapes": SHAPES, "colors": COLORS, "dominios": DOMINIOS, "estilos": ESTILOS,
|
||||
}
|
||||
for nombre, valores in pools_texto.items():
|
||||
previos = set(POOLS_PRE_FASE6.get(nombre, []))
|
||||
repetidos = sorted(set(valores) & previos)
|
||||
if repetidos:
|
||||
raise AssertionError(f"pool '{nombre}' reusa valores del corpus viejo: {repetidos}")
|
||||
for valor in valores:
|
||||
if normalize(valor) in corpus_text:
|
||||
raise AssertionError(
|
||||
f"valor '{valor}' del pool '{nombre}' aparece en los seeds/train/eval: "
|
||||
"el holdout tiene que usar vocabulario propio"
|
||||
)
|
||||
|
||||
for nombre, valores in (("widths", WIDTHS), ("heights", HEIGHTS)):
|
||||
repetidos = sorted(set(valores) & set(POOLS_PRE_FASE6[nombre]))
|
||||
if repetidos:
|
||||
raise AssertionError(f"pool numerico '{nombre}' reusa valores viejos: {repetidos}")
|
||||
|
||||
print("[INFO] pools de valores disjuntos verificados "
|
||||
f"({sum(len(v) for v in pools_texto.values())} valores de texto, "
|
||||
f"{len(WIDTHS) + len(HEIGHTS)} numericos)")
|
||||
|
||||
|
||||
def render(rng, template):
|
||||
return template.format(
|
||||
w=rng.choice(WIDTHS),
|
||||
h=rng.choice(HEIGHTS),
|
||||
board=rng.choice(BOARDS),
|
||||
color=rng.choice(COLORS),
|
||||
shape=f"{rng.choice(SHAPES)}-{rng.randint(1, 99)}",
|
||||
@@ -144,24 +370,53 @@ def build_prompts(rng, mcp_name, count):
|
||||
repo=rng.choice(REPOS),
|
||||
branch=f"{rng.choice(BRANCHES)}-{rng.randint(1, 99)}",
|
||||
num=rng.randint(1, 500),
|
||||
)
|
||||
prompts.append(text)
|
||||
dominio=rng.choice(DOMINIOS),
|
||||
estilo=rng.choice(ESTILOS),
|
||||
)
|
||||
|
||||
|
||||
def build_prompts(rng, mcp_name, count, seen):
|
||||
"""Un prompt por plantilla, ciclando, sin repetir texto ya generado.
|
||||
|
||||
Las plantillas con pocos placeholders (p.ej. solo {repo}) colisionaban al reusarse:
|
||||
el holdout anterior tenia 15 prompts duplicados sobre 200, o sea 15 mediciones
|
||||
desperdiciadas. Se reintenta con otros valores hasta obtener un texto nuevo.
|
||||
"""
|
||||
templates = MCP_TEMPLATES[mcp_name]
|
||||
prompts = []
|
||||
for i in range(count):
|
||||
template, expect = templates[i % len(templates)]
|
||||
for _ in range(200):
|
||||
text = render(rng, template)
|
||||
if text not in seen:
|
||||
break
|
||||
else:
|
||||
raise AssertionError(
|
||||
f"la plantilla {template!r} no tiene suficientes combinaciones de valores "
|
||||
"para generar un prompt nuevo: agregar valores a los pools"
|
||||
)
|
||||
seen.add(text)
|
||||
prompts.append((text, expect))
|
||||
return prompts
|
||||
|
||||
|
||||
def main():
|
||||
rng = random.Random(SEED)
|
||||
existing_texts = load_existing_texts()
|
||||
print(f"[INFO] {len(existing_texts)} prompts de usuario existentes en train.jsonl/eval.jsonl")
|
||||
existing_texts, reference_shingles = load_reference_corpus()
|
||||
print(f"[INFO] {len(existing_texts)} prompts de usuario existentes en train.jsonl/eval.jsonl, "
|
||||
f"{len(reference_shingles)} shingles de {SHINGLE_N}-gramas de referencia")
|
||||
|
||||
assert_pools_disjuntos(load_corpus_text())
|
||||
|
||||
examples = []
|
||||
max_overlap = 0.0
|
||||
seen = set()
|
||||
for mcp_name, count in MCP_TARGETS.items():
|
||||
tools = load_tools(mcp_name)
|
||||
prompts = build_prompts(rng, mcp_name, count)
|
||||
for prompt in prompts:
|
||||
if normalize(prompt) in existing_texts:
|
||||
raise AssertionError(f"prompt held-out colisiona con train/eval: {prompt!r}")
|
||||
examples.append({"prompt": prompt, "mcp": mcp_name, "tools": tools})
|
||||
for prompt, expect in build_prompts(rng, mcp_name, count, seen):
|
||||
overlap = assert_no_contamination(prompt, existing_texts, reference_shingles)
|
||||
max_overlap = max(max_overlap, overlap)
|
||||
examples.append({"prompt": prompt, "mcp": mcp_name, "expect": expect, "tools": tools})
|
||||
|
||||
rng.shuffle(examples)
|
||||
|
||||
@@ -173,9 +428,15 @@ def main():
|
||||
f.write(json.dumps(ex, ensure_ascii=False) + "\n")
|
||||
|
||||
print(f"[INFO] {len(examples)} prompts held-out escritos en {OUT_PATH}")
|
||||
print(f"[INFO] solapamiento maximo de shingles contra train/eval: {max_overlap:.0%} "
|
||||
f"(umbral {MAX_SHINGLE_OVERLAP:.0%})")
|
||||
for mcp_name in MCP_TARGETS:
|
||||
n = sum(1 for ex in examples if ex["mcp"] == mcp_name)
|
||||
print(f" {mcp_name}: {n}")
|
||||
print("[INFO] por expect:")
|
||||
for expect in sorted({ex["expect"] for ex in examples}):
|
||||
n = sum(1 for ex in examples if ex["expect"] == expect)
|
||||
print(f" {expect}: {n}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user