""" ReformulatEE — Gradio standalone app. Equivalente à interface do notebook, mas sem precisar do Jupyter. Uso: .venv\\Scripts\\python app.py """ import hashlib import json import logging import os import sys import warnings warnings.filterwarnings("ignore") # Audit logger — escreve JSON estruturado para stderr (capturado pelo HF Space logs) _audit = logging.getLogger("reformulatee.audit") _audit.setLevel(logging.INFO) _audit_handler = logging.StreamHandler(sys.stderr) _audit_handler.setFormatter(logging.Formatter("%(message)s")) _audit.addHandler(_audit_handler) _audit.propagate = False def _audit_log(action: str, **kwargs) -> None: import time record = {"action": action, "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), **kwargs} _audit.info(json.dumps(record, ensure_ascii=False)) sys.path.insert(0, os.path.dirname(__file__)) from dotenv import load_dotenv load_dotenv(override=True) # Online (HF Space): usa Claude API — HF Inference API não suporta este modelo # Local: auto-detecta (Ollama se disponível, senão Claude) if os.getenv("SPACE_ID"): os.environ["INFERENCE_BACKEND"] = "claude" else: os.environ.setdefault("INFERENCE_BACKEND", "auto") import gradio as gr from src.db.historico import init_db from src.db.historico import registrar_feedback from src.db.historico import salvar from src.db.historico import ultimas init_db() # Validação antecipada de variáveis obrigatórias — falha rápido com mensagem clara _on_spaces_early = bool(os.getenv("SPACE_ID")) if _on_spaces_early and not os.getenv("ANTHROPIC_API_KEY"): raise EnvironmentError( "[startup] ANTHROPIC_API_KEY não configurada. " "Adicione o secret em Settings → Secrets do HF Space." ) if not os.getenv("HF_TOKEN"): print("[startup] HF_TOKEN ausente — logging e histórico cross-session desativados.") _initialized = False def _init(): global _initialized if _initialized: return from src.rl.inference import _get_index _get_index() _initialized = True _MAX_INPUT_CHARS = 500 # Rate limiting: máx 10 requisições por sessão por janela de 60 segundos import collections import threading import time as _time _rl_lock = threading.Lock() _rl_windows: dict[str, collections.deque] = {} _RL_MAX = 10 _RL_WINDOW = 60 def _check_rate_limit(session_id: str) -> bool: """Retorna True se a requisição está dentro do limite, False se excedeu.""" now = _time.monotonic() with _rl_lock: if session_id not in _rl_windows: _rl_windows[session_id] = collections.deque() dq = _rl_windows[session_id] while dq and now - dq[0] > _RL_WINDOW: dq.popleft() if len(dq) >= _RL_MAX: return False dq.append(now) return True def reformular_ui(pergunta, idioma, request: gr.Request = None): """Processa a pergunta e retorna (html, dataset, record_id, feedback_row, btn+, btn-).""" _vazio = ( '

Digite uma pergunta.

', gr.update(), None, gr.update(visible=False), gr.update(interactive=True, value="👍 Boa reformulação"), gr.update(interactive=True, value="👎 Pode melhorar"), ) if not pergunta.strip(): return _vazio session_id = getattr(request, "session_hash", "anon") if request else "anon" # Hash da sessão para não logar identificador direto session_hash = hashlib.sha256(session_id.encode()).hexdigest()[:12] if not _check_rate_limit(session_id): _audit_log("rate_limit_exceeded", session=session_hash, idioma=idioma) return ( '

Limite de requisições atingido. Aguarde 1 minuto e tente novamente.

', gr.update(), None, gr.update(visible=False), gr.update(interactive=True, value="👍 Boa reformulação"), gr.update(interactive=True, value="👎 Pode melhorar"), ) pergunta = pergunta[:_MAX_INPUT_CHARS] _init() from src.rl.inference import reformular from src.rl.inference import reformular_ptbr ptbr = idioma == "Português" try: r = reformular_ptbr(pergunta, n=8) if ptbr else reformular(pergunta, n=8) except Exception as e: print(f"[reformular_ui] erro: {e}") erro = ( '

Ocorreu um erro ao processar a pergunta. Tente novamente.

', gr.update(), None, gr.update(visible=False), gr.update(interactive=True, value="👍 Boa reformulação"), gr.update(interactive=True, value="👎 Pode melhorar"), ) return erro if ptbr: entrada, saida = r.q_bad_pt, r.best_pt sub_in = f'
🔤 {r.q_bad_en}
' sub_out = f'
🔤 {r.best_en}
' cands, ee_bad, ee_best, passed = r.candidates, r.ee_bad, r.ee_best, r.stage1_pass else: entrada, saida = r.q_bad, r.best sub_in = sub_out = "" cands, ee_bad, ee_best, passed = r.candidates, r.ee_bad, r.ee_best, r.stage1_pass # Persiste no banco e obtém o ID para o feedback record_id = salvar( idioma=idioma, pergunta_orig=pergunta, pergunta_en=r.q_bad_en if ptbr else None, candidatos=cands, melhor=r.best_en if ptbr else r.best, melhor_pt=r.best_pt if ptbr else None, ee_antes=ee_bad, ee_depois=ee_best, stage1_pass=passed, ) _audit_log( "reformulate", session=session_hash, idioma=idioma, record_id=record_id, ee_antes=round(ee_bad, 3), ee_depois=round(ee_best, 3), stage1_pass=passed, ) delta = ee_best - ee_bad ganho_pct = delta / max(ee_bad, 0.001) * 100 filtro = "✅ PASS" if passed else "⚠️ Fallback" fc = "#27ae60" if passed else "#e67e22" def bar(v): pct = min(int(v * 100), 100) c = "#2ecc71" if pct >= 70 else "#f39c12" if pct >= 40 else "#e74c3c" return ( f'
' f'
' f'
' f'{v:.3f}
' ) top = sorted(cands, key=lambda c: c["score"], reverse=True) rows = "".join( f'' f'{"🟢" if c["ee"]>ee_bad+0.05 else "🔴"} {i}' f'{c["ee"]:.3f}' f'{c["text"]}' for i, c in enumerate(top, 1) ) html = f"""
Pergunta original
{entrada}
{sub_in}
Reformulação epistêmica
{saida}
{sub_out}
EE ANTES
{bar(ee_bad)}
EE DEPOIS
{bar(ee_best)}
GANHO
+{delta:.3f} ({ganho_pct:.0f}%)
FILTRO
{filtro}
▶ Ver {len(cands)} candidatos {rows}
# EE Reformulação
""" return ( html, gr.update(samples=ultimas(8)), record_id, gr.update(visible=True), gr.update(interactive=True, value="👍 Boa reformulação"), gr.update(interactive=True, value="👎 Pode melhorar"), ) def dar_feedback(record_id, valor: int): """Registra feedback e desativa os botões.""" if record_id is not None: registrar_feedback(record_id, valor) _audit_log("feedback", record_id=record_id, valor=valor) label = "✅ Obrigado!" if valor == 1 else "✅ Registrado!" return ( gr.update(interactive=False, value=label if valor == 1 else "👍 Boa reformulação"), gr.update(interactive=False, value=label if valor == -1 else "👎 Pode melhorar"), ) with gr.Blocks(title="ReformulatEE", theme=gr.themes.Soft()) as app: gr.Markdown("## 🔬 ReformulatEE — Reformulação Epistêmica") gr.Markdown( "

" "As perguntas submetidas são registradas anonimamente para fins de pesquisa e melhoria do modelo. " "Não submeta informações pessoais ou confidenciais." "

" ) with gr.Row(): with gr.Column(scale=3): inp_q = gr.Textbox( label="Pergunta de pesquisa", placeholder="Digite sua pergunta de pesquisa aqui...", lines=3, ) with gr.Column(scale=1): inp_idioma = gr.Radio( choices=["Português", "English"], value="Português", label="Idioma" ) btn = gr.Button("🔄 Reformular", variant="primary", size="lg") out = gr.HTML() # Feedback — oculto até haver resultado estado_id = gr.State(value=None) with gr.Row(visible=False) as feedback_row: btn_pos = gr.Button("👍 Boa reformulação", variant="secondary", size="sm") btn_neg = gr.Button("👎 Pode melhorar", variant="secondary", size="sm") _EXEMPLOS = [ ["O que causa o envelhecimento biológico?", "Português"], ["O que é consciência?", "Português"], ["Livre-arbítrio existe?", "Português"], ["What is the meaning of life?", "English"], ["Is consciousness fundamental?", "English"], ["What causes aging?", "English"], ] _samples_iniciais = ultimas(8) or _EXEMPLOS historico = gr.Dataset( components=[inp_q, inp_idioma], samples=_samples_iniciais, label="📋 Últimas perguntas", headers=["Pergunta", "Idioma"], ) # Wiring btn.click( fn=reformular_ui, inputs=[inp_q, inp_idioma], outputs=[out, historico, estado_id, feedback_row, btn_pos, btn_neg], concurrency_limit=5, ) historico.click( fn=lambda x: x, inputs=[historico], outputs=[inp_q, inp_idioma], ) btn_pos.click( fn=lambda rid: dar_feedback(rid, 1), inputs=[estado_id], outputs=[btn_pos, btn_neg], ) btn_neg.click( fn=lambda rid: dar_feedback(rid, -1), inputs=[estado_id], outputs=[btn_pos, btn_neg], ) if __name__ == "__main__": import sys _on_spaces = bool(os.getenv("SPACE_ID")) # HuggingFace Spaces detectado if not _on_spaces and sys.platform == "win32": # Mata processos na porta apenas localmente no Windows import subprocess def _kill_port(port: int): try: result = subprocess.run(["netstat", "-ano"], capture_output=True, text=True) for line in result.stdout.splitlines(): if f":{port} " in line and "LISTENING" in line: pid = line.split()[-1] if pid.isdigit(): subprocess.run(["taskkill", "/PID", pid, "/F"], capture_output=True) except Exception: pass _kill_port(7860) # HF Spaces requer 0.0.0.0; local usa 127.0.0.1 host = "0.0.0.0" if _on_spaces else "127.0.0.1" app.queue(max_size=20) app.launch(server_port=7860, server_name=host)