- 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.
183 lines
7.6 KiB
Python
183 lines
7.6 KiB
Python
"""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()
|