estrela-rome-v2 / scripts /run_blind_eval.py
mrj-crom's picture
Upload folder using huggingface_hub
28d5f26 verified
Raw
History Blame Contribute Delete
32.1 kB
"""
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 # 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"(?<![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()