File size: 18,093 Bytes
a2d6a0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
#!/usr/bin/env python3
# SPDX-License-Identifier: BUSL-1.1
# Copyright (c) 2024-2026 Lucas Ricardo Mella Chillemi
"""

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 de evaluaciΓ³n (entornos sintΓ©ticos nunca vistos en entrenamiento)
# ─────────────────────────────────────────────────────────────────────────────

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,  # PostgreSQL/Redis estaban inactivos
        },
    },
    {
        "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,
        },
    },
]


# ─────────────────────────────────────────────────────────────────────────────
# Carga de modelo (Γ­dem eval_v3.py)
# ─────────────────────────────────────────────────────────────────────────────

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


# ─────────────────────────────────────────────────────────────────────────────
# GeneraciΓ³n (igual que eval_v3.py pero con max_tokens mΓ‘s alto)
# ─────────────────────────────────────────────────────────────────────────────

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

        # Detener en nueva secciΓ³n de instrucciΓ³n
        if decoded.count("### Scan:") > 0:
            idx = decoded.index("### Scan:")
            if idx > 50:
                return decoded[:idx].rstrip()

        # Detener si terminΓ³ el documento (lΓ­nea de separador final)
        if decoded.rstrip().endswith("```") and len(decoded) > 200:
            return decoded.rstrip()

    return tok.Decode(gen[len(ids):]).replace("\u2047", "\n")


# ─────────────────────────────────────────────────────────────────────────────
# VerificaciΓ³n
# ─────────────────────────────────────────────────────────────────────────────

@dataclass
class ResultadoCaso:
    nombre: str
    score: float       # 0.0 – 1.0
    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()

    # 1. Secciones mΓ­nimas
    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  # 75%

    # 2. Info del scan reflejada
    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  # 60%

    # 3. Formato vΓ‘lido (tiene headers ## y alguna tabla o bloque)
    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)


# ─────────────────────────────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────────────────────────────

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,
        ))

    # Resultado final
    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}")

    # Detalles por criterio
    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()