# 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(""" """, visible=False) demo.launch()