Spaces:
Sleeping
Sleeping
| # 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() |