infinex-chatbot / app.py
wilfredolm21's picture
Update app.py
9303b23 verified
# app.py — Asistente de tienda 100% automático con Supabase y Centro de Ayuda
import os
import re
import gradio as gr
from typing import List, Dict, Tuple
# ====== 1. CONEXIÓN A SUPABASE ======
try:
from supabase import create_client, Client
SUPABASE_URL = os.environ.get("SUPABASE_URL")
SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
if SUPABASE_URL and SUPABASE_KEY:
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
else:
supabase = None
except Exception as e:
print("No se pudo conectar a Supabase. Revisa requirements.txt", e)
supabase = None
# ====== LLM opcional ======
try:
from transformers import pipeline
USE_LLM = True
except Exception:
USE_LLM = False
# ====== CONFIG ======
WHATSAPP = "https://wa.me/59172386302"
MARCA = "MULTI-GAME STORE / INFINEX INNOVATION"
WELCOME = f"¡Hola! Soy el asistente de {MARCA}. ¿Qué necesitas hoy? 😊 (Escribe 'ayuda' para ver tutoriales)"
SYSTEM_PROMPT = f"""Eres el asistente de {MARCA} en Bolivia.
Respondes en español, claro y breve. Si te preguntan precios, usa el CATÁLOGO.
Si piden ayuda técnica o cosas que no sabes, ofrece el canal humano: {WHATSAPP}.
Sé profesional, amable y no inventes precios."""
# ====== CENTRO DE AYUDA ======
HELP_KWS = {
"comprar": ["comprar", "recargar", "pago", "pagar", "qr", "pasos"],
"cupones": ["cupón", "cupon", "descuento", "código", "codigo", "aplicar"],
"verificar": ["verificar", "uid", "id", "región", "region", "cuenta ff"],
"seguimiento": ["seguimiento", "historial", "estado", "pedidos", "rastrear"],
"baules": ["baúl", "baul", "premios", "monedas", "juego", "jugar"],
"rasca": ["rasca", "raspa", "tarjeta", "minijuego"],
"cuenta": ["cuenta", "registro", "registrarse", "contraseña", "perfil"],
"referidos": ["referido", "invitar", "amigos", "código de amigo"],
"ayuda": ["ayuda", "soporte", "opciones", "info", "tutorial", "como funciona"]
}
HELP_RESPONSES = {
"comprar": "🛒 **1. Cómo comprar o recargar:**\nAdquirir tus productos es automatizado y seguro:\n- Navega por el catálogo y haz clic en *Comprar*. Aplica tu cupón si tienes.\n- (Solo Free Fire) Usa el verificador de cuenta.\n- Selecciona tu método de pago (QR BNB, Tigo Money, Yape, PayPal).\n- Escanea el QR, paga y guarda captura.\n- Haz clic en *Enviar Comprobante*. El sistema abrirá WhatsApp con tus datos automáticos. ¡Envía la foto y listo!",
"cupones": "🎟️ **2. Cómo aplicar código de descuento:**\n- Al comprar, busca la casilla *¿Tienes un cupón?*.\n- Escribe tu código en mayúsculas y haz clic en *Aplicar* (verás un check verde ✅).\n- ⚠️ **IMPORTANTE:** Los cupones son de 1 solo uso. Al presionar 'Aplicar', el código se quema. Si cancelas la compra en ese momento, el código se desechará para siempre.",
"verificar": "✅ **3. Cómo verificar tu cuenta de Free Fire:**\nPara evitar errores de recarga, tenemos conexión con Garena:\n- Selecciona tu región: América (BR), Global (SG) o India (IND).\n- Escribe tu UID y haz clic en *Verificar*. Tu Nick y Nivel aparecerán en verde.\n- ⚠️ *Seguridad:* El sistema limita las consultas a 1 verificación cada 30 minutos.",
"seguimiento": "📦 **4. Seguimiento de tu compra:**\n- Inicia sesión, ve a 'Mi Cuenta' y haz clic en 'Pedidos'.\n- Verás tu historial detallado con el código de rastreo.\n- El estado pasará de *Pendiente* a *Completado* al entregarse.\n- ⚠️ *Aviso:* Si no envías la foto del comprobante al WhatsApp, el pedido no se procesará. Los pendientes sin pago se limpian mensualmente.",
"baules": "🎁 **5. Ganar premios (Baúles):**\n- Ve a tu Perfil y haz clic en 'JUGAR Y GANAR PREMIOS'.\n- Elige el Baúl de Plata, Oro o Diamante.\n- **¿Cómo obtener monedas?** Reclamando tu regalo diario o viendo anuncios (máx 5 al día).\n- **Reclamos:** El oro y los cupones son automáticos. Los productos físicos/recargas tienen un botón 'Reclamar' que te conecta con nuestro WhatsApp para la entrega.",
"rasca": "🃏 **6. Rasca y Gana (Premios Diarios):**\n- Abre el menú principal y busca 'Rasca y Gana'. Se reinicia a diario.\n- Raspa la zona gris de la tarjeta.\n- Si ganas una recarga o licencia, presiona 'Reclamar' y se abrirá WhatsApp. Si ganas un cupón (ej. GANA-50), cópialo y úsalo al pagar (un solo uso).",
"cuenta": "👤 **7. Cómo crear y gestionar tu cuenta:**\n- Haz clic en 'Mi Cuenta' y luego en 'REGISTRARSE'.\n- Pon un correo válido y crea una contraseña segura.\n- 📧 **Verificación obligatoria:** Te enviaremos un correo. Haz clic en el enlace para entrar (no compartas ese enlace con nadie).\n- Si olvidas tu contraseña, usa la opción '¿Olvidaste tu contraseña?' para recuperarla.",
"referidos": "🤝 **8. Programa de Referidos:**\n¡Gana premios por traer amigos!\n- Entra a tu perfil y copia tu CÓDIGO DE INVITACIÓN (ej. TCI-A1B2).\n- Pásale el código a tus amigos. Ellos deben ir a 'Canjear Código de Amigo' e ingresarlo.\n- ¡Ambos ganarán un premio al instante!",
"menu": "🤖 **Centro de Ayuda de Multigame Store**\nPuedo explicarte cómo funciona nuestra plataforma. Pregúntame sobre:\n\n1️⃣ Cómo comprar o recargar\n2️⃣ Cómo usar cupones\n3️⃣ Verificar cuenta de Free Fire\n4️⃣ Seguimiento de compras\n5️⃣ Juego de Baúles (Premios)\n6️⃣ Rasca y Gana\n7️⃣ Crear/Recuperar Cuenta\n8️⃣ Programa de Referidos\n\n*(Ejemplo: Escribe 'Cómo usar cupones' o 'Cómo comprar')*"
}
# ====== 2. LECTURA 100% EN VIVO DESDE SUPABASE ======
def get_live_catalog():
if not supabase:
return {}
try:
# Lee tu tabla "productos" en vivo
res = supabase.table("productos").select("*").execute()
datos = res.data
if not datos:
return {}
catalogo_dinamico = {}
for item in datos:
# Agrupa dinámicamente por la "marca" que tengas en Supabase
marca = str(item.get("marca", item.get("categoria", "Otros"))).strip()
if marca not in catalogo_dinamico:
catalogo_dinamico[marca] = []
# Carga el producto con el formato del bot
catalogo_dinamico[marca].append({
"name": item.get("nombre", "Producto"),
"desc": item.get("descripcion", item.get("desc", "Entrega inmediata ⚡")),
"bs": item.get("precio", 0),
"usd": item.get("precio_usd", "") # Si no usas dólares, quedará vacío
})
return catalogo_dinamico
except Exception as e:
print("Error leyendo Supabase:", e)
return {}
# ====== BÚSQUEDA INTELIGENTE ======
def search_help(query: str) -> str:
q = query.lower()
for key, words in HELP_KWS.items():
if any(w in q for w in words):
return HELP_RESPONSES.get(key, "")
return ""
def search_catalog(query: str) -> List[Tuple[str, Dict]]:
q = (query or "").lower().strip()
hits = []
# 🚀 OBTENEMOS EL CATÁLOGO FRESCO DE SUPABASE
CATALOGO_ACTUAL = get_live_catalog()
if not CATALOGO_ACTUAL:
return []
# 1. Búsqueda por MARCA (Si el cliente escribe "Free Fire", muestra todo Free Fire)
marca_encontrada = False
for marca, items in CATALOGO_ACTUAL.items():
marca_limpia = marca.lower().replace("_", " ")
if marca_limpia in q or q in marca_limpia:
hits.extend([(marca, it) for it in items])
marca_encontrada = True
# 2. Si no mencionó una marca, buscamos por el NOMBRE del producto
if not marca_encontrada:
q_clean = ' '.join(w for w in q.split() if len(w) > 2 and w not in ['quiero', 'comprar', 'busco'])
if q_clean:
for marca, items in CATALOGO_ACTUAL.items():
for it in items:
if q_clean in it["name"].lower() or any(word in it["name"].lower() for word in q_clean.split()):
hits.append((marca, it))
# Limpiar duplicados
unique_hits, seen = [], set()
for marca, it in hits:
if it['name'] not in seen:
unique_hits.append((marca, it))
seen.add(it['name'])
return unique_hits
def render_products(hits: List[Tuple[str, Dict]]) -> str:
if not hits: return ""
out = []
by_cat: Dict[str, List[Dict]] = {}
for cat, items in hits:
by_cat.setdefault(cat, []).append(items)
for cat, items in by_cat.items():
# Usa el nombre de la marca que tienes en Supabase y le pone un emoji
titulo = str(cat).replace("_", " ")
emoji = "🎮" if "fire" in titulo.lower() or "chess" in titulo.lower() or "legends" in titulo.lower() else "🛒"
if "windows" in titulo.lower(): emoji = "🪟"
if "office" in titulo.lower(): emoji = "📦"
if "recarga" in titulo.lower(): emoji = "📲"
out.append(f"**{emoji} {titulo}**")
for it in items:
precio_bs = f"Bs {it['bs']} Bs" if not str(it['bs']).startswith("Bs") else it['bs']
precio_str = f"**{precio_bs}**"
if it.get('usd'): precio_str += f" | {it['usd']} $"
out.append(f"- **{it['name']}** — {it['desc']}\n {precio_str}")
out.append("")
return "\n".join(out).strip()
# ====== LLM ======
if USE_LLM:
llm = pipeline("text2text-generation", model="google/flan-t5-base")
def llm_answer(message: str, history: List[Tuple[str, str]]) -> str:
if not USE_LLM:
return f"Puedes escribir 'ayuda' para ver los tutoriales o consultar precios de un juego. Soporte: {WHATSAPP}"
ctx = ""
for u, a in (history or [])[-2:]:
ctx += f"Usuario: {u}\nAsistente: {a}\n"
prompt = f"{SYSTEM_PROMPT}\n\n{ctx}Usuario: {message}\nAsistente:"
try:
out = llm(prompt, max_new_tokens=220, temperature=0.35, num_beams=4)[0]["generated_text"].strip()
if "Asistente:" in out: out = out.split("Asistente:")[-1].strip()
return out or "¿En qué más puedo ayudarte?"
except Exception:
return f"Lo siento, puedes escribir 'ayuda' o contactarnos directamente: {WHATSAPP}."
# ====== FLUJO DEL CHAT ======
def chat_fn(message, history):
# 1. Chequea si el cliente pide Ayuda/Soporte
help_text = search_help(message or "")
if help_text:
history.append((message, help_text))
return history, history
# 2. Si no es ayuda, busca en Supabase (Catálogo en vivo)
hits = search_catalog(message or "")
if hits:
answer = render_products(hits)
history.append((message, answer))
return history, history
# 3. Si no encuentra nada, usa la Inteligencia Artificial
answer = llm_answer(message or "", history or [])
if not answer or "no está disponible" in answer:
if not USE_LLM:
answer = f"No encontré ese producto en nuestra base de datos. Escribe 'ayuda' para ver el Centro de Soporte o escríbenos al WhatsApp: {WHATSAPP}"
history.append((message, answer))
return history, history
# ====== UI MINIMAL CON GRADIO ======
SPACE_CSS = """:root, html, body, .gradio-container { background: transparent !important; }header, footer { display:none !important; }.gradio-container { padding: 0 !important; }.chatbot { border-radius: 10px !important; }button, .btn { border-radius: 10px !important; }"""
with gr.Blocks(theme=gr.themes.Soft(primary_hue="cyan", neutral_hue="slate"), css=SPACE_CSS, fill_height=True) as demo:
gr.Markdown(f"### {WELCOME}")
chat = gr.Chatbot(height=480, show_copy_button=True)
txt = gr.Textbox(placeholder="Ej.: Magic Chess, Free Fire, ayuda, cupón...", autofocus=True)
send = gr.Button("Enviar", variant="primary")
clear = gr.Button("Limpiar chat", variant="secondary")
state = gr.State([])
def submit(user_msg, hist):
if not user_msg or not user_msg.strip(): return gr.update(), hist
new_hist, _ = chat_fn(user_msg.strip(), hist or [])
return new_hist, new_hist
send.click(submit, inputs=[txt, state], outputs=[chat, state]).then(lambda: gr.update(value=""), None, [txt])
txt.submit(submit, inputs=[txt, state], outputs=[chat, state]).then(lambda: gr.update(value=""), None, [txt])
clear.click(lambda: ([], []), outputs=[chat, state])
def init(request: gr.Request):
q = (request.query_params or {}).get("text", "")
if not q: q = (request.query_params or {}).get("prefill", "")
return gr.update(value=q)
demo.load(fn=init, inputs=None, outputs=txt)
gr.HTML("""
<script>
(function(){
const params = new URLSearchParams(window.location.search);
if (params.get('text') || params.get('prefill')) {
setTimeout(()=>{
const btns = Array.from(document.querySelectorAll('button'));
const sendBtn = btns.find(b => b.innerText.trim() === 'Enviar');
if (sendBtn) sendBtn.click();
}, 600);
}
})();
</script>
""", visible=False)
demo.launch()