Fase 4: puerta 1 (eval-loss offline por bucket) y contenedor/scripts de puertas 2-4

- scripts/30_eval_suite.py --gate 1: eval-loss sobre el checkpoint mergeado, agrupado por
  meta.bucket (aislando replay), comparado contra eval_loss=0.275 de Fase 3.
- docker-compose.eval.yml: servicio vllm-eval propio (puerto 8001), sirviendo el
  checkpoint mergeado en BF16, con tool-call-parser=qwen3_coder y reasoning-parser=qwen3.
  No se pudo leer el compose real de produccion (/data/compose/43/docker-compose.yml no
  existe en spark, probablemente vive en el host del servidor Portainer) -- flags basados
  en la arquitectura conocida del modelo.
- scripts/31_build_holdout_prompts.py: genera data/holdout_prompts.jsonl (200 prompts,
  40 por MCP, sin overlap verificado contra train.jsonl/eval.jsonl).
- scripts/32_gate2_toolcalls.py: valida tool-calls devueltas por vllm-eval (parser real
  de vLLM, nunca una regex propia) contra los 200 prompts held-out.
- scripts/33_gate3_adherencia.py: checklists de adherencia por skill + no-activacion,
  con baseline opcional contra vllm-qwen36 si esta corriendo.
- scripts/34_gate4_e2e.py: arma el plan de llamadas E2E contra los 5 MCPs y 5 skills via
  el checkpoint mergeado, para que el agente orquestador las ejecute con sus MCPs reales.
This commit is contained in:
2026-07-29 17:37:02 +00:00
parent 268451aed7
commit ceba5f80cd
7 changed files with 1090 additions and 0 deletions
+182
View File
@@ -0,0 +1,182 @@
"""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.
Cada linea: {"prompt": "...", "mcp": "penpot|gitea|github-personal|docmost|atlassian",
"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).
"""
import json
import random
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
SCHEMAS_DIR = REPO_ROOT / "data" / "schemas"
TRAIN_PATH = REPO_ROOT / "data" / "train.jsonl"
EVAL_PATH = REPO_ROOT / "data" / "eval.jsonl"
OUT_PATH = REPO_ROOT / "data" / "holdout_prompts.jsonl"
SEED = 43
TARGET_TOTAL = 200
MCP_TARGETS = {
"penpot": 40,
"gitea": 40,
"github-personal": 40,
"docmost": 40,
"atlassian": 40,
}
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.",
]
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"))
def normalize(text):
return " ".join(text.lower().split())
def load_existing_texts():
texts = set()
for path in (TRAIN_PATH, EVAL_PATH):
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
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
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]),
board=rng.choice(BOARDS),
color=rng.choice(COLORS),
shape=f"{rng.choice(SHAPES)}-{rng.randint(1, 99)}",
text=f"{rng.choice(WORDS)} {rng.randint(1, 999)}",
repo=rng.choice(REPOS),
branch=f"{rng.choice(BRANCHES)}-{rng.randint(1, 99)}",
num=rng.randint(1, 500),
)
prompts.append(text)
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")
examples = []
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})
rng.shuffle(examples)
if len(examples) < TARGET_TOTAL:
raise AssertionError(f"solo se generaron {len(examples)} prompts, se esperaban >= {TARGET_TOTAL}")
with open(OUT_PATH, "w", encoding="utf-8") as f:
for ex in examples:
f.write(json.dumps(ex, ensure_ascii=False) + "\n")
print(f"[INFO] {len(examples)} prompts held-out escritos en {OUT_PATH}")
for mcp_name in MCP_TARGETS:
n = sum(1 for ex in examples if ex["mcp"] == mcp_name)
print(f" {mcp_name}: {n}")
if __name__ == "__main__":
main()