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:
@@ -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()
|
||||
Reference in New Issue
Block a user