""" 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'{v:.3f}| # | EE | Reformulação |
|---|
" "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)