| """ |
| 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/<rodada>/: |
| 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 |
| 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) |
| |
| |
| 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]]] |
| 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"(?<![A-Za-z])([A-Ea-e])\)", text) |
| if m_parentese: |
| return m_parentese[0].upper() |
| m = re.search( |
| r"(?:alternativa|resposta|letra|opção|opcao)\s*(?:correta|certa)?\s*" |
| r"(?:é|e|:|-|\)|\()?\s*[\"']?\(?([A-Ea-e])\)?[\"']?(?![A-Za-z])", |
| text, |
| re.IGNORECASE, |
| ) |
| if m: |
| return m.group(1).upper() |
| isoladas = re.findall(r"(?<![A-Za-z])([A-E])(?![A-Za-z])", text.upper()) |
| return isoladas[-1] if isoladas else None |
|
|
|
|
| def judge(case, raw_output, py_eval): |
| ref = case["ref"] |
| if ref["tipo"] == "multipla_escolha": |
| pred = extract_mcq_answer(raw_output) |
| ok = pred is not None and pred.upper() == ref["correta"].upper() |
| return ok, {"previsto": pred, "correta": ref["correta"]} |
| if ref["tipo"] == "execucao": |
| code = py_eval.extract_python_code(raw_output) |
| ok, detail = run_python_task(code, ref["test_code"]) |
| return ok, {"execucao": detail} |
| if ref["tipo"] == "rejeicao": |
| low = raw_output.lower() |
| alucinou = any(kw.lower() in low for kw in ref["alucinacao"]) |
| recusou = any(kw in low for kw in REJEICAO_GENERICA) or any( |
| kw.lower() in low for kw in ref["rejeicao"] |
| ) |
| if alucinou: |
| ok, msg = False, "Falha: Alucinou e inventou fatos falsos." |
| elif recusou: |
| ok, msg = True, "Correto: Rejeitou a premissa falsa sem alucinar!" |
| else: |
| ok, msg = False, "Resposta ambígua: sem recusa nem alucinação explícita." |
| return ok, { |
| "veredito_cetico": msg, |
| "pistas_recusa": [k for k in REJEICAO_GENERICA + ref["rejeicao"] if k.lower() in low][:5], |
| "pistas_alucinacao": [k for k in ref["alucinacao"] if k.lower() in low], |
| } |
| return False, {"erro": "tipo de caso desconhecido"} |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="Protocolo de Teste Cego EstrelaRosa") |
| parser.add_argument("--backend", choices=["sovereign", "hf", "hf_reft", "selfcheck"], default=None) |
| parser.add_argument("--checkpoint", default="checkpoints_sovereign/estrela_50k_sft_final.safetensors") |
| parser.add_argument("--model", default=None, help="model_id HF quando --backend hf|hf_reft") |
| parser.add_argument("--reft", default=None, |
| help="diretório do programa de intervenção (meta.json + intervencion.bin) " |
| "quando --backend hf_reft") |
| parser.add_argument("--reft-gate", choices=["off", "codigo"], default="off", |
| help="gate=codigo aplica a intervenção só em prompts de código (Opção A)") |
| parser.add_argument("--dtype", choices=["half", "float"], default="half") |
| parser.add_argument("--limit", type=int, default=None, help="casos por categoria") |
| parser.add_argument("--selfcheck", action="store_true", help="validação da fundação do avaliador") |
| parser.add_argument("--enem-file", default=None, |
| help="JSON externo de questões ENEM (ex.: data/enem/enem_2022_2024_formatado.json)") |
| parser.add_argument("--enem-sample", type=int, default=None, |
| help="sorteia N questões do arquivo externo (seed 42 fixa)") |
| parser.add_argument("--cetico-file", default=None, |
| help="JSON com banco expandido de armadilhas (ex.: data/cetico/armadilhas_expandidas.json)") |
| parser.add_argument("--codigo-file", default=None, |
| help="JSON com tarefas de código extras (ex.: data/codigo/tarefas_expandidas.json)") |
| parser.add_argument("--receita", default=None, |
| help="YAML do dev: bancos locais/HF + juízes registráveis") |
| args = parser.parse_args() |
|
|
| backend_name = "selfcheck" if args.selfcheck else (args.backend or "sovereign") |
|
|
| stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d_%H%M%S") |
| run_dir = os.path.join(OUTPUT_ROOT, f"{stamp}_{backend_name}") |
| os.makedirs(run_dir, exist_ok=True) |
|
|
| print("=" * 78) |
| print(f"盲 PROTOCOLO DE TESTE CEGO ESTRELAROSA — backend={backend_name}") |
| print(f"Logs: {run_dir}") |
| print("=" * 78) |
|
|
| if backend_name == "selfcheck": |
| backend = SelfcheckBackend() |
| elif backend_name == "sovereign": |
| backend = SovereignBackend(args.checkpoint, args.dtype) |
| elif backend_name == "hf_reft": |
| if not args.model or not args.reft: |
| parser.error("--model e --reft são obrigatórios com --backend hf_reft") |
| if args.dtype != "float": |
| print("[aviso] hf_reft força dtype=float (intervenção fp32 + bf16 CPU lento)") |
| backend = HFReftBackend(args.model, args.reft, gate=args.reft_gate) |
| else: |
| if not args.model: |
| parser.error("--model é obrigatório com --backend hf (ex.: Qwen/Qwen2.5-1.5B-Instruct)") |
| backend = HFBackend(args.model, args.dtype) |
|
|
| if getattr(args,'receita',None): |
| cases = build_case_bank(args.limit, receita_path=args.receita) |
| else: |
| cases = build_case_bank(args.limit, enem_file=args.enem_file, |
| enem_sample=args.enem_sample, cetico_file=args.cetico_file, |
| codigo_file=args.codigo_file) |
| print(f"\n[FASE A — GERAÇÃO CEGA] {len(cases)} casos, params fixos: {GEN_PARAMS}\n") |
|
|
| results_path = os.path.join(run_dir, "results.jsonl") |
| total_gen_s = 0.0 |
| total_tokens = 0 |
| with open(results_path, "w", encoding="utf-8") as rf: |
| for i, case in enumerate(cases, 1): |
| raw, n_tok, dt = backend.respond(case["prompt"]) |
| total_gen_s += dt |
| total_tokens += n_tok |
| rec = { |
| "idx": i, |
| "categoria": case["categoria"], |
| "id": case["id"], |
| "prompt": case["prompt"], |
| "raw_output": raw, |
| "tokens_gerados": n_tok, |
| "latencia_s": round(dt, 3), |
| "veredito": "PENDENTE_FASE_B", |
| } |
| rf.write(json.dumps(rec, ensure_ascii=False) + "\n") |
| preview = raw[:90].replace("\n", " ") |
| print(f" [{i:>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() |
|
|