Ir al contenido principal

Creando un Asistente Virtual en PythonAnywhere con Gemma

Crea tu Asistente de Ventas con IA (Gemma), Flask y PythonAnywhere

¡Hola! En esta guía, te mostraré cómo construir e implementar un chatbot asistente de ventas utilizando el poder de la inteligencia artificial generativa de Google (con el modelo Gemma), el framework web Flask y la plataforma de hosting PythonAnywhere. Este asistente podrá responder preguntas sobre productos, precios, políticas y guiar a los usuarios en el proceso de compra.

¿Qué Necesitarás?

  • Una cuenta en PythonAnywhere (puedes empezar con una cuenta gratuita).
  • Una Clave API (API Key) de Google AI Studio para usar Gemma. Puedes obtenerla gratis en Google AI Studio.
  • Conocimientos básicos de Python y HTML.

Paso 1: Configuración Inicial en PythonAnywhere

  1. Inicia sesión en tu cuenta de PythonAnywhere.
  2. Limpia tu directorio: Ve a la pestaña "Files". Es recomendable empezar con un espacio limpio. Si tienes archivos o directorios de proyectos anteriores en la raíz (/home/tu_usuario/) que no necesitas, puedes eliminarlos (con cuidado). Para este proyecto, trabajaremos dentro de una carpeta llamada mysite que PythonAnywhere crea a menudo por defecto, o que crearemos.
  3. Crea tu aplicación web:
    • Ve a la pestaña "Web".
    • Haz clic en "Add a new web app".
    • Confirma el nombre de dominio (tu_usuario.pythonanywhere.com).
    • Selecciona "Flask" como el framework de Python.
    • Elige la versión de Python más reciente disponible (ej. Python 3.10 o superior).
    • PythonAnywhere creará automáticamente un archivo básico flask_app.py en /home/tu_usuario/mysite/.
  4. Verifica la configuración inicial: Visita tu URL (http://tu_usuario.pythonanywhere.com). Deberías ver un mensaje simple como "Hello from Flask!". Esto confirma que la configuración básica funciona.

Paso 2: Instalar Dependencias

  1. Ve a la pestaña "Consoles".
  2. Abre una nueva consola "Bash".
  3. Dentro de la consola Bash, instala las librerías necesarias ejecutando los siguientes comandos uno por uno:
pip install Flask
pip install google-generativeai
pip install Flask-CORS

Espera a que cada comando termine antes de ejecutar el siguiente.

Paso 3: Estructura de Archivos

Dentro de tu directorio principal en PythonAnywhere (usualmente /home/tu_usuario/), asegúrate de tener la siguiente estructura de carpetas y archivos dentro de la carpeta mysite:

mysite/
├── flask_app.py      # Archivo principal de la aplicación Flask
├── contexto.py       # Lógica para construir el prompt de la IA
└── templates/
    └── index.html    # Interfaz de usuario del chat (frontend)

Si la carpeta templates no existe dentro de mysite, créala usando el gestor de archivos de PythonAnywhere.

Paso 4: Código de la Aplicación

4.1. flask_app.py

Este es el corazón de tu aplicación web. Maneja las rutas, recibe las preguntas del usuario, interactúa con la API de Gemma y devuelve las respuestas.

  1. Ve a la pestaña "Files" y navega a /home/tu_usuario/mysite/.
  2. Abre el archivo flask_app.py.
  3. Elimina todo el contenido existente que PythonAnywhere generó por defecto.
  4. Copia y pega el siguiente código en flask_app.py:
from flask import Flask, request, jsonify, render_template
import os
import google.generativeai as genai
import traceback


from contexto import pregunta_con_contexto 

app = Flask(__name__)

# Clave de API para Gemini
API_KEY = "agrega tu apikey aqui"


# Configura la API si se proporciona una clave válida
if not API_KEY:
    print("ADVERTENCIA: La variable de entorno GEMINI_API_KEY no está configurada.")
else:
    try:
        genai.configure(api_key=API_KEY)
    except Exception as e:
        print(f"Error al configurar la API de Gemini: {e}")



# Función que genera la respuesta usando el modelo de Gemini
def generate_response(pregunta_con_contexto: str) -> str:
    if not API_KEY:
        return "Error: La API Key de Gemini no fue configurada correctamente."
    if not pregunta_con_contexto or not isinstance(pregunta_con_contexto, str):
        return "La pregunta no es válida"
    try:
        model_name = "gemma-3-27b-it"
        model = genai.GenerativeModel(model_name)
        generation_config = genai.types.GenerationConfig(
            temperature=0.9,
            top_p=1.0,
            top_k=40,
            max_output_tokens=2048
        )
        response = model.generate_content(
            pregunta_con_contexto,
            generation_config=generation_config
        )
        raw_text = ""
        if not response.parts:
            block_reason = response.prompt_feedback.block_reason if response.prompt_feedback else 'No especificada'
            safety_ratings = response.prompt_feedback.safety_ratings if response.prompt_feedback else 'No disponibles'
            print(f"Respuesta bloqueada. Razón: {block_reason}, Calificaciones: {safety_ratings}")
            return f"La respuesta fue bloqueada por motivos de seguridad o no se generó contenido. Razón: {block_reason}"
        if hasattr(response, 'text'):
            raw_text = response.text
        elif response.parts:
            raw_text = " ".join(part.text for part in response.parts if hasattr(part, 'text'))
        else:
            return "El modelo no generó una respuesta con texto."
        
        return raw_text
    except Exception as e:
        print("--- Error Detallado en generate_response ---")
        traceback.print_exc()
        print("--- Fin Error Detallado ---")
        return f"Error al generar la respuesta con la API: {str(e)}"



# Ruta principal que sirve el HTML
@app.route('/')
def home():
    return render_template('index.html')



# Ruta para recibir preguntas y retornar respuestas generadas
@app.route('/api/ask', methods=['POST'])
def ask_question():
    try:
        if not request.is_json:
            return jsonify({'error': 'El contenido debe ser JSON (Content-Type: application/json)'}), 415
        data = request.get_json()
        if not data:
            return jsonify({'error': 'No se proporcionaron datos JSON en la solicitud'}), 400
        pregunta = data.get('question', '').strip()
        historial = data.get('history', [])
        if not pregunta:
            return jsonify({'error': 'La clave "question" no puede estar vacía en el JSON'}), 400
        contexto = pregunta_con_contexto(pregunta, historial)
        print("--- inicio contexto ---")
        print(contexto)
        print("--- fin contexto ---")
        response_text = generate_response(contexto)
        if response_text.startswith("Error:") or "La respuesta fue bloqueada" in response_text:
            return jsonify({'error': response_text}), 500
        return jsonify({'response': response_text})
    except Exception as e:
        print("--- Error Detallado en ask_question ---")
        traceback.print_exc()
        print("--- Fin Error Detallado ---")
        return jsonify({'error': f'Ocurrió un error inesperado en el servidor: {str(e)}'}), 500




¡MUY IMPORTANTE! Busca la línea API_KEY = "agrega tu apikey aqui" en el código que acabas de pegar y reemplaza "agrega tu apikey aqui" con tu clave API real obtenida de Google AI Studio. Sin esto, la aplicación no podrá conectarse a la IA.

4.2. contexto.py

Este archivo contiene la lógica para construir el "prompt" (la instrucción) que se enviará a Gemma. Incluye el rol del asistente, instrucciones específicas, el historial de la conversación y la pregunta actual del usuario, además de lógica para añadir información dinámica basada en palabras clave.

  1. En la pestaña "Files", dentro de mysite, crea un nuevo archivo llamado contexto.py.
  2. Copia y pega el siguiente código en contexto.py:
        
import datetime
import os
import re # Necesario para búsqueda por características más flexible

# -------------------------------------------------------------------
# DEFINICIÓN DEL PROMPT Y PALABRAS CLAVE
# -------------------------------------------------------------------

def pregunta_con_contexto(pregunta, historial) -> str:
    """
    Crea un prompt optimizado para un asistente virtual especializado en ventas,
    siguiendo las mejores prácticas de contexto, claridad y tono persuasivo y amable.
    """
    historial = f"\nHistorial de la conversación:\n{historial}" if historial else "No hay historial disponible."
    condiciones_dinamicas = generar_condiciones_dinamicas(pregunta)
    if condiciones_dinamicas.strip():
        condiciones_dinamicas = "\n🧠 **Información relevante encontrada para tu consulta:**\n" + condiciones_dinamicas # Mensaje más claro
    prompt = f"""
## Rol del Asistente:
Eres un asistente virtual especializado en **ventas** para Comercial Nova. Tu misión es ayudar a los clientes a elegir productos o servicios adecuados, brindar información clara y útil sobre precios, promociones, formas de pago, disponibilidad y beneficios, además de guiarlos paso a paso durante el proceso de compra. También brindás atención post-venta para asegurar la satisfacción del cliente.

Instrucciones:
1. **Tono Profesional, Cercano y Persuasivo:**
   - "Si el usuario inicia con un saludo (ej: 'hola'), responde con un saludo amable. En otros casos, ve directo al tema."
   - Solo saludá si la **pregunta actual** no contiene palabras de saludo.
   - Ayuda al cliente a sentirse seguro con su compra.
   - Si el cliente tiene dudas, explícalas con paciencia y seguridad, buscando cerrar la venta sin ser insistente.

2. **Información sobre Productos y Servicios:**
   - Brinda detalles sobre productos: características, beneficios, precios, disponibilidad, formas de pago, promociones y garantías.
   - Ofrecé recomendaciones personalizadas según lo que el cliente necesita o está buscando. Basado en el historial y la pregunta actual.

3. **Proceso de Compra:**
   - Guiá al cliente en los pasos para comprar: cómo hacerlo, qué opciones tiene y cómo confirmar su compra.
   - Si el cliente tiene dificultades, resolvélas de forma simple y directa.

4. **Promociones y Descuentos:**
   - Informá sobre cualquier promoción activa, cupones, combos o descuentos relevantes a la consulta.
   - Aprovechá las oportunidades para sugerir productos complementarios o de mayor valor (upselling/cross-selling) si es pertinente.

5. **Atención Post-Venta:**
   - Informá sobre tiempos de entrega, seguimiento de pedidos, cambios o devoluciones si lo solicita.
   - Consultá si quedó conforme con su compra y ofrecé asistencia adicional si es necesario.

6. **Escalación si es Necesario:**
   - Si el cliente tiene una consulta muy específica que no puedes resolver (ej: stock en tiempo real en una sucursal específica, problema técnico complejo) o requiere atención humana, informá que vas a escalar su caso y derivarlo al equipo adecuado.

7. **Detecta emociones o indecisión:**
   - Si notas que el cliente está dudando o frustrado (palabras como "no sé", "caro", "duda", "complicado"), tranquilízalo, valida su sentir y ofrécele alternativas, beneficios adicionales o explica mejor el valor del producto.

8. **Al finalizar, pide feedback:**
   - Pregunta amablemente si la atención fue útil y si hay algo más en lo que puedas ayudar.

{condiciones_dinamicas}
{historial}

Pregunta actual:
{pregunta}

Notas adicionales:
- Si la consulta es sobre un tipo de producto (ej: "celulares", "notebooks"), muestra los productos disponibles de esa categoría con sus detalles clave (precio, características principales).
- Si el cliente está listo para comprar, guiá el proceso de cierre de venta.
- Si el cliente no sabe qué quiere, ayudalo a decidir con preguntas breves ("¿Qué uso le darías?", "¿Qué presupuesto tienes?") o sugerencias basadas en productos populares.
- Siempre agradecé su interés y ofrecé ayuda adicional antes de terminar.
- Si el cliente ya compró (basado en historial), consultá si quedó conforme y ofrecé productos relacionados o soporte.
**Max_token:**
- 200
    """

    guardar_prompt_log(prompt.strip())

    return prompt.strip()







# Bloques de palabras clave (podrían estar en un archivo aparte como constants.py)
PALABRAS_CLAVE = {
    "promocion": {"promoción", "oferta", "ofertas", "descuento", "cupón", "rebaja", "promo"},
    "politicas": {"garantía", "devolución", "reembolso", "cambio", "entrega", "envío", "política", "instalación"},
    "precio": {"precio", "precios", "cuánto cuesta", "valor", "costó", "cuanto sale", "cotización"},
    "caracteristicas": {
        # Generales
        "cámara", "batería", "almacenamiento", "color", "pantalla", "ram", "procesador", "tamaño", "memoria",
        # Específicas TV/Monitor
        "resolución", "pulgadas", "hz", "hercios", "oled", "qled", "hdmi",
        # Específicas Audio
        "bluetooth", "cancelación", "ruido", "inalámbrico", "potencia", "watts",
        # Específicas Smartwatch
        "gps", "resistencia", "agua", "cardiaco", "ecg", "oxígeno",
        # Específicas Consola
        "juegos", "fps", "disco", "ssd", "gráficos", "teraflops"
    },
    "categorias": {
        "celular", "celulares", "teléfono", "móvil",
        "notebook", "notebooks", "laptop", "portátil",
        "tablet", "tablets", "tableta",
        "accesorio", "accesorios", "auriculares", "cargador", "funda", "teclado", "mouse",
        "smartwatch", "reloj", "relojes",
        "consola", "consolas", "videojuego", "videojuegos", "playstation", "xbox", "nintendo",
        "tv", "televisor", "televisores", "tele", "pantalla",
        "audio", "sonido", "parlante", "altavoz", "auricular", "barra"
     }
}

def generar_condiciones_dinamicas(pregunta: str) -> str:
    """Genera texto con información relevante basado en las palabras clave detectadas en la pregunta."""
    condiciones = []
    pregunta_lower = pregunta.lower()

    # 1. Detección más eficiente usando conjuntos
    palabras_en_pregunta = set(re.findall(r'\b\w+\b', pregunta_lower)) # Extrae palabras individuales

    # 2. Manejo de promociones optimizado
    if PALABRAS_CLAVE["promocion"] & palabras_en_pregunta:
        promos = obtener_promociones_actuales()
        # Filtrar promos activas (opcional, si la fecha es relevante)
        # from datetime import datetime
        # hoy = datetime.now().strftime("%d/%m/%Y")
        # promos_activas = [p for p in promos if p['valido_hasta'] >= hoy] # Simplificado, cuidado con formato fecha
        promos_texto = "\n".join([
            f"• 🎁 **{p['nombre']}**: {p['detalle']} (Válido hasta: {p['valido_hasta']})"
            for p in promos # Usar promos_activas si se filtra
        ])
        if promos_texto:
             condiciones.append(f"📢 **Promociones vigentes:**\n{promos_texto}")
        else:
             condiciones.append("📢 No tenemos promociones especiales activas en este momento, pero nuestros precios son muy competitivos.")

    # 3. Manejo de políticas con detección mejorada
    palabras_politica_encontradas = PALABRAS_CLAVE["politicas"] & palabras_en_pregunta
    if palabras_politica_encontradas:
        politicas = obtener_politicas_completas()
        politicas_texto = "\n".join([
            # Muestra solo las políticas mencionadas o todas si es genérico ("política")
            f"• 🔧 **{key.capitalize()}**: {value}"
            for key, value in politicas.items() if key in palabras_politica_encontradas or "política" in palabras_politica_encontradas
        ])
        condiciones.append(f"📜 **Información sobre políticas ({', '.join(p.capitalize() for p in palabras_politica_encontradas)}):**\n{politicas_texto}")

    # 4. Búsqueda por categoría optimizada (ESTA ES LA CLAVE PARA TU REQUERIMIENTO)
    #   Usamos intersección para ver qué categorías de nuestra lista están en la pregunta
    categorias_solicitadas = PALABRAS_CLAVE["categorias"] & palabras_en_pregunta
    catalogo = None # Cargar solo si es necesario

    if categorias_solicitadas:
        if catalogo is None: catalogo = obtener_catalogo_completo()
        for categoria_keyword in categorias_solicitadas:
            # Mapear palabra clave a nombre de categoría real en el catálogo (ej: "celulares" -> "celular")
            categoria_real = next((cat for cat, data in catalogo.items() if categoria_keyword in PALABRAS_CLAVE["categorias"] and categoria_keyword in cat or cat in categoria_keyword), None)
            # Intento adicional por si la palabra clave es parte del nombre de la categoría (ej: "playstation" -> "consola")
            if not categoria_real:
                 categoria_real = next((cat for cat, data in catalogo.items() if categoria_keyword in cat), None)

            if categoria_real and categoria_real in catalogo:
                productos_texto = generar_info_productos(catalogo[categoria_real])
                if productos_texto:
                    condiciones.append(f" Búsqueda por Categoría: **{categoria_real.capitalize()}** \n{productos_texto}")
                else:
                     condiciones.append(f" No encontré productos específicos en la categoría '{categoria_real.capitalize()}' en este momento.")
            #else:
            #    print(f"Debug: No se encontró mapeo para keyword '{categoria_keyword}'") # Para depuración

    # 5. Búsqueda por precio más precisa
    if PALABRAS_CLAVE["precio"] & palabras_en_pregunta:
        if catalogo is None: catalogo = obtener_catalogo_completo()
        if categorias_solicitadas: # Si ya se pidió categoría, mostrar precios de esa
             for categoria_real in {cat for cat_key in categorias_solicitadas
                                   for cat, data in catalogo.items()
                                   if cat_key in PALABRAS_CLAVE["categorias"] and (cat_key in cat or cat in cat_key)}:
                if categoria_real in catalogo:
                    precios_texto = "\n".join([
                        f"• {prod['nombre']}: {prod['precio_usd']:.2f} USD / {prod['precio_gs']:,.0f} Gs".replace(",",".") # Formato mejorado
                        for prod in catalogo[categoria_real]
                    ])
                    condiciones.append(f" **Precios en {categoria_real.capitalize()}:**\n{precios_texto}")
        else: # Si solo se pide precio sin categoría, mostrar algunos ejemplos o pedir aclaración
             condiciones.append(" Sobre qué categoría o producto te gustaría saber el precio? Tenemos celulares, notebooks, TVs y más.")


    # 6. Búsqueda por características optimizada
    caracteristicas_solicitadas = PALABRAS_CLAVE["caracteristicas"] & palabras_en_pregunta
    if caracteristicas_solicitadas:
        if catalogo is None: catalogo = obtener_catalogo_completo()
        # Pasar las características detectadas a la función de búsqueda
        resultados = buscar_por_caracteristicas(pregunta_lower, caracteristicas_solicitadas, catalogo)
        if resultados:
            condiciones.append(f" **Productos relacionados con '{', '.join(caracteristicas_solicitadas)}':**\n{resultados}")
        else:
            condiciones.append(f" No encontré productos que coincidan específicamente con las características '{', '.join(caracteristicas_solicitadas)}'. ¿Podrías darme más detalles?")


    # 7. Búsqueda de productos específicos (por nombre)
    #    Lista de nombres de productos comunes para búsqueda rápida
    nombres_productos_comunes = {"iphone", "galaxy", "macbook", "ipad", "airpods", "ps5", "playstation 5", "xbox series", "apple watch", "galaxy watch"}
    palabras_producto_detectadas = {palabra for palabra in nombres_productos_comunes if palabra in pregunta_lower}

    if palabras_producto_detectadas:
         if catalogo is None: catalogo = obtener_catalogo_completo()
         productos_encontrados_texto = []
         for categoria, productos in catalogo.items():
             for prod in productos:
                 nombre_prod_lower = prod['nombre'].lower()
                 # Comprobar si alguna palabra detectada está en el nombre del producto
                 if any(palabra_detectada in nombre_prod_lower for palabra_detectada in palabras_producto_detectadas):
                     precio_gs_f = f"{prod['precio_gs']:,.0f}".replace(",", ".")
                     producto_detalle = (
                         f"📌 **{prod['nombre']}** ({categoria.capitalize()})\n"
                         f"   💵 Precio: ${prod['precio_usd']:.2f}$ USD / ${precio_gs_f}$ Gs\n"
                         f"   🎨 Colores disponibles: {', '.join(prod['colores'])}\n"
                         f"   💾 Almacenamiento: {', '.join(prod.get('almacenamiento', ['N/D']))}\n"
                         f"   ⚙️ Destacado: {'; '.join(prod['caracteristicas'][:3])}..." # Primeras 3 características
                     )
                     productos_encontrados_texto.append(producto_detalle)

         if productos_encontrados_texto:
             condiciones.append(f" **Información sobre productos específicos mencionados:**\n" + "\n\n".join(productos_encontrados_texto))


    # 8. Caso genérico "productos" (si no se activó nada más específico)
    #    Se activa si se dice "productos" pero no una categoría o nombre específico.
    if not categorias_solicitadas and not palabras_producto_detectadas and not caracteristicas_solicitadas and any(p in pregunta_lower for p in ["producto", "productos", "artículo", "artículos", "item", "items"]):
         if catalogo is None: catalogo = obtener_catalogo_completo()
         condiciones.append(" Tenemos una gran variedad de productos electrónicos. ¿Te interesa alguna categoría en particular como celulares, notebooks, TVs, consolas, audio o accesorios?")


    return "\n\n".join(condiciones) if condiciones else ""


# -------------------------------------------------------------------
# MÓDULO DE DATOS (Considera mover a un archivo JSON si crece mucho)
# -------------------------------------------------------------------
def obtener_politicas_completas():
    """Devuelve un diccionario con las políticas de la tienda."""
    return {
        "garantia": "12 meses contra defectos de fábrica en la mayoría de los productos. Notebooks y TVs pueden tener garantía extendida del fabricante (consultar modelo).",
        "devolucion": "10 días corridos desde la compra para devoluciones si el producto está sin uso, en su empaque original sellado y con factura.",
        "cambio": "30 días para cambios por otro producto (aplican condiciones de devolución). Si hay diferencia de precio, se ajusta.",
        "reembolso": "Se procesa dentro de los 7 días hábiles posteriores a la aceptación de la devolución, por el mismo medio de pago original.",
        "entrega": "Envío estándar 24-72hs hábiles en Asunción y Gran Asunción (costo según zona, gratis para compras > 1.500.000 Gs). Envíos al interior vía transportadora (costo a cargo del cliente).",
        "instalación": "Ofrecemos servicio de instalación básica para TVs y configuración inicial para notebooks con costo adicional (consultar precios)."
    }

def obtener_promociones_actuales():
    """Devuelve una lista de diccionarios con promociones vigentes."""
    # Asegúrate de mantener estas fechas actualizadas o implementar lógica para filtrar
    return [
        {
            "nombre": "TecnoFest Verano",
            "detalle": "Hasta 25% de descuento en Smartphones seleccionados y 12 cuotas sin interés con bancos asociados.",
            "valido_hasta": "31/05/2025" # Ejemplo: Usar formato AAAA-MM-DD para facilitar comparación
        },
        {
            "nombre": "Vuelta al Cole Tech",
            "detalle": "Notebooks y Tablets con 10% OFF + Mochila de regalo en modelos seleccionados.",
            "valido_hasta": "15/03/2025" # Ejemplo pasado
        },
        {
            "nombre": "Combo Gamer Pro",
            "detalle": "Lleva una Consola PS5 + Juego de lanzamiento + Auriculares Gamer con 1.000.000 Gs de ahorro.",
            "valido_hasta": "30/04/2025"
        },
         {
            "nombre": "Audio Inmersivo",
            "detalle": "20% de descuento en Barras de Sonido y Auriculares con cancelación de ruido.",
            "valido_hasta": "15/05/2025"
        }
    ]

def obtener_catalogo_completo():
    """Devuelve el catálogo completo de productos por categoría."""
    USD_TO_GS = 7450  # Actualizar tipo de cambio periódicamente

    return {
        "celular": [
            {
                "nombre": "iPhone 16 Pro", # Modelo hipotético futuro
                "precio_usd": 1199, "precio_gs": 1199 * USD_TO_GS,
                "colores": ["Titanio Natural", "Titanio Azul", "Titanio Blanco", "Titanio Negro"],
                "almacenamiento": ["128GB", "256GB", "512GB", "1TB"],
                "caracteristicas": ["Chip A18 Pro", "Pantalla Super Retina XDR ProMotion", "Sistema de cámaras Pro avanzado 48MP", "Botón de Acción configurable", "USB-C Thunderbolt"]
            },
            {
                "nombre": "iPhone 16", # Modelo hipotético futuro
                "precio_usd": 899, "precio_gs": 899 * USD_TO_GS,
                "colores": ["Azul", "Rosa", "Verde", "Negro", "Blanco"],
                "almacenamiento": ["128GB", "256GB", "512GB"],
                "caracteristicas": ["Chip A17 (mejorado)", "Pantalla Super Retina XDR", "Cámara dual 48MP", "Dynamic Island", "USB-C"]
            },
            {
                "nombre": "Samsung Galaxy S25 Ultra", # Modelo hipotético futuro
                "precio_usd": 1299, "precio_gs": 1299 * USD_TO_GS,
                "colores": ["Phantom Black", "Phantom Silver", "Emerald Green", "Sapphire Blue"],
                "almacenamiento": ["256GB", "512GB", "1TB"],
                "caracteristicas": ["Pantalla Dynamic AMOLED 3X 120Hz", "Procesador Snapdragon Gen 4 for Galaxy", "Cámara principal 200MP con IA", "S-Pen integrado", "Batería 5500mAh"]
            },
             {
                "nombre": "Samsung Galaxy A56", # Modelo hipotético futuro
                "precio_usd": 450, "precio_gs": 450 * USD_TO_GS,
                "colores": ["Awesome Black", "Awesome White", "Awesome Blue"],
                "almacenamiento": ["128GB", "256GB"],
                "caracteristicas": ["Pantalla Super AMOLED 120Hz", "Cámara 64MP OIS", "Batería 5000mAh", "Resistencia IP67", "Procesador Exynos eficiente"]
            },
            {
                "nombre": "Xiaomi 15 Pro", # Modelo hipotético futuro
                "precio_usd": 950, "precio_gs": 950 * USD_TO_GS,
                "colores": ["Negro Cerámico", "Blanco Nieve", "Verde Bosque"],
                "almacenamiento": ["256GB", "512GB"],
                "caracteristicas": ["Sensor de cámara 1 pulgada", "Lentes Leica Summilux", "Pantalla LTPO AMOLED 144Hz", "Carga rápida 120W", "Snapdragon Gen 4"]
            }
        ],
        "notebook": [
            {
                "nombre": "MacBook Air 13\" M3",
                "precio_usd": 1099, "precio_gs": 1099 * USD_TO_GS,
                "colores": ["Medianoche", "Blanco estelar", "Gris espacial", "Plata"],
                "almacenamiento": ["256GB SSD", "512GB SSD"], "ram": ["8GB", "16GB"],
                "caracteristicas": ["Chip M3 Apple Silicon", "Pantalla Liquid Retina 13.6\"", "Hasta 18h de batería", "Diseño ultra delgado y ligero", "Magic Keyboard"]
            },
            {
                "nombre": "MacBook Pro 14\" M3 Pro",
                "precio_usd": 1999, "precio_gs": 1999 * USD_TO_GS,
                "colores": ["Negro espacial", "Plata"],
                "almacenamiento": ["512GB SSD", "1TB SSD"], "ram": ["18GB", "36GB"],
                "caracteristicas": ["Chip M3 Pro Apple Silicon", "Pantalla Liquid Retina XDR 14.2\"", "Rendimiento extremo para profesionales", "Sistema de sonido avanzado", "Puertos Pro (HDMI, SDXC)"]
            },
            {
                "nombre": "Dell XPS 15 (Modelo 9540)", # Modelo hipotético
                "precio_usd": 1650, "precio_gs": 1650 * USD_TO_GS,
                "colores": ["Platino", "Grafito"],
                "almacenamiento": ["512GB SSD", "1TB SSD", "2TB SSD"], "ram": ["16GB", "32GB", "64GB"],
                "caracteristicas": ["Procesador Intel Core Ultra 7/9", "Pantalla InfinityEdge OLED 3.5K táctil (opcional)", "Gráficos NVIDIA GeForce RTX 4050/4060 (opcional)", "Chasis de aluminio premium", "Windows 11 Pro"]
            },
             {
                "nombre": "HP Spectre x360 14 (2025)", # Modelo hipotético
                "precio_usd": 1400, "precio_gs": 1400 * USD_TO_GS,
                "colores": ["Nightfall Black", "Poseidon Blue"],
                "almacenamiento": ["512GB SSD", "1TB SSD"], "ram": ["16GB", "32GB"],
                "caracteristicas": ["Diseño convertible 2-en-1", "Pantalla OLED 2.8K 120Hz", "Procesador Intel Core Ultra 7", "Lápiz óptico incluido", "Cámara IA 5MP IR"]
            }
        ],
        "tablet": [
            {
                "nombre": "iPad Pro 11\" M3", # Modelo hipotético
                "precio_usd": 999, "precio_gs": 999 * USD_TO_GS,
                "colores": ["Plata", "Gris espacial"],
                "almacenamiento": ["128GB", "256GB", "512GB", "1TB", "2TB"],
                "caracteristicas": ["Chip M3 Apple Silicon", "Pantalla Ultra Retina XDR (OLED)", "Diseño más delgado", "Apple Pencil Pro compatible", "Face ID"]
            },
             {
                "nombre": "iPad Air 13\" M2",
                "precio_usd": 799, "precio_gs": 799 * USD_TO_GS,
                "colores": ["Azul", "Púrpura", "Blanco estelar", "Gris espacial"],
                "almacenamiento": ["128GB", "256GB", "512GB", "1TB"],
                "caracteristicas": ["Chip M2 Apple Silicon", "Pantalla Liquid Retina 13\"", "Apple Pencil Pro compatible", "Touch ID en botón superior", "Cámara frontal horizontal"]
            },
            {
                "nombre": "Samsung Galaxy Tab S10 Ultra", # Modelo hipotético
                "precio_usd": 1100, "precio_gs": 1100 * USD_TO_GS,
                "colores": ["Beige", "Grafito"],
                "almacenamiento": ["256GB", "512GB", "1TB"], "ram": ["12GB", "16GB"],
                "caracteristicas": ["Pantalla Dynamic AMOLED 2X 14.6\"", "Procesador Snapdragon Gen 4 for Galaxy", "S-Pen incluido (baja latencia)", "Resistencia IP68", "Samsung DeX mejorado"]
            }
        ],
         "smartwatch": [
            {
                "nombre": "Apple Watch Series 10", # Modelo hipotético
                "precio_usd": 399, "precio_gs": 399 * USD_TO_GS,
                "colores": ["Aluminio: Medianoche, Blanco Estelar, Plata, Rojo (Product)RED", "Acero: Grafito, Oro, Plata"],
                "tamaño": ["41mm", "45mm"],
                "caracteristicas": ["Nuevo diseño (posiblemente más delgado)", "Sensor de presión arterial (rumoreado)", "Detección de apnea del sueño (rumoreado)", "Chip S10 más rápido", "watchOS 11"]
            },
             {
                "nombre": "Apple Watch Ultra 3", # Modelo hipotético
                "precio_usd": 799, "precio_gs": 799 * USD_TO_GS,
                "colores": ["Titanio natural (nuevos acabados posibles)"],
                "tamaño": ["49mm"],
                "caracteristicas": ["Chip S10", "Pantalla MicroLED (rumoreado, podría retrasarse)", "Mayor duración de batería", "Funciones avanzadas para deportes extremos", "Resistencia grado militar"]
             },
             {
                "nombre": "Samsung Galaxy Watch 7 Pro", # Modelo hipotético
                "precio_usd": 449, "precio_gs": 449 * USD_TO_GS,
                "colores": ["Negro Titanio", "Gris Titanio"],
                "tamaño": ["47mm"],
                "caracteristicas": ["Wear OS Powered by Samsung", "Nuevo procesador Exynos W1000 (rumoreado)", "Sensor BioActive (ECG, Presión Arterial, Composición Corporal)", "Batería de larga duración (hasta 80h)", "Bisel giratorio (posiblemente solo en Classic)"]
             }
        ],
        "consola": [
            {
                "nombre": "PlayStation 5 Slim (PS5 Slim)",
                "precio_usd": 499, "precio_gs": 499 * USD_TO_GS,
                "colores": ["Blanco/Negro"],
                "almacenamiento": ["1TB SSD (útil ~825GB)"],
                "caracteristicas": ["Diseño más compacto", "Lector de discos Blu-ray Ultra HD (extraíble en versión digital)", "CPU AMD Zen 2 8-core", "GPU AMD RDNA 2 (10.3 TFLOPS)", "Audio 3D TempestTech", "Retrocompatible con PS4"]
            },
            {
                "nombre": "Xbox Series X",
                "precio_usd": 499, "precio_gs": 499 * USD_TO_GS,
                "colores": ["Negro"],
                "almacenamiento": ["1TB NVMe SSD (útil ~802GB)"],
                "caracteristicas": ["La consola más potente (12 TFLOPS)", "CPU AMD Zen 2 8-core", "GPU AMD RDNA 2", "Quick Resume", "Xbox Game Pass (requiere suscripción)", "Retrocompatible con Xbox One, 360, Original"]
             },
            {
                "nombre": "Nintendo Switch - Modelo OLED",
                "precio_usd": 349, "precio_gs": 349 * USD_TO_GS,
                "colores": ["Blanco", "Rojo Neón/Azul Neón"],
                "almacenamiento": ["64GB internos (ampliable con microSD)"],
                "caracteristicas": ["Pantalla OLED vibrante de 7 pulgadas", "Modos de juego: TV, sobremesa, portátil", "Joy-Con extraíbles", "Amplio catálogo de juegos exclusivos", "Soporte ajustable ancho"]
             }
        ],
         "tv": [
             {
                "nombre": "Samsung 65\" QN90D Neo QLED 4K TV", # Modelo hipotético 2025
                "precio_usd": 2200, "precio_gs": 2200 * USD_TO_GS,
                "colores": ["Negro"], "tamaño": ["65 pulgadas"],
                "caracteristicas": ["Tecnología Quantum Mini LED", "Resolución 4K UHD (3840x2160)", "Procesador NQ4 AI Gen2", "Tasa de refresco 120Hz (hasta 144Hz para gaming)", "Smart TV Tizen OS", "Sonido OTS+ (Object Tracking Sound+)"]
             },
             {
                "nombre": "LG 55\" C4 OLED evo 4K TV", # Modelo 2024 real
                "precio_usd": 1800, "precio_gs": 1800 * USD_TO_GS,
                "colores": ["Negro"], "tamaño": ["55 pulgadas"],
                "caracteristicas": ["Panel OLED evo autoiluminado", "Resolución 4K UHD", "Procesador α9 AI Gen7", "Perfect Black, Contraste Infinito", "Dolby Vision, Dolby Atmos", "webOS 24 Smart TV", "Ideal para cine y gaming (G-Sync, FreeSync)"]
             },
              {
                "nombre": "Sony 75\" BRAVIA 7 (XR70) QLED 4K TV", # Modelo 2024 real
                "precio_usd": 2800, "precio_gs": 2800 * USD_TO_GS,
                "colores": ["Negro"], "tamaño": ["75 pulgadas"],
                "caracteristicas": ["Panel QLED (Mini LED Backlight)", "Resolución 4K UHD", "Procesador XR", "XR Backlight Master Drive", "Google TV", "Acoustic Multi-Audio+", "Perfect for PlayStation 5"]
              }
         ],
         "audio": [
             {
                "nombre": "Sony WH-1000XM6 Auriculares inalámbricos", # Modelo hipotético
                "precio_usd": 399, "precio_gs": 399 * USD_TO_GS,
                "colores": ["Negro", "Plata", "Azul Noche"],
                "caracteristicas": ["Cancelación de ruido líder en la industria (mejorada)", "Nuevo diseño más cómodo", "Procesador de audio V2 (hipotético)", "Hasta 35 horas de batería (con NC)", "Conexión multipunto", "Audio Hi-Res inalámbrico (LDAC)"]
             },
             {
                "nombre": "Bose QuietComfort Ultra Headphones", # Modelo real
                "precio_usd": 429, "precio_gs": 429 * USD_TO_GS,
                "colores": ["Black", "White Smoke"],
                "caracteristicas": ["Cancelación de ruido de clase mundial", "Bose Immersive Audio (audio espacial)", "Modo Aware (transparencia)", "Ajuste cómodo y estable", "Hasta 24 horas de batería"]
             },
             {
                "nombre": "JBL Charge 6 Altavoz Bluetooth portátil", # Modelo hipotético
                "precio_usd": 199, "precio_gs": 199 * USD_TO_GS,
                "colores": ["Negro", "Azul", "Rojo", "Verde Camuflado"],
                "caracteristicas": ["Sonido JBL Original Pro potente", "Hasta 25 horas de reproducción", "Resistente al agua y al polvo (IP67)", "Powerbank incorporado para cargar dispositivos", "PartyBoost (conectar múltiples altavoces JBL)"]
             },
              {
                 "nombre": "Sonos Era 300 Altavoz inteligente", # Modelo real
                 "precio_usd": 449, "precio_gs": 449 * USD_TO_GS,
                 "colores": ["Negro", "Blanco"],
                 "caracteristicas": ["Audio espacial con Dolby Atmos", "Seis drivers posicionados para dispersión de sonido", "Control por voz (Sonos Voice, Alexa)", "WiFi y Bluetooth", "Trueplay tuning (ajuste acústico)", "Ideal para música y cine en casa (como traseros)"]
              }
         ],
        "accesorio": [
             {
                "nombre": "Apple AirPods Pro (2ª generación) con estuche USB-C",
                "precio_usd": 249, "precio_gs": 249 * USD_TO_GS,
                "colores": ["Blanco"],
                "caracteristicas": ["Cancelación Activa de Ruido mejorada", "Modo Ambiente Adaptativo", "Audio Espacial personalizado", "Chip H2", "Resistencia IP54 (auriculares y estuche)"]
            },
            {
                "nombre": "Samsung Galaxy Buds3 Pro", # Modelo hipotético
                "precio_usd": 229, "precio_gs": 229 * USD_TO_GS,
                "colores": ["Phantom Black", "Phantom Silver", "Bora Purple"],
                "caracteristicas": ["Cancelación de ruido inteligente", "Audio Hi-Fi 24 bits", "360 Audio con seguimiento de cabeza", "Diseño ergonómico mejorado", "Resistencia IPX7"]
            },
            {
                "nombre": "Anker PowerCore 20000 PD Power Bank",
                "precio_usd": 50, "precio_gs": 50 * USD_TO_GS,
                "colores": ["Negro"],
                "caracteristicas": ["Capacidad 20,000mAh", "Power Delivery (PD) 20W USB-C", "PowerIQ USB-A", "Carga hasta 2 dispositivos simultáneamente", "Compacto y fiable"]
             },
            {
                "nombre": "Logitech MX Master 3S Ratón inalámbrico",
                "precio_usd": 99, "precio_gs": 99 * USD_TO_GS,
                "colores": ["Grafito", "Gris Pálido"],
                "caracteristicas": ["Sensor óptico 8K DPI", "Clics silenciosos", "Scroll electromagnético MagSpeed", "Diseño ergonómico", " multidispositivo (hasta 3)", "USB-C carga rápida"]
             }
        ]
        # Añadir más categorías y productos aquí si es necesario
    }

# -------------------------------------------------------------------
# FUNCIONES DE APOYO
# -------------------------------------------------------------------
def generar_info_productos(productos: list) -> str:
    """Formatea la información de una lista de productos para mostrarla."""
    if not productos:
        return "No hay productos disponibles en esta categoría por el momento."

    texto = []
    # Limitar la cantidad de productos mostrados por categoría para no saturar
    MAX_PRODUCTOS_POR_CATEGORIA = 5
    for i, prod in enumerate(productos):
        if i >= MAX_PRODUCTOS_POR_CATEGORIA:
            texto.append(f"... y {len(productos) - MAX_PRODUCTOS_POR_CATEGORIA} más. Pregúntame si buscas algo específico.")
            break

        precio_gs_f = f"{prod['precio_gs']:,.0f}".replace(",", ".")
        # Incluir RAM o Tamaño si están presentes
        detalles_extra = ""
        if 'ram' in prod:
            detalles_extra += f"   🧠 RAM: {', '.join(prod['ram'])}\n"
        if 'tamaño' in prod:
             detalles_extra += f"   📏 Tamaño: {prod['tamaño'] if isinstance(prod['tamaño'], str) else ', '.join(prod['tamaño'])}\n"

        texto.append(
            f"📌 **{prod['nombre']}**\n"
            f"   💵 Precio: ${prod['precio_usd']:.2f}$ USD / ${precio_gs_f}$ Gs\n"
            f"   🎨 Colores: {', '.join(prod['colores'])}\n"
            f"   💾 Almacenamiento: {', '.join(prod.get('almacenamiento', ['N/D']))}\n"
            f"{detalles_extra}" # Añade RAM/Tamaño si existe
            f"   ⚙️ Destacado: {'; '.join(prod['caracteristicas'][:3])}..." # Muestra las 3 primeras características
        )
    return "\n\n".join(texto)


def buscar_por_caracteristicas(pregunta_lower: str, caracteristicas_solicitadas: set, catalogo: dict) -> str:
    """Busca productos que coincidan con las características solicitadas."""
    resultados_productos = []

    for categoria, productos in catalogo.items():
        for prod in productos:
            # Combina nombre, características y otros detalles en un texto para buscar
            texto_producto = f"{prod['nombre'].lower()} {' '.join(prod['caracteristicas']).lower()}"
            if 'almacenamiento' in prod: texto_producto += f" {' '.join(prod.get('almacenamiento', [])).lower()}"
            if 'colores' in prod: texto_producto += f" {' '.join(prod.get('colores', [])).lower()}"
            if 'tamaño' in prod: texto_producto += f" {prod['tamaño'] if isinstance(prod['tamaño'], str) else ' '.join(prod.get('tamaño', [])).lower()}"
            if 'ram' in prod: texto_producto += f" {' '.join(prod.get('ram', [])).lower()}"

            # Comprobar si *todas* las características solicitadas están en el texto del producto o pregunta
            # O si alguna característica clave está directamente en la pregunta
            mencionadas_en_producto = all(caract in texto_producto for caract in caracteristicas_solicitadas)
            mencionadas_en_pregunta = any(caract in pregunta_lower for caract in caracteristicas_solicitadas)

            # Priorizar si se mencionan características específicas en la pregunta
            if mencionadas_en_pregunta and any(caract in texto_producto for caract in caracteristicas_solicitadas):
                 # Podríamos refinar esto para que solo añada si la característica específica está en el producto
                if any(caract in texto_producto for caract in caracteristicas_solicitadas if caract in pregunta_lower):
                     resultados_productos.append(prod) # Añade el diccionario completo

            # Si no se menciona específicamente en la pregunta, pero todas las características están en el producto
            elif mencionadas_en_producto and not mencionadas_en_pregunta:
                 resultados_productos.append(prod) # Añade el diccionario completo

    # Eliminar duplicados si un producto coincide por varias vías
    resultados_unicos = []
    nombres_vistos = set()
    for prod in resultados_productos:
        if prod['nombre'] not in nombres_vistos:
            resultados_unicos.append(prod)
            nombres_vistos.add(prod['nombre'])

    # Formatear los resultados usando la función existente
    if resultados_unicos:
        # Aquí podrías llamar a generar_info_productos con la lista filtrada
        # return generar_info_productos(resultados_unicos[:5]) # Limitar a 5 resultados
        # O un formato más simple si prefieres:
        texto_resultados = []
        for prod in resultados_unicos[:5]: # Limitar a 5 resultados
             precio_gs_f = f"{prod['precio_gs']:,.0f}".replace(",", ".")
             categoria_prod = next((cat for cat, prods in catalogo.items() if prod in prods), "Desconocida")
             texto_resultados.append(f"• **{prod['nombre']}** ({categoria_prod.capitalize()}) - Precio: ${prod['precio_usd']:.2f}$ USD / ${precio_gs_f}$ Gs. *Características clave*: {', '.join(prod['caracteristicas'][:2])}...")
        if len(resultados_unicos) > 5:
             texto_resultados.append("... y algunos más.")
        return "\n".join(texto_resultados)

    return "" # Devuelve vacío si no hay resultados


# -------------------------------------------------------------------
# LOG
# -------------------------------------------------------------------

def guardar_prompt_log(prompt_completo):
    # Crear carpeta "log" si no existe
    os.makedirs("log", exist_ok=True)

    # Nombre del archivo basado en la fecha actual
    fecha = datetime.datetime.now().strftime("%Y-%m-%d")
    archivo = f"log/promt_log_{fecha}.txt"

    # Agregar contenido con timestamp
    timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    log_content = f"\n\n--- PROMPT GENERADO PARA EL MODELO ({timestamp}) ---\n"
    log_content += prompt_completo
    log_content += "\n--- FIN DEL PROMPT ---\n"

    with open(archivo, "a", encoding="utf-8") as f:
        f.write(log_content)        
        

4.3. templates/index.html

Este archivo define la interfaz de usuario del chat que tus visitantes verán. Utiliza Tailwind CSS para el estilo (incluido mediante CDN) y JavaScript para enviar las preguntas al backend y mostrar las respuestas.

  1. Ve a la pestaña "Files", navega a mysite/templates/.
  2. Crea un nuevo archivo llamado index.html.
  3. Copia y pega el siguiente código HTML en index.html:
Nota: El siguiente bloque contiene código HTML. Ha sido "escapado" para que se muestre correctamente como código en esta publicación. Debes copiarlo y pegarlo tal cual en tu archivo index.html en PythonAnywhere.
        


<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Chat Gemma</title>
  <script src="https://cdn.tailwindcss.com"></script>
</head>

<body class="bg-gray-900 text-gray-100 h-screen flex flex-col">
  <header class="text-center py-4 text-xl font-semibold border-b border-gray-700">
    Asistente de ventas - Gemma
  </header>

  <main class="flex-1 overflow-hidden">
    <div id="chat-container" class="h-full overflow-y-auto p-4 text-lg space-y-3">
      <div id="empty-state" class="text-center text-gray-500 mt-32">
        Escribe algo para comenzar...
      </div>
    </div>
  </main>

  <footer class="border-t border-gray-700 p-3 bg-gray-800">
    <div class="flex gap-2">
      <input
        id="user-input"
        type="text"
        placeholder="Tu mensaje..."
        class="flex-1 bg-gray-700 text-white rounded px-4 py-3 text-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
        autocomplete="off"
      >
      <button
        id="send-btn"
        class="px-5 py-3 bg-blue-600 text-white text-lg rounded hover:bg-blue-700"
      >
        Enviar
      </button>
    </div>
  </footer>

  <script>
    document.addEventListener('DOMContentLoaded', () => {
      const chat = document.getElementById('chat-container');
      const input = document.getElementById('user-input');
      const sendBtn = document.getElementById('send-btn');
      const empty = document.getElementById('empty-state');

      const demoMessageDiv = document.createElement('div');
      demoMessageDiv.className = 'bg-gray-800 text-gray-300 py-2 px-4 text-sm text-center border-b border-gray-700 mb-3 rounded-md';
      demoMessageDiv.innerHTML = '⚠️ <strong>Observación:</strong> Este chat opera como una demostración y no está conectado a una base de datos en tiempo real. Las respuestas se basan en información contenida en archivos estáticos. Puede realizar consultas para evaluar su funcionalidad.';
      chat.prepend(demoMessageDiv);

      function addMessage(role, content) {
        if (empty) empty.style.display = 'none';

        const div = document.createElement('div');
        div.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'}`;

        const bubble = document.createElement('div');
        bubble.className = `max-w-xl px-4 py-3 rounded-lg ${
          role === 'user'
            ? 'bg-blue-600 text-white rounded-br-none'
            : 'bg-gray-700 text-gray-100 rounded-bl-none'
        }`;
        bubble.innerHTML = content;

        div.appendChild(bubble);
        chat.appendChild(div);
        chat.scrollTop = chat.scrollHeight;
      }

      async function sendMessage() {
        const historial = getHistorial();
        const question = input.value.trim();
        if (!question) return;

        addMessage('user', question);
        input.value = '';

        const typing = document.createElement('div');
        typing.className = 'flex justify-start';
        typing.innerHTML = `
          <div class="bg-gray-700 text-gray-300 rounded px-4 py-3 text-lg flex items-center gap-2">
            <span class="flex gap-1">
              <span class="animate-bounce [animation-delay:-0.3s]">.</span>
              <span class="animate-bounce [animation-delay:-0.15s]">.</span>
              <span class="animate-bounce">.</span>
            </span>
          </div>
        `;
        chat.appendChild(typing);
        chat.scrollTop = chat.scrollHeight;

        try {
          const res = await fetch('/api/ask', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              question,
              history: historial
            })
          });

          chat.removeChild(typing);
          const data = await res.json();
          const htmlFormatted = markdownToHTML(data.response || '');
          addMessage('assistant', htmlFormatted);

        } catch (err) {
          chat.removeChild(typing);
          addMessage('assistant', `Error: ${err.message}`);
        }
      }

      function markdownToHTML(text) {
        return text
          .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
          .replace(/\*(.*?)\*/g, '<em>$1</em>')
          .replace(/^\s*[-*]\s+(.*)$/gm, '<li>$1</li>')
          .replace(/(<li>.*<\/li>)/gms, '<ul>$1</ul>')
          .replace(/\n{2,}/g, '</p><p>')
          .replace(/\n/g, ' ')
          .replace(/^/, '<p>').replace(/$/, '</p>');
      }

      function getHistorial() {
        const mensajes = Array.from(document.querySelectorAll('#chat-container > div'));
        const partes = [];

        mensajes.forEach(div => {
          const bubble = div.querySelector('div');
          if (bubble) {
            const texto = bubble.textContent.trim();
            if (div.classList.contains('justify-end')) {
              partes.push(`Usuario: ${texto}`);
            } else {
              partes.push(`Asistente: ${texto}`);
            }
          }
        });

        return partes.join('\n');
      }

      sendBtn.addEventListener('click', sendMessage);
      input.addEventListener('keypress', e => {
        if (e.key === 'Enter') sendMessage();
      });
    });
  </script>

</body>
</html>



Paso 5: Obtener y Configurar la API Key de Google

  1. Ve a Google AI Studio.
  2. Inicia sesión con tu cuenta de Google.
  3. Haz clic en "Create API key in new project" (o selecciona un proyecto existente).
  4. Copia la clave API que se genera. ¡Guárdala en un lugar seguro!
  5. Vuelve a PythonAnywhere, abre el archivo mysite/flask_app.py.
  6. Busca la línea:
    API_KEY = "agrega tu apikey aqui"
  7. Reemplaza "agrega tu apikey aqui" con la clave API que copiaste, manteniendo las comillas. Debería quedar algo así:
    API_KEY = "AIzaSyB...xxxxxxxxxxxxxx..._Q"
  8. Guarda el archivo flask_app.py.

Paso 6: Desplegar y Probar

  1. Ve a la pestaña "Web" en PythonAnywhere.
  2. Haz clic en el botón verde "Reload tu_usuario.pythonanywhere.com". Esto cargará los cambios que hiciste en los archivos.
  3. Espera unos segundos a que la aplicación se recargue.
  4. Abre una nueva pestaña en tu navegador y visita tu URL: http://tu_usuario.pythonanywhere.com.
  5. ¡Deberías ver la interfaz del chat! Prueba a escribir una pregunta como "Hola", "¿Qué celulares tienen en oferta?" o "¿Cuál es la garantía de las notebooks?".
  6. Observa cómo el asistente (Gemma) responde a través de tu aplicación Flask.

Si encuentras errores, revisa los logs de errores en la misma pestaña "Web" de PythonAnywhere (Server log y Error log) para obtener pistas sobre qué podría estar fallando.

Conclusión

¡Felicidades! Has creado e implementado un asistente de ventas básico usando Flask, PythonAnywhere y la IA de Google. Este es un punto de partida excelente. Desde aquí, puedes explorar muchas mejoras:

  • Integrar una base de datos real para el catálogo de productos y políticas.
  • Mejorar la gestión del historial de conversación.
  • Añadir más lógica al `contexto.py` para manejar escenarios de venta más complejos.
  • Personalizar aún más la interfaz de usuario.
  • Implementar autenticación si fuera necesario.

¡Espero que esta guía te haya sido útil! No dudes en dejar tus preguntas o comentarios.

Comentarios

Entradas populares de este blog

Instalación y Configuración de MySQL 5.7 en Ubuntu 24.04 LTS

Instalar MySQL 5.7 en Ubuntu 24.04 1. Descargar e instalar MySQL Copiar mkdir ~/mysql57 cd ~/mysql57 wget https://cdn.mysql.com/archives/mysql-5.7/mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz tar -zxvf mysql-5.7.44-linux-glibc2.12-x86_64.tar.gz sudo mv mysql-5.7.44-linux-glibc2.12-x86_64 /usr/local/mysql sudo ln -s /usr/local/mysql/bin/mysql /usr/local/bin/mysql 2. Instalar dependencias necesarias IMPORTANTE: Se descargan las versiones nuevas de las librerías y se las vincula con las librerías que necesita MySQL. Copiar sudo apt update # Reemplazo de libaio sudo apt install libaio1t64 # Reemplazo de libtinfo y ncurses sudo apt install libtinfo6 libncurses6 Copiar # Crear los enlaces simbólicos sudo ln -sf /usr/lib/x86_64-linux-gnu/libaio.so.1t64 /usr/lib/libaio.so.1 sudo ln -sf /usr/lib/x86_64-linux-gnu/libtinfo.so.6 /usr/lib/x86_64-linux-gnu/libtinfo.so.5 sudo ln -sf /usr/lib/x86_64-linux-gnu/libncurses.so.6 /usr/lib/x86_64...

Instalar DeepSeek R1 1.5B en Ubuntu 24.04 sin GPU

Instalar DeepSeek en tu sistema sin GPU, pasos: Especificaciones del Entorno de Pruebas Componente Detalle SO Ubuntu Cinnamon 24.04 LTS x86_64 Kernel 6.8.0-51-generic CPU Intel i7-6820HQ (8 núcleos) @ 3.600GHz GPUs AMD ATI Radeon HD 8830M / R7 250 / R7 M465X Intel HD Graphics 530 RAM 15.882 GB (3.716 GB en uso) Resolución 1440x810 Escritorio Cinnamon 6.0.4 1. Instalar Git LFS sudo apt-get install git-lfs git lfs install 2. Clonar el repositorio cd /opt sudo mkdir deepseek && sudo chown $USER:$USER deepseek cd deepseek git clone https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B 3. Crear y activar un entorno virtual python -m ve...

Instalar Jasper Studio 6.21 para Ubuntu 24.04

Instalar js-studiocomm_6.21.3 en Ubuntu 24.4 Para instalar Jaspersoft Studio en Ubuntu 24.4, sigue estos pasos: 1. Descargar Jasper Studio Descarga la versión js-studiocomm_6.21.3 desde el siguiente enlace: Jaspersoft Studio 6.21.3 2. Crear el directorio de instalación mkdir /opt/jasperstudio 3. Mover el archivo descargado mv /dir_descarga/js-studiocomm_6.21.3_linux_x86_64.tgz /opt/jasperstudio/ cd /opt/jasperstudio 4. Extraer el archivo tar -xvzf js-studiocomm_6.21.3_linux_x86_64.tgz cd js-studiocomm_6.21.3 5. Ejecutar Jaspersoft Studio ./Jaspersoft\ Studio 6. Crear acceso directo en el escritorio Para facilitar el acceso, crea un archivo .desktop en el escritorio: gedit ~/Escritorio/jaspersoft-studio.desktop En el archivo jaspersoft-studio.desktop , agrega lo siguiente: [Desktop Entry] Version=1.0 Ty...