Spaces:
Sleeping
Sleeping
| """ | |
| Layer 3 — Classificador humano dos pares divergentes. | |
| Uso: .venv\Scripts\python classify_divergent.py | |
| Teclas: | |
| S — Sim, concordante (q_good é genuinamente melhor que q_bad) | |
| N — Não, rejeitar (q_good NÃO é genuinamente melhor) | |
| ? — Incerto / deixar para depois | |
| Q — Salvar e sair (retoma de onde parou na próxima execução) | |
| Critério de avaliação: | |
| Uma reformulação é CONCORDANTE quando: | |
| 1. A nova pergunta tem metodologia mais clara — existem ferramentas, | |
| experimentos ou formalismos conhecidos para avançar nela. | |
| 2. Responder a nova pergunta faz progresso real em direção à original. | |
| 3. Não é apenas uma paráfrase ou troca de vocabulário. | |
| """ | |
| import json | |
| import os | |
| import sys | |
| from pathlib import Path | |
| from dotenv import load_dotenv | |
| load_dotenv(override=True) | |
| DIVERGENTES = Path("data/pairs/pairs_layer2_divergent.jsonl") | |
| CONCORDANTES = Path("data/pairs/pairs_layer2.jsonl") | |
| LOG_HUMANO = Path("data/pairs/layer3_decisoes_humanas.jsonl") | |
| # Cores ANSI (desabilitadas automaticamente fora de terminal) | |
| def _cor(texto: str, codigo: str) -> str: | |
| if not sys.stdout.isatty(): | |
| return texto | |
| return f"{codigo}{texto}\033[0m" | |
| AMARELO = "\033[93m" | |
| VERDE = "\033[92m" | |
| VERMELHO = "\033[91m" | |
| CIANO = "\033[96m" | |
| NEGRITO = "\033[1m" | |
| FRACO = "\033[2m" | |
| def carregar_decididos() -> set[str]: | |
| if not LOG_HUMANO.exists(): | |
| return set() | |
| decididos = set() | |
| for linha in LOG_HUMANO.read_text(encoding="utf-8").splitlines(): | |
| if linha.strip(): | |
| rec = json.loads(linha) | |
| if rec.get("decisao") in ("concordante", "rejeitado"): | |
| decididos.add(rec["source_id"]) | |
| return decididos | |
| def salvar_decisao(par: dict, decisao: str) -> None: | |
| LOG_HUMANO.parent.mkdir(parents=True, exist_ok=True) | |
| registro = { | |
| "source_id": par.get("source_id", ""), | |
| "q_bad": par["q_bad"], | |
| "q_good": par["q_good"], | |
| "dominio": par.get("domain", ""), | |
| "fonte": par.get("source", ""), | |
| "decisao": decisao, # "concordante" | "rejeitado" | "incerto" | |
| "n_anotadores": par.get("n_annotators", 0), | |
| "resumo_anotadores": [ | |
| { | |
| "modelo": a["model"].split("-")[0], | |
| "direcao": a["direction"], | |
| "magnitude": a["magnitude"], | |
| } | |
| for a in par.get("annotations", []) | |
| if not a.get("error") | |
| ], | |
| } | |
| with LOG_HUMANO.open("a", encoding="utf-8") as f: | |
| f.write(json.dumps(registro, ensure_ascii=False) + "\n") | |
| def aplicar_decisoes() -> tuple[int, int]: | |
| """Copia pares 'concordante' para pairs_layer2.jsonl.""" | |
| if not LOG_HUMANO.exists(): | |
| return 0, 0 | |
| decisoes = {} | |
| for linha in LOG_HUMANO.read_text(encoding="utf-8").splitlines(): | |
| if linha.strip(): | |
| rec = json.loads(linha) | |
| decisoes[rec["source_id"]] = rec["decisao"] | |
| divergentes = [ | |
| json.loads(l) for l in DIVERGENTES.read_text(encoding="utf-8").splitlines() if l.strip() | |
| ] | |
| adicionados = rejeitados = 0 | |
| with CONCORDANTES.open("a", encoding="utf-8") as f: | |
| for par in divergentes: | |
| sid = par.get("source_id", "") | |
| decisao = decisoes.get(sid) | |
| if decisao == "concordante": | |
| par["validado_humano"] = True | |
| par["passes_layer2"] = True | |
| f.write(json.dumps(par, ensure_ascii=False) + "\n") | |
| adicionados += 1 | |
| elif decisao == "rejeitado": | |
| rejeitados += 1 | |
| return adicionados, rejeitados | |
| def formatar_anotadores(par: dict) -> str: | |
| anots = [a for a in par.get("annotations", []) if not a.get("error")] | |
| if not anots: | |
| return _cor(" Nenhum anotador automático funcionou (falha de API)", FRACO) | |
| linhas = [] | |
| for a in anots: | |
| modelo = a["model"].split("-")[0].capitalize() | |
| direcao = "SIM" if a["direction"] else "NÃO" | |
| cor = VERDE if a["direction"] else VERMELHO | |
| linhas.append(f" {modelo}: {_cor(direcao, cor)} (magnitude {a['magnitude']:.2f})") | |
| return "\n".join(linhas) | |
| def limpar_tela() -> None: | |
| os.system("cls" if os.name == "nt" else "clear") | |
| # Cache de traduções para não repetir chamadas se relançar o script | |
| _cache_traducao: dict[str, dict] = {} | |
| def traduzir_par(par: dict) -> dict: | |
| """Retorna contexto, q_bad e q_good traduzidos para pt-br via Claude.""" | |
| sid = par.get("source_id", "") | |
| if sid in _cache_traducao: | |
| return _cache_traducao[sid] | |
| import anthropic | |
| client = anthropic.Anthropic() | |
| textos = { | |
| "contexto": (par.get("context") or "").strip()[:400], | |
| "q_bad": par["q_bad"], | |
| "q_good": par["q_good"], | |
| } | |
| prompt = ( | |
| "Traduza os três campos abaixo para o português brasileiro. " | |
| "Preserve termos técnicos científicos entre parênteses em inglês quando necessário. " | |
| "Responda SOMENTE com JSON válido, sem markdown.\n\n" | |
| f"{json.dumps(textos, ensure_ascii=False)}" | |
| ) | |
| try: | |
| msg = client.messages.create( | |
| model="claude-haiku-4-5-20251001", | |
| max_tokens=512, | |
| messages=[{"role": "user", "content": prompt}], | |
| ) | |
| import re | |
| raw = re.sub(r"```[a-z]*\n?", "", msg.content[0].text).strip() | |
| resultado = json.loads(raw) | |
| except Exception: | |
| # Fallback: retorna original sem tradução | |
| resultado = textos | |
| _cache_traducao[sid] = resultado | |
| return resultado | |
| def main() -> None: | |
| if not DIVERGENTES.exists(): | |
| print("Arquivo pairs_layer2_divergent.jsonl não encontrado.") | |
| return | |
| divergentes = [ | |
| json.loads(l) for l in DIVERGENTES.read_text(encoding="utf-8").splitlines() if l.strip() | |
| ] | |
| decididos = carregar_decididos() | |
| pendentes = [p for p in divergentes if p.get("source_id", "") not in decididos] | |
| limpar_tela() | |
| print(_cor("=== Layer 3 — Classificador Humano ===", NEGRITO)) | |
| print(f"Total divergentes : {len(divergentes)}") | |
| print(f"Já decididos : {len(decididos)}") | |
| print(f"Pendentes : {len(pendentes)}") | |
| print(_cor("\nTeclas: [S] Concordante [N] Rejeitar [?] Incerto [Q] Sair\n", CIANO)) | |
| input("Pressione ENTER para começar...") | |
| concordantes_n = rejeitados_n = incertos_n = 0 | |
| for idx, par in enumerate(pendentes, 1): | |
| n_ann = par.get("n_annotators", 0) | |
| if n_ann == 0: | |
| status = _cor(" ⚠ SEM ANOTAÇÃO AUTOMÁTICA", AMARELO) | |
| elif not par.get("agreement_direction"): | |
| status = _cor(" ⚠ DIREÇÃO DISCORDANTE", AMARELO) | |
| else: | |
| status = "" | |
| limpar_tela() | |
| # Traduz antes de exibir | |
| print( | |
| _cor(f"─── Par {idx} de {len(pendentes)}{status} ── traduzindo... ───", FRACO), | |
| end="\r", | |
| flush=True, | |
| ) | |
| trad = traduzir_par(par) | |
| limpar_tela() | |
| # Cabeçalho | |
| print(_cor(f"─── Par {idx} de {len(pendentes)}{status} ───", NEGRITO)) | |
| print( | |
| f"{_cor('Fonte:', FRACO)} {par.get('source','')} " | |
| f"{_cor('Domínio:', FRACO)} {par.get('domain','?')[:65]}" | |
| ) | |
| # Contexto traduzido | |
| contexto = trad.get("contexto", "").strip() | |
| if contexto: | |
| print(f"\n{_cor('Contexto do texto original:', CIANO)}") | |
| palavras = contexto.split() | |
| linha = " " | |
| for palavra in palavras: | |
| if len(linha) + len(palavra) > 90: | |
| print(linha) | |
| linha = " " + palavra + " " | |
| else: | |
| linha += palavra + " " | |
| if linha.strip(): | |
| print(linha) | |
| # Pergunta original traduzida | |
| print(f"\n{_cor('PERGUNTA ORIGINAL (q_bad):', VERMELHO)}") | |
| print(f" {trad.get('q_bad', par['q_bad'])}") | |
| # Reformulação traduzida | |
| print(f"\n{_cor('REFORMULAÇÃO (q_good):', VERDE)}") | |
| print(f" {trad.get('q_good', par['q_good'])}") | |
| # Anotadores automáticos | |
| print(f"\n{_cor('Anotadores automáticos:', FRACO)}") | |
| print(formatar_anotadores(par)) | |
| # Critério rápido | |
| print( | |
| f"\n{_cor('Critério:', FRACO)} q_good tem metodologia mais clara e " | |
| "avança em direção à resposta da q_bad?" | |
| ) | |
| print() | |
| while True: | |
| try: | |
| tecla = input(_cor(" Decisão [S / N / ? / Q]: ", NEGRITO)).strip().upper() | |
| except (EOFError, KeyboardInterrupt): | |
| tecla = "Q" | |
| if tecla == "Q": | |
| adicionados, rej = aplicar_decisoes() | |
| print(f"\n💾 Salvo. {adicionados} adicionados à Layer 2, " f"{rej} rejeitados.") | |
| restantes = len(pendentes) - idx | |
| if restantes > 0: | |
| print(f" {restantes} par(es) restante(s) — execute novamente para continuar.") | |
| return | |
| if tecla == "S": | |
| salvar_decisao(par, "concordante") | |
| concordantes_n += 1 | |
| print(_cor(" ✔ Marcado como CONCORDANTE", VERDE)) | |
| break | |
| if tecla == "N": | |
| salvar_decisao(par, "rejeitado") | |
| rejeitados_n += 1 | |
| print(_cor(" ✗ Marcado como REJEITADO", VERMELHO)) | |
| break | |
| if tecla == "?": | |
| salvar_decisao(par, "incerto") | |
| incertos_n += 1 | |
| print(_cor(" ~ Marcado como INCERTO", AMARELO)) | |
| break | |
| print(" Tecla inválida. Use S, N, ? ou Q.") | |
| # Todos classificados | |
| adicionados, rej = aplicar_decisoes() | |
| limpar_tela() | |
| print(_cor("=== Classificação concluída! ===\n", NEGRITO)) | |
| print(f" Concordantes : {concordantes_n}") | |
| print(f" Rejeitados : {rejeitados_n}") | |
| print(f" Incertos : {incertos_n}") | |
| print(f"\n {adicionados} par(es) adicionados a pairs_layer2.jsonl") | |
| print(f" {rej} par(es) rejeitados") | |
| print(f"\n Decisões salvas em: {LOG_HUMANO}") | |
| if __name__ == "__main__": | |
| main() | |