GroqEnglish / app.py
OttoUlbrich's picture
feat: Update AGENTS.md and app.py to support GLM-4.7-Flash provider
31d2214
import gradio as gr
import pandas as pd
import os
import secrets
import time
from collections import defaultdict
from dotenv import load_dotenv
from core import analyze_text, validate_api_key, validate_glm_api_key
# Cargar variables de entorno desde .env
load_dotenv()
# DEBUG: Verificar carga de variables
print("=" * 50)
print("DEBUG - Variables de entorno cargadas:")
print(f"GROQ_API_KEY existe: {bool(os.environ.get('GROQ_API_KEY'))}")
print(f"pass_owner value: '{os.environ.get('pass_owner')}'")
print("=" * 50)
# Rate limiting (almacenado en memoria del servidor)
login_attempts = defaultdict(list)
MAX_ATTEMPTS = 5
LOCKOUT_TIME = 300 # 5 minutos
def interface_fn(input_text, api_key, provider):
"""
Wrapper function for Gradio interface.
CRITICAL: Environment Variable Logic
- If api_key == "owner_authenticated": use environment variable based on provider
- Otherwise: use user-provided key
"""
# Si es el token del owner, usar la key del entorno según el provider
if api_key == "owner_authenticated":
if provider == "glm":
actual_key = os.environ.get("GLM_API_KEY")
else:
actual_key = os.environ.get("GROQ_API_KEY")
if not actual_key:
return (
"",
"",
pd.DataFrame(),
f"⚠️ Error de configuración del servidor ({provider.upper()})",
)
else:
actual_key = api_key
result = analyze_text(input_text, actual_key, provider)
# Process errors for DataFrame
errors_list = result.get("errors", [])
if errors_list:
df_data = [
[
e.get("original"),
e.get("correction"),
e.get("explanation"),
e.get("type"),
]
for e in errors_list
]
else:
df_data = [] # Empty dataframe if no errors
return (
result.get("corrected_text", ""),
result.get("spanish_translation", ""),
pd.DataFrame(
df_data, columns=["Original", "Corrección", "Explicación", "Tipo"]
),
result.get("general_feedback", ""),
)
def verify_key(key, request: gr.Request):
"""
Verifies the input key and implements the owner bypass.
SECURE VERSION for Hugging Face Spaces.
"""
# Rate limiting por IP
client_ip = request.client.host if request else "unknown"
now = time.time()
# Limpiar intentos antiguos
login_attempts[client_ip] = [
t for t in login_attempts[client_ip] if now - t < LOCKOUT_TIME
]
if len(login_attempts[client_ip]) >= MAX_ATTEMPTS:
return (
None,
gr.update(visible=True),
gr.update(visible=False),
"⚠️ Demasiados intentos. Espera 5 minutos.",
)
key = key.strip()
# Check for bypass con comparación de tiempo constante
if key.startswith("pass"):
pass_owner = os.environ.get("pass_owner")
if pass_owner:
expected = "pass" + pass_owner
# Usar secrets.compare_digest para evitar timing attacks
if len(key) == len(expected) and secrets.compare_digest(key, expected):
# ✅ NO retornar la API key real
# Usar un token de sesión en su lugar
session_token = "owner_authenticated"
login_attempts[client_ip] = [] # Reset intentos
return (
session_token,
gr.update(visible=False),
gr.update(visible=True),
"",
)
else:
login_attempts[client_ip].append(now)
return (
None,
gr.update(visible=True),
gr.update(visible=False),
"⚠️ Credenciales incorrectas.",
)
# Validar formato de API key de usuario (Groq keys empiezan con gsk_)
if key and key.startswith("gsk_") and len(key) > 30:
if validate_api_key(key):
login_attempts[client_ip] = []
return key, gr.update(visible=False), gr.update(visible=True), ""
else:
login_attempts[client_ip].append(now)
return (
None,
gr.update(visible=True),
gr.update(visible=False),
"⚠️ API Key inválida (rechazada por Groq).",
)
# GLM key validation (new)
if len(key) > 20:
if validate_glm_api_key(key):
login_attempts[client_ip] = []
return key, gr.update(visible=False), gr.update(visible=True), ""
else:
login_attempts[client_ip].append(now)
return (
None,
gr.update(visible=True),
gr.update(visible=False),
"⚠️ API Key inválida (rechazada por GLM).",
)
login_attempts[client_ip].append(now)
return (
None,
gr.update(visible=True),
gr.update(visible=False),
"⚠️ API Key inválida.",
)
def logout_fn():
"""
Clears the API key and returns to the login screen.
"""
return (
None,
gr.update(visible=True),
gr.update(visible=False),
"Sesión cerrada correctamente.",
)
def update_provider(choice):
"""Map UI choice to internal provider value."""
return "glm" if "GLM" in choice else "groq"
custom_css = """
.gradio-container {
width: 95% !important;
max-width: 95% !important;
}
#main_view_container {
padding: 40px;
}
"""
with gr.Blocks(
title="GroqEnglish Assistant",
theme=gr.themes.Soft(primary_hue="orange"),
css=custom_css,
) as demo:
# State to store the authenticated API key
api_key_state = gr.State()
# State to store selected provider (default: "groq")
provider_state = gr.State("groq")
# --- Login View ---
with gr.Column(visible=True) as login_view:
with gr.Row(elem_id="login_container"):
with gr.Column(scale=1):
pass # Spacer
with gr.Column(scale=2):
gr.Markdown(
"""
# 🚀 GroqEnglish
### Asistente Inteligente de Escritura (Inglés L2)
Bienvenido. Este asistente utiliza modelos avanzados de Groq para ayudarte a pulir tu inglés.
"""
)
with gr.Group():
api_key_input = gr.Textbox(
label="🔑 Groq API Key",
type="password",
placeholder="gsk_...",
lines=1,
info="Tu llave es necesaria para acceder al modelo.",
)
login_btn = gr.Button("Ingresar", variant="primary", size="lg")
gr.Markdown(
"""
> **¿No tienes una API Key?**
> Consíguela gratis en [Groq Console](https://console.groq.com/keys).
"""
)
login_msg = gr.Markdown("", visible=True)
with gr.Column(scale=1):
pass # Spacer
# --- Main View ---
with gr.Column(visible=False, elem_id="main_view_container") as main_view:
# Header with Title and Logout
with gr.Row(variant="panel"):
with gr.Column(scale=6):
gr.Markdown("# 🇬🇧 GroqEnglish Studio")
with gr.Column(scale=2, min_width=150):
provider_radio = gr.Radio(
choices=["Groq Llama 3.3", "GLM-4.7-Flash"],
value="Groq Llama 3.3",
label="Modelo AI",
interactive=True,
)
with gr.Column(scale=1, min_width=100):
logout_btn = gr.Button("Cerrar Sesión", variant="secondary", size="sm")
gr.Markdown(
"Transforma tu inglés con correcciones gramaticales, traducciones inversas y explicaciones detalladas."
)
with gr.Row(equal_height=True):
# Left Column: Input
with gr.Column(scale=1):
with gr.Group():
gr.Markdown("### ✍️ Tu Texto")
input_box = gr.Textbox(
show_label=False,
placeholder="Escribe aquí tu frase en inglés... (ej. I goes to the park yesterday)",
lines=12,
elem_id="input_text",
)
submit_btn = gr.Button(
"✨ Analizar y Mejorar", variant="primary", size="lg"
)
# Right Column: Output
with gr.Column(scale=1):
with gr.Group():
gr.Markdown("### 💎 Resultado Pulido")
with gr.Row():
polished_text = gr.Textbox(
show_label=False, interactive=False, lines=4, scale=6
)
copy_polished_btn = gr.Button("📋", scale=1, min_width=50)
with gr.Group():
gr.Markdown("### 🇪🇸 Verificación (Traducción)")
with gr.Row():
spanish_translation = gr.Textbox(
show_label=False, interactive=False, lines=3, scale=6
)
copy_translation_btn = gr.Button("📋", scale=1, min_width=50)
with gr.Group():
feedback_msg = gr.Markdown(
"### 💡 Feedback General\n*(El análisis aparecerá aquí)*"
)
# Bottom Section: Detailed Errors
with gr.Accordion("🔍 Detalle de Errores y Explicaciones", open=True):
error_table = gr.Dataframe(
headers=["Original", "Corrección", "Explicación", "Tipo"],
label="Tabla de Correcciones",
interactive=False,
wrap=True,
datatype=["str", "str", "str", "str"],
)
with gr.Row():
copy_table_btn = gr.Button("📋 Copiar Tabla de Errores", size="sm")
copy_table_output = gr.Textbox(visible=False)
# Functionality wiring
submit_btn.click(
fn=interface_fn,
inputs=[input_box, api_key_state, provider_state],
outputs=[polished_text, spanish_translation, error_table, feedback_msg],
)
# Copy functionality
copy_polished_btn.click(
lambda x: x,
inputs=[polished_text],
outputs=[polished_text],
js="(x) => {navigator.clipboard.writeText(x); return x;}",
)
copy_translation_btn.click(
lambda x: x,
inputs=[spanish_translation],
outputs=[spanish_translation],
js="(x) => {navigator.clipboard.writeText(x); return x;}",
)
def format_table_for_copy(df):
if df is None or len(df) == 0:
return ""
lines = []
for _, row in df.iterrows():
lines.append(
f"Original: {row['Original']} | Corrección: {row['Corrección']} | Explicación: {row['Explicación']}"
)
return "\n".join(lines)
copy_table_btn.click(
fn=format_table_for_copy,
inputs=[error_table],
outputs=[copy_table_output],
js="(text) => {navigator.clipboard.writeText(text); return text;}",
)
# --- Global Event Wiring ---
# Login
login_btn.click(
fn=verify_key,
inputs=[api_key_input],
outputs=[api_key_state, login_view, main_view, login_msg],
)
api_key_input.submit(
fn=verify_key,
inputs=[api_key_input],
outputs=[api_key_state, login_view, main_view, login_msg],
)
# Logout
logout_btn.click(
fn=logout_fn,
inputs=None,
outputs=[api_key_state, login_view, main_view, login_msg],
)
# Provider selection
provider_radio.change(
fn=update_provider, inputs=[provider_radio], outputs=[provider_state]
)
if __name__ == "__main__":
demo.launch()