"""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()