|
|
|
|
|
|
| """
|
| eval_agents.py β EvaluaciΓ³n de generaciΓ³n de AGENTS.md (Milestone 3).
|
|
|
| Dado un scan sintΓ©tico, el modelo debe generar un AGENTS.md vΓ‘lido.
|
| Se evalΓΊan 12 casos con entornos variados.
|
|
|
| MΓ©tricas:
|
| - Secciones presentes: Quick Reference, Sistema detectado, Paquetes, Boot protocol
|
| - Info del scan reflejada: OS, Python version, GPU (si existe)
|
| - Formato vΓ‘lido: lΓneas con '|' para tablas, '```' para bloques, '##' headers
|
| - Sin hallucination grave: no inventa servicios que no existΓan
|
|
|
| Uso:
|
| python -X utf8 scripts/eval_agents.py --checkpoint checkpoints/v3_sft_v8.pt
|
| """
|
|
|
| import argparse
|
| import re
|
| import sys
|
| import time
|
| from dataclasses import dataclass
|
| from pathlib import Path
|
|
|
| import sentencepiece as spm
|
| import torch
|
| import torch.nn.functional as F
|
|
|
| sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
| TOKENIZER_PATHS = [
|
| "data/tokenizer/pampar_48k.model",
|
| "data/tokenizer/code_tokenizer.model",
|
| ]
|
|
|
|
|
|
|
|
|
|
|
| CASOS_EVAL = [
|
| {
|
| "nombre": "ML PyTorch GPU",
|
| "scan": (
|
| "El Scanner detectΓ³ el siguiente entorno. "
|
| "Genera el archivo AGENTS.md contextual para este despliegue.\n\n"
|
| "## Entorno detectado\n\n"
|
| "- **OS**: Linux Ubuntu 22.04\n"
|
| "- **Python**: 3.11.9\n"
|
| "- **GPU**: NVIDIA GeForce RTX 4090 (24576 MB)\n"
|
| "- **RAM**: 64.0 GB\n"
|
| "- **Archivos**: Python: 52, JSON: 14, Markdown: 9\n"
|
| "- **Paquetes** (6 relevantes): torch==2.5.1, transformers==4.47.1, "
|
| "peft==0.13.2, datasets==3.1.0, trl==0.12.2, bitsandbytes==0.45.0\n"
|
| "- **Servicios inactivos**: PostgreSQL, Redis\n"
|
| "- **Voz**: espeak\n\n"
|
| "**Proyecto**: llm-finetune β pipeline de fine-tuning con LoRA/QLoRA"
|
| ),
|
| "espera": {
|
| "secciones": ["Quick Reference", "Sistema", "Paquetes", "Boot"],
|
| "info_scan": ["Linux", "3.11.9", "RTX 4090", "torch", "peft"],
|
| "servicios_correctos": False,
|
| },
|
| },
|
| {
|
| "nombre": "FastAPI + PostgreSQL",
|
| "scan": (
|
| "El Scanner detectΓ³ el siguiente entorno. "
|
| "Genera el archivo AGENTS.md contextual para este despliegue.\n\n"
|
| "## Entorno detectado\n\n"
|
| "- **OS**: macOS 14.0 Sonoma\n"
|
| "- **Python**: 3.12.4\n"
|
| "- **GPU**: no disponible (solo CPU)\n"
|
| "- **RAM**: 32.0 GB\n"
|
| "- **Archivos**: Python: 40, TypeScript: 8, JSON: 12, Markdown: 6\n"
|
| "- **Paquetes** (6 relevantes): fastapi==0.115.0, uvicorn==0.32.0, "
|
| "pydantic==2.10.0, sqlalchemy==2.0.36, alembic==1.14.0, httpx==0.28.0\n"
|
| "- **Servicios activos**: PostgreSQL, HTTP-8000\n"
|
| "- **Voz**: say\n\n"
|
| "**Proyecto**: api-service β API REST con FastAPI y PostgreSQL"
|
| ),
|
| "espera": {
|
| "secciones": ["Quick Reference", "Sistema", "Paquetes", "Boot"],
|
| "info_scan": ["macOS", "3.12.4", "fastapi", "PostgreSQL"],
|
| "servicios_correctos": True,
|
| },
|
| },
|
| {
|
| "nombre": "CLI / Script simple",
|
| "scan": (
|
| "El Scanner detectΓ³ el siguiente entorno. "
|
| "Genera el archivo AGENTS.md contextual para este despliegue.\n\n"
|
| "## Entorno detectado\n\n"
|
| "- **OS**: Windows 11.0.22621\n"
|
| "- **Python**: 3.13.3\n"
|
| "- **GPU**: no disponible (solo CPU)\n"
|
| "- **RAM**: 8.0 GB\n"
|
| "- **Archivos**: Python: 12, Markdown: 3\n"
|
| "- **Paquetes** (4 relevantes): click==8.1.8, rich==13.9.4, "
|
| "httpx==0.28.0, python-dotenv==1.0.1\n"
|
| "- **Voz**: SAPI\n\n"
|
| "**Proyecto**: data-fetcher β herramienta CLI para descarga de datos"
|
| ),
|
| "espera": {
|
| "secciones": ["Quick Reference", "Sistema", "Paquetes", "Boot"],
|
| "info_scan": ["Windows", "11", "click", "rich"],
|
| "servicios_correctos": True,
|
| },
|
| },
|
| {
|
| "nombre": "Django + Celery",
|
| "scan": (
|
| "El Scanner detectΓ³ el siguiente entorno. "
|
| "Genera el archivo AGENTS.md contextual para este despliegue.\n\n"
|
| "## Entorno detectado\n\n"
|
| "- **OS**: Linux Debian 12\n"
|
| "- **Python**: 3.11.9\n"
|
| "- **GPU**: no disponible (solo CPU)\n"
|
| "- **RAM**: 16.0 GB\n"
|
| "- **Archivos**: Python: 58, HTML: 22, CSS: 12, JavaScript: 9, Markdown: 5\n"
|
| "- **Paquetes** (5 relevantes): django==5.1.4, djangorestframework==3.15.2, "
|
| "celery==5.4.0, redis==5.2.1, pillow==11.0.0\n"
|
| "- **Servicios activos**: PostgreSQL, Redis, HTTP-8000\n"
|
| "- **Voz**: espeak\n\n"
|
| "**Proyecto**: plataforma-web β aplicaciΓ³n Django con tareas asΓncronas"
|
| ),
|
| "espera": {
|
| "secciones": ["Quick Reference", "Sistema", "Paquetes", "Boot"],
|
| "info_scan": ["Linux", "django", "celery", "Redis"],
|
| "servicios_correctos": True,
|
| },
|
| },
|
| {
|
| "nombre": "Data Science sin GPU",
|
| "scan": (
|
| "El Scanner detectΓ³ el siguiente entorno. "
|
| "Genera el archivo AGENTS.md contextual para este despliegue.\n\n"
|
| "## Entorno detectado\n\n"
|
| "- **OS**: macOS 14.0 Sonoma\n"
|
| "- **Python**: 3.10.14\n"
|
| "- **GPU**: no disponible (solo CPU)\n"
|
| "- **RAM**: 16.0 GB\n"
|
| "- **Archivos**: Python: 20, JSON: 18, Markdown: 10\n"
|
| "- **Paquetes** (6 relevantes): pandas==2.2.3, numpy==2.1.3, "
|
| "matplotlib==3.9.3, scikit-learn==1.5.2, jupyter==1.1.1, plotly==5.24.1\n"
|
| "- **Servicios activos**: PostgreSQL\n"
|
| "- **Voz**: say\n\n"
|
| "**Proyecto**: market-analysis β anΓ‘lisis de datos financieros"
|
| ),
|
| "espera": {
|
| "secciones": ["Quick Reference", "Sistema", "Paquetes", "Boot"],
|
| "info_scan": ["macOS", "pandas", "scikit-learn", "PostgreSQL"],
|
| "servicios_correctos": True,
|
| },
|
| },
|
| {
|
| "nombre": "GTX 1650 Local",
|
| "scan": (
|
| "El Scanner detectΓ³ el siguiente entorno. "
|
| "Genera el archivo AGENTS.md contextual para este despliegue.\n\n"
|
| "## Entorno detectado\n\n"
|
| "- **OS**: Windows 10.0.26200\n"
|
| "- **Python**: 3.13.3\n"
|
| "- **GPU**: NVIDIA GeForce GTX 1650 (4095 MB)\n"
|
| "- **RAM**: 31.9 GB\n"
|
| "- **Archivos**: Python: 65, Markdown: 10, JSON: 8\n"
|
| "- **Paquetes** (6 relevantes): torch==2.5.1, sentencepiece==0.2.0, "
|
| "transformers==4.47.1, accelerate==1.12.0, pytest==9.0.1, peft==0.13.2\n"
|
| "- **Servicios inactivos**: PostgreSQL, Redis\n"
|
| "- **Voz**: SAPI\n\n"
|
| "**Proyecto**: pampar-coder β modelo de lenguaje 108M entrenado localmente"
|
| ),
|
| "espera": {
|
| "secciones": ["Quick Reference", "Sistema", "Paquetes", "Boot"],
|
| "info_scan": ["Windows", "GTX 1650", "torch", "sentencepiece"],
|
| "servicios_correctos": False,
|
| },
|
| },
|
| ]
|
|
|
|
|
|
|
|
|
|
|
|
|
| def _cargar_modelo(checkpoint: Path, device: torch.device):
|
| import dataclasses
|
| from pampar.coder.v3.config import PRESET_V3, ConfigV3
|
| from pampar.coder.v3.modelo import PamparV3
|
|
|
| ckpt = torch.load(checkpoint, map_location="cpu", weights_only=False)
|
| raw_cfg = ckpt.get("config", {})
|
| state = ckpt.get("modelo", ckpt)
|
| if isinstance(raw_cfg, dict) and "dim" in raw_cfg:
|
| valid = {f.name for f in dataclasses.fields(ConfigV3)}
|
| cfg = ConfigV3(**{k: v for k, v in raw_cfg.items() if k in valid})
|
| else:
|
| cfg = PRESET_V3
|
| modelo = PamparV3(cfg)
|
| modelo.load_state_dict(state, strict=False)
|
| return modelo.to(device).eval(), cfg
|
|
|
|
|
| def _cargar_tok(vocab_size: int) -> spm.SentencePieceProcessor:
|
| sp = spm.SentencePieceProcessor()
|
| for p in TOKENIZER_PATHS:
|
| pp = Path(p)
|
| if pp.exists():
|
| sp.Load(str(pp))
|
| if sp.vocab_size() == vocab_size:
|
| return sp
|
| raise FileNotFoundError(f"Tokenizer {vocab_size} no encontrado")
|
|
|
|
|
|
|
|
|
|
|
|
|
| def _generar(modelo, tok, prompt: str, device, max_tokens=800, temp=0.1, rep_pen=1.15) -> str:
|
| ids = tok.Encode(prompt)
|
| gen = list(ids)
|
| rep_window = 32
|
|
|
| for _ in range(max_tokens):
|
| ctx = torch.tensor([gen[-512:]], dtype=torch.long, device=device)
|
| logits, _, _ = modelo(ctx)
|
| nxt = logits[0, -1]
|
|
|
| if rep_pen != 1.0 and len(gen) > len(ids):
|
| ws = max(len(ids), len(gen) - rep_window)
|
| seen = set(gen[ws:])
|
| for t in seen:
|
| nxt[t] = nxt[t] / rep_pen if nxt[t] > 0 else nxt[t] * rep_pen
|
|
|
| if temp <= 0.0:
|
| next_tok = int(nxt.argmax())
|
| else:
|
| probs = F.softmax(nxt / temp, dim=-1)
|
| next_tok = int(torch.multinomial(probs, 1))
|
|
|
| gen.append(next_tok)
|
| decoded = tok.Decode(gen[len(ids):]).replace("\u2047", "\n")
|
|
|
|
|
| if decoded.count("### Scan:") > 0:
|
| idx = decoded.index("### Scan:")
|
| if idx > 50:
|
| return decoded[:idx].rstrip()
|
|
|
|
|
| if decoded.rstrip().endswith("```") and len(decoded) > 200:
|
| return decoded.rstrip()
|
|
|
| return tok.Decode(gen[len(ids):]).replace("\u2047", "\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
| @dataclass
|
| class ResultadoCaso:
|
| nombre: str
|
| score: float
|
| ok_secciones: bool
|
| ok_info: bool
|
| ok_formato: bool
|
| n_secciones: int
|
| n_info: int
|
| texto: str
|
|
|
|
|
| def _verificar(texto: str, espera: dict) -> tuple[bool, bool, bool, int, int]:
|
| """Verifica que el texto generado cumpla con las expectativas."""
|
| texto_lower = texto.lower()
|
|
|
|
|
| secciones_esperadas = espera["secciones"]
|
| n_encontradas = sum(1 for s in secciones_esperadas if s.lower() in texto_lower)
|
| ok_secciones = n_encontradas >= len(secciones_esperadas) * 0.75
|
|
|
|
|
| info = espera["info_scan"]
|
| n_info = sum(1 for i in info if i.lower() in texto_lower)
|
| ok_info = n_info >= len(info) * 0.6
|
|
|
|
|
| tiene_header = bool(re.search(r'^##\s+\w', texto, re.MULTILINE))
|
| tiene_tabla_o_bloque = '|' in texto or '```' in texto
|
| ok_formato = tiene_header and tiene_tabla_o_bloque
|
|
|
| return ok_secciones, ok_info, ok_formato, n_encontradas, n_info
|
|
|
|
|
| def _score(ok_s: bool, ok_i: bool, ok_f: bool, n_s: int, n_i: int,
|
| max_s: int, max_i: int) -> float:
|
| """Score ponderado: secciones 40%, info 40%, formato 20%."""
|
| s = (n_s / max_s) * 0.4 + (n_i / max_i) * 0.4 + (0.2 if ok_f else 0.0)
|
| return round(min(s, 1.0), 3)
|
|
|
|
|
|
|
|
|
|
|
|
|
| def main() -> None:
|
| parser = argparse.ArgumentParser()
|
| parser.add_argument("--checkpoint", default="checkpoints/v3_sft_v8.pt")
|
| parser.add_argument("--device", default="auto")
|
| parser.add_argument("--temp", type=float, default=0.1)
|
| parser.add_argument("--rep-penalty", type=float, default=1.15)
|
| parser.add_argument("--max-tokens", type=int, default=800)
|
| parser.add_argument("--verbose", action="store_true")
|
| args = parser.parse_args()
|
|
|
| device = torch.device(
|
| "cuda" if args.device == "auto" and torch.cuda.is_available() else
|
| args.device if args.device != "auto" else "cpu"
|
| )
|
|
|
| sep = "β" * 70
|
| print(f"\n{sep}")
|
| print(f" EVAL MILESTONE 3 β GeneraciΓ³n de AGENTS.md")
|
| print(f" Checkpoint : {args.checkpoint}")
|
| print(f" Device : {device} | Temp: {args.temp} | RepPen: {args.rep_penalty}")
|
| print(f" Casos : {len(CASOS_EVAL)}")
|
| print(f"{sep}\n")
|
|
|
| t0_total = time.time()
|
| modelo, cfg = _cargar_modelo(Path(args.checkpoint), device)
|
| tok = _cargar_tok(cfg.vocab_size)
|
| modelo.registrar_tokenizer(tok)
|
| print(f" Modelo cargado ({sum(p.numel() for p in modelo.parameters())/1e6:.1f}M params)\n")
|
|
|
| resultados: list[ResultadoCaso] = []
|
|
|
| for i, caso in enumerate(CASOS_EVAL, 1):
|
| prompt = f"### Scan:\n{caso['scan']}\n### Protocolo:\n"
|
| print(f" [{i:02d}/{len(CASOS_EVAL)}] {caso['nombre']}", end=" ", flush=True)
|
| t0 = time.time()
|
|
|
| texto = _generar(modelo, tok, prompt, device,
|
| max_tokens=args.max_tokens, temp=args.temp, rep_pen=args.rep_penalty)
|
|
|
| dt = time.time() - t0
|
| espera = caso["espera"]
|
| ok_s, ok_i, ok_f, n_s, n_i = _verificar(texto, espera)
|
| sc = _score(ok_s, ok_i, ok_f, n_s, n_i,
|
| len(espera["secciones"]), len(espera["info_scan"]))
|
|
|
| emoji = "β
" if sc >= 0.6 else "β οΈ " if sc >= 0.4 else "β"
|
| print(f"[{dt:.1f}s] {emoji} score={sc:.2f} "
|
| f"secciones={n_s}/{len(espera['secciones'])} "
|
| f"info={n_i}/{len(espera['info_scan'])} "
|
| f"formato={'β' if ok_f else 'β'}")
|
|
|
| if args.verbose:
|
| print(f"\n --- Output (primeros 400 chars) ---")
|
| for line in texto[:400].splitlines():
|
| print(f" {line}")
|
| print(" ...\n")
|
|
|
| resultados.append(ResultadoCaso(
|
| nombre=caso["nombre"],
|
| score=sc,
|
| ok_secciones=ok_s,
|
| ok_info=ok_i,
|
| ok_formato=ok_f,
|
| n_secciones=n_s,
|
| n_info=n_i,
|
| texto=texto,
|
| ))
|
|
|
|
|
| total = time.time() - t0_total
|
| scores = [r.score for r in resultados]
|
| aprobados = sum(1 for s in scores if s >= 0.6)
|
| promedio = sum(scores) / len(scores)
|
|
|
| print(f"\n{sep}")
|
| print(f" RESULTADO MILESTONE 3 β {total:.0f}s total")
|
| print(f"{sep}")
|
| print(f" β
Aprobados : {aprobados}/{len(resultados)} (umbral score β₯ 0.60)")
|
| print(f" π Score prom : {promedio:.3f}")
|
| print(f" π Score mΓn : {min(scores):.3f}")
|
| print(f" π Score mΓ‘x : {max(scores):.3f}")
|
|
|
|
|
| ok_s_total = sum(1 for r in resultados if r.ok_secciones)
|
| ok_i_total = sum(1 for r in resultados if r.ok_info)
|
| ok_f_total = sum(1 for r in resultados if r.ok_formato)
|
| print(f"\n Por criterio:")
|
| print(f" Secciones correctas : {ok_s_total}/{len(resultados)}")
|
| print(f" Info del scan : {ok_i_total}/{len(resultados)}")
|
| print(f" Formato vΓ‘lido : {ok_f_total}/{len(resultados)}")
|
|
|
| if promedio >= 0.7:
|
| print(f"\n π― MILESTONE 3 SUPERADO β el modelo genera AGENTS.md vΓ‘lidos")
|
| elif promedio >= 0.5:
|
| print(f"\n π PROGRESO β necesita mΓ‘s SFT sobre agents_sft.jsonl")
|
| else:
|
| print(f"\n π§ NECESITA SFT β score por debajo del umbral de progreso")
|
| print(f"{sep}\n")
|
|
|
|
|
| if __name__ == "__main__":
|
| main()
|
|
|