""" Avaliador Cego EstrelaRosa — Protocolo de Teste Cego com Logs Completos (Fase 12) Princípios do protocolo: 1. GERAÇÃO CEGA: todas as respostas são geradas ANTES do julgamento, com parâmetros fixos (temperature=0.0 greedy, sem repetição penalizada) para reprodutibilidade. 2. ZERO VAZAMENTO: prompts nunca contêm o gabarito; referências só entram na Fase B. 3. EXECUÇÃO REAL: código Python gerado é executado em subprocess isolado com timeout. 4. LOG INTEGRAL: nenhuma resposta é truncada nos logs (diferente dos avaliadores antigos). 5. LEDGER SHA-256: cadeia criptográfica de todos os artefatos da rodada. 6. COMPARAÇÃO AUTOMÁTICA: cada rodada é comparada com todas as rodadas anteriores. Uso: python3 scripts/run_blind_eval.py --selfcheck python3 scripts/run_blind_eval.py --backend sovereign \ --checkpoint checkpoints_sovereign/estrela_50k_sft_final.safetensors [--limit N] python3 scripts/run_blind_eval.py --backend hf --model Qwen/Qwen2.5-1.5B-Instruct [--limit N] Logs gerados em output/blind_eval//: meta.json | results.jsonl | judged.jsonl | summary.json | ledger.sha256 | ANALISE.md """ import argparse import datetime import hashlib import json import os import re import subprocess import sys import tempfile import time ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) if ROOT_DIR not in sys.path: sys.path.insert(0, ROOT_DIR) from sovereign_llm_engine.benchmarks.enem_evaluator import ENEMEvaluator from sovereign_llm_engine.benchmarks.oab_evaluator import OABEvaluator from sovereign_llm_engine.benchmarks.python_coding_evaluator import PythonCodingEvaluator from sovereign_llm_engine.benchmarks.skeptical_blind_evaluator import SkepticalBlindEvaluator GEN_PARAMS = { "temperature": 0.0, "top_k": 0, "top_p": 1.0, "repetition_penalty": 1.0, "max_new_tokens": 160, } OUTPUT_ROOT = os.path.join(ROOT_DIR, "output", "blind_eval") def sha256_file(path): h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(1 << 20), b""): h.update(chunk) return h.hexdigest() def build_case_bank(limit=None, enem_file=None, enem_sample=None, cetico_file=None, codigo_file=None, receita_path=None, usar_padrao=True): if receita_path: sys.path.insert(0, ROOT_DIR if (ROOT_DIR:=os.path.dirname(os.path.abspath(__file__)).replace('/scripts','')) else '') from scripts.julgadores import carregar_banco, registrar_judges_dummy # noqa import yaml cfg = yaml.safe_load(open(receita_path, encoding='utf-8')) os.chdir(os.path.dirname(os.path.abspath(__file__)) or '.') if cfg.get('judges_custom'): from scripts.julgadores import carregar_judges_custom as _c; _c(cfg['judges_custom']) cases=[] limite=cfg.get('limites_por_categoria') contador={} for b in cfg.get('bancos',[]): cs=carregar_banco(b) for c in cs: cat=c['categoria'] if limite and contador.get(cat,0)>=limite: continue contador[cat]=contador.get(cat,0)+1 ref={'tipo':'execucao'} if c['juiz']=='execucao_python' else {'tipo':'receita'} ref.update(c['ref']); ref['_juiz']=c['juiz']; ref['_caso']=c cases.append({'categoria':cat,'id':c['id'],'prompt':c['prompt'],'ref':ref}) return cases """Monta o banco de casos REAIS reutilizando os bancos curados existentes. enem_file: JSON externo no formato do ENEMEvaluator (ex.: 537 questões INEP). enem_sample: N questões sorteadas com seed fixa (reprodutibilidade). cetico_file: JSON com banco expandido de armadilhas (data/cetico/*.json). codigo_file: JSON com tarefas de código adicionais (data/codigo/*.json).""" cases = [] casos_cetico_vistos = set() if enem_file: ev = ENEMEvaluator(external_file=enem_file) pool = list(ev.questions) if enem_sample and enem_sample < len(pool): import random random.Random(42).shuffle(pool) pool = pool[:enem_sample] for q in pool: cases.append({ "categoria": "enem_raciocinio", "id": q["id"], "prompt": ev.format_prompt(q), "ref": {"tipo": "multipla_escolha", "correta": q["correct_answer"], "area": q.get("area", "")}, }) if cetico_file: with open(cetico_file, encoding="utf-8") as f: banco = json.load(f) for c in banco.get("casos", []): casos_cetico_vistos.add(c["id"]) cases.append({ "categoria": "cetico_armadilhas", "id": c["id"], "prompt": c["prompt"], "ref": { "tipo": "rejeicao", "rejeicao": c.get("expected_rejection", []), "alucinacao": c.get("hallucination_indicators", []), }, }) for q in ENEMEvaluator().questions: if any(c["id"] == q["id"] for c in cases): continue cases.append({ "categoria": "enem_raciocinio", "id": q["id"], "prompt": ENEMEvaluator().format_prompt(q), "ref": {"tipo": "multipla_escolha", "correta": q["correct_answer"], "area": q.get("area", "")}, }) for q in OABEvaluator().questions: cases.append({ "categoria": "direito_oab", "id": q["id"], "prompt": OABEvaluator().format_prompt(q), "ref": {"tipo": "multipla_escolha", "correta": q["correct_answer"], "area": q.get("area", "")}, }) for t in PythonCodingEvaluator().tasks: cases.append({ "categoria": "codigo_python", "id": t.get("id", t.get("task_id", "PY")), "prompt": t["prompt"] if "prompt" in t else t.get("instruction", ""), "ref": {"tipo": "execucao", "test_code": t["test_code"], "assinatura": t.get("function_name", "")}, }) if codigo_file: with open(codigo_file, encoding="utf-8") as f: banco_cod = json.load(f) for t in banco_cod.get("tarefas", []): cases.append({ "categoria": "codigo_python", "id": t.get("id", "PY-X"), "prompt": t["prompt"], "ref": {"tipo": "execucao", "test_code": t["test_code"], "assinatura": t.get("function_name", "")}, }) for c in SkepticalBlindEvaluator().trap_cases: if c["id"] in casos_cetico_vistos: continue cases.append({ "categoria": "cetico_armadilhas", "id": c["id"], "prompt": c["prompt"], "ref": { "tipo": "rejeicao", "rejeicao": c["expected_rejection"], "alucinacao": c["hallucination_indicators"], }, }) if limit: by_cat = {} selected = [] for c in cases: n = by_cat.get(c["categoria"], 0) if n < limit: selected.append(c) by_cat[c["categoria"]] = n + 1 cases = selected return cases class SovereignBackend: """Modelo EstrelaRosa treinado na H100 (arquitetura densa, config model_1_5b.json).""" def __init__(self, checkpoint, dtype="half"): import torch from tokenizers import Tokenizer from safetensors.torch import load_file torch.set_num_threads(max(1, (os.cpu_count() or 4) // 2)) torch.set_num_interop_threads(1) self.torch = torch cfg_path = os.path.join(ROOT_DIR, "sovereign_llm_engine/configs/model_1_5b.json") tok_path = os.path.join(ROOT_DIR, "sovereign_llm_engine/datasets/tokenizer_32k/tokenizer.json") ckpt_path = os.path.join(ROOT_DIR, checkpoint) from sovereign_llm_engine.training.sovereign_transformer import ( SovereignTransformerLM, SovereignModelConfig, precompute_rope_freqs_cis, ) print(f"[backend=sovereign] Carregando tokenizer: {tok_path}") self.tokenizer = Tokenizer.from_file(tok_path) config = SovereignModelConfig.from_json(cfg_path) print(f"[backend=sovereign] Instanciando modelo ({dtype})...") with torch.device("meta"): model = SovereignTransformerLM(config) model = model.to_empty(device="cpu") if dtype == "half": model = model.half() print(f"[backend=sovereign] Carregando pesos: {ckpt_path}") state = load_file(ckpt_path) cast = (lambda v: v.cpu().half()) if dtype == "half" else (lambda v: v.cpu().float()) loaded = {k: cast(v) for k, v in state.items()} missing, unexpected = model.load_state_dict(loaded, strict=False) if missing: print(f"[AVISO] Tensores ausentes não carregados: {len(missing)} -> {missing[:4]}") if unexpected: print(f"[AVISO] Tensores inesperados no checkpoint: {len(unexpected)}") head_dim = config.dim // config.n_heads model.freqs_cis = precompute_rope_freqs_cis(head_dim, config.max_seq_len * 2, config.rope_theta) model.eval() self.model = model self.fingerprint_files = [ckpt_path] def respond(self, prompt): torch = self.torch chatml = f"<|user|>\n{prompt}\n<|assistant|>\n" ids = self.tokenizer.encode(chatml).ids inp = torch.tensor([ids], dtype=torch.long) t0 = time.time() with torch.no_grad(): out = self.model.generate(inp, eos_token_id=self.tokenizer.token_to_id("<|eos|>"), **GEN_PARAMS) dt = time.time() - t0 n_new = out.shape[1] - len(ids) text = self.tokenizer.decode(out[0][len(ids):].tolist()) return text.strip(), n_new, dt class HFBackend: """Modelo base pré-treinado do HuggingFace (ex.: Qwen2.5-1.5B-Instruct) — Passo 2.""" def __init__(self, model_id, dtype="half"): import torch from transformers import AutoModelForCausalLM, AutoTokenizer torch.set_num_threads(max(1, (os.cpu_count() or 4) // 2)) self.torch = torch print(f"[backend=hf] Baixando/carregando {model_id}...") self.tok = AutoTokenizer.from_pretrained(model_id) self.model = AutoModelForCausalLM.from_pretrained( model_id, torch_dtype=torch.float16 if dtype == "half" else torch.float32, ).eval() self._dev = ("cuda" if os.environ.get("ESTRELA_DEVICE", "auto") != "cpu" and torch.cuda.is_available() else "cpu") if self._dev == "cuda": self.model = self.model.to("cuda") print(f"[backend=hf] device=cuda ({torch.cuda.get_device_name(0)})") self.model_id = model_id self.fingerprint_files = [] def fingerprint(self): return {"model_id": self.model_id} def respond(self, prompt): torch = self.torch messages = [{"role": "user", "content": prompt}] try: enc = self.tok.apply_chat_template(messages, add_generation_prompt=True, return_dict=True) token_ids = enc["input_ids"] if token_ids and isinstance(token_ids[0], list): token_ids = token_ids[0] ids = torch.tensor([token_ids], dtype=torch.long) except Exception: ids = self.tok(prompt, return_tensors="pt").input_ids ids = ids.to(getattr(self, "_dev", "cpu")) t0 = time.time() with torch.no_grad(): out = self.model.generate( ids, max_new_tokens=GEN_PARAMS["max_new_tokens"], do_sample=False, pad_token_id=self.tok.eos_token_id, ) dt = time.time() - t0 n_new = out.shape[1] - ids.shape[1] text = self.tok.decode(out[0][ids.shape[1]:], skip_special_tokens=True) return text.strip(), n_new, dt class HFReftBackend: """Modelo HF + intervenção ReFT (programa de cérebro <1MB) — Fase 3.2. Geração com intervenção aplicada na última posição do prompt (intervene_on_prompt). dtype float OBRIGATÓRIO: intervenção é fp32 e bf16/fp16 CPU é ~860× lento nesta máquina. Gate opcional (--reft-gate codigo): aplica a intervenção APENAS em prompts que parecem pedidos de código (heurística conservadora) — localidade por construção.""" PALAVRAS_CODIGO = [ "python", "função", "funcao", "escreva uma função", "crie uma função", "implemente", "```", "def ", "script de computador", "algoritmo", "pass@1", "assert", "programa em python", ] def __init__(self, model_id, reft_dir, gate="off"): import torch import pyreft from pyreft import LoreftIntervention from transformers import AutoModelForCausalLM, AutoTokenizer torch.set_num_threads(max(1, (os.cpu_count() or 4) // 2)) self.torch = torch print(f"[backend=hf_reft] Carregando {model_id} (fp32) + intervenção {reft_dir}") meta_path = os.path.join(reft_dir, "meta.json") with open(meta_path, encoding="utf-8") as f: self.meta = json.load(f) layer = int(self.meta["layer"]) rank = int(self.meta["rank"]) hidden = int(self.meta.get("embed_dim", 0)) or None self.tok = AutoTokenizer.from_pretrained(model_id) self.model = AutoModelForCausalLM.from_pretrained( model_id, torch_dtype=torch.float32).eval() if hidden is None: hidden = self.model.config.hidden_size iv = LoreftIntervention(embed_dim=hidden, low_rank_dimension=rank, dtype=torch.float32) sd = torch.load(os.path.join(reft_dir, "intervencion.bin"), weights_only=True) # formato canônico pyreft: rotate_layer [embed_dim, rank]; o load custom da classe # recria o rotate layer e valida allclose — learned_source entra por strict=False. iv.load_state_dict(sd, strict=False) self.reft_config = pyreft.ReftConfig(representations=[{ "layer": layer, "component": "block_output", "intervention": iv, }]) self.reft_model = pyreft.get_reft_model(self.model, self.reft_config) self._dev = ("cuda" if os.environ.get("ESTRELA_DEVICE", "auto") != "cpu" and torch.cuda.is_available() else "cpu") if self._dev == "cuda": self.reft_model.to("cuda") print(f"[backend=hf_reft] device=cuda ({torch.cuda.get_device_name(0)})") self.layer = layer self.gate = gate self.model_id = model_id self.fingerprint_files = [ os.path.join(model_id, "model.safetensors"), os.path.join(reft_dir, "intervencion.bin"), ] def _parece_codigo(self, prompt): low = prompt.lower() return any(p in low for p in self.PALAVRAS_CODIGO) def fingerprint(self): return { "model_id": self.model_id, "reft_gate": self.gate, "reft": {k: self.meta[k] for k in ("nome", "metodo", "layer", "rank", "dataset", "modelo_base_sha256") if k in self.meta}, } def respond(self, prompt): torch = self.torch messages = [{"role": "user", "content": prompt}] try: enc = self.tok.apply_chat_template(messages, add_generation_prompt=True, return_dict=True) token_ids = enc["input_ids"] if token_ids and isinstance(token_ids[0], list): token_ids = token_ids[0] ids = torch.tensor([token_ids], dtype=torch.long) except Exception: ids = self.tok(prompt, return_tensors="pt").input_ids ids = ids.to(getattr(self, "_dev", "cpu")) aplicar = self.gate == "off" or (self.gate == "codigo" and self._parece_codigo(prompt)) pos_ultima = [[[ids.shape[1] - 1]]] # [layers=1][batch=1][posições=[última do prompt]] t0 = time.time() with torch.no_grad(): if aplicar: _, out = self.reft_model.generate( {"input_ids": ids}, unit_locations={"sources->base": (None, pos_ultima)}, intervene_on_prompt=True, max_new_tokens=GEN_PARAMS["max_new_tokens"], do_sample=False, pad_token_id=self.tok.eos_token_id, ) else: out = self.model.generate( ids, max_new_tokens=GEN_PARAMS["max_new_tokens"], do_sample=False, pad_token_id=self.tok.eos_token_id, ) dt = time.time() - t0 n_new = out.shape[1] - ids.shape[1] text = self.tok.decode(out[0][ids.shape[1]:], skip_special_tokens=True) return text.strip(), n_new, dt class SelfcheckBackend: """Valida a FUNDAÇÃO do avaliador (plumbing) com respondedor determinístico. Não é resultado de modelo — serve apenas para provar que geração, julgamento, execução real de código, logs e ledger funcionam ponta a ponta.""" def __init__(self): self.fingerprint_files = [] self._seq = 0 def fingerprint(self): return {"mode": "selfcheck"} def respond(self, prompt): self._seq += 1 low = prompt.lower() if "quil" in low and "leite" in low or "marte" in low or "brasília" in low.replace("brasília", "brasilia"): text = "Essa pergunta parte de uma premissa falsa: isso não existe." elif "fatorial" in low: text = "```python\ndef fatorial(n):\n return 1 if n <= 1 else n * fatorial(n - 1)\n```" elif any(k in low for k in ["constituição", "clt", "código civil", "maioridade"]): text = "Resposta: C" else: text = f"Resposta: {'ABCCB'[self._seq % 5]}" return text, len(text.split()), 0.001 def run_python_task(code_str, test_code, timeout=10): """Executa código gerado em subprocess isolado (sandbox por processo+timeout).""" full = code_str.rstrip() + "\n\n" + test_code + "\nprint('__OK__')\n" with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False, encoding="utf-8") as f: f.write(full) tmp = f.name try: proc = subprocess.run( [sys.executable, tmp], capture_output=True, text=True, timeout=timeout, cwd=tempfile.gettempdir(), ) passed = proc.returncode == 0 and "__OK__" in proc.stdout detail = (proc.stdout[-400:] + "|ERR|" + proc.stderr[-400:]) if not passed else "asserts OK" return passed, detail except subprocess.TimeoutExpired: return False, f"TIMEOUT {timeout}s" except Exception as e: return False, f"{type(e).__name__}: {e}" finally: os.unlink(tmp) REJEICAO_GENERICA = [ "não existe", "não tem", "não há", "não foi", "não possui", "não faz sentido", "não é possível", "não pode", "não é uma medida", "não há resposta", "nenhum", "premissa falsa", "incorreta", "incompatíveis", "unidades diferentes", "desabitado", "não corresponde", "não se aplica", "não faz parte", ] def extract_mcq_answer(model_output): """Extrator robusto: (0) letra com parêntese 'C)' tem prioridade máxima; (1) padrões explícitos 'alternativa/resposta é X'; (2) última letra isolada. O passo 0 existe porque 'e' português dentro da frase capturava como letra no fallback (bug documentado na sessão 26/08).""" text = re.sub(r"[*_`#>]", "", model_output).strip() m_parentese = re.findall(r"(?3}/{len(cases)}] {case['categoria']:<18} {case['id']:<14} {dt:6.2f}s {preview}") print("\n[FASE B — JULGAMENTO] Gabaritos aplicados APÓS toda geração concluída.\n") py_eval = PythonCodingEvaluator() refs = {c["id"]: c for c in cases} judged_path = os.path.join(run_dir, "judged.jsonl") scores = {} judged_records = [] with open(results_path, encoding="utf-8") as rf, open(judged_path, "w", encoding="utf-8") as jf: for line in rf: rec = json.loads(line) case = refs[rec["id"]] ok, evidence = judge(case, rec["raw_output"], py_eval) cat = rec["categoria"] s = scores.setdefault(cat, {"passed": 0, "total": 0}) s["total"] += 1 s["passed"] += int(ok) rec.update({ "veredito": "PASSOU" if ok else "FALHOU", "evidencia": evidence, "referencia": case["ref"], }) judged_records.append(rec) jf.write(json.dumps(rec, ensure_ascii=False) + "\n") for cat, s in scores.items(): s["score_0_100"] = round(s["passed"] / s["total"] * 100, 2) if s["total"] else 0.0 meta = { "timestamp_utc": stamp, "backend": backend_name, "protocolo": "cego_duas_fases_v1", "gen_params": GEN_PARAMS, "limit_por_categoria": args.limit, "modelo_fingerprint": ( {os.path.basename(p): sha256_file(p)[:16] for p in getattr(backend, "fingerprint_files", [])} or backend.fingerprint() ), "total_casos": len(cases), } meta_path = os.path.join(run_dir, "meta.json") with open(meta_path, "w", encoding="utf-8") as f: json.dump(meta, f, indent=2, ensure_ascii=False) overall = round(sum(s["score_0_100"] for s in scores.values()) / len(scores), 2) if scores else 0.0 summary = { **meta, "scores_por_categoria": scores, "media_geral_0_100": overall, "tokens_totais": total_tokens, "tempo_geracao_total_s": round(total_gen_s, 2), "tokens_por_segundo_medios": round(total_tokens / total_gen_s, 2) if total_gen_s > 0 else None, } summary_path = os.path.join(run_dir, "summary.json") with open(summary_path, "w", encoding="utf-8") as f: json.dump(summary, f, indent=2, ensure_ascii=False) hash_lines = [] for fname in ["meta.json", "results.jsonl", "judged.jsonl", "summary.json"]: p = os.path.join(run_dir, fname) hash_lines.append(f"{sha256_file(p)} {fname}") chain = hashlib.sha256("\n".join(hash_lines).encode()).hexdigest() hash_lines.append(f"{chain} CHAIN_SHA256") ledger_path = os.path.join(run_dir, "ledger.sha256") with open(ledger_path, "w", encoding="utf-8") as f: f.write("\n".join(hash_lines) + "\n") anteriores = [] if os.path.isdir(OUTPUT_ROOT): for d in sorted(os.listdir(OUTPUT_ROOT)): sp = os.path.join(OUTPUT_ROOT, d, "summary.json") if d != os.path.basename(run_dir) and os.path.isfile(sp): with open(sp, encoding="utf-8") as f: anteriores.append(json.load(f)) comp_md = "" if anteriores: rows = ["| Rodada | Backend | Média Geral | ENEM | OAB | Código | Cético |", "| :--- | :--- | ---: | ---: | ---: | ---: | ---: |"] for a in anteriores + [summary]: sc = a.get("scores_por_categoria", {}) def g(k): return sc.get(k, {}).get("score_0_100", "-") rows.append( f"| {a['timestamp_utc']} | {a['backend']} | {a.get('media_geral_0_100','-')} " f"| {g('enem_raciocinio')} | {g('direito_oab')} | {g('codigo_python')} | {g('cetico_armadilhas')} |" ) comp_md = "\n## 📈 Comparação Histórica Automática\n\n" + "\n".join(rows) + "\n" linhas_tabela = [ f"| {cat} | {s['passed']}/{s['total']} | **{s['score_0_100']} / 100** |" for cat, s in scores.items() ] analise_md = f"""# 🕵️ Análise Final — Rodada Cega `{stamp}` ({backend_name}) - **Protocolo:** cego em duas fases (geração → julgamento), parâmetros fixos greedy - **Casos:** {len(cases)} | **Tokens gerados:** {total_tokens} | **Velocidade média:** {summary['tokens_por_segundo_medios']} tok/s - **Média geral:** **{overall} / 100** - **Integridade:** cadeia SHA-256 em `ledger.sha256` (chain `{chain[:12]}…`) ## 🏆 Resultados por Categoria | Categoria | Acertos | Nota | | :--- | :--- | ---: | {chr(10).join(linhas_tabela)} {comp_md} ## 🔍 Como Analisar 1. **Código Python:** veredito por EXECUÇÃO REAL em subprocess isolado — olhe `evidencia.execucao` nos registros `FALHOU` de `judged.jsonl` para ver stderr exato. 2. **Cético:** `veredito_cetico` mostra se o modelo recusou a premissa falsa ou alucinou. 3. **Múltipla escolha:** compare `evidencia.previsto` vs `referencia.correta`. 4. **Reprodutibilidade:** mesmos `gen_params` + mesmo fingerprint de modelo ⇒ mesma saída esperada. 5. Rodadas anteriores ficam em `output/blind_eval/` — a tabela acima compara tudo automaticamente. > ⚠️ Se esta rodada foi `--selfcheck`, os números validam a FUNDAÇÃO do avaliador > (logs, julgamento, execução, hashes) — não representam qualidade de nenhum modelo. """ analise_path = os.path.join(run_dir, "ANALISE.md") with open(analise_path, "w", encoding="utf-8") as f: f.write(analise_md) print("\n" + "=" * 78) print(f"📊 RESULTADO ({'SELFCHECK' if backend_name == 'selfcheck' else 'MODELO REAL'}):") for cat, s in scores.items(): print(f" • {cat:<20} {s['passed']:>2}/{s['total']:<3} -> {s['score_0_100']:>6.2f}/100") print(f" 🌟 MÉDIA GERAL: {overall}/100") print("=" * 78) print(f"📁 Logs completos: {run_dir}") for fname in ["meta.json", "results.jsonl", "judged.jsonl", "summary.json", "ledger.sha256", "ANALISE.md"]: print(f" • {fname}") if __name__ == "__main__": main()