{
  "updatedAt": "2026-03-30T22:18:19.769Z",
  "createdAt": "2026-03-09T14:05:16.356Z",
  "id": "sNeOUViiSYyROtea",
  "name": "\ud83e\uddea LABR - nuevo asistente (REPARADO)",
  "description": null,
  "active": true,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ycloud-lab-test-v2",
        "options": {}
      },
      "id": "2daa57db-e4ab-4696-a96a-4b4cad12b8fc",
      "name": "\ud83d\udce5 Webhook YCloud",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -528,
        1200
      ],
      "typeVersion": 2
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "OK",
        "options": {}
      },
      "id": "e0703b6d-d536-4e7f-bd8a-6c0adf2969e4",
      "name": "\u2705 Responder OK",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -160,
        976
      ],
      "typeVersion": 1.4
    },
    {
      "parameters": {
        "jsCode": "// ======================================================\n// \ud83d\udee1\ufe0f FIX: Verificar que el nodo se ejecut\u00f3 antes de acceder\n// ======================================================\nlet cliente = {};\ntry {\n  cliente = $('2. Obtener Cliente').first().json || {};\n} catch (e) {\n  cliente = { id: null, telefono: '', nombre: 'Cliente' };\n}\n\n// ======================================================\n// \ud83e\udde0 CONTIN\u00daA CON EL RESTO DEL C\u00d3DIGO ORIGINAL\n// ======================================================\n\n// \ud83e\udde0 PRE-PROCESAMIENTO v15 - BLOQUEO DE ESCALADOS\n// =====================================================\n// \u00daltima actualizaci\u00f3n: 2026-02-14\n// NUEVO: Bloqueo autom\u00e1tico de clientes escalados\n// =====================================================\n\nconst rawJson = $input.item.json;\nconst body = rawJson.body || rawJson;\nlet whatsappMsg = body.whatsappInboundMessage;\n\nif (!whatsappMsg && body.body && body.body.whatsappInboundMessage) {\n  whatsappMsg = body.body.whatsappInboundMessage;\n}\n\n// =====================================================\n// \ud83d\udee1\ufe0f DETECTAR MODO ADMIN/CLIENTE (MAURICIO)\n// =====================================================\nconst NUMEROS_DIRECTOR = ['573203062007', '3203062007'];\n\nconst fromRaw = whatsappMsg?.from || '';\nconst fromNormalizado = fromRaw.replace(/[\\+\\s-]/g, '');\nconst from = (whatsappMsg?.from || '').replace('+', '');\nconst to = (whatsappMsg?.to || '').replace('+', '');\nconst customerName = whatsappMsg?.customerProfile?.name || 'Cliente';\nconst messageType = whatsappMsg?.type || 'unknown';\nconst messageTextRaw = whatsappMsg?.text?.body || '';\nconst messageText = (NUMEROS_DIRECTOR.some(num => fromNormalizado === num || fromNormalizado.endsWith(num))) && (messageTextRaw.trim().startsWith('>')) ? messageTextRaw.trim().substring(1).trim() : messageTextRaw;\n\n// Saludo seg\u00fan hora (COLOMBIA UTC-5)\nconst now = new Date();\nconst horaCol = new Date(now.getTime() - 5 * 60 * 60 * 1000).getHours();\nlet saludo;\nif (horaCol < 12) saludo = 'Buenos d\u00edas';\nelse if (horaCol < 19) saludo = 'Buenas tardes';\nelse saludo = 'Buenas noches';\n\nconst esNumeroDirector = NUMEROS_DIRECTOR.some(num =>\n    fromNormalizado === num || fromNormalizado.endsWith(num)\n);\n\n// =====================================================\n// \ud83d\udd04 PREFIJO: > para modo CLIENTE\n// =====================================================\nconst mensajeEmpiezaConPrefijo = messageTextRaw.trim().startsWith('>');\nconst mensajeSinPrefijo = mensajeEmpiezaConPrefijo\n    ? messageTextRaw.trim().substring(1).trim()\n    : messageTextRaw;\n\n// =====================================================\n// \ud83d\udeab NUEVO: BLOQUEO DE CLIENTES ESCALADOS\n// =====================================================\n// IMPORTANTE: Si el cliente est\u00e1 escalado, NO procesar el mensaje\n// El cliente debe esperar respuesta humana\nconst ESTADOS_ESCALADOS = ['ESCALADO', 'PENDIENTE_HUMANO', 'EN_REVISION_HUMANA'];\n\n// Variable para guardar si el mensaje fue bloqueado\nlet mensajeBloqueado = false;\n\n// =====================================================\n// DETECTAR RESPUESTAS DE BOTONES INTERACTIVOS\n// =====================================================\nlet esRespuestaBoton = false;\nlet botonId = '';\nlet botonTexto = '';\nif (messageType === 'interactive') {\n    const interactive = whatsappMsg.interactive;\n    if (interactive?.type === 'button_reply') {\n        esRespuestaBoton = true;\n        botonId = interactive.button_reply?.id || '';\n        botonTexto = interactive.button_reply?.title || '';\n    } else if (interactive?.type === 'list_reply') {\n        esRespuestaBoton = true;\n        botonId = interactive.list_reply?.id || '';\n        botonTexto = interactive.list_reply?.title || '';\n    }\n}\n\n// =====================================================\n// MANEJO DE IM\u00c1GENES Y MEDIA\n// =====================================================\nconst esReaccion = messageType === 'reaction';\nconst esImagen = messageType === 'image';\nconst esDocumento = messageType === 'document';\nconst esAudio = messageType === 'audio';\nconst esVideo = messageType === 'video';\nconst esSticker = messageType === 'sticker';\nconst esMedia = esImagen || esDocumento || esAudio || esVideo || esSticker;\n\n// =====================================================\n// MANEJO DE REACCIONES (ESTADOS)\n// =====================================================\nif (esReaccion) {\n    const emoji = whatsappMsg.reaction?.emoji || '\u2764\ufe0f';\n    return [{\n        json: {\n            esComandoCopiloto: false,\n            mensajeCliente: `[Reacci\u00f3n: ${emoji}]`,\n            from, to, customerName, saludo,\n            esMediaNoSoportado: false,\n            esMedia: false,\n            esReaccion: true,\n            emojiReaccion: emoji,\n            tipoMensaje: 'reaction',\n            messageText: `[El cliente reaccion\u00f3 con ${emoji}]`,\n            esRespuestaBoton: false,\n            botonId: '', botonTexto: '',\n            productoIdDelBoton: null,\n            accionBoton: '',\n            esSoloSaludo: false,\n            esPreguntaConversacional: false,\n            debeHacerBusqueda: false,\n            esPedidoPlataforma: false,\n            infoPedidoPlataforma: null,\n            tieneMultiplesProductos: false,\n            terminosIndividuales: [],\n            terminoBusqueda: '',\n            terminoNormalizado: '',\n            variantesBusqueda: [],\n            terminosNormalizados: [],\n            timestamp: whatsappMsg?.sendTime\n        }\n    }];\n}\n\n// Si es media, no importa si est\u00e1 escalado o no\nif (esMedia) {\n    let textoMedia = '[Mensaje multimedia]';\n    if (esImagen) textoMedia = '[\ud83d\udcf8 Imagen recibida]';\n    else if (esDocumento) textoMedia = '[\ud83d\udcc4 Documento recibido]';\n    else if (esAudio) textoMedia = '[\ud83c\udfb5 Audio recibido]';\n    else if (esVideo) textoMedia = '[\ud83c\udfa5 Video recibido]';\n    else if (esSticker) textoMedia = '[\ud83d\ude0a Sticker recibido]';\n    \n    return [{\n        json: {\n            esComandoCopiloto: false,\n            mensajeCliente: messageText,\n        from, to, customerName, saludo,\n            esMediaNoSoportado: false,\n            esMedia: true,\n            esImagen, esDocumento, esAudio, esVideo, esSticker,\n            tipoMensaje: messageType,\n            messageText: textoMedia,\n            captionImagen: whatsappMsg?.image?.caption ||\n                whatsappMsg?.document?.caption || '',\n            esRespuestaBoton: false,\n            botonId: '', botonTexto: '',\n            productoIdDelBoton: null,\n            accionBoton: '',\n            esSoloSaludo: false,\n            esPreguntaConversacional: false,\n            debeHacerBusqueda: false,\n            esPedidoPlataforma: false,\n            infoPedidoPlataforma: null,\n            tieneMultiplesProductos: false,\n            terminosIndividuales: [],\n            terminoBusqueda: '',\n            terminoNormalizado: '',\n            variantesBusqueda: [],\n            terminosNormalizados: [],\n            timestamp: whatsappMsg?.sendTime\n        }\n    }];\n}\n\n// =====================================================\n// MANEJO DE RESPUESTAS DE BOTONES\n// =====================================================\nif (esRespuestaBoton) {\n    let productoIdDelBoton = null;\n    let accionBoton = 'otro';\n    \n    if (botonId.startsWith('agregar_producto_')) {\n        productoIdDelBoton = parseInt(botonId.replace('agregar_producto_', ''));\n        accionBoton = 'agregar';\n    } else if (botonId.startsWith('ver_mas_producto_')) {\n        productoIdDelBoton = parseInt(botonId.replace('ver_mas_producto_', ''));\n        accionBoton = 'ver_mas';\n    } else if (botonId === 'ver_carrito') {\n        accionBoton = 'ver_carrito';\n    } else if (botonId === 'completar_pedido') {\n        accionBoton = 'completar_pedido';\n    } else if (botonId === 'cancelar_pedido') {\n        accionBoton = 'cancelar_pedido';\n    }\n    \n    return [{\n        json: {\n            esComandoCopiloto: false,\n            mensajeCliente: messageText,\n        from, to, customerName, saludo,\n            esMediaNoSoportado: false,\n            esMedia: false,\n            tipoMensaje: messageType,\n            messageText: botonTexto,\n            esRespuestaBoton: true,\n            botonId, botonTexto,\n            productoIdDelBoton,\n            accionBoton,\n            esSoloSaludo: false,\n            esPreguntaConversacional: false,\n            debeHacerBusqueda: false,\n            esPedidoPlataforma: false,\n            infoPedidoPlataforma: null,\n            tieneMultiplesProductos: false,\n            terminosIndividuales: [],\n            terminoBusqueda: '',\n            terminoNormalizado: '',\n            variantesBusqueda: [],\n            terminosNormalizados: [],\n            timestamp: whatsappMsg?.sendTime\n        }\n    }];\n}\n\n// =====================================================\n// VALIDAR MENSAJE DE TEXTO\n// =====================================================\nif (!whatsappMsg || messageType !== 'text') {\n    return [{\n        json: {\n            esComandoCopiloto: false,\n            mensajeCliente: messageText,\n        from, to, customerName, saludo,\n            esMediaNoSoportado: true,\n            esMedia: false,\n            tipoMensaje: messageType,\n            messageText: '',\n            esRespuestaBoton: false,\n            botonId: '', botonTexto: '',\n            productoIdDelBoton: null,\n            accionBoton: '',\n            esSoloSaludo: false,\n            esPreguntaConversacional: false,\n            debeHacerBusqueda: false,\n            esPedidoPlataforma: false,\n            infoPedidoPlataforma: null,\n            tieneMultiplesProductos: false,\n            terminosIndividuales: [],\n            terminoBusqueda: '',\n            terminoNormalizado: '',\n            variantesBusqueda: [],\n            terminosNormalizados: [],\n            timestamp: whatsappMsg?.sendTime\n        }\n    }];\n}\n\n// =====================================================\n// \ud83d\udeab NUEVO: VERIFICAR SI EST\u00c1 ESCALADO\n// =====================================================\n// Obtener estado del cliente (si viene del input) - CON SEGURIDAD\nlet clienteInput = {};\ntry {\n    clienteInput = $('2. Obtener Cliente').first().json || {};\n} catch (e) {\n    clienteInput = {};\n}\n\n// Verificar si el cliente est\u00e1 escalado\nif (clienteInput && clienteInput.estado_conversacion && \n    ESTADOS_ESCALADOS.includes(clienteInput.estado_conversacion)) {\n    \n    console.log('\ud83d\udeab Cliente escalado detectado. Bloqueando respuesta autom\u00e1tica.');\n    console.log(`\ud83d\udcde Cliente: ${from}`);\n    console.log(`\ud83d\udcca Estado: ${clienteInput.estado_conversacion}`);\n    console.log(`\ud83d\udcdd Mensaje: ${messageTextRaw}`);\n    \n    // \u2705 RETORNAR MENSAJE DE ESPERA (NO PROCESAR M\u00c1S)\n    mensajeBloqueado = true;\n    \n    return [{\n        json: {\n            mensajeCliente: messageText,\n        from, to, customerName, saludo,\n            esComandoCopiloto: false,\n            esMediaNoSoportado: false,\n            esMedia: false,\n            tipoMensaje: 'text',\n            messageText: messageTextRaw,\n            esRespuestaBoton: false,\n            botonId: '', botonTexto: '',\n            productoIdDelBoton: null,\n            accionBoton: '',\n            // Indicadores para el siguiente flujo\n            esClienteEscalado: true,\n            estado_conversacion: clienteInput.estado_conversacion,\n            // Marcador para que el flujo sepa que est\u00e1 escalado\n            bloquearRespuesta: true\n        }\n    }];\n}\n\n// =====================================================\n// SI ES DIRECTOR\n// =====================================================\nif (esNumeroDirector) {\n    if (mensajeEmpiezaConPrefijo) {\n        // Contin\u00faa como cliente normal\n    } else {\n        // MODO COPILOTO\n        const normalizedText = messageTextRaw.replace(/[^\\d]/g, '');\n        const phoneMatch = normalizedText.match(/(?:57)?(3\\d{9})/);\n        const telefono_objetivo = phoneMatch ? phoneMatch[1].replace(/^57/, '') : null;\n        \n        let nombre_extraido = null;\n        const nombrePatterns = [\n            /(?:a|al nombre)\\s+[\"']?([A-Za-z\u00c1-\u00ff\\s]+?)[\"']?(?:\\s|$)/i,\n            /(?:nombre es|llama)\\s+[\"']?([A-Za-z\u00c1-\u00ff\\s]+?)[\"']?(?:\\s+y|\\s*$)/i,\n        ];\n        for (const pattern of nombrePatterns) {\n            const match = messageTextRaw.match(pattern);\n            if (match && match[1]) {\n                nombre_extraido = match[1].trim().replace(/\\b(del|cliente|el|la)\\b/gi, '').trim();\n                if (nombre_extraido.length >= 2) break;\n                else nombre_extraido = null;\n            }\n        }\n        \n        return [{\n            json: {\n                esComandoCopiloto: true,\n                mensajeCliente: messageText,\n        from, to, customerName, saludo,\n                messageText: messageTextRaw,\n                prompt: messageTextRaw,\n                mensajeClienteActual: messageTextRaw,\n                telefono_objetivo: telefono_objetivo,\n                nombre_extraido: nombre_extraido,\n                esMediaNoSoportado: false,\n                esMedia: false,\n                tipoMensaje: messageType,\n                timestamp: whatsappMsg?.sendTime\n            }\n        }];\n    }\n}\n\n// =====================================================\n// RESTO DEL C\u00d3DIGO (CLIENTES NORMALES O DIRECTOR CON >)\n// =====================================================\n// messageText movido al inicio\n\n// Detectar saludos simples\nconst esSoloSaludo = [\n    /^hola[,!\\.\\s]*$/i,\n    /^buenos?\\s+(d[i\u00ed]as?|tardes?|noches?)[,!\\.\\s]*$/i,\n    /^buenas[,!\\.\\s]*$/i,\n    /^hey[,!\\.\\s]*$/i,\n    /^qu[e\u00e9]\\s+tal[,!\\.\\s]*$/i,\n    /^c[o\u00f3]mo\\s+est[a\u00e1](s|n)?[,!\\.\\s]*$/i,\n    /^gracias[,!\\.\\s]*$/i,\n    /^ok[,!\\.\\s]*$/i,\n    /^s[i\u00ed][,!\\.\\s]*$/i,\n    /^no[,!\\.\\s]*$/i,\n    /^perfecto[,!\\.\\s]*$/i,\n    /^listo[,!\\.\\s]*$/i,\n    /^dale[,!\\.\\s]*$/i,\n    /^bien[,!\\.\\s]*$/i\n].some(p => p.test(messageText));\n\n// Detectar pedido desde plataforma\nconst esPedidoPlataforma = [\n    /acabo\\s+de\\s+hacer\\s+un\\s+pedido/i,\n    /hice\\s+un\\s+pedido\\s+en\\s+(la\\s+)?tienda/i,\n    /tus-aguacates\\.vercel\\.app/i,\n    /mi\\s+pedido:.*\\n.*total/is\n].some(p => p.test(messageText));\n\nlet infoPedidoPlataforma = null;\nif (esPedidoPlataforma) {\n    const nombreMatch = messageText.match(/me\\s+llamo\\s+(\\w+)/i);\n    const pagoMatch = messageText.match(/pago:\\s*(.+?)(\\n|$)/i);\n    const totalMatch = messageText.match(/total:\\s*\\$?([.\\d,]+)/i);\n    infoPedidoPlataforma = {\n        nombre: nombreMatch ? nombreMatch[1] : null,\n        metodoPago: pagoMatch ? pagoMatch[1].trim() : null,\n        total: totalMatch ? totalMatch[1] : null\n    };\n}\n\n// Detectar preguntas conversacionales\nconst esPreguntaConversacional = [\n    /c[o\u00f3]mo\\s+(veo|entro|accedo|visito|llego|abro)/i,\n    /d[o\u00f3]nde\\s+(est[a\u00e1]|queda|encuentro|veo)/i,\n    /tienda\\s+(en\\s+)?l[i\u00ed]nea/i,\n    /p[a\u00e1]gina\\s+web/i,\n    /\\b(link|enlace|url)\\b/i,\n    /\\bhorario/i,\n    /a\\s+qu[e\u00e9]\\s+hora/i,\n    /hasta\\s+qu[e\u00e9]\\s+hora/i,\n    /\\benv[i\u00ed]o/i,\n    /\\bentrega/i,\n    /\\bdomicilio/i,\n    /hacen\\s+env[i\u00ed]o/i,\n    /zona\\s+de\\s+(cobertura|entrega)/i,\n    /m[e\u00e9]todo(s)?\\s+de\\s+pago/i,\n    /aceptan\\s+(tarjeta|nequi|daviplata|efectivo)/i,\n    /puedo\\s+pagar\\s+con/i,\n    /\\bcontacto/i,\n    /\\bubicaci[o\u00f3]n/i,\n    /\\bdirecci[o\u00f3]n/i,\n    /d[o\u00f3]nde\\s+est[a\u00e1]n/i,\n    /mi\\s+pedido/i,\n    /qu[e\u00e9]\\s+llevo/i,\n    /eso\\s+es\\s+todo/i,\n    /total\\s+(del\\s+)?pedido/i,\n    /cu[a\u00e1]nto\\s+es\\s+(el\\s+)?total/i,\n    /cu[a\u00e1]nto\\s+te\\s+debo/i\n].some(p => p.test(messageText));\n\n// =====================================================\n// \ud83d\udd24 EXTRACCI\u00d3N DE T\u00c9RMINO DE B\u00daSQUEDA - v14 ENRIQUECIDO\n// =====================================================\nconst palabrasIgnorar = new Set([\n    // === PALABRAS DE PRUEBA ===\n    'test', 'prueba', 'testing', 'probando', 'probar',\n    // === PREGUNTAS ===\n    'cuales', 'cu\u00e1les', 'cual', 'cu\u00e1l', 'donde', 'd\u00f3nde',\n    'cuando', 'cu\u00e1ndo', 'porque', 'porqu\u00e9', 'como', 'c\u00f3mo',\n    // === VERBOS PODER (todas conjugaciones) ===\n    'poder', 'puedo', 'puedes', 'puede', 'podemos', 'pueden',\n    'podr\u00eda', 'podr\u00edas', 'podr\u00edamos', 'podr\u00edan',\n    'pudiera', 'pudieras', 'pudieran', 'pude', 'pudiste',\n    // === VERBOS QUERER (todas conjugaciones) ===\n    'querer', 'quiero', 'quieres', 'quiere', 'queremos', 'quieren',\n    'quisiera', 'quisieras', 'quisi\u00e9ramos', 'quisieran',\n    'querr\u00eda', 'querr\u00edas', 'querr\u00edamos', 'querr\u00edan',\n    // === VERBOS AGREGAR/A\u00d1ADIR (todas conjugaciones) ===\n    'agregar', 'agrego', 'agregas', 'agrega', 'agregamos', 'agregan',\n    'agr\u00e9game', 'agregarme', 'agregarle', 'agr\u00e9gale',\n    'a\u00f1adir', 'a\u00f1ado', 'a\u00f1ades', 'a\u00f1ade', 'a\u00f1adimos', 'a\u00f1aden',\n    'a\u00f1ademe', 'a\u00f1adirme', 'a\u00f1adirle',\n    // === VERBOS DAR (todas conjugaciones) ===\n    'dar', 'doy', 'das', 'da', 'damos', 'dan',\n    'dame', 'deme', 'darme', 'darle', 'd\u00e1melo',\n    'd\u00e1ndome', 'd\u00e1ndole',\n    // === VERBOS TRAER/ENVIAR ===\n    'traer', 'traigo', 'traes', 'trae', 'traemos', 'traen',\n    'traeme', 'traerme', 'traerle',\n    'enviar', 'env\u00edo', 'env\u00edas', 'env\u00eda', 'enviamos', 'env\u00edan',\n    'enviame', 'enviarme', 'enviarle', 'env\u00edame',\n    // === VERBOS PONER/METER ===\n    'poner', 'pongo', 'pones', 'pone', 'ponemos', 'ponen',\n    'ponme', 'ponerle',\n    'meter', 'meto', 'metes', 'mete', 'metemos', 'meten',\n    'meteme', 'meterme', 'meterle',\n    // === VERBOS CONSEGUIR ===\n    'conseguir', 'consigo', 'consigues', 'consigue', 'conseguimos', 'consiguen',\n    'conseguirme', 'conseguirle',\n    // === VERBOS TENER/HABER ===\n    'tener', 'tengo', 'tienes', 'tiene', 'tenemos', 'tienen',\n    'tendr\u00eda', 'tendr\u00edas', 'tendr\u00edamos', 'tendr\u00edan', 'tendr\u00e1n',\n    'haber', 'hay', 'hab\u00eda', 'habr\u00e1', 'habr\u00eda', 'habr\u00edamos', 'habr\u00edan',\n    // === VERBOS BUSCAR ===\n    'buscar', 'busco', 'buscas', 'busca', 'buscamos', 'buscan',\n    'buscando', 'b\u00fascame', 'buscarme',\n    // === VERBOS VENDER/MANEJAR ===\n    'vender', 'vendo', 'vendes', 'vende', 'vemos', 'venden',\n    'manejar', 'manejo', 'manejas', 'maneja', 'manejamos', 'manejan',\n    'ofrecer', 'ofrezco', 'ofreces', 'ofrece', 'ofrecemos', 'ofrecen',\n    // === VERBOS NECESITAR ===\n    'necesitar', 'necesito', 'necesitas', 'necesita', 'necesitamos', 'necesitan',\n    // === VERBOS LLEVAR/COMPRAR/PEDIR ===\n    'llevar', 'llevo', 'llevas', 'leva', 'llevamos', 'levan',\n    'comprar', 'compro', 'compras', 'compra', 'compramos', 'compran',\n    'pedir', 'pido', 'pides', 'pide', 'pedimos', 'piden',\n    'pedido', 'pedidos',\n    // === VERBOS SER/ESTAR ===\n    'ser', 'soy', 'eres', 'es', 'somos', 'son',\n    'era', 'fue', 'era', 'estoy', 'est\u00e1s', 'est\u00e1', 'estamos', 'est\u00e1n',\n    'ser\u00eda', 'ser\u00edas', 'ser\u00edamos', 'ser\u00edan',\n    'estar\u00eda', 'estar\u00edas', 'estar\u00edamos', 'estar\u00edan',\n    // === VERBOS VER/MOSTRAR ===\n    'ver', 'veo', 'ves', 've', 'vemos', 'ven',\n    'mostrar', 'muestro', 'muestras', 'muestra', 'mostramos', 'muestran',\n    'ense\u00f1ar', 'ense\u00f1o', 'ense\u00f1as', 'ense\u00f1a', 'ense\u00f1amos', 'ense\u00f1an',\n    'mirar', 'miro', 'miras', 'mira', 'miramos', 'miran',\n    // === ART\u00cdCULOS ===\n    'el', 'la', 'los', 'las', 'un', 'una', 'unos', 'unas',\n    'este', 'esta', 'estos', 'estas', 'ese', 'esa',\n    'aquel', 'aquella', 'aquellos', 'aquellas',\n    // === PRONOMBRES DEMOSTRATIVOS ===\n    'este', 'esta', 'estos', 'estas', 'ese', 'esa',\n    'aquel', 'aquella', 'aquellos', 'aquellas',\n    // === PRONOMBRES PERSONALES ===\n    'yo', 'tu', 't\u00fa', 'usted', '\u00e9l', 'ella', 'nosotros', 'ustedes', 'ellos', 'ellas',\n    'me', 'te', 'se', 'le', 'lo', 'la', 'les',\n    // === PREPOSICIONES ===\n    'de', 'del', 'a', 'al', 'para', 'por', 'con', 'sin', 'hacia', 'desde', 'entre', 'sobre', 'en',\n    // === INTERROGATIVOS ===\n    'que', 'qu\u00e9', 'cu\u00e1l', 'cu\u00e1les', 'd\u00f3nde', 'cu\u00e1ndo', 'cu\u00e1ndo', 'cu\u00e1nto', 'cu\u00e1nta',\n    'cu\u00e1ntos', 'cu\u00e1ntas', 'c\u00f3mo', 'c\u00f3mo', 'cu\u00e1l',\n    // === PRECIOS ===\n    'vale', 'valen', 'cuesta', 'cuestan', 'costar', 'costar\u00eda', 'precio', 'precios', 'costo', 'costos',\n    'valor', 'valores',\n    // === SALUDOS ===\n    'hola', 'buenos', 'buenas', 'hey', 'oye', 'oiga', 'disculpa', 'disculpe', 'perd\u00f3n', 'perdon',\n    'gracias', 'muchas', 'mil', 'please', 'plz', 'plis',\n    // === CORTES\u00cdA ===\n    'm\u00e1s', 'mas', 'menos', 'muy', 'mucho', 'mucha', 'poco', 'poca', 'tambi\u00e9n', 'tambien', 'adem\u00e1s', 'ademas', 'incluso',\n    'bueno', 'buena', 'buenos', 'buenas', 'mejor', 'mejores', 'peor', 'peores', 'grande', 'peque\u00f1o', 'peque\u00f1a',\n    'fresco', 'fresca', 'frescos', 'frescas',\n    // === ADJETIVOS/ADVERBIOS ===\n    'm\u00e1s', 'mas', 'menos', 'muy', 'mucho', 'mucha', 'poco', 'poca', 'tambi\u00e9n', 'tambien', 'adem\u00e1s', 'ademas', 'incluso',\n    'bueno', 'buena', 'buenos', 'buenas', 'mejor', 'mejores', 'peor', 'peores', 'grande', 'peque\u00f1o', 'peque\u00f1a',\n    'fresco', 'fresca', 'frescos', 'frescas',\n    // === N\u00daMEROS ===\n    'uno', 'dos', 'tres', 'cuatro', 'cinco', 'seis', 'siete', 'ocho', 'nueve', 'diez',\n    'primero', 'segundo', 'tercero', 'cuarto', 'quinto', 'sexto', 's\u00e9ptimo', 'octavo', 'noveno', 'd\u00e9cimo',\n    // === UNIDADES ===\n    'kilo', 'kilos', 'gramos', 'grs', 'libra', 'libras', 'unidad', 'unidades', 'paquete', 'paquetes',\n    'bandeja', 'bandejas', 'caja', 'cajas',\n    // === CONECTORES ===\n    'pero', 'aunque', 'entonces', 'pues', 'bueno', 'mira', 'oiga', 'digame', 'd\u00edgame', 'cu\u00e9ntame', 'cuentame',\n    'okay', 'vale', 'listo', 'dale', 'aja', 'aj\u00e1',\n    // === ADVERBIOS TIEMPO/LUGAR ===\n    'solo', 's\u00f3lo', 'solamente', '\u00fanicamente', 'unicamente',\n    'siempre', 'nunca', 'ahora', 'despu\u00e9s', 'despues', 'antes', 'antes',\n    'aqu\u00ed', 'aqui', 'ac\u00e1', 'aca', 'all\u00e1', 'alla', 'ah\u00ed', 'ahi',\n    // === OTRAS PALABRAS COMUNES ===\n    'ok', 'si', 'no', 'perfecto', 'listo', 'bien', 'todo', 'nada', 'alguno', 'alguna',\n    'otro', 'otra', 'otros', 'otras', 'misma', 'mismo', 'mismos', 'mismas',\n    'tal', 'vez', 'general', 'claro', 'casi', 'aprox', 'realmente', 'cierto'\n]);\n\nlet terminoBusqueda = messageText\n    .replace(/[^\\w\\s\u00e1\u00e9\u00ed\u00f3\u00fa\u00f1\u00fc\u00c1-\u00ff\u00cd-\u00da\\s]/g, '')\n    .split(/\\s+/)\n    .filter(w => w.length > 2 && !palabrasIgnorar.has(w.toLowerCase()))\n    .join(' ')\n    .trim();\n\n// Detectar m\u00faltiples productos\nconst separadores = /\\s+(?:y|o|,)\\s+/gi;\nconst tieneMultiplesProductos = separadores.test(terminoBusqueda);\n\nlet terminosIndividuales = [];\nif (tieneMultiplesProductos) {\n    terminosIndividuales = terminoBusqueda\n        .split(/s*(?:y|o|,)\\s*/gi)\n        .map(t => t.trim())\n        .filter(t => t.length > 0);\n} else if (terminoBusqueda) {\n    terminosIndividuales = [terminoBusqueda];\n}\n\n// NORMALIZACI\u00d3N DE PLURALES\nlet terminoNormalizado = terminoBusqueda;\nlet variantesBusqueda = [];\nlet terminosNormalizados = [];\n\nterminosIndividuales.forEach(termino => {\n    const palabras = termino.split(' ');\n    const palabrasNormalizadas = palabras.map(palabra => {\n        if (palabra.endsWith('s') && palabra.length > 3) {\n            return palabra.slice(0, -1);\n        }\n        if (palabra.endsWith('es') && palabra.length > 4) {\n            return palabra.slice(0, -2);\n        }\n        return palabra;\n    });\n    \n    const terminoNormalizado = palabrasNormalizadas.join(' ');\n    terminosNormalizados.push(terminoNormalizado);\n    \n    // Mantener el t\u00e9rmino original tambi\u00e9n\n    if (!variantesBusqueda.includes(termino)) {\n        variantesBusqueda.push(termino);\n    }\n    if (!variantesBusqueda.includes(terminoNormalizado)) {\n        variantesBusqueda.push(terminoNormalizado);\n    }\n});\n\nterminoNormalizado = terminosNormalizados.join(' ');\nvariantesBusqueda = [...new Set(variantesBusqueda)];\n\n// DECISI\u00d3N DE B\u00daSQUEDA\nconst debeHacerBusqueda =\n    !esSoloSaludo &&\n    !esPreguntaConversacional &&\n    terminoBusqueda.length > 0;\n\n// RESULTADO FINAL (CLIENTE NORMAL)\nreturn [{\n    json: {\n        esComandoCopiloto: false,\n        mensajeCliente: messageText,\n        from, to, customerName, saludo,\n        esMediaNoSoportado: false,\n        esMedia: false,\n        esImagen: false,\n        esDocumento: false,\n        esAudio: false,\n        esVideo: false,\n        esSticker: false,\n        tipoMensaje: 'text',\n        captionImagen: '',\n        esRespuestaBoton: false,\n        botonId: '',\n        botonTexto: '',\n        productoIdDelBoton: null,\n        accionBoton: '',\n        esSoloSaludo,\n        esPreguntaConversacional,\n        debeHacerBusqueda,\n        esPedidoPlataforma,\n        infoPedidoPlataforma,\n        tieneMultiplesProductos,\n        terminosIndividuales,\n        terminoBusqueda,\n        terminoNormalizado,\n        variantesBusqueda,\n        timestamp: whatsappMsg.sendTime\n    }\n}];\n"
      },
      "id": "c2d8c4de-9bdd-4b66-bbd8-abac56aa9c8f",
      "name": "1. Pre-procesamiento YCloud",
      "type": "n8n-nodes-base.code",
      "position": [
        -224,
        1440
      ],
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "es-media",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.esMediaNoSoportado }}",
              "rightValue": true
            }
          ]
        },
        "options": {}
      },
      "id": "2ce881a6-e0cc-4836-91ab-c43a398a3b68",
      "name": "\u2753 \u00bfEs Media?",
      "type": "n8n-nodes-base.if",
      "position": [
        320,
        1168
      ],
      "typeVersion": 2.2
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://httpbin.org/post",
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {}
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        416,
        976
      ],
      "id": "1c1dc511-7909-42f1-89bc-538c2ba4245a",
      "name": "\ud83d\udcf1 Responder Media No Soportado"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH nuevo_cliente AS (\n  INSERT INTO clientes (telefono, nombre, estado_conversacion)\n  SELECT\n    '{{ $json.from }}',\n    NULL,\n    'NUEVO'\n  WHERE NOT EXISTS (\n    SELECT 1 FROM clientes WHERE telefono = '{{ $json.from }}'\n  )\n  RETURNING id\n), candidatos AS (\n  SELECT\n    id,\n    telefono,\n    nombre,\n    direccion,\n    estado_conversacion,\n    pre_pedido,\n    total_pedidos,\n    CASE\n      WHEN COALESCE(jsonb_array_length(COALESCE(pre_pedido, '[]'::jsonb)), 0) > 0 THEN 0\n      ELSE 1\n    END AS prioridad_carrito\n  FROM clientes\n  WHERE telefono = '{{ $json.from }}'\n)\nSELECT id, telefono, nombre, direccion, estado_conversacion, pre_pedido, total_pedidos\nFROM candidatos\nORDER BY prioridad_carrito ASC, id DESC\nLIMIT 1;",
        "options": {}
      },
      "name": "2. Obtener Cliente",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "id": "61583a03-3ecc-4030-94f8-3acc01b29774",
      "position": [
        464,
        1344
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "es-busqueda",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $('1. Pre-procesamiento YCloud').first().json.debeHacerBusqueda }}",
              "rightValue": true
            }
          ]
        },
        "options": {}
      },
      "id": "9987fdfe-c019-409d-a941-1538ada467b3",
      "name": "\u2753 \u00bfBusca Producto?",
      "type": "n8n-nodes-base.if",
      "position": [
        1152,
        1184
      ],
      "typeVersion": 2.2
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH search_terms AS (\n  SELECT\n    LOWER('{{ $json.terminoBusqueda }}') AS term_original,\n    LOWER(\n      CASE\n        WHEN LOWER('{{ $json.terminoBusqueda }}') IN ('hass', 'aguacate', 'aguacate hass') THEN '{{ $json.terminoBusqueda }}'\n        WHEN LOWER('{{ $json.terminoBusqueda }}') ~ 'es$' AND LENGTH('{{ $json.terminoBusqueda }}') > 4 THEN TRIM(TRAILING 'es' FROM LOWER('{{ $json.terminoBusqueda }}'))\n        WHEN LOWER('{{ $json.terminoBusqueda }}') ~ 's$' AND LENGTH('{{ $json.terminoBusqueda }}') > 3 THEN TRIM(TRAILING 's' FROM LOWER('{{ $json.terminoBusqueda }}'))\n        ELSE '{{ $json.terminoBusqueda }}'\n      END\n    ) AS term_base,\n    LOWER('{{ $json.mensajeCliente || \"\" }}') AS full_message\n), productos_match AS (\n  SELECT\n    p.id, p.name, p.price, p.main_image_url, p.description, p.category_name, p.stock, p.supabase_id,\n    CASE\n      WHEN s.term_original = 'hass' AND LOWER(p.name) LIKE '%hass%' THEN 0\n      WHEN s.term_original LIKE '%aguacate%' AND LOWER(p.name) LIKE '%aguacate%' AND LOWER(p.name) LIKE 'caja %' THEN 0\n      WHEN s.full_message LIKE '%24%' AND LOWER(p.name) LIKE '%24%' THEN 0\n      WHEN s.full_message LIKE '%caja%' AND LOWER(p.name) LIKE 'caja %' THEN 1\n      WHEN regexp_replace(LOWER(p.name), '[^a-z0-9\u00e1\u00e9\u00ed\u00f3\u00fa\u00f1\u00fc ]', '', 'g') ~ ('(^| )' || regexp_replace(s.term_original, '[^a-z0-9\u00e1\u00e9\u00ed\u00f3\u00fa\u00f1\u00fc ]', '', 'g') || '( |$)') THEN 2\n      WHEN LOWER(p.name) LIKE '%' || s.term_original || '%' THEN 3\n      WHEN LOWER(COALESCE(p.category_name, '')) LIKE '%' || s.term_original || '%' THEN 4\n      ELSE 99\n    END AS match_priority\n  FROM public.productos_tienda p, search_terms s\n  WHERE p.is_active = true\n    AND (\n      LOWER(p.name) LIKE '%' || s.term_original || '%'\n      OR LOWER(COALESCE(p.category_name, '')) LIKE '%' || s.term_original || '%'\n      OR LOWER(p.description) LIKE '%' || s.term_original || '%'\n    )\n)\nSELECT pm.id, pm.name, pm.price, pm.main_image_url, pm.description, pm.category_name, pm.stock, pm.supabase_id, pm.match_priority,\n  (SELECT COALESCE(json_agg(json_build_object('variante_id', v.id,'tipo', v.variant_name,'presentacion', v.variant_value,'precio', v.price,'stock', v.stock_quantity) ORDER BY v.price), '[]'::json)\n   FROM variantes_productos v WHERE v.product_supabase_id = pm.supabase_id AND v.is_active = true) AS variantes\nFROM productos_match pm\nGROUP BY pm.id, pm.name, pm.price, pm.main_image_url, pm.description, pm.category_name, pm.stock, pm.supabase_id, pm.match_priority\nORDER BY pm.match_priority ASC, CASE WHEN LOWER(pm.name) LIKE 'caja %' THEN 0 ELSE 1 END, pm.name\nLIMIT 3;",
        "options": {}
      },
      "id": "2a74cbde-7789-4f86-a771-9eed4733e6b1",
      "name": "3. B\u00fasqueda Autom\u00e1tica Productos",
      "type": "n8n-nodes-base.postgres",
      "position": [
        1296,
        752
      ],
      "typeVersion": 2.6,
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Combinar datos del cliente y resultados de b\u00fasqueda\n// v5: Manejo robusto de clientes y pedidos web\n\nconst preproceso = $('1. Pre-procesamiento YCloud').first().json;\n\n// Obtener cliente con manejo de errores\nlet cliente = {};\ntry {\n  cliente = $('2. Obtener Cliente').first().json || {};\n} catch (e) {\n  // Si no existe el cliente, crear uno vac\u00edo\n  cliente = { id: null, telefono: preproceso.from, nombre: preproceso.customerName || 'Cliente' };\n}\n\n// Obtener productos de b\u00fasqueda\nlet productosEncontrados = [];\ntry {\n  const busquedaItems = $('3. B\u00fasqueda Autom\u00e1tica Productos').all();\n  productosEncontrados = busquedaItems\n    .map(item => item.json)\n    .filter(p => p && p.id && p.name && typeof p.price !== 'undefined');\n} catch (e) {\n  productosEncontrados = [];\n}\n\n// Formatear productos de forma SIMPLE\nlet productosTexto = '';\nif (productosEncontrados.length > 0) {\n  const lineas = [];\n  productosEncontrados.forEach(p => {\n    if (p.variantes && Array.isArray(p.variantes) && p.variantes.length > 0) {\n      p.variantes.forEach(v => {\n        lineas.push(`\u2022 ${p.name} - ${v.presentacion} $${Number(v.precio).toLocaleString('es-CO')} [ID:${p.id}]`);\n      });\n    } else {\n      lineas.push(`\u2022 ${p.name} - $${Number(p.price).toLocaleString('es-CO')} [ID:${p.id}]`);\n    }\n  });\n  productosTexto = [...new Set(lineas)].join('\\n');\n} else if (preproceso.debeHacerBusqueda && preproceso.terminoBusqueda) {\n  productosTexto = `\u274c No se encontraron productos para: \"${preproceso.terminoBusqueda}\"`;\n} else {\n  productosTexto = '(No se realiz\u00f3 b\u00fasqueda)';\n}\n\n// Contexto de tiempo\nconst now = new Date();\nconst dayOfWeek = now.getDay();\nconst hour = now.getHours();\nconst pasoCutoff = hour >= 10;\n\nlet diasHastaEntrega, mensajeEntrega;\nswitch(dayOfWeek) {\n  case 0: diasHastaEntrega = 2; mensajeEntrega = \"Pr\u00f3xima entrega: Martes\"; break;\n  case 1: diasHastaEntrega = 1; mensajeEntrega = \"Pr\u00f3xima entrega: Martes\"; break;\n  case 2: \n    if (pasoCutoff) { diasHastaEntrega = 3; mensajeEntrega = \"Pr\u00f3xima entrega: Viernes\"; }\n    else { diasHastaEntrega = 0; mensajeEntrega = \"\u00a1Entrega HOY antes 10AM!\"; }\n    break;\n  case 3: diasHastaEntrega = 2; mensajeEntrega = \"Pr\u00f3xima entrega: Viernes\"; break;\n  case 4: diasHastaEntrega = 1; mensajeEntrega = \"Pr\u00f3xima entrega: Viernes\"; break;\n  case 5:\n    if (pasoCutoff) { diasHastaEntrega = 4; mensajeEntrega = \"Pr\u00f3xima entrega: Martes\"; }\n    else { diasHastaEntrega = 0; mensajeEntrega = \"\u00a1Entrega HOY antes 10AM!\"; }\n    break;\n  case 6: diasHastaEntrega = 3; mensajeEntrega = \"Pr\u00f3xima entrega: Martes\"; break;\n  default: diasHastaEntrega = 1; mensajeEntrega = \"Pr\u00f3xima entrega: Martes o Viernes\";\n}\n\nconst fechaEntrega = new Date(now);\nfechaEntrega.setDate(now.getDate() + diasHastaEntrega);\nconst proximaEntrega = fechaEntrega.toLocaleDateString('es-CO', { weekday: 'long', day: 'numeric', month: 'long' });\nconst fechaActual = now.toLocaleDateString('es-CO', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });\nconst horaActual = now.toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit', hour12: true });\n\nreturn {\n  json: {\n    clienteId: cliente.id || null,\n    clienteTelefono: cliente.telefono || preproceso.from,\n    clienteNombre: cliente.nombre || preproceso.customerName || 'Cliente',\n    clienteEstado: cliente.estado_conversacion || 'NUEVO',\n    clienteDireccion: cliente.direccion || '',\n    clienteCarrito: cliente.pre_pedido || [],\n    clienteTotalPedidos: cliente.total_pedidos || 0,\n    \n    mensajeCliente: preproceso.mensajeCliente || preproceso.messageText,\n    terminoBusqueda: preproceso.terminoBusqueda || '',\n    esIntentoBusqueda: preproceso.esIntentoBusqueda || false,\n    esPedidoPlataforma: preproceso.esPedidoPlataforma || false,\n    infoPedidoPlataforma: preproceso.infoPedidoPlataforma || null,\n    \n    productosEncontrados,\n    productosTexto,\n    \n    from: preproceso.from,\n    to: preproceso.to,\n    saludo: preproceso.saludo,\n    \n    fechaActual, horaActual, horaNumero: hour, diaNumero: dayOfWeek,\n    pasoCutoff, proximaEntrega, mensajeEntrega, diasHastaEntrega,\n    retry_count: 0\n  }\n};\n"
      },
      "id": "4313e79a-64c2-46a3-8e81-e3e4efa7236d",
      "name": "4. Merge Datos + Productos",
      "type": "n8n-nodes-base.code",
      "position": [
        1584,
        592
      ],
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "USA OBLIGATORIAMENTE cuando el cliente diga \"quiero\", \"dame\", \"agr\u00e9game\" o similares. NO digas que ya lo hiciste si no has ejecutado esta herramienta. Requiere: producto_id, producto_nombre, precio, cantidad.",
        "operation": "executeQuery",
        "query": "UPDATE clientes \nSET pre_pedido = COALESCE(pre_pedido, '[]'::jsonb) || \n  jsonb_build_object(\n    'producto_id', $1::int,\n    'producto_nombre', $2,\n    'precio', $3::numeric,\n    'cantidad', COALESCE($4::int, 1)\n  ),\n  estado_conversacion = 'EN_PEDIDO'\nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\nRETURNING pre_pedido;",
        "options": {
          "queryReplacement": "={{ $fromAI('producto_id','ID del producto a agregar','number',0) }}\n{{ $fromAI('producto_nombre','Nombre del producto','string','Producto') }}\n{{ $fromAI('precio','Precio del producto','number',0) }}\n{{ $fromAI('cantidad','Cantidad a agregar','number',1) }}"
        }
      },
      "id": "211df281-543d-497a-b643-25dca132695c",
      "name": "TOOL_AnadirAlCarrito",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        1584,
        1552
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "USA cuando el cliente diga \"eso es todo\", \"cu\u00e1nto es\", \"calcular total\". Devuelve el total del carrito.",
        "operation": "executeQuery",
        "query": "SELECT \n  COALESCE(\n    (SELECT SUM((item->>'precio')::numeric * COALESCE((item->>'cantidad')::int, 1))\n     FROM clientes, jsonb_array_elements(pre_pedido) AS item\n     WHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\n    ), 0\n  ) as total,\n  pre_pedido as items\nFROM clientes\nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').item.json.from }}';",
        "options": {}
      },
      "id": "68d9c2da-d254-46d7-be1e-e702329223e9",
      "name": "TOOL_CalcularTotalPrePedido",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2512,
        1680
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Guardar el nombre del cliente cuando lo proporcione.",
        "operation": "executeQuery",
        "query": "UPDATE clientes \nSET \n  nombre = $1, \n  estado_conversacion = 'ATENCION_LUZ' \nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\nRETURNING nombre, estado_conversacion;",
        "options": {
          "queryReplacement": "={{ $fromAI('nombre_cliente','Nombre del cliente','string','Cliente') }}"
        }
      },
      "id": "6a73ac10-62d0-490d-a7a8-59007eab3613",
      "name": "TOOL_GuardarNombreCliente",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        1872,
        1824
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "USA SIEMPRE despu\u00e9s de cada acci\u00f3n importante. Estados: EN_PEDIDO (agreg\u00f3 productos), PEDIDO_CONFIRMADO (confirm\u00f3), PEDIDO_ONLINE (de tienda), ESCALADO (queja).",
        "operation": "executeQuery",
        "query": "UPDATE clientes \nSET estado_conversacion = $1\nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\nRETURNING estado_conversacion;",
        "options": {
          "queryReplacement": "={{ $fromAI('nuevo_estado','Estado v\u00e1lido','string','ATENCION_LUZ') }}"
        }
      },
      "id": "84728a22-7913-434d-91b4-15690b9a14b2",
      "name": "TOOL_CambiarEstadoCliente",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2224,
        1744
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const contexto = $('4. Merge Datos + Productos').first().json;\nconst respuestaIA = $input.first().json.text || $input.first().json.output || '';\nlet mensaje = String(respuestaIA || '').replace(/\\*/g, '').trim();\nconst nombreCompleto = String(contexto.clienteNombre || '').trim();\nconst tieneNombreCompleto = nombreCompleto.split(/\\s+/).filter(Boolean).length >= 2;\nconst tieneDireccion = String(contexto.clienteDireccion || '').trim().length > 5;\nconst carrito = Array.isArray(contexto.clienteCarrito) ? contexto.clienteCarrito : [];\nconst pareceResumen = /subtotal|total:|resumen/i.test(mensaje);\nif (pareceResumen && carrito.length > 0 && (!tieneNombreCompleto || !tieneDireccion)) {\n  mensaje = '\u00a1Casi listo! Para coordinar el env\u00edo, por favor conf\u00edrmame tu nombre completo y la direcci\u00f3n de entrega.';\n}\nif (!mensaje) return [];\nreturn [{ json: { from: contexto.to, to: contexto.from, type: 'text', text: { body: mensaje, preview_url: false } } }];"
      },
      "id": "0262ccbe-0660-46a8-a027-0e674dd48319",
      "name": "\ud83d\udce4 Preparar Respuesta",
      "type": "n8n-nodes-base.code",
      "position": [
        2864,
        720
      ],
      "typeVersion": 2
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://httpbin.org/post",
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "lab_intercept",
              "value": "={{ $json.toJsonString() }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3424,
        592
      ],
      "id": "4503a0ed-b573-452a-9788-4c5e27b600a5",
      "name": "\ud83d\udcf1 Enviar WhatsApp YCloud"
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Herramienta de FALLBACK para buscar productos. \u00dasala SOLO cuando los productos encontrados autom\u00e1ticamente NO coinciden con lo que pidi\u00f3 el cliente. Input: t\u00e9rmino de b\u00fasqueda del producto.",
        "operation": "executeQuery",
        "query": "SELECT \n    id,\n    name,\n    COALESCE(discount_price, price) as precio,\n    description,\n    category_name\nFROM productos_tienda p\nWHERE \n    is_active = true\n    AND p.name NOT ILIKE '%Fresa Premium 250g%' AND p.price > 0 AND p.stock > 0 AND (\n        name ILIKE '%' || $1 || '%'\n        OR description ILIKE '%' || $1 || '%'\n        OR category_name ILIKE '%' || $1 || '%'\n    )\nORDER BY \n    CASE \n        WHEN is_featured = true THEN 0 \n        ELSE 1 \n    END,\n    name\nLIMIT 5;\n",
        "options": {
          "queryReplacement": "={{ $fromAI('termino_busqueda','Producto a buscar','string','aguacate') }}"
        }
      },
      "id": "37802349-d077-4ffe-a17d-acbf9652535d",
      "name": "TOOL_BuscarProductos",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2640,
        1568
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "ESTA HERRAMIENTA ES OBLIGATORIA PARA ESCALAR. \u00dasala para transferir la sesi\u00f3n a un humano ante cualquier queja, molestia o pedido de soporte humano. NO PUEDES CONECTAR CON UN HUMANO SOLO CON TEXTO, DEBES ACTIVAR ESTA HERRAMIENTA.",
        "operation": "executeQuery",
        "query": "UPDATE clientes \nSET \n    estado_conversacion = 'ESCALADO'\nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\nRETURNING \n    nombre,\n    telefono,\n    estado_conversacion,\n    'Conversaci\u00f3n escalada. Motivo: ' || $1 as mensaje_confirmacion;\n",
        "options": {
          "queryReplacement": "={{ $fromAI('motivo_escalado','Motivo del escalado','string','Cliente solicita hablar con humano') }}"
        }
      },
      "id": "19f90844-7253-4e05-ab20-53780c3e8338",
      "name": "TOOL_EscalarServicioCliente",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        1744,
        1664
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Consultar el estado del cliente y su \u00faltimo pedido. Usarla cuando el cliente pregunte: \u00bfc\u00f3mo va mi pedido? \u00bftengo pedidos?",
        "operation": "executeQuery",
        "query": "SELECT \n  c.nombre,\n  c.telefono,\n  c.estado_conversacion as estado,\n  c.direccion,\n  c.pre_pedido as carrito,\n  c.updated_at as ultima_actualizacion,\n  CASE \n    WHEN c.estado_conversacion = 'PEDIDO_CONFIRMADO' THEN '\u2705 Tu pedido fue confirmado. Te contactaremos para coordinar la entrega.'\n    WHEN c.estado_conversacion = 'PEDIDO_ONLINE' THEN '\ud83d\uded2 Tu pedido de la tienda online est\u00e1 siendo procesado.'\n    WHEN c.estado_conversacion = 'EN_PEDIDO' THEN '\ud83d\udce6 Tienes un pedido en proceso. \u00bfDeseas confirmarlo?'\n    WHEN c.estado_conversacion = 'ESCALADO' THEN '\ud83d\udc64 Un asesor te contactar\u00e1 pronto.'\n    ELSE '\ud83d\udcac No encontr\u00e9 pedidos recientes. \u00bfQuieres hacer uno nuevo?'\n  END as mensaje_estado\nFROM clientes c\nWHERE c.telefono LIKE '%' || RIGHT('{{ $('1. Pre-procesamiento YCloud').first().json.from }}', 10) || '%'\nLIMIT 1;",
        "options": {}
      },
      "id": "65cf129f-27f4-4962-afb3-139f78304515",
      "name": "TOOL_ConsultarEstadoPedido",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2816,
        1488
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Guarda la direcci\u00f3n de entrega del cliente. Usar cuando el cliente proporciona su direcci\u00f3n completa. Input: direccion (la direcci\u00f3n del cliente).",
        "operation": "executeQuery",
        "query": "UPDATE clientes \nSET direccion = $1\nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\nRETURNING telefono, nombre, direccion, 'Direcci\u00f3n guardada correctamente' as status;",
        "options": {
          "queryReplacement": "=={{ [$fromAI('direccion','Direcci\u00f3n completa del cliente incluyendo calle, n\u00famero, barrio y ciudad','string','')] }}"
        }
      },
      "id": "7e6be0a3-dadf-4979-950a-e18cf3d5e7b0",
      "name": "TOOL_GuardarDireccionCliente",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2976,
        1408
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {},
      "id": "d6bc2944-ed4b-47f0-bf9d-9e1f2c5e40d3",
      "name": "Calculator",
      "type": "@n8n/n8n-nodes-langchain.toolCalculator",
      "position": [
        2336,
        1760
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "USA SIEMPRE cuando el cliente pregunte por un producto. Obtiene tama\u00f1os, pesos y precios. Input: supabase_id del producto (b\u00fascalo en productosEncontrados).",
        "operation": "executeQuery",
        "query": "SELECT \n  v.variant_name as tipo,\n  v.variant_value as presentacion,\n  v.price as precio,\n  v.stock_quantity as stock,\n  p.name as producto\nFROM variantes_productos v\nJOIN productos_tienda p ON p.supabase_id = v.product_supabase_id\nWHERE p.id = $1\n  AND v.is_active = true\nORDER BY v.variant_name, v.price;",
        "options": {
          "queryReplacement": "={{ $fromAI('producto_id','ID num\u00e9rico del producto','number',0) }}"
        }
      },
      "id": "fb1e25a8-8f9b-417a-924f-5e713233d2f9",
      "name": "TOOL_ObtenerVariantes",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2048,
        1744
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Obtener el producto de la consulta\nconst producto = $input.first().json;\n// Obtener el contexto del preprocesamiento\nconst contexto = $('1. Pre-procesamiento YCloud').first().json;\n// Formatear producto al formato esperado\nconst productoFormateado = {\n  producto_id: producto.id,\n  producto_nombre: producto.name,\n  name: producto.name,\n  id: producto.id,\n  precio: producto.price,\n  price: producto.price,\n  main_image_url: producto.main_image_url,\n  description: producto.description,\n  category: producto.category,\n  stock: producto.stock\n};\n// Retornar contexto con el producto encontrado\nreturn {\n  json: {\n    ...contexto,\n    productosEncontrados: [productoFormateado],\n    productosTexto: `Producto del bot\u00f3n: ${producto.name} - $${Number(producto.price).toLocaleString('es-CO')}`\n  }\n};"
      },
      "id": "89f8b753-f1c0-4d14-9c34-ad9238beb3c5",
      "name": "Code - Formatear Producto",
      "type": "n8n-nodes-base.code",
      "position": [
        1040,
        960
      ],
      "typeVersion": 2
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT \n  p.id,\n  p.name,\n  p.price,\n  p.main_image_url,\n  p.description,\n  p.category_id,\n  p.stock\nFROM public.productos_tienda p\nWHERE p.id = {{ $json.productoIdDelBoton }}\nLIMIT 1;",
        "options": {}
      },
      "id": "23d2a521-41a3-482b-b38f-de96a5d7317b",
      "name": "Buscar Producto del Bot\u00f3n",
      "type": "n8n-nodes-base.postgres",
      "position": [
        768,
        912
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "31594272-3c47-4074-9324-e697e674cd9b",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.esRespuestaBoton }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "Buscar Producto"
            },
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "c5c155de-73a0-4e73-8ec0-0c21914d6b18",
                    "operator": {
                      "type": "string",
                      "operation": "notEmpty",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.esRespuestaBoton }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "Sin Producto"
            }
          ]
        },
        "looseTypeValidation": true,
        "options": {
          "fallbackOutput": "none"
        }
      },
      "id": "2ac9fb8b-0172-48ac-9ab2-cb27f0ec396a",
      "name": "Detectar Click de Bot\u00f3n",
      "type": "n8n-nodes-base.switch",
      "position": [
        624,
        1232
      ],
      "typeVersion": 3.3
    },
    {
      "parameters": {},
      "id": "77068eff-d068-48d8-9f7b-f309db2c675d",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        976,
        1488
      ],
      "typeVersion": 3.2
    },
    {
      "parameters": {},
      "id": "eaf862ec-e845-4496-a1b4-fc4ccbca2e93",
      "name": "Merge1",
      "type": "n8n-nodes-base.merge",
      "position": [
        416,
        1600
      ],
      "typeVersion": 3.2
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "45a7a667-e7c8-41ab-823c-ea0048f4ca53",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.esComandoCopiloto }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "Copiloto"
            },
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "6e38428c-70d8-45a2-a0f1-a98aea4009f7",
                    "operator": {
                      "type": "boolean",
                      "operation": "false",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.esComandoCopiloto }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "Cliente"
            }
          ]
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "757b19ad-3ad2-47e9-bf59-a9b94b898bc3",
      "name": "\ud83d\udd00 \u00bfEs Copiloto?",
      "type": "n8n-nodes-base.switch",
      "position": [
        144,
        1312
      ],
      "typeVersion": 3.3
    },
    {
      "parameters": {
        "sessionIdType": "customKey",
        "sessionKey": "copiloto_operaciones",
        "contextWindowLength": 2
      },
      "id": "90dfdd22-402b-41b4-b9b3-ae50eb6513f3",
      "name": "\ud83e\udde0 Memoria Copiloto",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        3296,
        1696
      ],
      "typeVersion": 1.3
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Listar clientes SIN NOMBRE. Usar cuando pidan 'clientes sin nombre', 'an\u00f3nimos'.",
        "operation": "executeQuery",
        "query": "SELECT telefono, estado_conversacion, total_pedidos FROM clientes WHERE nombre IS NULL OR TRIM(nombre) = '' OR nombre ILIKE '%cliente%' ORDER BY total_pedidos DESC LIMIT 50;",
        "options": {}
      },
      "id": "ede8d0ad-2614-419a-97fc-189c92b73006",
      "name": "TOOL_ListarClientesSinNombre",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        4032,
        1648
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Actualizar el NOMBRE de un cliente. Requiere: telefono del cliente y nuevo nombre.",
        "operation": "executeQuery",
        "query": "UPDATE clientes SET nombre = $1 WHERE REPLACE(REPLACE(telefono, '+', ''), ' ', '') LIKE '%' || $2 || '%' RETURNING telefono, nombre, 'Actualizado' as status;",
        "options": {
          "queryReplacement": "={{ $fromAI('nombre','Nuevo nombre del cliente','string','') }}\n{{ $fromAI('telefono','Tel\u00e9fono del cliente (solo n\u00fameros)','string','') }}"
        }
      },
      "id": "73d7abf3-eb92-4fff-a438-843d591a5de0",
      "name": "TOOL_ADMIN_ActualizarNombre",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        3552,
        1680
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Consultar datos completos de UN cliente por tel\u00e9fono. Devuelve nombre, direcci\u00f3n, carrito (pre_pedido), estado y total de pedidos.",
        "operation": "executeQuery",
        "query": "SELECT telefono, nombre, direccion, estado_conversacion, total_pedidos, jsonb_array_length(COALESCE(pre_pedido, '[]'::jsonb)) as items_en_carrito, (SELECT COALESCE(SUM((item->>'precio')::numeric), 0) FROM jsonb_array_elements(COALESCE(pre_pedido, '[]'::jsonb)) item) as total_carrito, pre_pedido as carrito FROM clientes WHERE REPLACE(REPLACE(telefono, '+', ''), ' ', '') LIKE '%' || $1 || '%' LIMIT 1;",
        "options": {
          "queryReplacement": "={{ $fromAI('telefono','Tel\u00e9fono del cliente (\u00faltimos d\u00edgitos)','string','') }}"
        }
      },
      "id": "b369d2c5-d560-4673-a943-323dc0dc22f7",
      "name": "TOOL_ADMIN_ConsultarCliente",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        3792,
        1664
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Buscar clientes por NOMBRE. Usar cuando pidan 'perfil de X', 'busca a X', 'datos de X' donde X es un nombre. Devuelve todos los clientes cuyo nombre coincida.",
        "operation": "executeQuery",
        "query": "SELECT telefono, nombre, direccion, estado_conversacion, total_pedidos, jsonb_array_length(COALESCE(pre_pedido, '[]'::jsonb)) as items_en_carrito FROM clientes WHERE LOWER(nombre) LIKE LOWER('%' || $1 || '%') ORDER BY total_pedidos DESC LIMIT 10;",
        "options": {
          "queryReplacement": "={{ $fromAI('nombre','Nombre del cliente a buscar','string','') }}"
        }
      },
      "id": "33238adf-7434-4f6c-8847-9f6aac9b4f69",
      "name": "TOOL_ADMIN_BuscarPorNombre",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        4016,
        1664
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Contar total de clientes y cu\u00e1ntos tienen nombre.",
        "operation": "executeQuery",
        "query": "SELECT COUNT(*) as total, COUNT(CASE WHEN nombre IS NOT NULL AND TRIM(nombre) != '' THEN 1 END) as con_nombre, COUNT(CASE WHEN nombre IS NULL OR TRIM(nombre) = '' THEN 1 END) as sin_nombre FROM clientes;",
        "options": {}
      },
      "id": "a6c607c5-6787-474e-bbfe-3e781592a6f5",
      "name": "TOOL_ADMIN_ContarClientes",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        4320,
        1648
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Vaciar el carrito de UN cliente espec\u00edfico. Usar cuando pidan 'vac\u00eda el carrito de X' o 'limpia el carrito del cliente X'. Requiere: tel\u00e9fono del cliente.",
        "operation": "executeQuery",
        "query": "UPDATE clientes SET pre_pedido = '[]'::jsonb WHERE REPLACE(REPLACE(telefono, '+', ''), ' ', '') LIKE '%' || $1 || '%' RETURNING telefono, nombre, 'Carrito vaciado' as status;",
        "options": {
          "queryReplacement": "={{ $fromAI('telefono','Tel\u00e9fono del cliente (\u00faltimos d\u00edgitos)','string','') }}"
        }
      },
      "id": "670fd6d2-9ee5-41ee-9d58-78038a92ae29",
      "name": "TOOL_ADMIN_VaciarCarrito",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        4544,
        1648
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Vaciar TODOS los carritos de TODOS los clientes. Usar cuando pidan 'vac\u00eda todos los carritos', 'resetea todos los pedidos', 'limpia todos los carritos'. \u00a1CUIDADO! Acci\u00f3n masiva.",
        "operation": "executeQuery",
        "query": "UPDATE clientes SET pre_pedido = '[]'::jsonb WHERE pre_pedido IS NOT NULL AND jsonb_array_length(pre_pedido) > 0 RETURNING COUNT(*) as carritos_vaciados;",
        "options": {}
      },
      "id": "c14c9edb-4350-42da-99d7-6dc402eaa2c6",
      "name": "TOOL_ADMIN_VaciarTodosCarritos",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        4752,
        1648
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Listar clientes que tienen productos en su carrito. Usar cuando pidan 'qui\u00e9nes tienen carrito activo', 'clientes con pedido pendiente', 'carritos llenos'.",
        "operation": "executeQuery",
        "query": "SELECT telefono, nombre, jsonb_array_length(pre_pedido) as items, (SELECT COALESCE(SUM((item->>'precio')::numeric), 0) FROM jsonb_array_elements(pre_pedido) item) as total FROM clientes WHERE pre_pedido IS NOT NULL AND jsonb_array_length(pre_pedido) > 0 ORDER BY total DESC LIMIT 30;",
        "options": {}
      },
      "id": "89eb2cf0-27f5-4721-949d-04b733b9d9d5",
      "name": "TOOL_ADMIN_ListarCarritosActivos",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        4976,
        1648
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Estad\u00edsticas de carritos: cu\u00e1ntos clientes tienen carrito, total de items, valor total. Usar cuando pidan 'estad\u00edsticas de carritos', 'resumen de pedidos pendientes'.",
        "operation": "executeQuery",
        "query": "SELECT COUNT(*) as clientes_con_carrito, SUM(jsonb_array_length(pre_pedido)) as items_totales, (SELECT COALESCE(SUM((item->>'precio')::numeric), 0) FROM clientes c2, jsonb_array_elements(c2.pre_pedido) item WHERE c2.pre_pedido IS NOT NULL AND jsonb_array_length(c2.pre_pedido) > 0) as valor_total FROM clientes WHERE pre_pedido IS NOT NULL AND jsonb_array_length(pre_pedido) > 0;",
        "options": {}
      },
      "id": "7acba039-2fd6-4880-bb85-81cd25da935b",
      "name": "TOOL_ADMIN_ResumenCarritos",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        5184,
        1648
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "toolDescription": "Consultar si un cliente tiene un pedido activo por tel\u00e9fono en la tienda retail. \u00dasala cuando Mauricio o el equipo pregunte por estado de pedido, si un cliente ya tiene pedido en curso, si est\u00e1 pagado, la direcci\u00f3n de entrega o el contenido del pedido. Requiere: phone. Usa el tel\u00e9fono completo o los \u00faltimos 10 d\u00edgitos.",
        "method": "GET",
        "url": "https://tus-aguacates.vercel.app/api/agent/orders/active",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "specifyQuery": "keypair",
        "parametersQuery": {
          "values": [
            {
              "name": "phone",
              "valueProvider": "modelRequired"
            }
          ]
        },
        "sendHeaders": true,
        "specifyHeaders": "keypair",
        "parametersHeaders": {
          "values": [
            {
              "name": "Accept",
              "valueProvider": "fieldValue",
              "value": "application/json"
            }
          ]
        }
      },
      "id": "5b0e6d9a-8bdf-4eb7-bb2f-48a9aa9419c1",
      "name": "TOOL_ADMIN_ConsultarPedidoActivo",
      "type": "@n8n/n8n-nodes-langchain.toolHttpRequest",
      "position": [
        5392,
        1808
      ],
      "typeVersion": 1.1
    },
    {
      "parameters": {
        "name": "TOOL_ADMIN_ConfirmarPedido",
        "description": "Confirmar y convertir un PRE-PEDIDO de WhatsApp en un PEDIDO REAL en la tienda online. Usar cuando digan 'confirma el pedido de X', 'convierte el pre-pedido a pedido'. Requiere tel\u00e9fono del cliente.",
        "jsCode": "const telefono = $fromAI('telefono', 'Tel\u00e9fono del cliente (\u00faltimos d\u00edgitos)', 'string');\n\ntry {\n  const response = await this.helpers.httpRequest({\n    method: 'POST',\n    url: 'https://dep-n8n.n8ntusaguacates.space/webhook/confirmar-prepedido',\n    headers: { 'Content-Type': 'application/json' },\n    body: { telefono }\n  });\n  \n  if (response.success) {\n    return '\u2705 Pedido confirmado! ID: ' + (response.order_id || 'N/A') + ', Total: $' + (response.total || 0);\n  } else {\n    return '\u274c ' + (response.message || 'No se encontr\u00f3 pre-pedido');\n  }\n} catch (error) {\n  return '\u274c Error: ' + error.message;\n}"
      },
      "id": "cd842bc2-906e-45af-b9dc-2edc5088ed6e",
      "name": "TOOL_ADMIN_ConfirmarPedido",
      "type": "@n8n/n8n-nodes-langchain.toolCode",
      "position": [
        5408,
        1648
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "jsCode": "// =====================================================\n// \ud83d\udee1\ufe0f SOLUCI\u00d3N ROBUSTA PARA MENSAGES \"USE TOOLS\"\n// =====================================================\n// 1. Priorizar 'text' (respuesta limpia del agente)\n// 2. Si no existe, usar 'output' pero filtrar tool calls\n// 3. M\u00faltiples patrones de regex para m\u00e1ximo robustez\n// =====================================================\n\nlet respuestaIA = $input.first().json.text || $input.first().json.output || '';\n\n// FILTRADO ROBUSTO DE TOOL CALLS\nif (respuestaIA && respuestaIA.includes('Used tools')) {\n    // Patr\u00f3n 1: \"[Used tools: Tool: X, Input: Y, Result: Z]\"\n    respuestaIA = respuestaIA.replace(/\\[Used tools:[^\\]]*\\]\\s*/gi, '');\n\n    // Patr\u00f3n 2: \"Tool: TOOLNAME, Input: {...}, Result: {...}\"\n    respuestaIA = respuestaIA.replace(/Tool:\\s*[A-Z_]+,\\s*Input:\\s*\\{[^}]*\\},\\s*Result:/gi, '');\n\n    // Patr\u00f3n 3: \"[Tool: ...]\"\n    respuestaIA = respuestaIA.replace(/\\[Tool:[^\\]]*\\]\\s*/gi, '');\n\n    // Patr\u00f3n 4: \"Using tools:\" o similar\n    respuestaIA = respuestaIA.replace(/(?:Using|Used)\\s+tools?:[^\\n]*/gi, '');\n\n    // Patr\u00f3n 5: Eliminar brackets vac\u00edos o con solo comas que quedan\n    respuestaIA = respuestaIA.replace(/^\\s*[\\[\\]\\{\\}\\,]+\\s*/m, '');\n\n    // Patr\u00f3n 6: Limpiar saltos de l\u00ednea m\u00faltiples despu\u00e9s de eliminar tool calls\n    respuestaIA = respuestaIA.replace(/\\n{3,}/g, '\\n\\n');\n\n    respuestaIA = respuestaIA.trim();\n}\n\n// =====================================================\n// \ud83e\udde0 CONTIN\u00daA CON EL RESTO DEL C\u00d3DIGO ORIGINAL\n// =====================================================\n\nconst contexto = $('4. Merge Datos + Productos').first().json;\n\n// =====================================================\n// \ud83d\udd27 LIMPIEZA DEL MENSAJE (MANTENIENDO EMOJIS)\n// =====================================================\n\nlet mensaje = respuestaIA || '';\n\n// 1. ELIMINAR ASTERISCOS (negritas causan problemas)\nmensaje = mensaje.replace(/\\*/g, '');\n\n// 2. CONVERTIR \\n LITERAL A SALTO DE L\u00cdNEA REAL\nconst BACKSLASH = String.fromCharCode(92);\nconst LITERAL_N = BACKSLASH + 'n';\n\nlet intentos = 0;\nwhile (mensaje.indexOf(LITERAL_N) !== -1 && intentos < 50) {\n    mensaje = mensaje.split(LITERAL_N).join('\\n');\n    intentos++;\n}\n\n// 3. LIMPIAR SOLO emojis rotos (unicode suelto) - MANTENER EMOJIS NORMALES\nmensaje = mensaje.replace(/[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])/g, '');\nmensaje = mensaje.replace(/(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/g, '');\nmensaje = mensaje.replace(/\\\\u[0-9a-fA-F]{4}/g, '');\n\n// 4. NO ELIMINAR EMOJIS - Los emojis son importantes para el tono amigable\n// Solo eliminar flechas \u2192 y reemplazar por caracteres seguros\nmensaje = mensaje.replace(/\u2192/g, '\u2022');\n\n// 5. LIMPIAR ESPACIOS Y SALTOS EXCESIVOS\nmensaje = mensaje.replace(/\\n{3,}/g, '\\n\\n');\nmensaje = mensaje.replace(/  +/g, ' ');\nmensaje = mensaje.trim();\n\n// 6. FALLBACK\nif (!mensaje) {\n    mensaje = '\u00a1Hola! \ud83d\ude0a \u00bfEn qu\u00e9 puedo ayudarte?';\n}\n\n// =====================================================\n// \ud83d\udd17 DETECTAR SI HAY URLs PARA HABILITAR PREVIEW\n// =====================================================\nconst tieneUrl = mensaje.includes('http://') || mensaje.includes('https://') || mensaje.includes('tus-aguacates.vercel.app');\n\n// =====================================================\n// \ud83d\uddbc\ufe0f PREPARAR IMAGEN SI HAY PRODUCTOS\n// =====================================================\n\nconst prods = contexto.productosEncontrados || [];\nlet img = null;\nif (prods.length > 0) {\n    const p = prods.find(x => x.main_image_url && x.main_image_url.length > 0);\n    if (p) {\n        let url = p.main_image_url;\n        if (url.includes('supabase.co/storage') && url.includes('.webp')) {\n            url = url.replace('/object/public/', '/render/image/public/') + '?format=origin';\n        }\n        img = {\n            url,\n            nombre: p.name,\n            precio: p.price,\n            caption: p.name + ' - $' + Number(p.price).toLocaleString('es-CO')\n        };\n    }\n}\n\n// =====================================================\n// \ud83d\udce4 PREPARAR RESPUESTAS PARA YCLOUD\n// =====================================================\n\nconst resultados = [];\n\n// Agregar imagen si existe\nif (img && !contexto.esMedia) {\n    resultados.push({\n        json: {\n            from: contexto.to,\n            to: contexto.from,\n            type: 'image',\n            image: {\n                link: img.url,\n                caption: img.caption\n            }\n        }\n    });\n}\n\n// Agregar mensaje de texto CON preview_url si hay links\nconst mensajeJson = {\n    from: contexto.to,\n    to: contexto.from,\n    type: 'text',\n    text: { \n        body: mensaje,\n        preview_url: tieneUrl  // Habilita Link Preview cuando hay URLs\n    }\n};\n\nresultados.push({ json: mensajeJson });\n\nreturn resultados;\n"
      },
      "id": "09e19f9a-1a2b-46d0-981e-f128df2e76ef",
      "name": "\ud83d\udce4 Preparar Respuesta Copiloto",
      "type": "n8n-nodes-base.code",
      "position": [
        4496,
        1264
      ],
      "typeVersion": 2
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.messageText }}",
        "options": {
          "systemMessage": "## \ud83c\udfaf MISI\u00d3N\nEres el Copiloto de Operaciones de \"Tus Aguacates\". Tu interlocutor es Mauricio (Director).\n\n## \ud83d\udcda HERRAMIENTAS\n- TOOL_ListarClientesSinNombre: Lista clientes sin nombre\n- TOOL_ADMIN_ActualizarDatosCliente: Actualiza nombre/direcci\u00f3n\n- TOOL_ADMIN_ConsultarCliente: Consulta datos de un cliente\n- TOOL_ADMIN_ContarClientes: Cuenta total de clientes\n- TOOL_ReporteEstadoClientes: Distribuci\u00f3n por estado\n- TOOL_ListarVIPsEnRiesgo: Clientes VIP en riesgo\n- TOOL_ADMIN_ConsultarPedidoActivo: Revisa si un cliente tiene un pedido retail activo, su estado, total, direcci\u00f3n y pago\n\n## \ud83d\udcac EJEMPLOS\n**Actualizar nombre:**\nComando: \"Actualiza el nombre del 3161932558 a Consuelo\"\nAcci\u00f3n: TOOL_ADMIN_ActualizarDatosCliente(nombre='Consuelo', telefono='3161932558')\n\n**Lista sin nombre:**\nComando: \"Dame los clientes sin nombre\"\nAcci\u00f3n: TOOL_ListarClientesSinNombre()\n\n## \ud83c\udfaf PRINCIPIOS\n1. Usa datos pre-extra\u00eddos (telefono_objetivo, nombre_extraido)\n2. Respuestas concisas\n3. Nunca inventes informaci\u00f3n"
        }
      },
      "id": "e04f0b6d-c2aa-4f52-81f6-e95c5a2237ec",
      "name": "\ud83e\udde0 Agente Copiloto1",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        3888,
        1232
      ],
      "typeVersion": 3
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE envios_campana\nSET \n  respondio = true,\n  fecha_respuesta = NOW(),\n  mensaje_respuesta = LEFT('{{ $json.messageText || $json.botonTexto || \"respuesta\" }}', 500)\nWHERE \n  telefono LIKE '%' || RIGHT('{{ $json.from }}', 10)\n  AND respondio = false;",
        "options": {}
      },
      "id": "c4a6c5d9-4f9e-49cd-a5af-128b596b3fd6",
      "name": "\ud83d\udcca Marcar Respuesta Campa\u00f1a",
      "type": "n8n-nodes-base.postgres",
      "position": [
        0,
        1744
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "ADMIN: Cambiar ESTADO de conversaci\u00f3n de un cliente. Estados v\u00e1lidos: NUEVO, ATENCION_LUZ, EN_PEDIDO, PEDIDO_CONFIRMADO, PEDIDO_ONLINE, ESCALADO. Ejemplo: 'Pon el 3161932558 en NUEVO'",
        "operation": "executeQuery",
        "query": "UPDATE clientes SET estado_conversacion = $1 WHERE REPLACE(REPLACE(REPLACE(telefono, '+', ''), ' ', ''), '-', '') LIKE '%' || $2 || '%' RETURNING telefono, nombre, estado_conversacion, 'Estado cambiado' as status;",
        "options": {
          "queryReplacement": "={{ $fromAI('nuevo_estado','Estado: NUEVO, ATENCION_LUZ, EN_PEDIDO, PEDIDO_CONFIRMADO, PEDIDO_ONLINE, ESCALADO','string','NUEVO') }}\n{{ $fromAI('telefono','Telefono del cliente','string') }}"
        }
      },
      "id": "9aa57a0c-a7f8-4afc-a5a6-39de2b70a28c",
      "name": "TOOL_ADMIN_CambiarEstadoCliente",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2544,
        2048
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "ADMIN: Borrar MEMORIA de un cliente espec\u00edfico. Elimina SOLO las sesiones de chat del tel\u00e9fono indicado. Ejemplo: 'Borra la memoria del 3161932558'",
        "operation": "executeQuery",
        "query": "WITH deleted AS (\n  DELETE FROM n8n_chat_histories \n  WHERE session_id = $1 \n     OR session_id = '57' || $1\n     OR session_id = '+57' || $1\n  RETURNING session_id\n)\nSELECT \n  COUNT(*) as registros_eliminados,\n  'Memoria borrada para: ' || $1 as mensaje\nFROM deleted;",
        "options": {
          "queryReplacement": "={{ $fromAI('telefono','Telefono del cliente para borrar memoria','string') }}"
        }
      },
      "id": "91172ab9-870e-444d-a2ef-3c160d322ec6",
      "name": "TOOL_ADMIN_BorrarMemoriaCliente",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2736,
        2048
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "ADMIN: Limpiar/vaciar el CARRITO de un cliente. Ejemplo: 'Vac\u00eda el carrito del 3161932558'",
        "operation": "executeQuery",
        "query": "UPDATE clientes SET pre_pedido = '[]'::jsonb WHERE REPLACE(REPLACE(REPLACE(telefono, '+', ''), ' ', ''), '-', '') LIKE '%' || $1 || '%' RETURNING telefono, nombre, 'Carrito vaciado' as status;",
        "options": {
          "queryReplacement": "={{ $fromAI('telefono','Telefono del cliente para limpiar carrito','string') }}"
        }
      },
      "id": "12b89af1-d858-4712-8e6c-dfe4783abdfa",
      "name": "TOOL_ADMIN_LimpiarCarritoCliente",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2944,
        2048
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "name": "confirmar_pedido_etiqueta",
        "jsCode": "// TOOL: Confirmar Pedido + Etiqueta YCloud\nconst telefono = $('1. Pre-procesamiento YCloud').first().json.from;\n\ntry {\n  // Usar httpRequestWithAuthentication en lugar de httpRequest + credentials manual\n  const updateResult = await this.helpers.httpRequestWithAuthentication.call(this, 'YCloudApi', {\n    method: 'POST',\n    url: 'https://api.ycloud.com/v2/contacts',\n    body: {\n      phoneNumber: '+' + telefono,\n      tags: ['Confirmado']\n    },\n    json: true\n  });\n  \n  return 'Cliente ' + telefono + ' etiquetado como Confirmado en YCloud. Contin\u00faa con TOOL_CambiarEstadoCliente para cambiar el estado a PEDIDO_CONFIRMADO.';\n  \n} catch (e) {\n  return 'Error al etiquetar: ' + e.message + '. Contin\u00faa con TOOL_CambiarEstadoCliente para cambiar estado a PEDIDO_CONFIRMADO.';\n}"
      },
      "id": "cd147bfd-97cc-490b-929a-6c603b56e7e0",
      "name": "TOOL_ConfirmarPedidoConEtiqueta",
      "type": "@n8n/n8n-nodes-langchain.toolCode",
      "position": [
        3168,
        1328
      ],
      "typeVersion": 1.1
    },
    {
      "parameters": {
        "model": "nvidia/nemotron-3-super-120b-a12b:free",
        "options": {
          "temperature": 0
        }
      },
      "id": "1dfa0007-ab9b-4d0b-8d73-98da18818b7c",
      "name": "OpenRouter Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        1696,
        880
      ],
      "typeVersion": 1,
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      },
      "notes": "Reserved node; skipped by LAB Model Selector due to rate-limit instability"
    },
    {
      "parameters": {
        "numberInputs": 4,
        "rules": {
          "rule": [
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "lab-stable-primary",
                    "operator": {
                      "type": "number",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.retry_count || 0 }}",
                    "rightValue": 0
                  }
                ]
              }
            },
            {
              "modelIndex": 2,
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "lab-stable-secondary",
                    "operator": {
                      "type": "number",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.retry_count || 0 }}",
                    "rightValue": 1
                  }
                ]
              }
            },
            {
              "modelIndex": 3,
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "lab-stable-tertiary",
                    "operator": {
                      "type": "number",
                      "operation": "largerEqual"
                    },
                    "leftValue": "={{ $json.retry_count || 0 }}",
                    "rightValue": 2
                  }
                ]
              }
            }
          ]
        }
      },
      "type": "@n8n/n8n-nodes-langchain.modelSelector",
      "typeVersion": 1,
      "position": [
        1744,
        720
      ],
      "id": "8c31ecd7-938e-43be-bdc4-85925d3f31c8",
      "name": "Model Selector"
    },
    {
      "parameters": {
        "model": "mistral-vibe-cli-with-tools",
        "options": {}
      },
      "id": "769ed9f4-0dea-4905-80eb-d6a0e246510f",
      "name": "Mistral Cloud Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatMistralCloud",
      "position": [
        2032,
        832
      ],
      "typeVersion": 1,
      "credentials": {
        "mistralCloudApi": {
          "name": "<your credential>"
        }
      },
      "notes": "LAB tertiary fallback model"
    },
    {
      "parameters": {
        "model": "stepfun/step-3.5-flash:free",
        "options": {
          "temperature": 0
        }
      },
      "id": "77146c73-1d11-425a-bb55-ff5868c7d5c6",
      "name": "OpenRouter Chat Model2",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        1552,
        880
      ],
      "typeVersion": 1,
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      },
      "notes": "LAB secondary fallback model"
    },
    {
      "parameters": {
        "model": "=arcee-ai/trinity-large-preview:free",
        "options": {
          "temperature": 0
        }
      },
      "id": "d49c213e-3462-4111-b7f0-64175f01afcb",
      "name": "OpenRouter Chat Model3",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        3600,
        1360
      ],
      "typeVersion": 1,
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "cliente-bloqueado",
              "leftValue": "={{ $('1. Pre-procesamiento YCloud').first().json.bloquearRespuesta }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "4b1c7c4f-ed0f-4c3a-8727-87779ee620ad",
      "name": "\u2753 \u00bfCliente Bloqueado?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -192,
        208
      ]
    },
    {
      "parameters": {},
      "id": "3bd3dfa8-6e3f-4659-8cf4-c42aed253ba4",
      "name": "\ud83d\uded1 Fin - Cliente Escalado",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "CREATE TABLE IF NOT EXISTS mensaje_buffer (\n    id SERIAL PRIMARY KEY,\n    cliente_telefono VARCHAR(20) NOT NULL,\n    mensaje TEXT NOT NULL,\n    timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    procesado BOOLEAN NOT NULL DEFAULT FALSE,\n    session_data JSONB,\n    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\nCREATE INDEX IF NOT EXISTS idx_mb_tel ON mensaje_buffer(cliente_telefono);\nCREATE INDEX IF NOT EXISTS idx_mb_proc ON mensaje_buffer(procesado, timestamp);\nCREATE INDEX IF NOT EXISTS idx_mb_tel_proc ON mensaje_buffer(cliente_telefono, procesado);\nSELECT table_name, 'TABLA CREADA' as resultado FROM information_schema.tables \nWHERE table_name = 'mensaje_buffer' AND table_schema = 'public';",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        1312,
        1120
      ],
      "id": "284c53bf-db5f-4e37-85a5-774acc75a1d0",
      "name": "\ud83d\udd27 INIT - Crear tabla buffer",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "path": "init-buffer-db",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1,
      "position": [
        1008,
        1120
      ],
      "id": "8326f549-376f-4f93-a537-4df7a25178e3",
      "name": "\ud83d\udd27 Init Webhook"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.body?.fromBuffer || $json.fromBuffer || false }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        -368,
        1200
      ],
      "id": "6e8cad21-3a9c-457e-88eb-435cee8257d0",
      "name": "\ud83d\udd00 \u00bfViene del Buffer?"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "-- Extraer mensaje del payload YCloud est\u00e1ndar\nWITH msg_data AS (\n    SELECT \n        '{{ $json.body.whatsappInboundMessage.from }}'::varchar AS telefono,\n        '{{ $json.body.whatsappInboundMessage.text.body.replace(/'/g, \"''\") }}'::text AS mensaje,\n        jsonb_build_object(\n            'to', '{{ $json.body.whatsappInboundMessage.to }}',\n            'name', '{{ $json.body.whatsappInboundMessage.customerProfile.name }}'\n        ) AS session_data\n)\nINSERT INTO mensaje_buffer (cliente_telefono, mensaje, session_data)\nSELECT telefono, mensaje, session_data FROM msg_data\nWHERE telefono != '' AND mensaje != ''\nRETURNING id, cliente_telefono, timestamp;",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.4,
      "position": [
        -352,
        1472
      ],
      "id": "4fb69c37-4b61-4154-b029-4d3cd2a728cd",
      "name": "\ud83d\udcbe Guardar en Buffer",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        -288,
        1712
      ],
      "id": "4411e70a-3d10-440b-8452-bdc5f93ef6d4",
      "name": "\u23f9\ufe0f Buffer guardado OK"
    },
    {
      "parameters": {
        "model": "nvidia/nemotron-3-super-120b-a12b:free",
        "options": {
          "temperature": 0
        }
      },
      "id": "f37db32b-a756-4f3b-aa8e-2219d706a9e5",
      "name": "OpenRouter Chat Model1",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        1856,
        880
      ],
      "typeVersion": 1,
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      },
      "notes": "LAB primary model"
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "USA SIEMPRE cuando pregunten por precios, \u00bfcu\u00e1nto vale?, \u00bfqu\u00e9 costo tiene? Input: nombre del producto o tama\u00f1o.",
        "operation": "executeQuery",
        "query": "SELECT name, price, description FROM productos_tienda WHERE is_active = true AND (name ILIKE '%' || $1 || '%' OR description ILIKE '%' || $1 || '%') ORDER BY price LIMIT 5;",
        "options": {
          "queryReplacement": "={{ $fromAI('termino','Producto','string','') }}"
        }
      },
      "id": "d3c1a2b3-c4d5-4e6f-8a9b-0c1d2e3f4a5b",
      "name": "TOOL_ConsultarPrecio",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2800,
        1808
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "USA para buscar recetas con aguacate, ideas de cocina o platos. Input: ingrediente o tipo de plato.",
        "operation": "executeQuery",
        "query": "SELECT name, description FROM productos_tienda WHERE is_active = true AND (category_name ILIKE '%receta%' OR description ILIKE '%receta%' OR name ILIKE '%' || $1 || '%') LIMIT 5;",
        "options": {
          "queryReplacement": "={{ $fromAI('termino','Ingrediente/Plato','string','') }}"
        }
      },
      "id": "e5f1a2b3-c4d5-4e6f-8a9b-0c1d2e3f4a6c",
      "name": "TOOL_BuscarRecetas",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        3008,
        1808
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=# ========================================\n# USER MESSAGE (TEXT) - COPIAR Y PEGAR EN n8n\n# ========================================\n# D\u00f3nde: Nodo \"\ud83e\udd16 Agente Luz v4\" \u2192 text\n# ========================================\n## CONTEXTO DEL CLIENTE\n- Nombre: {{ $json.clienteNombre }}\n- Saludo: {{ $json.saludo }}\n- Estado: {{ $json.clienteEstado }}\n- Total pedidos anteriores: {{ $json.clienteTotalPedidos }}\n- Carrito actual: {{ JSON.stringify($json.clienteCarrito) }}\n## \ud83d\udd0d PRODUCTOS ENCONTRADOS EN B\u00daSQUEDA\n{{ $json.productosTexto }}\n- Tel\u00e9fono: {{ $json.clienteTelefono }}\n- Direcci\u00f3n: {{ $json.clienteDireccion }}\n## MENSAJE DEL CLIENTE\n{{ $json.mensajeCliente }}\n## \ud83d\udcc5 FECHA Y ENTREGA\n- Fecha actual: {{ $json.fechaActual }}\n- Hora actual: {{ $json.horaActual }}\n- Pr\u00f3xima entrega: {{ $json.proximaEntrega }}\n- Dias hasta entrega: {{ $json.diasHastaEntrega }}\n- Hoy entregar: {{ $json.pasoCutoff ? 'NO (despu\u00e9s de 10AM)' : 'S\u00cd' }}\n- Es saludo simple: {{ $json.esSoloSaludo }}",
        "options": {
          "systemMessage": "# ========================================\n# SYSTEM MESSAGE - COPIAR Y PEGAR EN n8n\n# ========================================\n# D\u00f3nde: Nodo \"\ud83e\udd16 Agente Luz v4\" \u2192 options \u2192 systemMessage\n# ========================================\nEres Luz \ud83e\udd51, la asistente de ventas de Tus Aguacates. Tu misi\u00f3n: Ayudar a que compren, de forma r\u00e1pida y amable.\n## \ud83d\udd27 USO DE HERRAMIENTAS (SIEMPRE PRIMERO)\nAntes de responder, revisa si aplica alguna herramienta:\n| Situaci\u00f3n | Herramienta |\n|-----------|-------------|\n| Pregunta por producto | TOOL_BuscarProductos |\n| Dice \"quiero\", \"dame\", \"agregar\" | TOOL_AnadirAlCarrito |\n| Da su nombre | TOOL_GuardarNombreCliente |\n| Da su direcci\u00f3n | TOOL_GuardarDireccionCliente |\n| Confirma pedido | TOOL_CalcularTotalPrePedido + TOOL_CambiarEstadoCliente |\n| Est\u00e1 molesto/queja | TOOL_EscalarServicioCliente |\n| \"\u00bfCu\u00e1ndo llega?\" | TOOL_ConsultarEstadoPedido |\n## \ud83d\udcac ESTILO DE COMUNICACI\u00d3N\n\n- **BREVEDAD M\u00c1XIMA**: Si hay muchos productos, muestra solo los 3 m\u00e1s relevantes. Si el cliente quiere ver m\u00e1s, que pregunte. No listes todo el cat\u00e1logo de una vez.\n1. **USA EMOJIS** - Cada producto con su emoji (\ud83e\udd51\ud83c\udf3d\ud83c\udf4b\ud83c\udf53\ud83e\udd55\ud83c\udf45)\n2. **SALTOS DE L\u00cdNEA** - Despu\u00e9s de cada oraci\u00f3n\n3. **USA EL NOMBRE** - Menciona el nombre naturalmente\n4. **TONO C\u00c1LIDO** - Como un amigo colombiano, NO como robot\n5. **BREVE** - M\u00e1ximo 4-5 l\u00edneas por mensaje\n6. **S\u00c9 EXPRESIVA** - Usa \u00a1! y expresiones naturales\n## \ud83d\udea8 REGLAS CR\u00cdTICAS\n- **LINK DE TIENDA (Solo una vez)**: Env\u00eda el link de la tienda https://tus-aguacates.vercel.app/ \u00fanicamente en la bienvenida SI el cliente a\u00fan no ha interactuado mucho. No lo repitas en cada turno. Si el cliente pregunta por la tienda, env\u00edalo de nuevo.\n- **SALUDO = SOLO UNA VEZ** - Solo si el cliente es NUEVO. En mensajes siguientes responde directo\n- **NUNCA agregues sin confirmaci\u00f3n** - Solo cuando diga expl\u00edcitamente \"quiero\", \"dame\"\n- **SIEMPRE pregunta cantidad** antes de agregar\n- **NUNCA inventes precios**\n- **NUNCA confirmes pagos** - escala a humano\n- **USA EL NOMBRE del cliente** - si no lo tienes, p\u00eddelo amablemente\n## \ud83d\udce6 ENTREGAS\n- Solo martes y viernes\n- Precio fijo de env\u00edo: $7.400\n- Env\u00edo gratis: >$68.900\n- Hora l\u00edmite: 10:00 AM\n- Nunca digas que entregamos un d\u00eda que no es martes o viernes\n## \ud83d\udcb3 M\u00c9TODOS DE PAGO\n- Nequi/Daviplata: 320 306 2007\n- Efectivo contra entrega\n- Pagos online en tienda\n## \ud83c\udfaf FLUJO DE VENTA\n1. Cliente pregunta \u2192 Muestra opciones con precios\n2. Cliente confirma \u2192 Agrega al carrito\n3. \"Eso es todo\" \u2192 Muestra resumen con total\n4. Confirma direcci\u00f3n\n5. Muestra m\u00e9todos de pago\n6. TOOL_CambiarEstadoCliente(\"PEDIDO_CONFIRMADO\")\n## \ud83d\udea8 ESCALAMIENTO\nSi el cliente:\n- Pide hablar con humano \u2192 TOOL_EscalarServicioCliente inmediatamente\n- Est\u00e1 molesto \u2192 Disculpar, empatizar, escalar\n- Env\u00eda comprobante de pago \u2192 \"\u00a1Gracias! Lo revisaremos\" + escalar\n- Pregunta por recetas \u2192 Dirigir a la tienda online\n## \u26a0\ufe0f PEDIDOS DE PLATAFORMA (TIENDA ONLINE)\nSi el mensaje contiene \"Acabo de hacer un pedido\" + URL de la tienda:\n- NO agregues productos nuevos\n- Confirma recepci\u00f3n: \"\u00a1Gracias por tu pedido! Lo recibimos correctamente.\"\n- Cambia estado: TOOL_CambiarEstadoCliente(\"PEDIDO_ONLINE\")\n- Indica que se contactar\u00e1n para coordinar entrega\n## \u26a0\ufe0f QUEJAS DE PEDIDOS\nSi el cliente dice: \"no ped\u00ed\", \"no es mi pedido\", \"recib\u00ed algo diferente\", \"me enviaron mal\":\n- NO intentes confirmar el pedido\n- USA TOOL_EscalarServicioCliente INMEDIATAMENTE\n- Empatiza: \"\u00a1Qu\u00e9 pena! Vamos a revisar tu caso...\"\n\u00a1Luz, s\u00e9 directa y efectiva! \ud83e\udd51\n\n\n## \ud83d\udc4b SALUDO SIMPLE\n- Si el cliente solo saluda (hola, buenas tardes, etc.), responde con una bienvenida personalizada seg\u00fan si tienes o no el nombre:\n\n- SI NO TENGO EL NOMBRE (El nombre es \"Cliente\"): \n'\u00a1Hola! Soy Luz, tu asistente de ventas de Tus Aguacates. Bienvenido. \ud83e\udd51\n\nSi deseas mirar nuestra tienda en l\u00ednea, aqu\u00ed est\u00e1 el link:\nhttps://tus-aguacates.vercel.app/\n\nO si prefieres, yo te atiendo por ac\u00e1. Por cierto, \u00bfc\u00f3mo te llamas para atenderte mejor? \ud83d\ude0a'\n\n- SI YA TENGO EL NOMBRE (Ej: \"Mauricio\"): \n'\u00a1Hola, {{ $json.clienteNombre }}! Buenas tardes. Bienvenido a Tus Aguacates. \ud83e\udd51\n\nMira nuestro cat\u00e1logo en l\u00ednea aqu\u00ed:\nhttps://tus-aguacates.vercel.app/\n\nO si prefieres, estoy aqu\u00ed para atenderte personalmente. \u00bfEn qu\u00e9 puedo ayudarte hoy? \ud83d\ude0a'\n## \ud83d\udee1\ufe0f FASE DE VALIDACI\u00d3N OBLIGATORIA\n- ANTES de enviar el resumen final del pedido, VERIFICA:\n  1. \u00bfTengo el NOMBRE COMPLETO (nombre y al menos un apellido)?\n  2. \u00bfTengo la DIRECCI\u00d3N DE ENTREGA completa?\n- Si falta alguno de estos datos, NO generes el resumen. Pide amablemente al cliente: '\u00a1Casi listo! Para coordinar el env\u00edo, por favor conf\u00edrmame tu nombre completo y la direcci\u00f3n de entrega.'",
          "maxIterations": 10
        }
      },
      "id": "270c1ff8-e4b0-413b-82f3-1b3cf7083d24",
      "name": "\ud83e\udd16 Agente Luz v",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2160,
        592
      ],
      "retryOnFail": true,
      "typeVersion": 3,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "sessionIdType": "customKey",
        "sessionKey": "={{ $json.from || $(\"1. Pre-procesamiento YCloud\").first().json.from || \"default_session\" }}",
        "contextWindowLength": 30
      },
      "id": "52d5bb56-fefc-4c9f-8dd3-75b443091259",
      "name": "Postgres Chat Memory",
      "type": "@n8n/n8n-nodes-langchain.memoryPostgresChat",
      "position": [
        1440,
        1456
      ],
      "typeVersion": 1.3,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "\ud83d\udce5 Webhook YCloud": {
      "main": [
        [
          {
            "node": "\ud83d\udd00 \u00bfViene del Buffer?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1. Pre-procesamiento YCloud": {
      "main": [
        [
          {
            "node": "\u2753 \u00bfCliente Bloqueado?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2753 \u00bfEs Media?": {
      "main": [
        [
          {
            "node": "\ud83d\udcf1 Responder Media No Soportado",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "2. Obtener Cliente",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Obtener Cliente": {
      "main": [
        [
          {
            "node": "Merge1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2753 \u00bfBusca Producto?": {
      "main": [
        [
          {
            "node": "3. B\u00fasqueda Autom\u00e1tica Productos",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "4. Merge Datos + Productos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. B\u00fasqueda Autom\u00e1tica Productos": {
      "main": [
        [
          {
            "node": "4. Merge Datos + Productos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. Merge Datos + Productos": {
      "main": [
        [
          {
            "node": "\ud83e\udd16 Agente Luz v",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_AnadirAlCarrito": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udd16 Agente Luz v",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_CalcularTotalPrePedido": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udd16 Agente Luz v",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_GuardarNombreCliente": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udd16 Agente Luz v",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_CambiarEstadoCliente": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udd16 Agente Luz v",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udce4 Preparar Respuesta": {
      "main": [
        [
          {
            "node": "\ud83d\udcf1 Enviar WhatsApp YCloud",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_BuscarProductos": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udd16 Agente Luz v",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_EscalarServicioCliente": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udd16 Agente Luz v",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ConsultarEstadoPedido": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udd16 Agente Luz v",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_GuardarDireccionCliente": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udd16 Agente Luz v",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Calculator": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udd16 Agente Luz v",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ObtenerVariantes": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udd16 Agente Luz v",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Code - Formatear Producto": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Buscar Producto del Bot\u00f3n": {
      "main": [
        [
          {
            "node": "Code - Formatear Producto",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detectar Click de Bot\u00f3n": {
      "main": [
        [
          {
            "node": "Buscar Producto del Bot\u00f3n",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "\u2753 \u00bfBusca Producto?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge1": {
      "main": [
        [
          {
            "node": "Detectar Click de Bot\u00f3n",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd00 \u00bfEs Copiloto?": {
      "main": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\u2753 \u00bfEs Media?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83e\udde0 Memoria Copiloto": {
      "ai_memory": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ListarClientesSinNombre": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ADMIN_ActualizarNombre": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ADMIN_ConsultarCliente": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ADMIN_BuscarPorNombre": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ADMIN_ContarClientes": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ADMIN_VaciarCarrito": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ADMIN_VaciarTodosCarritos": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ADMIN_ListarCarritosActivos": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ADMIN_ResumenCarritos": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ADMIN_ConfirmarPedido": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udce4 Preparar Respuesta Copiloto": {
      "main": [
        [
          {
            "node": "\ud83d\udcf1 Enviar WhatsApp YCloud",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83e\udde0 Agente Copiloto1": {
      "main": [
        [
          {
            "node": "\ud83d\udce4 Preparar Respuesta Copiloto",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ADMIN_CambiarEstadoCliente": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ADMIN_BorrarMemoriaCliente": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ADMIN_LimpiarCarritoCliente": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ConfirmarPedidoConEtiqueta": {
      "ai_tool": [
        [
          {
            "node": "\ud83e\udd16 Agente Luz v",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Model Selector",
            "type": "ai_languageModel",
            "index": 1
          }
        ]
      ]
    },
    "Model Selector": {
      "ai_languageModel": [
        [
          {
            "node": "\ud83e\udd16 Agente Luz v",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Mistral Cloud Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Model Selector",
            "type": "ai_languageModel",
            "index": 3
          }
        ]
      ]
    },
    "OpenRouter Chat Model2": {
      "ai_languageModel": [
        [
          {
            "node": "Model Selector",
            "type": "ai_languageModel",
            "index": 2
          }
        ]
      ]
    },
    "OpenRouter Chat Model3": {
      "ai_languageModel": [
        [
          {
            "node": "\ud83e\udde0 Agente Copiloto1",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "\u2753 \u00bfCliente Bloqueado?": {
      "main": [
        [
          {
            "node": "\ud83d\uded1 Fin - Cliente Escalado",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udd00 \u00bfEs Copiloto?",
            "type": "main",
            "index": 0
          },
          {
            "node": "\ud83d\udcca Marcar Respuesta Campa\u00f1a",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge1",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "\ud83d\udd27 Init Webhook": {
      "main": [
        [
          {
            "node": "\ud83d\udd27 INIT - Crear tabla buffer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd00 \u00bfViene del Buffer?": {
      "main": [
        [
          {
            "node": "1. Pre-procesamiento YCloud",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udcbe Guardar en Buffer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcbe Guardar en Buffer": {
      "main": [
        [
          {
            "node": "\u23f9\ufe0f Buffer guardado OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2daa57db-e4ab-4696-a96a-4b4cad12b8fc": {
      "main": [
        [
          {
            "node": "4a51a2f9-c6d8-483e-a9dd-4a379d2ec130",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenRouter Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "Model Selector",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "37802349-d077-4ffe-a17d-acbf9652535d": {
      "main": [
        [
          {
            "node": "4a51a2f9-c6d8-483e-a9dd-4a379d2ec130",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "fb1e25a8-8f9b-417a-924f-5e713233d2f9": {
      "main": [
        [
          {
            "node": "4a51a2f9-c6d8-483e-a9dd-4a379d2ec130",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "211df281-543d-497a-b643-25dca132695c": {
      "main": [
        [
          {
            "node": "4a51a2f9-c6d8-483e-a9dd-4a379d2ec130",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "21c4122f-796a-4149-9524-9891a5a75928": {
      "main": [
        [
          {
            "node": "4a51a2f9-c6d8-483e-a9dd-4a379d2ec130",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "TOOL_ConsultarPrecio": {
      "ai_tool": [
        []
      ]
    },
    "TOOL_BuscarRecetas": {
      "ai_tool": [
        []
      ]
    },
    "\ud83e\udde0 Buffer Window Memory": {
      "ai_memory": [
        [
          {
            "node": "\ud83e\udd16 Agente Luz v4",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "\ud83e\udd16 Agente Luz v": {
      "main": [
        [
          {
            "node": "\ud83d\udce4 Preparar Respuesta",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Postgres Chat Memory": {
      "ai_memory": [
        [
          {
            "node": "\ud83e\udd16 Agente Luz v",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false,
    "timeSavedMode": "fixed",
    "callerPolicy": "workflowsFromSameOwner",
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all",
    "saveManualExecutions": true,
    "saveExecutionProgress": true
  },
  "staticData": null,
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "versionId": "73971fb1-c4de-407a-a3f8-a680527441cd",
  "activeVersionId": "73971fb1-c4de-407a-a3f8-a680527441cd",
  "versionCounter": 384,
  "triggerCount": 2,
  "tags": [],
  "activeVersion": {
    "updatedAt": "2026-03-30T22:18:19.771Z",
    "createdAt": "2026-03-30T22:18:19.771Z",
    "versionId": "73971fb1-c4de-407a-a3f8-a680527441cd",
    "workflowId": "sNeOUViiSYyROtea",
    "nodes": [
      {
        "parameters": {
          "httpMethod": "POST",
          "path": "ycloud-lab-test-v2",
          "options": {}
        },
        "id": "2daa57db-e4ab-4696-a96a-4b4cad12b8fc",
        "name": "\ud83d\udce5 Webhook YCloud",
        "type": "n8n-nodes-base.webhook",
        "position": [
          -528,
          1200
        ],
        "webhookId": "tus-aguacates-ycloud-v4",
        "typeVersion": 2
      },
      {
        "parameters": {
          "respondWith": "text",
          "responseBody": "OK",
          "options": {}
        },
        "id": "e0703b6d-d536-4e7f-bd8a-6c0adf2969e4",
        "name": "\u2705 Responder OK",
        "type": "n8n-nodes-base.respondToWebhook",
        "position": [
          -160,
          976
        ],
        "typeVersion": 1.4
      },
      {
        "parameters": {
          "jsCode": "// ======================================================\n// \ud83d\udee1\ufe0f FIX: Verificar que el nodo se ejecut\u00f3 antes de acceder\n// ======================================================\nlet cliente = {};\ntry {\n  cliente = $('2. Obtener Cliente').first().json || {};\n} catch (e) {\n  cliente = { id: null, telefono: '', nombre: 'Cliente' };\n}\n\n// ======================================================\n// \ud83e\udde0 CONTIN\u00daA CON EL RESTO DEL C\u00d3DIGO ORIGINAL\n// ======================================================\n\n// \ud83e\udde0 PRE-PROCESAMIENTO v15 - BLOQUEO DE ESCALADOS\n// =====================================================\n// \u00daltima actualizaci\u00f3n: 2026-02-14\n// NUEVO: Bloqueo autom\u00e1tico de clientes escalados\n// =====================================================\n\nconst rawJson = $input.item.json;\nconst body = rawJson.body || rawJson;\nlet whatsappMsg = body.whatsappInboundMessage;\n\nif (!whatsappMsg && body.body && body.body.whatsappInboundMessage) {\n  whatsappMsg = body.body.whatsappInboundMessage;\n}\n\n// =====================================================\n// \ud83d\udee1\ufe0f DETECTAR MODO ADMIN/CLIENTE (MAURICIO)\n// =====================================================\nconst NUMEROS_DIRECTOR = ['573203062007', '3203062007'];\n\nconst fromRaw = whatsappMsg?.from || '';\nconst fromNormalizado = fromRaw.replace(/[\\+\\s-]/g, '');\nconst from = (whatsappMsg?.from || '').replace('+', '');\nconst to = (whatsappMsg?.to || '').replace('+', '');\nconst customerName = whatsappMsg?.customerProfile?.name || 'Cliente';\nconst messageType = whatsappMsg?.type || 'unknown';\nconst messageTextRaw = whatsappMsg?.text?.body || '';\nconst messageText = (NUMEROS_DIRECTOR.some(num => fromNormalizado === num || fromNormalizado.endsWith(num))) && (messageTextRaw.trim().startsWith('>')) ? messageTextRaw.trim().substring(1).trim() : messageTextRaw;\n\n// Saludo seg\u00fan hora (COLOMBIA UTC-5)\nconst now = new Date();\nconst horaCol = new Date(now.getTime() - 5 * 60 * 60 * 1000).getHours();\nlet saludo;\nif (horaCol < 12) saludo = 'Buenos d\u00edas';\nelse if (horaCol < 19) saludo = 'Buenas tardes';\nelse saludo = 'Buenas noches';\n\nconst esNumeroDirector = NUMEROS_DIRECTOR.some(num =>\n    fromNormalizado === num || fromNormalizado.endsWith(num)\n);\n\n// =====================================================\n// \ud83d\udd04 PREFIJO: > para modo CLIENTE\n// =====================================================\nconst mensajeEmpiezaConPrefijo = messageTextRaw.trim().startsWith('>');\nconst mensajeSinPrefijo = mensajeEmpiezaConPrefijo\n    ? messageTextRaw.trim().substring(1).trim()\n    : messageTextRaw;\n\n// =====================================================\n// \ud83d\udeab NUEVO: BLOQUEO DE CLIENTES ESCALADOS\n// =====================================================\n// IMPORTANTE: Si el cliente est\u00e1 escalado, NO procesar el mensaje\n// El cliente debe esperar respuesta humana\nconst ESTADOS_ESCALADOS = ['ESCALADO', 'PENDIENTE_HUMANO', 'EN_REVISION_HUMANA'];\n\n// Variable para guardar si el mensaje fue bloqueado\nlet mensajeBloqueado = false;\n\n// =====================================================\n// DETECTAR RESPUESTAS DE BOTONES INTERACTIVOS\n// =====================================================\nlet esRespuestaBoton = false;\nlet botonId = '';\nlet botonTexto = '';\nif (messageType === 'interactive') {\n    const interactive = whatsappMsg.interactive;\n    if (interactive?.type === 'button_reply') {\n        esRespuestaBoton = true;\n        botonId = interactive.button_reply?.id || '';\n        botonTexto = interactive.button_reply?.title || '';\n    } else if (interactive?.type === 'list_reply') {\n        esRespuestaBoton = true;\n        botonId = interactive.list_reply?.id || '';\n        botonTexto = interactive.list_reply?.title || '';\n    }\n}\n\n// =====================================================\n// MANEJO DE IM\u00c1GENES Y MEDIA\n// =====================================================\nconst esReaccion = messageType === 'reaction';\nconst esImagen = messageType === 'image';\nconst esDocumento = messageType === 'document';\nconst esAudio = messageType === 'audio';\nconst esVideo = messageType === 'video';\nconst esSticker = messageType === 'sticker';\nconst esMedia = esImagen || esDocumento || esAudio || esVideo || esSticker;\n\n// =====================================================\n// MANEJO DE REACCIONES (ESTADOS)\n// =====================================================\nif (esReaccion) {\n    const emoji = whatsappMsg.reaction?.emoji || '\u2764\ufe0f';\n    return [{\n        json: {\n            esComandoCopiloto: false,\n            mensajeCliente: `[Reacci\u00f3n: ${emoji}]`,\n            from, to, customerName, saludo,\n            esMediaNoSoportado: false,\n            esMedia: false,\n            esReaccion: true,\n            emojiReaccion: emoji,\n            tipoMensaje: 'reaction',\n            messageText: `[El cliente reaccion\u00f3 con ${emoji}]`,\n            esRespuestaBoton: false,\n            botonId: '', botonTexto: '',\n            productoIdDelBoton: null,\n            accionBoton: '',\n            esSoloSaludo: false,\n            esPreguntaConversacional: false,\n            debeHacerBusqueda: false,\n            esPedidoPlataforma: false,\n            infoPedidoPlataforma: null,\n            tieneMultiplesProductos: false,\n            terminosIndividuales: [],\n            terminoBusqueda: '',\n            terminoNormalizado: '',\n            variantesBusqueda: [],\n            terminosNormalizados: [],\n            timestamp: whatsappMsg?.sendTime\n        }\n    }];\n}\n\n// Si es media, no importa si est\u00e1 escalado o no\nif (esMedia) {\n    let textoMedia = '[Mensaje multimedia]';\n    if (esImagen) textoMedia = '[\ud83d\udcf8 Imagen recibida]';\n    else if (esDocumento) textoMedia = '[\ud83d\udcc4 Documento recibido]';\n    else if (esAudio) textoMedia = '[\ud83c\udfb5 Audio recibido]';\n    else if (esVideo) textoMedia = '[\ud83c\udfa5 Video recibido]';\n    else if (esSticker) textoMedia = '[\ud83d\ude0a Sticker recibido]';\n    \n    return [{\n        json: {\n            esComandoCopiloto: false,\n            mensajeCliente: messageText,\n        from, to, customerName, saludo,\n            esMediaNoSoportado: false,\n            esMedia: true,\n            esImagen, esDocumento, esAudio, esVideo, esSticker,\n            tipoMensaje: messageType,\n            messageText: textoMedia,\n            captionImagen: whatsappMsg?.image?.caption ||\n                whatsappMsg?.document?.caption || '',\n            esRespuestaBoton: false,\n            botonId: '', botonTexto: '',\n            productoIdDelBoton: null,\n            accionBoton: '',\n            esSoloSaludo: false,\n            esPreguntaConversacional: false,\n            debeHacerBusqueda: false,\n            esPedidoPlataforma: false,\n            infoPedidoPlataforma: null,\n            tieneMultiplesProductos: false,\n            terminosIndividuales: [],\n            terminoBusqueda: '',\n            terminoNormalizado: '',\n            variantesBusqueda: [],\n            terminosNormalizados: [],\n            timestamp: whatsappMsg?.sendTime\n        }\n    }];\n}\n\n// =====================================================\n// MANEJO DE RESPUESTAS DE BOTONES\n// =====================================================\nif (esRespuestaBoton) {\n    let productoIdDelBoton = null;\n    let accionBoton = 'otro';\n    \n    if (botonId.startsWith('agregar_producto_')) {\n        productoIdDelBoton = parseInt(botonId.replace('agregar_producto_', ''));\n        accionBoton = 'agregar';\n    } else if (botonId.startsWith('ver_mas_producto_')) {\n        productoIdDelBoton = parseInt(botonId.replace('ver_mas_producto_', ''));\n        accionBoton = 'ver_mas';\n    } else if (botonId === 'ver_carrito') {\n        accionBoton = 'ver_carrito';\n    } else if (botonId === 'completar_pedido') {\n        accionBoton = 'completar_pedido';\n    } else if (botonId === 'cancelar_pedido') {\n        accionBoton = 'cancelar_pedido';\n    }\n    \n    return [{\n        json: {\n            esComandoCopiloto: false,\n            mensajeCliente: messageText,\n        from, to, customerName, saludo,\n            esMediaNoSoportado: false,\n            esMedia: false,\n            tipoMensaje: messageType,\n            messageText: botonTexto,\n            esRespuestaBoton: true,\n            botonId, botonTexto,\n            productoIdDelBoton,\n            accionBoton,\n            esSoloSaludo: false,\n            esPreguntaConversacional: false,\n            debeHacerBusqueda: false,\n            esPedidoPlataforma: false,\n            infoPedidoPlataforma: null,\n            tieneMultiplesProductos: false,\n            terminosIndividuales: [],\n            terminoBusqueda: '',\n            terminoNormalizado: '',\n            variantesBusqueda: [],\n            terminosNormalizados: [],\n            timestamp: whatsappMsg?.sendTime\n        }\n    }];\n}\n\n// =====================================================\n// VALIDAR MENSAJE DE TEXTO\n// =====================================================\nif (!whatsappMsg || messageType !== 'text') {\n    return [{\n        json: {\n            esComandoCopiloto: false,\n            mensajeCliente: messageText,\n        from, to, customerName, saludo,\n            esMediaNoSoportado: true,\n            esMedia: false,\n            tipoMensaje: messageType,\n            messageText: '',\n            esRespuestaBoton: false,\n            botonId: '', botonTexto: '',\n            productoIdDelBoton: null,\n            accionBoton: '',\n            esSoloSaludo: false,\n            esPreguntaConversacional: false,\n            debeHacerBusqueda: false,\n            esPedidoPlataforma: false,\n            infoPedidoPlataforma: null,\n            tieneMultiplesProductos: false,\n            terminosIndividuales: [],\n            terminoBusqueda: '',\n            terminoNormalizado: '',\n            variantesBusqueda: [],\n            terminosNormalizados: [],\n            timestamp: whatsappMsg?.sendTime\n        }\n    }];\n}\n\n// =====================================================\n// \ud83d\udeab NUEVO: VERIFICAR SI EST\u00c1 ESCALADO\n// =====================================================\n// Obtener estado del cliente (si viene del input) - CON SEGURIDAD\nlet clienteInput = {};\ntry {\n    clienteInput = $('2. Obtener Cliente').first().json || {};\n} catch (e) {\n    clienteInput = {};\n}\n\n// Verificar si el cliente est\u00e1 escalado\nif (clienteInput && clienteInput.estado_conversacion && \n    ESTADOS_ESCALADOS.includes(clienteInput.estado_conversacion)) {\n    \n    console.log('\ud83d\udeab Cliente escalado detectado. Bloqueando respuesta autom\u00e1tica.');\n    console.log(`\ud83d\udcde Cliente: ${from}`);\n    console.log(`\ud83d\udcca Estado: ${clienteInput.estado_conversacion}`);\n    console.log(`\ud83d\udcdd Mensaje: ${messageTextRaw}`);\n    \n    // \u2705 RETORNAR MENSAJE DE ESPERA (NO PROCESAR M\u00c1S)\n    mensajeBloqueado = true;\n    \n    return [{\n        json: {\n            mensajeCliente: messageText,\n        from, to, customerName, saludo,\n            esComandoCopiloto: false,\n            esMediaNoSoportado: false,\n            esMedia: false,\n            tipoMensaje: 'text',\n            messageText: messageTextRaw,\n            esRespuestaBoton: false,\n            botonId: '', botonTexto: '',\n            productoIdDelBoton: null,\n            accionBoton: '',\n            // Indicadores para el siguiente flujo\n            esClienteEscalado: true,\n            estado_conversacion: clienteInput.estado_conversacion,\n            // Marcador para que el flujo sepa que est\u00e1 escalado\n            bloquearRespuesta: true\n        }\n    }];\n}\n\n// =====================================================\n// SI ES DIRECTOR\n// =====================================================\nif (esNumeroDirector) {\n    if (mensajeEmpiezaConPrefijo) {\n        // Contin\u00faa como cliente normal\n    } else {\n        // MODO COPILOTO\n        const normalizedText = messageTextRaw.replace(/[^\\d]/g, '');\n        const phoneMatch = normalizedText.match(/(?:57)?(3\\d{9})/);\n        const telefono_objetivo = phoneMatch ? phoneMatch[1].replace(/^57/, '') : null;\n        \n        let nombre_extraido = null;\n        const nombrePatterns = [\n            /(?:a|al nombre)\\s+[\"']?([A-Za-z\u00c1-\u00ff\\s]+?)[\"']?(?:\\s|$)/i,\n            /(?:nombre es|llama)\\s+[\"']?([A-Za-z\u00c1-\u00ff\\s]+?)[\"']?(?:\\s+y|\\s*$)/i,\n        ];\n        for (const pattern of nombrePatterns) {\n            const match = messageTextRaw.match(pattern);\n            if (match && match[1]) {\n                nombre_extraido = match[1].trim().replace(/\\b(del|cliente|el|la)\\b/gi, '').trim();\n                if (nombre_extraido.length >= 2) break;\n                else nombre_extraido = null;\n            }\n        }\n        \n        return [{\n            json: {\n                esComandoCopiloto: true,\n                mensajeCliente: messageText,\n        from, to, customerName, saludo,\n                messageText: messageTextRaw,\n                prompt: messageTextRaw,\n                mensajeClienteActual: messageTextRaw,\n                telefono_objetivo: telefono_objetivo,\n                nombre_extraido: nombre_extraido,\n                esMediaNoSoportado: false,\n                esMedia: false,\n                tipoMensaje: messageType,\n                timestamp: whatsappMsg?.sendTime\n            }\n        }];\n    }\n}\n\n// =====================================================\n// RESTO DEL C\u00d3DIGO (CLIENTES NORMALES O DIRECTOR CON >)\n// =====================================================\n// messageText movido al inicio\n\n// Detectar saludos simples\nconst esSoloSaludo = [\n    /^hola[,!\\.\\s]*$/i,\n    /^buenos?\\s+(d[i\u00ed]as?|tardes?|noches?)[,!\\.\\s]*$/i,\n    /^buenas[,!\\.\\s]*$/i,\n    /^hey[,!\\.\\s]*$/i,\n    /^qu[e\u00e9]\\s+tal[,!\\.\\s]*$/i,\n    /^c[o\u00f3]mo\\s+est[a\u00e1](s|n)?[,!\\.\\s]*$/i,\n    /^gracias[,!\\.\\s]*$/i,\n    /^ok[,!\\.\\s]*$/i,\n    /^s[i\u00ed][,!\\.\\s]*$/i,\n    /^no[,!\\.\\s]*$/i,\n    /^perfecto[,!\\.\\s]*$/i,\n    /^listo[,!\\.\\s]*$/i,\n    /^dale[,!\\.\\s]*$/i,\n    /^bien[,!\\.\\s]*$/i\n].some(p => p.test(messageText));\n\n// Detectar pedido desde plataforma\nconst esPedidoPlataforma = [\n    /acabo\\s+de\\s+hacer\\s+un\\s+pedido/i,\n    /hice\\s+un\\s+pedido\\s+en\\s+(la\\s+)?tienda/i,\n    /tus-aguacates\\.vercel\\.app/i,\n    /mi\\s+pedido:.*\\n.*total/is\n].some(p => p.test(messageText));\n\nlet infoPedidoPlataforma = null;\nif (esPedidoPlataforma) {\n    const nombreMatch = messageText.match(/me\\s+llamo\\s+(\\w+)/i);\n    const pagoMatch = messageText.match(/pago:\\s*(.+?)(\\n|$)/i);\n    const totalMatch = messageText.match(/total:\\s*\\$?([.\\d,]+)/i);\n    infoPedidoPlataforma = {\n        nombre: nombreMatch ? nombreMatch[1] : null,\n        metodoPago: pagoMatch ? pagoMatch[1].trim() : null,\n        total: totalMatch ? totalMatch[1] : null\n    };\n}\n\n// Detectar preguntas conversacionales\nconst esPreguntaConversacional = [\n    /c[o\u00f3]mo\\s+(veo|entro|accedo|visito|llego|abro)/i,\n    /d[o\u00f3]nde\\s+(est[a\u00e1]|queda|encuentro|veo)/i,\n    /tienda\\s+(en\\s+)?l[i\u00ed]nea/i,\n    /p[a\u00e1]gina\\s+web/i,\n    /\\b(link|enlace|url)\\b/i,\n    /\\bhorario/i,\n    /a\\s+qu[e\u00e9]\\s+hora/i,\n    /hasta\\s+qu[e\u00e9]\\s+hora/i,\n    /\\benv[i\u00ed]o/i,\n    /\\bentrega/i,\n    /\\bdomicilio/i,\n    /hacen\\s+env[i\u00ed]o/i,\n    /zona\\s+de\\s+(cobertura|entrega)/i,\n    /m[e\u00e9]todo(s)?\\s+de\\s+pago/i,\n    /aceptan\\s+(tarjeta|nequi|daviplata|efectivo)/i,\n    /puedo\\s+pagar\\s+con/i,\n    /\\bcontacto/i,\n    /\\bubicaci[o\u00f3]n/i,\n    /\\bdirecci[o\u00f3]n/i,\n    /d[o\u00f3]nde\\s+est[a\u00e1]n/i,\n    /mi\\s+pedido/i,\n    /qu[e\u00e9]\\s+llevo/i,\n    /eso\\s+es\\s+todo/i,\n    /total\\s+(del\\s+)?pedido/i,\n    /cu[a\u00e1]nto\\s+es\\s+(el\\s+)?total/i,\n    /cu[a\u00e1]nto\\s+te\\s+debo/i\n].some(p => p.test(messageText));\n\n// =====================================================\n// \ud83d\udd24 EXTRACCI\u00d3N DE T\u00c9RMINO DE B\u00daSQUEDA - v14 ENRIQUECIDO\n// =====================================================\nconst palabrasIgnorar = new Set([\n    // === PALABRAS DE PRUEBA ===\n    'test', 'prueba', 'testing', 'probando', 'probar',\n    // === PREGUNTAS ===\n    'cuales', 'cu\u00e1les', 'cual', 'cu\u00e1l', 'donde', 'd\u00f3nde',\n    'cuando', 'cu\u00e1ndo', 'porque', 'porqu\u00e9', 'como', 'c\u00f3mo',\n    // === VERBOS PODER (todas conjugaciones) ===\n    'poder', 'puedo', 'puedes', 'puede', 'podemos', 'pueden',\n    'podr\u00eda', 'podr\u00edas', 'podr\u00edamos', 'podr\u00edan',\n    'pudiera', 'pudieras', 'pudieran', 'pude', 'pudiste',\n    // === VERBOS QUERER (todas conjugaciones) ===\n    'querer', 'quiero', 'quieres', 'quiere', 'queremos', 'quieren',\n    'quisiera', 'quisieras', 'quisi\u00e9ramos', 'quisieran',\n    'querr\u00eda', 'querr\u00edas', 'querr\u00edamos', 'querr\u00edan',\n    // === VERBOS AGREGAR/A\u00d1ADIR (todas conjugaciones) ===\n    'agregar', 'agrego', 'agregas', 'agrega', 'agregamos', 'agregan',\n    'agr\u00e9game', 'agregarme', 'agregarle', 'agr\u00e9gale',\n    'a\u00f1adir', 'a\u00f1ado', 'a\u00f1ades', 'a\u00f1ade', 'a\u00f1adimos', 'a\u00f1aden',\n    'a\u00f1ademe', 'a\u00f1adirme', 'a\u00f1adirle',\n    // === VERBOS DAR (todas conjugaciones) ===\n    'dar', 'doy', 'das', 'da', 'damos', 'dan',\n    'dame', 'deme', 'darme', 'darle', 'd\u00e1melo',\n    'd\u00e1ndome', 'd\u00e1ndole',\n    // === VERBOS TRAER/ENVIAR ===\n    'traer', 'traigo', 'traes', 'trae', 'traemos', 'traen',\n    'traeme', 'traerme', 'traerle',\n    'enviar', 'env\u00edo', 'env\u00edas', 'env\u00eda', 'enviamos', 'env\u00edan',\n    'enviame', 'enviarme', 'enviarle', 'env\u00edame',\n    // === VERBOS PONER/METER ===\n    'poner', 'pongo', 'pones', 'pone', 'ponemos', 'ponen',\n    'ponme', 'ponerle',\n    'meter', 'meto', 'metes', 'mete', 'metemos', 'meten',\n    'meteme', 'meterme', 'meterle',\n    // === VERBOS CONSEGUIR ===\n    'conseguir', 'consigo', 'consigues', 'consigue', 'conseguimos', 'consiguen',\n    'conseguirme', 'conseguirle',\n    // === VERBOS TENER/HABER ===\n    'tener', 'tengo', 'tienes', 'tiene', 'tenemos', 'tienen',\n    'tendr\u00eda', 'tendr\u00edas', 'tendr\u00edamos', 'tendr\u00edan', 'tendr\u00e1n',\n    'haber', 'hay', 'hab\u00eda', 'habr\u00e1', 'habr\u00eda', 'habr\u00edamos', 'habr\u00edan',\n    // === VERBOS BUSCAR ===\n    'buscar', 'busco', 'buscas', 'busca', 'buscamos', 'buscan',\n    'buscando', 'b\u00fascame', 'buscarme',\n    // === VERBOS VENDER/MANEJAR ===\n    'vender', 'vendo', 'vendes', 'vende', 'vemos', 'venden',\n    'manejar', 'manejo', 'manejas', 'maneja', 'manejamos', 'manejan',\n    'ofrecer', 'ofrezco', 'ofreces', 'ofrece', 'ofrecemos', 'ofrecen',\n    // === VERBOS NECESITAR ===\n    'necesitar', 'necesito', 'necesitas', 'necesita', 'necesitamos', 'necesitan',\n    // === VERBOS LLEVAR/COMPRAR/PEDIR ===\n    'llevar', 'llevo', 'llevas', 'leva', 'llevamos', 'levan',\n    'comprar', 'compro', 'compras', 'compra', 'compramos', 'compran',\n    'pedir', 'pido', 'pides', 'pide', 'pedimos', 'piden',\n    'pedido', 'pedidos',\n    // === VERBOS SER/ESTAR ===\n    'ser', 'soy', 'eres', 'es', 'somos', 'son',\n    'era', 'fue', 'era', 'estoy', 'est\u00e1s', 'est\u00e1', 'estamos', 'est\u00e1n',\n    'ser\u00eda', 'ser\u00edas', 'ser\u00edamos', 'ser\u00edan',\n    'estar\u00eda', 'estar\u00edas', 'estar\u00edamos', 'estar\u00edan',\n    // === VERBOS VER/MOSTRAR ===\n    'ver', 'veo', 'ves', 've', 'vemos', 'ven',\n    'mostrar', 'muestro', 'muestras', 'muestra', 'mostramos', 'muestran',\n    'ense\u00f1ar', 'ense\u00f1o', 'ense\u00f1as', 'ense\u00f1a', 'ense\u00f1amos', 'ense\u00f1an',\n    'mirar', 'miro', 'miras', 'mira', 'miramos', 'miran',\n    // === ART\u00cdCULOS ===\n    'el', 'la', 'los', 'las', 'un', 'una', 'unos', 'unas',\n    'este', 'esta', 'estos', 'estas', 'ese', 'esa',\n    'aquel', 'aquella', 'aquellos', 'aquellas',\n    // === PRONOMBRES DEMOSTRATIVOS ===\n    'este', 'esta', 'estos', 'estas', 'ese', 'esa',\n    'aquel', 'aquella', 'aquellos', 'aquellas',\n    // === PRONOMBRES PERSONALES ===\n    'yo', 'tu', 't\u00fa', 'usted', '\u00e9l', 'ella', 'nosotros', 'ustedes', 'ellos', 'ellas',\n    'me', 'te', 'se', 'le', 'lo', 'la', 'les',\n    // === PREPOSICIONES ===\n    'de', 'del', 'a', 'al', 'para', 'por', 'con', 'sin', 'hacia', 'desde', 'entre', 'sobre', 'en',\n    // === INTERROGATIVOS ===\n    'que', 'qu\u00e9', 'cu\u00e1l', 'cu\u00e1les', 'd\u00f3nde', 'cu\u00e1ndo', 'cu\u00e1ndo', 'cu\u00e1nto', 'cu\u00e1nta',\n    'cu\u00e1ntos', 'cu\u00e1ntas', 'c\u00f3mo', 'c\u00f3mo', 'cu\u00e1l',\n    // === PRECIOS ===\n    'vale', 'valen', 'cuesta', 'cuestan', 'costar', 'costar\u00eda', 'precio', 'precios', 'costo', 'costos',\n    'valor', 'valores',\n    // === SALUDOS ===\n    'hola', 'buenos', 'buenas', 'hey', 'oye', 'oiga', 'disculpa', 'disculpe', 'perd\u00f3n', 'perdon',\n    'gracias', 'muchas', 'mil', 'please', 'plz', 'plis',\n    // === CORTES\u00cdA ===\n    'm\u00e1s', 'mas', 'menos', 'muy', 'mucho', 'mucha', 'poco', 'poca', 'tambi\u00e9n', 'tambien', 'adem\u00e1s', 'ademas', 'incluso',\n    'bueno', 'buena', 'buenos', 'buenas', 'mejor', 'mejores', 'peor', 'peores', 'grande', 'peque\u00f1o', 'peque\u00f1a',\n    'fresco', 'fresca', 'frescos', 'frescas',\n    // === ADJETIVOS/ADVERBIOS ===\n    'm\u00e1s', 'mas', 'menos', 'muy', 'mucho', 'mucha', 'poco', 'poca', 'tambi\u00e9n', 'tambien', 'adem\u00e1s', 'ademas', 'incluso',\n    'bueno', 'buena', 'buenos', 'buenas', 'mejor', 'mejores', 'peor', 'peores', 'grande', 'peque\u00f1o', 'peque\u00f1a',\n    'fresco', 'fresca', 'frescos', 'frescas',\n    // === N\u00daMEROS ===\n    'uno', 'dos', 'tres', 'cuatro', 'cinco', 'seis', 'siete', 'ocho', 'nueve', 'diez',\n    'primero', 'segundo', 'tercero', 'cuarto', 'quinto', 'sexto', 's\u00e9ptimo', 'octavo', 'noveno', 'd\u00e9cimo',\n    // === UNIDADES ===\n    'kilo', 'kilos', 'gramos', 'grs', 'libra', 'libras', 'unidad', 'unidades', 'paquete', 'paquetes',\n    'bandeja', 'bandejas', 'caja', 'cajas',\n    // === CONECTORES ===\n    'pero', 'aunque', 'entonces', 'pues', 'bueno', 'mira', 'oiga', 'digame', 'd\u00edgame', 'cu\u00e9ntame', 'cuentame',\n    'okay', 'vale', 'listo', 'dale', 'aja', 'aj\u00e1',\n    // === ADVERBIOS TIEMPO/LUGAR ===\n    'solo', 's\u00f3lo', 'solamente', '\u00fanicamente', 'unicamente',\n    'siempre', 'nunca', 'ahora', 'despu\u00e9s', 'despues', 'antes', 'antes',\n    'aqu\u00ed', 'aqui', 'ac\u00e1', 'aca', 'all\u00e1', 'alla', 'ah\u00ed', 'ahi',\n    // === OTRAS PALABRAS COMUNES ===\n    'ok', 'si', 'no', 'perfecto', 'listo', 'bien', 'todo', 'nada', 'alguno', 'alguna',\n    'otro', 'otra', 'otros', 'otras', 'misma', 'mismo', 'mismos', 'mismas',\n    'tal', 'vez', 'general', 'claro', 'casi', 'aprox', 'realmente', 'cierto'\n]);\n\nlet terminoBusqueda = messageText\n    .replace(/[^\\w\\s\u00e1\u00e9\u00ed\u00f3\u00fa\u00f1\u00fc\u00c1-\u00ff\u00cd-\u00da\\s]/g, '')\n    .split(/\\s+/)\n    .filter(w => w.length > 2 && !palabrasIgnorar.has(w.toLowerCase()))\n    .join(' ')\n    .trim();\n\n// Detectar m\u00faltiples productos\nconst separadores = /\\s+(?:y|o|,)\\s+/gi;\nconst tieneMultiplesProductos = separadores.test(terminoBusqueda);\n\nlet terminosIndividuales = [];\nif (tieneMultiplesProductos) {\n    terminosIndividuales = terminoBusqueda\n        .split(/s*(?:y|o|,)\\s*/gi)\n        .map(t => t.trim())\n        .filter(t => t.length > 0);\n} else if (terminoBusqueda) {\n    terminosIndividuales = [terminoBusqueda];\n}\n\n// NORMALIZACI\u00d3N DE PLURALES\nlet terminoNormalizado = terminoBusqueda;\nlet variantesBusqueda = [];\nlet terminosNormalizados = [];\n\nterminosIndividuales.forEach(termino => {\n    const palabras = termino.split(' ');\n    const palabrasNormalizadas = palabras.map(palabra => {\n        if (palabra.endsWith('s') && palabra.length > 3) {\n            return palabra.slice(0, -1);\n        }\n        if (palabra.endsWith('es') && palabra.length > 4) {\n            return palabra.slice(0, -2);\n        }\n        return palabra;\n    });\n    \n    const terminoNormalizado = palabrasNormalizadas.join(' ');\n    terminosNormalizados.push(terminoNormalizado);\n    \n    // Mantener el t\u00e9rmino original tambi\u00e9n\n    if (!variantesBusqueda.includes(termino)) {\n        variantesBusqueda.push(termino);\n    }\n    if (!variantesBusqueda.includes(terminoNormalizado)) {\n        variantesBusqueda.push(terminoNormalizado);\n    }\n});\n\nterminoNormalizado = terminosNormalizados.join(' ');\nvariantesBusqueda = [...new Set(variantesBusqueda)];\n\n// DECISI\u00d3N DE B\u00daSQUEDA\nconst debeHacerBusqueda =\n    !esSoloSaludo &&\n    !esPreguntaConversacional &&\n    terminoBusqueda.length > 0;\n\n// RESULTADO FINAL (CLIENTE NORMAL)\nreturn [{\n    json: {\n        esComandoCopiloto: false,\n        mensajeCliente: messageText,\n        from, to, customerName, saludo,\n        esMediaNoSoportado: false,\n        esMedia: false,\n        esImagen: false,\n        esDocumento: false,\n        esAudio: false,\n        esVideo: false,\n        esSticker: false,\n        tipoMensaje: 'text',\n        captionImagen: '',\n        esRespuestaBoton: false,\n        botonId: '',\n        botonTexto: '',\n        productoIdDelBoton: null,\n        accionBoton: '',\n        esSoloSaludo,\n        esPreguntaConversacional,\n        debeHacerBusqueda,\n        esPedidoPlataforma,\n        infoPedidoPlataforma,\n        tieneMultiplesProductos,\n        terminosIndividuales,\n        terminoBusqueda,\n        terminoNormalizado,\n        variantesBusqueda,\n        timestamp: whatsappMsg.sendTime\n    }\n}];\n"
        },
        "id": "c2d8c4de-9bdd-4b66-bbd8-abac56aa9c8f",
        "name": "1. Pre-procesamiento YCloud",
        "type": "n8n-nodes-base.code",
        "position": [
          -224,
          1440
        ],
        "typeVersion": 2,
        "alwaysOutputData": true
      },
      {
        "parameters": {
          "conditions": {
            "options": {
              "version": 2,
              "leftValue": "",
              "caseSensitive": true,
              "typeValidation": "strict"
            },
            "combinator": "and",
            "conditions": [
              {
                "id": "es-media",
                "operator": {
                  "type": "boolean",
                  "operation": "equals"
                },
                "leftValue": "={{ $json.esMediaNoSoportado }}",
                "rightValue": true
              }
            ]
          },
          "options": {}
        },
        "id": "2ce881a6-e0cc-4836-91ab-c43a398a3b68",
        "name": "\u2753 \u00bfEs Media?",
        "type": "n8n-nodes-base.if",
        "position": [
          320,
          1168
        ],
        "typeVersion": 2.2
      },
      {
        "parameters": {
          "method": "POST",
          "url": "https://httpbin.org/post",
          "sendBody": true,
          "bodyParameters": {
            "parameters": [
              {}
            ]
          },
          "options": {}
        },
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          416,
          976
        ],
        "id": "1c1dc511-7909-42f1-89bc-538c2ba4245a",
        "name": "\ud83d\udcf1 Responder Media No Soportado"
      },
      {
        "parameters": {
          "operation": "executeQuery",
          "query": "WITH nuevo_cliente AS (\n  INSERT INTO clientes (telefono, nombre, estado_conversacion)\n  SELECT\n    '{{ $json.from }}',\n    NULL,\n    'NUEVO'\n  WHERE NOT EXISTS (\n    SELECT 1 FROM clientes WHERE telefono = '{{ $json.from }}'\n  )\n  RETURNING id\n), candidatos AS (\n  SELECT\n    id,\n    telefono,\n    nombre,\n    direccion,\n    estado_conversacion,\n    pre_pedido,\n    total_pedidos,\n    CASE\n      WHEN COALESCE(jsonb_array_length(COALESCE(pre_pedido, '[]'::jsonb)), 0) > 0 THEN 0\n      ELSE 1\n    END AS prioridad_carrito\n  FROM clientes\n  WHERE telefono = '{{ $json.from }}'\n)\nSELECT id, telefono, nombre, direccion, estado_conversacion, pre_pedido, total_pedidos\nFROM candidatos\nORDER BY prioridad_carrito ASC, id DESC\nLIMIT 1;",
          "options": {}
        },
        "name": "2. Obtener Cliente",
        "type": "n8n-nodes-base.postgres",
        "typeVersion": 2.6,
        "id": "61583a03-3ecc-4030-94f8-3acc01b29774",
        "position": [
          464,
          1344
        ],
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "conditions": {
            "options": {
              "version": 2,
              "leftValue": "",
              "caseSensitive": true,
              "typeValidation": "strict"
            },
            "combinator": "and",
            "conditions": [
              {
                "id": "es-busqueda",
                "operator": {
                  "type": "boolean",
                  "operation": "equals"
                },
                "leftValue": "={{ $('1. Pre-procesamiento YCloud').first().json.debeHacerBusqueda }}",
                "rightValue": true
              }
            ]
          },
          "options": {}
        },
        "id": "9987fdfe-c019-409d-a941-1538ada467b3",
        "name": "\u2753 \u00bfBusca Producto?",
        "type": "n8n-nodes-base.if",
        "position": [
          1152,
          1184
        ],
        "typeVersion": 2.2
      },
      {
        "parameters": {
          "operation": "executeQuery",
          "query": "WITH search_terms AS (\n  SELECT\n    LOWER('{{ $json.terminoBusqueda }}') AS term_original,\n    LOWER(\n      CASE\n        WHEN LOWER('{{ $json.terminoBusqueda }}') IN ('hass', 'aguacate', 'aguacate hass') THEN '{{ $json.terminoBusqueda }}'\n        WHEN LOWER('{{ $json.terminoBusqueda }}') ~ 'es$' AND LENGTH('{{ $json.terminoBusqueda }}') > 4 THEN TRIM(TRAILING 'es' FROM LOWER('{{ $json.terminoBusqueda }}'))\n        WHEN LOWER('{{ $json.terminoBusqueda }}') ~ 's$' AND LENGTH('{{ $json.terminoBusqueda }}') > 3 THEN TRIM(TRAILING 's' FROM LOWER('{{ $json.terminoBusqueda }}'))\n        ELSE '{{ $json.terminoBusqueda }}'\n      END\n    ) AS term_base,\n    LOWER('{{ $json.mensajeCliente || \"\" }}') AS full_message\n), productos_match AS (\n  SELECT\n    p.id, p.name, p.price, p.main_image_url, p.description, p.category_name, p.stock, p.supabase_id,\n    CASE\n      WHEN s.term_original = 'hass' AND LOWER(p.name) LIKE '%hass%' THEN 0\n      WHEN s.term_original LIKE '%aguacate%' AND LOWER(p.name) LIKE '%aguacate%' AND LOWER(p.name) LIKE 'caja %' THEN 0\n      WHEN s.full_message LIKE '%24%' AND LOWER(p.name) LIKE '%24%' THEN 0\n      WHEN s.full_message LIKE '%caja%' AND LOWER(p.name) LIKE 'caja %' THEN 1\n      WHEN regexp_replace(LOWER(p.name), '[^a-z0-9\u00e1\u00e9\u00ed\u00f3\u00fa\u00f1\u00fc ]', '', 'g') ~ ('(^| )' || regexp_replace(s.term_original, '[^a-z0-9\u00e1\u00e9\u00ed\u00f3\u00fa\u00f1\u00fc ]', '', 'g') || '( |$)') THEN 2\n      WHEN LOWER(p.name) LIKE '%' || s.term_original || '%' THEN 3\n      WHEN LOWER(COALESCE(p.category_name, '')) LIKE '%' || s.term_original || '%' THEN 4\n      ELSE 99\n    END AS match_priority\n  FROM public.productos_tienda p, search_terms s\n  WHERE p.is_active = true\n    AND (\n      LOWER(p.name) LIKE '%' || s.term_original || '%'\n      OR LOWER(COALESCE(p.category_name, '')) LIKE '%' || s.term_original || '%'\n      OR LOWER(p.description) LIKE '%' || s.term_original || '%'\n    )\n)\nSELECT pm.id, pm.name, pm.price, pm.main_image_url, pm.description, pm.category_name, pm.stock, pm.supabase_id, pm.match_priority,\n  (SELECT COALESCE(json_agg(json_build_object('variante_id', v.id,'tipo', v.variant_name,'presentacion', v.variant_value,'precio', v.price,'stock', v.stock_quantity) ORDER BY v.price), '[]'::json)\n   FROM variantes_productos v WHERE v.product_supabase_id = pm.supabase_id AND v.is_active = true) AS variantes\nFROM productos_match pm\nGROUP BY pm.id, pm.name, pm.price, pm.main_image_url, pm.description, pm.category_name, pm.stock, pm.supabase_id, pm.match_priority\nORDER BY pm.match_priority ASC, CASE WHEN LOWER(pm.name) LIKE 'caja %' THEN 0 ELSE 1 END, pm.name\nLIMIT 3;",
          "options": {}
        },
        "id": "2a74cbde-7789-4f86-a771-9eed4733e6b1",
        "name": "3. B\u00fasqueda Autom\u00e1tica Productos",
        "type": "n8n-nodes-base.postgres",
        "position": [
          1296,
          752
        ],
        "typeVersion": 2.6,
        "alwaysOutputData": true,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "jsCode": "// Combinar datos del cliente y resultados de b\u00fasqueda\n// v5: Manejo robusto de clientes y pedidos web\n\nconst preproceso = $('1. Pre-procesamiento YCloud').first().json;\n\n// Obtener cliente con manejo de errores\nlet cliente = {};\ntry {\n  cliente = $('2. Obtener Cliente').first().json || {};\n} catch (e) {\n  // Si no existe el cliente, crear uno vac\u00edo\n  cliente = { id: null, telefono: preproceso.from, nombre: preproceso.customerName || 'Cliente' };\n}\n\n// Obtener productos de b\u00fasqueda\nlet productosEncontrados = [];\ntry {\n  const busquedaItems = $('3. B\u00fasqueda Autom\u00e1tica Productos').all();\n  productosEncontrados = busquedaItems\n    .map(item => item.json)\n    .filter(p => p && p.id && p.name && typeof p.price !== 'undefined');\n} catch (e) {\n  productosEncontrados = [];\n}\n\n// Formatear productos de forma SIMPLE\nlet productosTexto = '';\nif (productosEncontrados.length > 0) {\n  const lineas = [];\n  productosEncontrados.forEach(p => {\n    if (p.variantes && Array.isArray(p.variantes) && p.variantes.length > 0) {\n      p.variantes.forEach(v => {\n        lineas.push(`\u2022 ${p.name} - ${v.presentacion} $${Number(v.precio).toLocaleString('es-CO')} [ID:${p.id}]`);\n      });\n    } else {\n      lineas.push(`\u2022 ${p.name} - $${Number(p.price).toLocaleString('es-CO')} [ID:${p.id}]`);\n    }\n  });\n  productosTexto = [...new Set(lineas)].join('\\n');\n} else if (preproceso.debeHacerBusqueda && preproceso.terminoBusqueda) {\n  productosTexto = `\u274c No se encontraron productos para: \"${preproceso.terminoBusqueda}\"`;\n} else {\n  productosTexto = '(No se realiz\u00f3 b\u00fasqueda)';\n}\n\n// Contexto de tiempo\nconst now = new Date();\nconst dayOfWeek = now.getDay();\nconst hour = now.getHours();\nconst pasoCutoff = hour >= 10;\n\nlet diasHastaEntrega, mensajeEntrega;\nswitch(dayOfWeek) {\n  case 0: diasHastaEntrega = 2; mensajeEntrega = \"Pr\u00f3xima entrega: Martes\"; break;\n  case 1: diasHastaEntrega = 1; mensajeEntrega = \"Pr\u00f3xima entrega: Martes\"; break;\n  case 2: \n    if (pasoCutoff) { diasHastaEntrega = 3; mensajeEntrega = \"Pr\u00f3xima entrega: Viernes\"; }\n    else { diasHastaEntrega = 0; mensajeEntrega = \"\u00a1Entrega HOY antes 10AM!\"; }\n    break;\n  case 3: diasHastaEntrega = 2; mensajeEntrega = \"Pr\u00f3xima entrega: Viernes\"; break;\n  case 4: diasHastaEntrega = 1; mensajeEntrega = \"Pr\u00f3xima entrega: Viernes\"; break;\n  case 5:\n    if (pasoCutoff) { diasHastaEntrega = 4; mensajeEntrega = \"Pr\u00f3xima entrega: Martes\"; }\n    else { diasHastaEntrega = 0; mensajeEntrega = \"\u00a1Entrega HOY antes 10AM!\"; }\n    break;\n  case 6: diasHastaEntrega = 3; mensajeEntrega = \"Pr\u00f3xima entrega: Martes\"; break;\n  default: diasHastaEntrega = 1; mensajeEntrega = \"Pr\u00f3xima entrega: Martes o Viernes\";\n}\n\nconst fechaEntrega = new Date(now);\nfechaEntrega.setDate(now.getDate() + diasHastaEntrega);\nconst proximaEntrega = fechaEntrega.toLocaleDateString('es-CO', { weekday: 'long', day: 'numeric', month: 'long' });\nconst fechaActual = now.toLocaleDateString('es-CO', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });\nconst horaActual = now.toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit', hour12: true });\n\nreturn {\n  json: {\n    clienteId: cliente.id || null,\n    clienteTelefono: cliente.telefono || preproceso.from,\n    clienteNombre: cliente.nombre || preproceso.customerName || 'Cliente',\n    clienteEstado: cliente.estado_conversacion || 'NUEVO',\n    clienteDireccion: cliente.direccion || '',\n    clienteCarrito: cliente.pre_pedido || [],\n    clienteTotalPedidos: cliente.total_pedidos || 0,\n    \n    mensajeCliente: preproceso.mensajeCliente || preproceso.messageText,\n    terminoBusqueda: preproceso.terminoBusqueda || '',\n    esIntentoBusqueda: preproceso.esIntentoBusqueda || false,\n    esPedidoPlataforma: preproceso.esPedidoPlataforma || false,\n    infoPedidoPlataforma: preproceso.infoPedidoPlataforma || null,\n    \n    productosEncontrados,\n    productosTexto,\n    \n    from: preproceso.from,\n    to: preproceso.to,\n    saludo: preproceso.saludo,\n    \n    fechaActual, horaActual, horaNumero: hour, diaNumero: dayOfWeek,\n    pasoCutoff, proximaEntrega, mensajeEntrega, diasHastaEntrega,\n    retry_count: 0\n  }\n};\n"
        },
        "id": "4313e79a-64c2-46a3-8e81-e3e4efa7236d",
        "name": "4. Merge Datos + Productos",
        "type": "n8n-nodes-base.code",
        "position": [
          1584,
          592
        ],
        "typeVersion": 2,
        "alwaysOutputData": true
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "USA OBLIGATORIAMENTE cuando el cliente diga \"quiero\", \"dame\", \"agr\u00e9game\" o similares. NO digas que ya lo hiciste si no has ejecutado esta herramienta. Requiere: producto_id, producto_nombre, precio, cantidad.",
          "operation": "executeQuery",
          "query": "UPDATE clientes \nSET pre_pedido = COALESCE(pre_pedido, '[]'::jsonb) || \n  jsonb_build_object(\n    'producto_id', $1::int,\n    'producto_nombre', $2,\n    'precio', $3::numeric,\n    'cantidad', COALESCE($4::int, 1)\n  ),\n  estado_conversacion = 'EN_PEDIDO'\nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\nRETURNING pre_pedido;",
          "options": {
            "queryReplacement": "={{ $fromAI('producto_id','ID del producto a agregar','number',0) }}\n{{ $fromAI('producto_nombre','Nombre del producto','string','Producto') }}\n{{ $fromAI('precio','Precio del producto','number',0) }}\n{{ $fromAI('cantidad','Cantidad a agregar','number',1) }}"
          }
        },
        "id": "211df281-543d-497a-b643-25dca132695c",
        "name": "TOOL_AnadirAlCarrito",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          1584,
          1552
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "USA cuando el cliente diga \"eso es todo\", \"cu\u00e1nto es\", \"calcular total\". Devuelve el total del carrito.",
          "operation": "executeQuery",
          "query": "SELECT \n  COALESCE(\n    (SELECT SUM((item->>'precio')::numeric * COALESCE((item->>'cantidad')::int, 1))\n     FROM clientes, jsonb_array_elements(pre_pedido) AS item\n     WHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\n    ), 0\n  ) as total,\n  pre_pedido as items\nFROM clientes\nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').item.json.from }}';",
          "options": {}
        },
        "id": "68d9c2da-d254-46d7-be1e-e702329223e9",
        "name": "TOOL_CalcularTotalPrePedido",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          2512,
          1680
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "Guardar el nombre del cliente cuando lo proporcione.",
          "operation": "executeQuery",
          "query": "UPDATE clientes \nSET \n  nombre = $1, \n  estado_conversacion = 'ATENCION_LUZ' \nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\nRETURNING nombre, estado_conversacion;",
          "options": {
            "queryReplacement": "={{ $fromAI('nombre_cliente','Nombre del cliente','string','Cliente') }}"
          }
        },
        "id": "6a73ac10-62d0-490d-a7a8-59007eab3613",
        "name": "TOOL_GuardarNombreCliente",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          1872,
          1824
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "USA SIEMPRE despu\u00e9s de cada acci\u00f3n importante. Estados: EN_PEDIDO (agreg\u00f3 productos), PEDIDO_CONFIRMADO (confirm\u00f3), PEDIDO_ONLINE (de tienda), ESCALADO (queja).",
          "operation": "executeQuery",
          "query": "UPDATE clientes \nSET estado_conversacion = $1\nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\nRETURNING estado_conversacion;",
          "options": {
            "queryReplacement": "={{ $fromAI('nuevo_estado','Estado v\u00e1lido','string','ATENCION_LUZ') }}"
          }
        },
        "id": "84728a22-7913-434d-91b4-15690b9a14b2",
        "name": "TOOL_CambiarEstadoCliente",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          2224,
          1744
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "jsCode": "const contexto = $('4. Merge Datos + Productos').first().json;\nconst respuestaIA = $input.first().json.text || $input.first().json.output || '';\nlet mensaje = String(respuestaIA || '').replace(/\\*/g, '').trim();\nconst nombreCompleto = String(contexto.clienteNombre || '').trim();\nconst tieneNombreCompleto = nombreCompleto.split(/\\s+/).filter(Boolean).length >= 2;\nconst tieneDireccion = String(contexto.clienteDireccion || '').trim().length > 5;\nconst carrito = Array.isArray(contexto.clienteCarrito) ? contexto.clienteCarrito : [];\nconst pareceResumen = /subtotal|total:|resumen/i.test(mensaje);\nif (pareceResumen && carrito.length > 0 && (!tieneNombreCompleto || !tieneDireccion)) {\n  mensaje = '\u00a1Casi listo! Para coordinar el env\u00edo, por favor conf\u00edrmame tu nombre completo y la direcci\u00f3n de entrega.';\n}\nif (!mensaje) return [];\nreturn [{ json: { from: contexto.to, to: contexto.from, type: 'text', text: { body: mensaje, preview_url: false } } }];"
        },
        "id": "0262ccbe-0660-46a8-a027-0e674dd48319",
        "name": "\ud83d\udce4 Preparar Respuesta",
        "type": "n8n-nodes-base.code",
        "position": [
          2864,
          720
        ],
        "typeVersion": 2
      },
      {
        "parameters": {
          "method": "POST",
          "url": "https://httpbin.org/post",
          "sendBody": true,
          "bodyParameters": {
            "parameters": [
              {
                "name": "lab_intercept",
                "value": "={{ $json.toJsonString() }}"
              }
            ]
          },
          "options": {}
        },
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          3424,
          592
        ],
        "id": "4503a0ed-b573-452a-9788-4c5e27b600a5",
        "name": "\ud83d\udcf1 Enviar WhatsApp YCloud"
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "Herramienta de FALLBACK para buscar productos. \u00dasala SOLO cuando los productos encontrados autom\u00e1ticamente NO coinciden con lo que pidi\u00f3 el cliente. Input: t\u00e9rmino de b\u00fasqueda del producto.",
          "operation": "executeQuery",
          "query": "SELECT \n    id,\n    name,\n    COALESCE(discount_price, price) as precio,\n    description,\n    category_name\nFROM productos_tienda p\nWHERE \n    is_active = true\n    AND p.name NOT ILIKE '%Fresa Premium 250g%' AND p.price > 0 AND p.stock > 0 AND (\n        name ILIKE '%' || $1 || '%'\n        OR description ILIKE '%' || $1 || '%'\n        OR category_name ILIKE '%' || $1 || '%'\n    )\nORDER BY \n    CASE \n        WHEN is_featured = true THEN 0 \n        ELSE 1 \n    END,\n    name\nLIMIT 5;\n",
          "options": {
            "queryReplacement": "={{ $fromAI('termino_busqueda','Producto a buscar','string','aguacate') }}"
          }
        },
        "id": "37802349-d077-4ffe-a17d-acbf9652535d",
        "name": "TOOL_BuscarProductos",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          2640,
          1568
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "ESTA HERRAMIENTA ES OBLIGATORIA PARA ESCALAR. \u00dasala para transferir la sesi\u00f3n a un humano ante cualquier queja, molestia o pedido de soporte humano. NO PUEDES CONECTAR CON UN HUMANO SOLO CON TEXTO, DEBES ACTIVAR ESTA HERRAMIENTA.",
          "operation": "executeQuery",
          "query": "UPDATE clientes \nSET \n    estado_conversacion = 'ESCALADO'\nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\nRETURNING \n    nombre,\n    telefono,\n    estado_conversacion,\n    'Conversaci\u00f3n escalada. Motivo: ' || $1 as mensaje_confirmacion;\n",
          "options": {
            "queryReplacement": "={{ $fromAI('motivo_escalado','Motivo del escalado','string','Cliente solicita hablar con humano') }}"
          }
        },
        "id": "19f90844-7253-4e05-ab20-53780c3e8338",
        "name": "TOOL_EscalarServicioCliente",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          1744,
          1664
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "Consultar el estado del cliente y su \u00faltimo pedido. Usarla cuando el cliente pregunte: \u00bfc\u00f3mo va mi pedido? \u00bftengo pedidos?",
          "operation": "executeQuery",
          "query": "SELECT \n  c.nombre,\n  c.telefono,\n  c.estado_conversacion as estado,\n  c.direccion,\n  c.pre_pedido as carrito,\n  c.updated_at as ultima_actualizacion,\n  CASE \n    WHEN c.estado_conversacion = 'PEDIDO_CONFIRMADO' THEN '\u2705 Tu pedido fue confirmado. Te contactaremos para coordinar la entrega.'\n    WHEN c.estado_conversacion = 'PEDIDO_ONLINE' THEN '\ud83d\uded2 Tu pedido de la tienda online est\u00e1 siendo procesado.'\n    WHEN c.estado_conversacion = 'EN_PEDIDO' THEN '\ud83d\udce6 Tienes un pedido en proceso. \u00bfDeseas confirmarlo?'\n    WHEN c.estado_conversacion = 'ESCALADO' THEN '\ud83d\udc64 Un asesor te contactar\u00e1 pronto.'\n    ELSE '\ud83d\udcac No encontr\u00e9 pedidos recientes. \u00bfQuieres hacer uno nuevo?'\n  END as mensaje_estado\nFROM clientes c\nWHERE c.telefono LIKE '%' || RIGHT('{{ $('1. Pre-procesamiento YCloud').first().json.from }}', 10) || '%'\nLIMIT 1;",
          "options": {}
        },
        "id": "65cf129f-27f4-4962-afb3-139f78304515",
        "name": "TOOL_ConsultarEstadoPedido",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          2816,
          1488
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "Guarda la direcci\u00f3n de entrega del cliente. Usar cuando el cliente proporciona su direcci\u00f3n completa. Input: direccion (la direcci\u00f3n del cliente).",
          "operation": "executeQuery",
          "query": "UPDATE clientes \nSET direccion = $1\nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\nRETURNING telefono, nombre, direccion, 'Direcci\u00f3n guardada correctamente' as status;",
          "options": {
            "queryReplacement": "=={{ [$fromAI('direccion','Direcci\u00f3n completa del cliente incluyendo calle, n\u00famero, barrio y ciudad','string','')] }}"
          }
        },
        "id": "7e6be0a3-dadf-4979-950a-e18cf3d5e7b0",
        "name": "TOOL_GuardarDireccionCliente",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          2976,
          1408
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {},
        "id": "d6bc2944-ed4b-47f0-bf9d-9e1f2c5e40d3",
        "name": "Calculator",
        "type": "@n8n/n8n-nodes-langchain.toolCalculator",
        "position": [
          2336,
          1760
        ],
        "typeVersion": 1
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "USA SIEMPRE cuando el cliente pregunte por un producto. Obtiene tama\u00f1os, pesos y precios. Input: supabase_id del producto (b\u00fascalo en productosEncontrados).",
          "operation": "executeQuery",
          "query": "SELECT \n  v.variant_name as tipo,\n  v.variant_value as presentacion,\n  v.price as precio,\n  v.stock_quantity as stock,\n  p.name as producto\nFROM variantes_productos v\nJOIN productos_tienda p ON p.supabase_id = v.product_supabase_id\nWHERE p.id = $1\n  AND v.is_active = true\nORDER BY v.variant_name, v.price;",
          "options": {
            "queryReplacement": "={{ $fromAI('producto_id','ID num\u00e9rico del producto','number',0) }}"
          }
        },
        "id": "fb1e25a8-8f9b-417a-924f-5e713233d2f9",
        "name": "TOOL_ObtenerVariantes",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          2048,
          1744
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "jsCode": "// Obtener el producto de la consulta\nconst producto = $input.first().json;\n// Obtener el contexto del preprocesamiento\nconst contexto = $('1. Pre-procesamiento YCloud').first().json;\n// Formatear producto al formato esperado\nconst productoFormateado = {\n  producto_id: producto.id,\n  producto_nombre: producto.name,\n  name: producto.name,\n  id: producto.id,\n  precio: producto.price,\n  price: producto.price,\n  main_image_url: producto.main_image_url,\n  description: producto.description,\n  category: producto.category,\n  stock: producto.stock\n};\n// Retornar contexto con el producto encontrado\nreturn {\n  json: {\n    ...contexto,\n    productosEncontrados: [productoFormateado],\n    productosTexto: `Producto del bot\u00f3n: ${producto.name} - $${Number(producto.price).toLocaleString('es-CO')}`\n  }\n};"
        },
        "id": "89f8b753-f1c0-4d14-9c34-ad9238beb3c5",
        "name": "Code - Formatear Producto",
        "type": "n8n-nodes-base.code",
        "position": [
          1040,
          960
        ],
        "typeVersion": 2
      },
      {
        "parameters": {
          "operation": "executeQuery",
          "query": "SELECT \n  p.id,\n  p.name,\n  p.price,\n  p.main_image_url,\n  p.description,\n  p.category_id,\n  p.stock\nFROM public.productos_tienda p\nWHERE p.id = {{ $json.productoIdDelBoton }}\nLIMIT 1;",
          "options": {}
        },
        "id": "23d2a521-41a3-482b-b38f-de96a5d7317b",
        "name": "Buscar Producto del Bot\u00f3n",
        "type": "n8n-nodes-base.postgres",
        "position": [
          768,
          912
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "rules": {
            "values": [
              {
                "conditions": {
                  "options": {
                    "version": 2,
                    "leftValue": "",
                    "caseSensitive": true,
                    "typeValidation": "loose"
                  },
                  "combinator": "and",
                  "conditions": [
                    {
                      "id": "31594272-3c47-4074-9324-e697e674cd9b",
                      "operator": {
                        "type": "boolean",
                        "operation": "true",
                        "singleValue": true
                      },
                      "leftValue": "={{ $json.esRespuestaBoton }}",
                      "rightValue": ""
                    }
                  ]
                },
                "renameOutput": true,
                "outputKey": "Buscar Producto"
              },
              {
                "conditions": {
                  "options": {
                    "version": 2,
                    "leftValue": "",
                    "caseSensitive": true,
                    "typeValidation": "loose"
                  },
                  "combinator": "and",
                  "conditions": [
                    {
                      "id": "c5c155de-73a0-4e73-8ec0-0c21914d6b18",
                      "operator": {
                        "type": "string",
                        "operation": "notEmpty",
                        "singleValue": true
                      },
                      "leftValue": "={{ $json.esRespuestaBoton }}",
                      "rightValue": ""
                    }
                  ]
                },
                "renameOutput": true,
                "outputKey": "Sin Producto"
              }
            ]
          },
          "looseTypeValidation": true,
          "options": {
            "fallbackOutput": "none"
          }
        },
        "id": "2ac9fb8b-0172-48ac-9ab2-cb27f0ec396a",
        "name": "Detectar Click de Bot\u00f3n",
        "type": "n8n-nodes-base.switch",
        "position": [
          624,
          1232
        ],
        "typeVersion": 3.3
      },
      {
        "parameters": {},
        "id": "77068eff-d068-48d8-9f7b-f309db2c675d",
        "name": "Merge",
        "type": "n8n-nodes-base.merge",
        "position": [
          976,
          1488
        ],
        "typeVersion": 3.2
      },
      {
        "parameters": {},
        "id": "eaf862ec-e845-4496-a1b4-fc4ccbca2e93",
        "name": "Merge1",
        "type": "n8n-nodes-base.merge",
        "position": [
          416,
          1600
        ],
        "typeVersion": 3.2
      },
      {
        "parameters": {
          "rules": {
            "values": [
              {
                "conditions": {
                  "options": {
                    "version": 2,
                    "leftValue": "",
                    "caseSensitive": true,
                    "typeValidation": "loose"
                  },
                  "combinator": "and",
                  "conditions": [
                    {
                      "id": "45a7a667-e7c8-41ab-823c-ea0048f4ca53",
                      "operator": {
                        "type": "boolean",
                        "operation": "true",
                        "singleValue": true
                      },
                      "leftValue": "={{ $json.esComandoCopiloto }}",
                      "rightValue": ""
                    }
                  ]
                },
                "renameOutput": true,
                "outputKey": "Copiloto"
              },
              {
                "conditions": {
                  "options": {
                    "version": 2,
                    "leftValue": "",
                    "caseSensitive": true,
                    "typeValidation": "loose"
                  },
                  "combinator": "and",
                  "conditions": [
                    {
                      "id": "6e38428c-70d8-45a2-a0f1-a98aea4009f7",
                      "operator": {
                        "type": "boolean",
                        "operation": "false",
                        "singleValue": true
                      },
                      "leftValue": "={{ $json.esComandoCopiloto }}",
                      "rightValue": ""
                    }
                  ]
                },
                "renameOutput": true,
                "outputKey": "Cliente"
              }
            ]
          },
          "looseTypeValidation": true,
          "options": {}
        },
        "id": "757b19ad-3ad2-47e9-bf59-a9b94b898bc3",
        "name": "\ud83d\udd00 \u00bfEs Copiloto?",
        "type": "n8n-nodes-base.switch",
        "position": [
          144,
          1312
        ],
        "typeVersion": 3.3
      },
      {
        "parameters": {
          "sessionIdType": "customKey",
          "sessionKey": "copiloto_operaciones",
          "contextWindowLength": 2
        },
        "id": "90dfdd22-402b-41b4-b9b3-ae50eb6513f3",
        "name": "\ud83e\udde0 Memoria Copiloto",
        "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
        "position": [
          3296,
          1696
        ],
        "typeVersion": 1.3
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "Listar clientes SIN NOMBRE. Usar cuando pidan 'clientes sin nombre', 'an\u00f3nimos'.",
          "operation": "executeQuery",
          "query": "SELECT telefono, estado_conversacion, total_pedidos FROM clientes WHERE nombre IS NULL OR TRIM(nombre) = '' OR nombre ILIKE '%cliente%' ORDER BY total_pedidos DESC LIMIT 50;",
          "options": {}
        },
        "id": "ede8d0ad-2614-419a-97fc-189c92b73006",
        "name": "TOOL_ListarClientesSinNombre",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          4032,
          1648
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "Actualizar el NOMBRE de un cliente. Requiere: telefono del cliente y nuevo nombre.",
          "operation": "executeQuery",
          "query": "UPDATE clientes SET nombre = $1 WHERE REPLACE(REPLACE(telefono, '+', ''), ' ', '') LIKE '%' || $2 || '%' RETURNING telefono, nombre, 'Actualizado' as status;",
          "options": {
            "queryReplacement": "={{ $fromAI('nombre','Nuevo nombre del cliente','string','') }}\n{{ $fromAI('telefono','Tel\u00e9fono del cliente (solo n\u00fameros)','string','') }}"
          }
        },
        "id": "73d7abf3-eb92-4fff-a438-843d591a5de0",
        "name": "TOOL_ADMIN_ActualizarNombre",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          3552,
          1680
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "Consultar datos completos de UN cliente por tel\u00e9fono. Devuelve nombre, direcci\u00f3n, carrito (pre_pedido), estado y total de pedidos.",
          "operation": "executeQuery",
          "query": "SELECT telefono, nombre, direccion, estado_conversacion, total_pedidos, jsonb_array_length(COALESCE(pre_pedido, '[]'::jsonb)) as items_en_carrito, (SELECT COALESCE(SUM((item->>'precio')::numeric), 0) FROM jsonb_array_elements(COALESCE(pre_pedido, '[]'::jsonb)) item) as total_carrito, pre_pedido as carrito FROM clientes WHERE REPLACE(REPLACE(telefono, '+', ''), ' ', '') LIKE '%' || $1 || '%' LIMIT 1;",
          "options": {
            "queryReplacement": "={{ $fromAI('telefono','Tel\u00e9fono del cliente (\u00faltimos d\u00edgitos)','string','') }}"
          }
        },
        "id": "b369d2c5-d560-4673-a943-323dc0dc22f7",
        "name": "TOOL_ADMIN_ConsultarCliente",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          3792,
          1664
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "Buscar clientes por NOMBRE. Usar cuando pidan 'perfil de X', 'busca a X', 'datos de X' donde X es un nombre. Devuelve todos los clientes cuyo nombre coincida.",
          "operation": "executeQuery",
          "query": "SELECT telefono, nombre, direccion, estado_conversacion, total_pedidos, jsonb_array_length(COALESCE(pre_pedido, '[]'::jsonb)) as items_en_carrito FROM clientes WHERE LOWER(nombre) LIKE LOWER('%' || $1 || '%') ORDER BY total_pedidos DESC LIMIT 10;",
          "options": {
            "queryReplacement": "={{ $fromAI('nombre','Nombre del cliente a buscar','string','') }}"
          }
        },
        "id": "33238adf-7434-4f6c-8847-9f6aac9b4f69",
        "name": "TOOL_ADMIN_BuscarPorNombre",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          4016,
          1664
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "Contar total de clientes y cu\u00e1ntos tienen nombre.",
          "operation": "executeQuery",
          "query": "SELECT COUNT(*) as total, COUNT(CASE WHEN nombre IS NOT NULL AND TRIM(nombre) != '' THEN 1 END) as con_nombre, COUNT(CASE WHEN nombre IS NULL OR TRIM(nombre) = '' THEN 1 END) as sin_nombre FROM clientes;",
          "options": {}
        },
        "id": "a6c607c5-6787-474e-bbfe-3e781592a6f5",
        "name": "TOOL_ADMIN_ContarClientes",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          4320,
          1648
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "Vaciar el carrito de UN cliente espec\u00edfico. Usar cuando pidan 'vac\u00eda el carrito de X' o 'limpia el carrito del cliente X'. Requiere: tel\u00e9fono del cliente.",
          "operation": "executeQuery",
          "query": "UPDATE clientes SET pre_pedido = '[]'::jsonb WHERE REPLACE(REPLACE(telefono, '+', ''), ' ', '') LIKE '%' || $1 || '%' RETURNING telefono, nombre, 'Carrito vaciado' as status;",
          "options": {
            "queryReplacement": "={{ $fromAI('telefono','Tel\u00e9fono del cliente (\u00faltimos d\u00edgitos)','string','') }}"
          }
        },
        "id": "670fd6d2-9ee5-41ee-9d58-78038a92ae29",
        "name": "TOOL_ADMIN_VaciarCarrito",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          4544,
          1648
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "Vaciar TODOS los carritos de TODOS los clientes. Usar cuando pidan 'vac\u00eda todos los carritos', 'resetea todos los pedidos', 'limpia todos los carritos'. \u00a1CUIDADO! Acci\u00f3n masiva.",
          "operation": "executeQuery",
          "query": "UPDATE clientes SET pre_pedido = '[]'::jsonb WHERE pre_pedido IS NOT NULL AND jsonb_array_length(pre_pedido) > 0 RETURNING COUNT(*) as carritos_vaciados;",
          "options": {}
        },
        "id": "c14c9edb-4350-42da-99d7-6dc402eaa2c6",
        "name": "TOOL_ADMIN_VaciarTodosCarritos",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          4752,
          1648
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "Listar clientes que tienen productos en su carrito. Usar cuando pidan 'qui\u00e9nes tienen carrito activo', 'clientes con pedido pendiente', 'carritos llenos'.",
          "operation": "executeQuery",
          "query": "SELECT telefono, nombre, jsonb_array_length(pre_pedido) as items, (SELECT COALESCE(SUM((item->>'precio')::numeric), 0) FROM jsonb_array_elements(pre_pedido) item) as total FROM clientes WHERE pre_pedido IS NOT NULL AND jsonb_array_length(pre_pedido) > 0 ORDER BY total DESC LIMIT 30;",
          "options": {}
        },
        "id": "89eb2cf0-27f5-4721-949d-04b733b9d9d5",
        "name": "TOOL_ADMIN_ListarCarritosActivos",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          4976,
          1648
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "Estad\u00edsticas de carritos: cu\u00e1ntos clientes tienen carrito, total de items, valor total. Usar cuando pidan 'estad\u00edsticas de carritos', 'resumen de pedidos pendientes'.",
          "operation": "executeQuery",
          "query": "SELECT COUNT(*) as clientes_con_carrito, SUM(jsonb_array_length(pre_pedido)) as items_totales, (SELECT COALESCE(SUM((item->>'precio')::numeric), 0) FROM clientes c2, jsonb_array_elements(c2.pre_pedido) item WHERE c2.pre_pedido IS NOT NULL AND jsonb_array_length(c2.pre_pedido) > 0) as valor_total FROM clientes WHERE pre_pedido IS NOT NULL AND jsonb_array_length(pre_pedido) > 0;",
          "options": {}
        },
        "id": "7acba039-2fd6-4880-bb85-81cd25da935b",
        "name": "TOOL_ADMIN_ResumenCarritos",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          5184,
          1648
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "toolDescription": "Consultar si un cliente tiene un pedido activo por tel\u00e9fono en la tienda retail. \u00dasala cuando Mauricio o el equipo pregunte por estado de pedido, si un cliente ya tiene pedido en curso, si est\u00e1 pagado, la direcci\u00f3n de entrega o el contenido del pedido. Requiere: phone. Usa el tel\u00e9fono completo o los \u00faltimos 10 d\u00edgitos.",
          "method": "GET",
          "url": "https://tus-aguacates.vercel.app/api/agent/orders/active",
          "authentication": "genericCredentialType",
          "genericAuthType": "httpHeaderAuth",
          "sendQuery": true,
          "specifyQuery": "keypair",
          "parametersQuery": {
            "values": [
              {
                "name": "phone",
                "valueProvider": "modelRequired"
              }
            ]
          },
          "sendHeaders": true,
          "specifyHeaders": "keypair",
          "parametersHeaders": {
            "values": [
              {
                "name": "Accept",
                "valueProvider": "fieldValue",
                "value": "application/json"
              }
            ]
          }
        },
        "id": "5b0e6d9a-8bdf-4eb7-bb2f-48a9aa9419c1",
        "name": "TOOL_ADMIN_ConsultarPedidoActivo",
        "type": "@n8n/n8n-nodes-langchain.toolHttpRequest",
        "position": [
          5392,
          1808
        ],
        "typeVersion": 1.1
      },
      {
        "parameters": {
          "name": "TOOL_ADMIN_ConfirmarPedido",
          "description": "Confirmar y convertir un PRE-PEDIDO de WhatsApp en un PEDIDO REAL en la tienda online. Usar cuando digan 'confirma el pedido de X', 'convierte el pre-pedido a pedido'. Requiere tel\u00e9fono del cliente.",
          "jsCode": "const telefono = $fromAI('telefono', 'Tel\u00e9fono del cliente (\u00faltimos d\u00edgitos)', 'string');\n\ntry {\n  const response = await this.helpers.httpRequest({\n    method: 'POST',\n    url: 'https://dep-n8n.n8ntusaguacates.space/webhook/confirmar-prepedido',\n    headers: { 'Content-Type': 'application/json' },\n    body: { telefono }\n  });\n  \n  if (response.success) {\n    return '\u2705 Pedido confirmado! ID: ' + (response.order_id || 'N/A') + ', Total: $' + (response.total || 0);\n  } else {\n    return '\u274c ' + (response.message || 'No se encontr\u00f3 pre-pedido');\n  }\n} catch (error) {\n  return '\u274c Error: ' + error.message;\n}"
        },
        "id": "cd842bc2-906e-45af-b9dc-2edc5088ed6e",
        "name": "TOOL_ADMIN_ConfirmarPedido",
        "type": "@n8n/n8n-nodes-langchain.toolCode",
        "position": [
          5408,
          1648
        ],
        "typeVersion": 1
      },
      {
        "parameters": {
          "jsCode": "// =====================================================\n// \ud83d\udee1\ufe0f SOLUCI\u00d3N ROBUSTA PARA MENSAGES \"USE TOOLS\"\n// =====================================================\n// 1. Priorizar 'text' (respuesta limpia del agente)\n// 2. Si no existe, usar 'output' pero filtrar tool calls\n// 3. M\u00faltiples patrones de regex para m\u00e1ximo robustez\n// =====================================================\n\nlet respuestaIA = $input.first().json.text || $input.first().json.output || '';\n\n// FILTRADO ROBUSTO DE TOOL CALLS\nif (respuestaIA && respuestaIA.includes('Used tools')) {\n    // Patr\u00f3n 1: \"[Used tools: Tool: X, Input: Y, Result: Z]\"\n    respuestaIA = respuestaIA.replace(/\\[Used tools:[^\\]]*\\]\\s*/gi, '');\n\n    // Patr\u00f3n 2: \"Tool: TOOLNAME, Input: {...}, Result: {...}\"\n    respuestaIA = respuestaIA.replace(/Tool:\\s*[A-Z_]+,\\s*Input:\\s*\\{[^}]*\\},\\s*Result:/gi, '');\n\n    // Patr\u00f3n 3: \"[Tool: ...]\"\n    respuestaIA = respuestaIA.replace(/\\[Tool:[^\\]]*\\]\\s*/gi, '');\n\n    // Patr\u00f3n 4: \"Using tools:\" o similar\n    respuestaIA = respuestaIA.replace(/(?:Using|Used)\\s+tools?:[^\\n]*/gi, '');\n\n    // Patr\u00f3n 5: Eliminar brackets vac\u00edos o con solo comas que quedan\n    respuestaIA = respuestaIA.replace(/^\\s*[\\[\\]\\{\\}\\,]+\\s*/m, '');\n\n    // Patr\u00f3n 6: Limpiar saltos de l\u00ednea m\u00faltiples despu\u00e9s de eliminar tool calls\n    respuestaIA = respuestaIA.replace(/\\n{3,}/g, '\\n\\n');\n\n    respuestaIA = respuestaIA.trim();\n}\n\n// =====================================================\n// \ud83e\udde0 CONTIN\u00daA CON EL RESTO DEL C\u00d3DIGO ORIGINAL\n// =====================================================\n\nconst contexto = $('4. Merge Datos + Productos').first().json;\n\n// =====================================================\n// \ud83d\udd27 LIMPIEZA DEL MENSAJE (MANTENIENDO EMOJIS)\n// =====================================================\n\nlet mensaje = respuestaIA || '';\n\n// 1. ELIMINAR ASTERISCOS (negritas causan problemas)\nmensaje = mensaje.replace(/\\*/g, '');\n\n// 2. CONVERTIR \\n LITERAL A SALTO DE L\u00cdNEA REAL\nconst BACKSLASH = String.fromCharCode(92);\nconst LITERAL_N = BACKSLASH + 'n';\n\nlet intentos = 0;\nwhile (mensaje.indexOf(LITERAL_N) !== -1 && intentos < 50) {\n    mensaje = mensaje.split(LITERAL_N).join('\\n');\n    intentos++;\n}\n\n// 3. LIMPIAR SOLO emojis rotos (unicode suelto) - MANTENER EMOJIS NORMALES\nmensaje = mensaje.replace(/[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])/g, '');\nmensaje = mensaje.replace(/(?<![\\uD800-\\uDBFF])[\\uDC00-\\uDFFF]/g, '');\nmensaje = mensaje.replace(/\\\\u[0-9a-fA-F]{4}/g, '');\n\n// 4. NO ELIMINAR EMOJIS - Los emojis son importantes para el tono amigable\n// Solo eliminar flechas \u2192 y reemplazar por caracteres seguros\nmensaje = mensaje.replace(/\u2192/g, '\u2022');\n\n// 5. LIMPIAR ESPACIOS Y SALTOS EXCESIVOS\nmensaje = mensaje.replace(/\\n{3,}/g, '\\n\\n');\nmensaje = mensaje.replace(/  +/g, ' ');\nmensaje = mensaje.trim();\n\n// 6. FALLBACK\nif (!mensaje) {\n    mensaje = '\u00a1Hola! \ud83d\ude0a \u00bfEn qu\u00e9 puedo ayudarte?';\n}\n\n// =====================================================\n// \ud83d\udd17 DETECTAR SI HAY URLs PARA HABILITAR PREVIEW\n// =====================================================\nconst tieneUrl = mensaje.includes('http://') || mensaje.includes('https://') || mensaje.includes('tus-aguacates.vercel.app');\n\n// =====================================================\n// \ud83d\uddbc\ufe0f PREPARAR IMAGEN SI HAY PRODUCTOS\n// =====================================================\n\nconst prods = contexto.productosEncontrados || [];\nlet img = null;\nif (prods.length > 0) {\n    const p = prods.find(x => x.main_image_url && x.main_image_url.length > 0);\n    if (p) {\n        let url = p.main_image_url;\n        if (url.includes('supabase.co/storage') && url.includes('.webp')) {\n            url = url.replace('/object/public/', '/render/image/public/') + '?format=origin';\n        }\n        img = {\n            url,\n            nombre: p.name,\n            precio: p.price,\n            caption: p.name + ' - $' + Number(p.price).toLocaleString('es-CO')\n        };\n    }\n}\n\n// =====================================================\n// \ud83d\udce4 PREPARAR RESPUESTAS PARA YCLOUD\n// =====================================================\n\nconst resultados = [];\n\n// Agregar imagen si existe\nif (img && !contexto.esMedia) {\n    resultados.push({\n        json: {\n            from: contexto.to,\n            to: contexto.from,\n            type: 'image',\n            image: {\n                link: img.url,\n                caption: img.caption\n            }\n        }\n    });\n}\n\n// Agregar mensaje de texto CON preview_url si hay links\nconst mensajeJson = {\n    from: contexto.to,\n    to: contexto.from,\n    type: 'text',\n    text: { \n        body: mensaje,\n        preview_url: tieneUrl  // Habilita Link Preview cuando hay URLs\n    }\n};\n\nresultados.push({ json: mensajeJson });\n\nreturn resultados;\n"
        },
        "id": "09e19f9a-1a2b-46d0-981e-f128df2e76ef",
        "name": "\ud83d\udce4 Preparar Respuesta Copiloto",
        "type": "n8n-nodes-base.code",
        "position": [
          4496,
          1264
        ],
        "typeVersion": 2
      },
      {
        "parameters": {
          "promptType": "define",
          "text": "={{ $json.messageText }}",
          "options": {
            "systemMessage": "## \ud83c\udfaf MISI\u00d3N\nEres el Copiloto de Operaciones de \"Tus Aguacates\". Tu interlocutor es Mauricio (Director).\n\n## \ud83d\udcda HERRAMIENTAS\n- TOOL_ListarClientesSinNombre: Lista clientes sin nombre\n- TOOL_ADMIN_ActualizarDatosCliente: Actualiza nombre/direcci\u00f3n\n- TOOL_ADMIN_ConsultarCliente: Consulta datos de un cliente\n- TOOL_ADMIN_ContarClientes: Cuenta total de clientes\n- TOOL_ReporteEstadoClientes: Distribuci\u00f3n por estado\n- TOOL_ListarVIPsEnRiesgo: Clientes VIP en riesgo\n- TOOL_ADMIN_ConsultarPedidoActivo: Revisa si un cliente tiene un pedido retail activo, su estado, total, direcci\u00f3n y pago\n\n## \ud83d\udcac EJEMPLOS\n**Actualizar nombre:**\nComando: \"Actualiza el nombre del 3161932558 a Consuelo\"\nAcci\u00f3n: TOOL_ADMIN_ActualizarDatosCliente(nombre='Consuelo', telefono='3161932558')\n\n**Lista sin nombre:**\nComando: \"Dame los clientes sin nombre\"\nAcci\u00f3n: TOOL_ListarClientesSinNombre()\n\n## \ud83c\udfaf PRINCIPIOS\n1. Usa datos pre-extra\u00eddos (telefono_objetivo, nombre_extraido)\n2. Respuestas concisas\n3. Nunca inventes informaci\u00f3n"
          }
        },
        "id": "e04f0b6d-c2aa-4f52-81f6-e95c5a2237ec",
        "name": "\ud83e\udde0 Agente Copiloto1",
        "type": "@n8n/n8n-nodes-langchain.agent",
        "position": [
          3888,
          1232
        ],
        "typeVersion": 3
      },
      {
        "parameters": {
          "operation": "executeQuery",
          "query": "UPDATE envios_campana\nSET \n  respondio = true,\n  fecha_respuesta = NOW(),\n  mensaje_respuesta = LEFT('{{ $json.messageText || $json.botonTexto || \"respuesta\" }}', 500)\nWHERE \n  telefono LIKE '%' || RIGHT('{{ $json.from }}', 10)\n  AND respondio = false;",
          "options": {}
        },
        "id": "c4a6c5d9-4f9e-49cd-a5af-128b596b3fd6",
        "name": "\ud83d\udcca Marcar Respuesta Campa\u00f1a",
        "type": "n8n-nodes-base.postgres",
        "position": [
          0,
          1744
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "ADMIN: Cambiar ESTADO de conversaci\u00f3n de un cliente. Estados v\u00e1lidos: NUEVO, ATENCION_LUZ, EN_PEDIDO, PEDIDO_CONFIRMADO, PEDIDO_ONLINE, ESCALADO. Ejemplo: 'Pon el 3161932558 en NUEVO'",
          "operation": "executeQuery",
          "query": "UPDATE clientes SET estado_conversacion = $1 WHERE REPLACE(REPLACE(REPLACE(telefono, '+', ''), ' ', ''), '-', '') LIKE '%' || $2 || '%' RETURNING telefono, nombre, estado_conversacion, 'Estado cambiado' as status;",
          "options": {
            "queryReplacement": "={{ $fromAI('nuevo_estado','Estado: NUEVO, ATENCION_LUZ, EN_PEDIDO, PEDIDO_CONFIRMADO, PEDIDO_ONLINE, ESCALADO','string','NUEVO') }}\n{{ $fromAI('telefono','Telefono del cliente','string') }}"
          }
        },
        "id": "9aa57a0c-a7f8-4afc-a5a6-39de2b70a28c",
        "name": "TOOL_ADMIN_CambiarEstadoCliente",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          2544,
          2048
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "ADMIN: Borrar MEMORIA de un cliente espec\u00edfico. Elimina SOLO las sesiones de chat del tel\u00e9fono indicado. Ejemplo: 'Borra la memoria del 3161932558'",
          "operation": "executeQuery",
          "query": "WITH deleted AS (\n  DELETE FROM n8n_chat_histories \n  WHERE session_id = $1 \n     OR session_id = '57' || $1\n     OR session_id = '+57' || $1\n  RETURNING session_id\n)\nSELECT \n  COUNT(*) as registros_eliminados,\n  'Memoria borrada para: ' || $1 as mensaje\nFROM deleted;",
          "options": {
            "queryReplacement": "={{ $fromAI('telefono','Telefono del cliente para borrar memoria','string') }}"
          }
        },
        "id": "91172ab9-870e-444d-a2ef-3c160d322ec6",
        "name": "TOOL_ADMIN_BorrarMemoriaCliente",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          2736,
          2048
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "ADMIN: Limpiar/vaciar el CARRITO de un cliente. Ejemplo: 'Vac\u00eda el carrito del 3161932558'",
          "operation": "executeQuery",
          "query": "UPDATE clientes SET pre_pedido = '[]'::jsonb WHERE REPLACE(REPLACE(REPLACE(telefono, '+', ''), ' ', ''), '-', '') LIKE '%' || $1 || '%' RETURNING telefono, nombre, 'Carrito vaciado' as status;",
          "options": {
            "queryReplacement": "={{ $fromAI('telefono','Telefono del cliente para limpiar carrito','string') }}"
          }
        },
        "id": "12b89af1-d858-4712-8e6c-dfe4783abdfa",
        "name": "TOOL_ADMIN_LimpiarCarritoCliente",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          2944,
          2048
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "name": "confirmar_pedido_etiqueta",
          "jsCode": "// TOOL: Confirmar Pedido + Etiqueta YCloud\nconst telefono = $('1. Pre-procesamiento YCloud').first().json.from;\n\ntry {\n  // Usar httpRequestWithAuthentication en lugar de httpRequest + credentials manual\n  const updateResult = await this.helpers.httpRequestWithAuthentication.call(this, 'YCloudApi', {\n    method: 'POST',\n    url: 'https://api.ycloud.com/v2/contacts',\n    body: {\n      phoneNumber: '+' + telefono,\n      tags: ['Confirmado']\n    },\n    json: true\n  });\n  \n  return 'Cliente ' + telefono + ' etiquetado como Confirmado en YCloud. Contin\u00faa con TOOL_CambiarEstadoCliente para cambiar el estado a PEDIDO_CONFIRMADO.';\n  \n} catch (e) {\n  return 'Error al etiquetar: ' + e.message + '. Contin\u00faa con TOOL_CambiarEstadoCliente para cambiar estado a PEDIDO_CONFIRMADO.';\n}"
        },
        "id": "cd147bfd-97cc-490b-929a-6c603b56e7e0",
        "name": "TOOL_ConfirmarPedidoConEtiqueta",
        "type": "@n8n/n8n-nodes-langchain.toolCode",
        "position": [
          3168,
          1328
        ],
        "typeVersion": 1.1
      },
      {
        "parameters": {
          "model": "nvidia/nemotron-3-super-120b-a12b:free",
          "options": {
            "temperature": 0
          }
        },
        "id": "1dfa0007-ab9b-4d0b-8d73-98da18818b7c",
        "name": "OpenRouter Chat Model",
        "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
        "position": [
          1696,
          880
        ],
        "typeVersion": 1,
        "credentials": {
          "openRouterApi": {
            "id": "nmNGRJ9W11eOuYbL",
            "name": "OpenRouter account 2"
          }
        },
        "notes": "Reserved node; skipped by LAB Model Selector due to rate-limit instability"
      },
      {
        "parameters": {
          "numberInputs": 4,
          "rules": {
            "rule": [
              {
                "conditions": {
                  "options": {
                    "version": 2,
                    "leftValue": "",
                    "caseSensitive": true,
                    "typeValidation": "strict"
                  },
                  "combinator": "and",
                  "conditions": [
                    {
                      "id": "lab-stable-primary",
                      "operator": {
                        "type": "number",
                        "operation": "equals"
                      },
                      "leftValue": "={{ $json.retry_count || 0 }}",
                      "rightValue": 0
                    }
                  ]
                }
              },
              {
                "modelIndex": 2,
                "conditions": {
                  "options": {
                    "version": 2,
                    "leftValue": "",
                    "caseSensitive": true,
                    "typeValidation": "strict"
                  },
                  "combinator": "and",
                  "conditions": [
                    {
                      "id": "lab-stable-secondary",
                      "operator": {
                        "type": "number",
                        "operation": "equals"
                      },
                      "leftValue": "={{ $json.retry_count || 0 }}",
                      "rightValue": 1
                    }
                  ]
                }
              },
              {
                "modelIndex": 3,
                "conditions": {
                  "options": {
                    "version": 2,
                    "leftValue": "",
                    "caseSensitive": true,
                    "typeValidation": "strict"
                  },
                  "combinator": "and",
                  "conditions": [
                    {
                      "id": "lab-stable-tertiary",
                      "operator": {
                        "type": "number",
                        "operation": "largerEqual"
                      },
                      "leftValue": "={{ $json.retry_count || 0 }}",
                      "rightValue": 2
                    }
                  ]
                }
              }
            ]
          }
        },
        "type": "@n8n/n8n-nodes-langchain.modelSelector",
        "typeVersion": 1,
        "position": [
          1744,
          720
        ],
        "id": "8c31ecd7-938e-43be-bdc4-85925d3f31c8",
        "name": "Model Selector"
      },
      {
        "parameters": {
          "model": "mistral-vibe-cli-with-tools",
          "options": {}
        },
        "id": "769ed9f4-0dea-4905-80eb-d6a0e246510f",
        "name": "Mistral Cloud Chat Model",
        "type": "@n8n/n8n-nodes-langchain.lmChatMistralCloud",
        "position": [
          2032,
          832
        ],
        "typeVersion": 1,
        "credentials": {
          "mistralCloudApi": {
            "id": "xxCUooDM4wFtCGFv",
            "name": "Mistral Cloud account"
          }
        },
        "notes": "LAB tertiary fallback model"
      },
      {
        "parameters": {
          "model": "stepfun/step-3.5-flash:free",
          "options": {
            "temperature": 0
          }
        },
        "id": "77146c73-1d11-425a-bb55-ff5868c7d5c6",
        "name": "OpenRouter Chat Model2",
        "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
        "position": [
          1552,
          880
        ],
        "typeVersion": 1,
        "credentials": {
          "openRouterApi": {
            "id": "nmNGRJ9W11eOuYbL",
            "name": "OpenRouter account 2"
          }
        },
        "notes": "LAB secondary fallback model"
      },
      {
        "parameters": {
          "model": "=arcee-ai/trinity-large-preview:free",
          "options": {
            "temperature": 0
          }
        },
        "id": "d49c213e-3462-4111-b7f0-64175f01afcb",
        "name": "OpenRouter Chat Model3",
        "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
        "position": [
          3600,
          1360
        ],
        "typeVersion": 1,
        "credentials": {
          "openRouterApi": {
            "id": "nmNGRJ9W11eOuYbL",
            "name": "OpenRouter account 2"
          }
        }
      },
      {
        "parameters": {
          "conditions": {
            "options": {
              "caseSensitive": true,
              "typeValidation": "strict",
              "version": 2
            },
            "conditions": [
              {
                "id": "cliente-bloqueado",
                "leftValue": "={{ $('1. Pre-procesamiento YCloud').first().json.bloquearRespuesta }}",
                "rightValue": true,
                "operator": {
                  "type": "boolean",
                  "operation": "equals"
                }
              }
            ],
            "combinator": "and"
          },
          "options": {}
        },
        "id": "4b1c7c4f-ed0f-4c3a-8727-87779ee620ad",
        "name": "\u2753 \u00bfCliente Bloqueado?",
        "type": "n8n-nodes-base.if",
        "typeVersion": 2.2,
        "position": [
          -192,
          208
        ]
      },
      {
        "parameters": {},
        "id": "3bd3dfa8-6e3f-4659-8cf4-c42aed253ba4",
        "name": "\ud83d\uded1 Fin - Cliente Escalado",
        "type": "n8n-nodes-base.noOp",
        "typeVersion": 1,
        "position": [
          0,
          0
        ]
      },
      {
        "parameters": {
          "operation": "executeQuery",
          "query": "CREATE TABLE IF NOT EXISTS mensaje_buffer (\n    id SERIAL PRIMARY KEY,\n    cliente_telefono VARCHAR(20) NOT NULL,\n    mensaje TEXT NOT NULL,\n    timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    procesado BOOLEAN NOT NULL DEFAULT FALSE,\n    session_data JSONB,\n    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\nCREATE INDEX IF NOT EXISTS idx_mb_tel ON mensaje_buffer(cliente_telefono);\nCREATE INDEX IF NOT EXISTS idx_mb_proc ON mensaje_buffer(procesado, timestamp);\nCREATE INDEX IF NOT EXISTS idx_mb_tel_proc ON mensaje_buffer(cliente_telefono, procesado);\nSELECT table_name, 'TABLA CREADA' as resultado FROM information_schema.tables \nWHERE table_name = 'mensaje_buffer' AND table_schema = 'public';",
          "options": {}
        },
        "type": "n8n-nodes-base.postgres",
        "typeVersion": 2.4,
        "position": [
          1312,
          1120
        ],
        "id": "284c53bf-db5f-4e37-85a5-774acc75a1d0",
        "name": "\ud83d\udd27 INIT - Crear tabla buffer",
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "path": "init-buffer-db",
          "options": {}
        },
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 1,
        "position": [
          1008,
          1120
        ],
        "id": "8326f549-376f-4f93-a537-4df7a25178e3",
        "name": "\ud83d\udd27 Init Webhook",
        "webhookId": "347800b0-87b9-45d8-b20c-cacedf5d4cee"
      },
      {
        "parameters": {
          "conditions": {
            "options": {
              "caseSensitive": true,
              "leftValue": "",
              "typeValidation": "strict"
            },
            "conditions": [
              {
                "leftValue": "={{ $json.body?.fromBuffer || $json.fromBuffer || false }}",
                "rightValue": true,
                "operator": {
                  "type": "boolean",
                  "operation": "equals"
                }
              }
            ],
            "combinator": "and"
          },
          "options": {}
        },
        "type": "n8n-nodes-base.if",
        "typeVersion": 2,
        "position": [
          -368,
          1200
        ],
        "id": "6e8cad21-3a9c-457e-88eb-435cee8257d0",
        "name": "\ud83d\udd00 \u00bfViene del Buffer?"
      },
      {
        "parameters": {
          "operation": "executeQuery",
          "query": "-- Extraer mensaje del payload YCloud est\u00e1ndar\nWITH msg_data AS (\n    SELECT \n        '{{ $json.body.whatsappInboundMessage.from }}'::varchar AS telefono,\n        '{{ $json.body.whatsappInboundMessage.text.body.replace(/'/g, \"''\") }}'::text AS mensaje,\n        jsonb_build_object(\n            'to', '{{ $json.body.whatsappInboundMessage.to }}',\n            'name', '{{ $json.body.whatsappInboundMessage.customerProfile.name }}'\n        ) AS session_data\n)\nINSERT INTO mensaje_buffer (cliente_telefono, mensaje, session_data)\nSELECT telefono, mensaje, session_data FROM msg_data\nWHERE telefono != '' AND mensaje != ''\nRETURNING id, cliente_telefono, timestamp;",
          "options": {}
        },
        "type": "n8n-nodes-base.postgres",
        "typeVersion": 2.4,
        "position": [
          -352,
          1472
        ],
        "id": "4fb69c37-4b61-4154-b029-4d3cd2a728cd",
        "name": "\ud83d\udcbe Guardar en Buffer",
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {},
        "type": "n8n-nodes-base.noOp",
        "typeVersion": 1,
        "position": [
          -288,
          1712
        ],
        "id": "4411e70a-3d10-440b-8452-bdc5f93ef6d4",
        "name": "\u23f9\ufe0f Buffer guardado OK"
      },
      {
        "parameters": {
          "model": "nvidia/nemotron-3-super-120b-a12b:free",
          "options": {
            "temperature": 0
          }
        },
        "id": "f37db32b-a756-4f3b-aa8e-2219d706a9e5",
        "name": "OpenRouter Chat Model1",
        "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
        "position": [
          1856,
          880
        ],
        "typeVersion": 1,
        "credentials": {
          "openRouterApi": {
            "id": "nmNGRJ9W11eOuYbL",
            "name": "OpenRouter account 2"
          }
        },
        "notes": "LAB primary model"
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "USA SIEMPRE cuando pregunten por precios, \u00bfcu\u00e1nto vale?, \u00bfqu\u00e9 costo tiene? Input: nombre del producto o tama\u00f1o.",
          "operation": "executeQuery",
          "query": "SELECT name, price, description FROM productos_tienda WHERE is_active = true AND (name ILIKE '%' || $1 || '%' OR description ILIKE '%' || $1 || '%') ORDER BY price LIMIT 5;",
          "options": {
            "queryReplacement": "={{ $fromAI('termino','Producto','string','') }}"
          }
        },
        "id": "d3c1a2b3-c4d5-4e6f-8a9b-0c1d2e3f4a5b",
        "name": "TOOL_ConsultarPrecio",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          2800,
          1808
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "descriptionType": "manual",
          "toolDescription": "USA para buscar recetas con aguacate, ideas de cocina o platos. Input: ingrediente o tipo de plato.",
          "operation": "executeQuery",
          "query": "SELECT name, description FROM productos_tienda WHERE is_active = true AND (category_name ILIKE '%receta%' OR description ILIKE '%receta%' OR name ILIKE '%' || $1 || '%') LIMIT 5;",
          "options": {
            "queryReplacement": "={{ $fromAI('termino','Ingrediente/Plato','string','') }}"
          }
        },
        "id": "e5f1a2b3-c4d5-4e6f-8a9b-0c1d2e3f4a6c",
        "name": "TOOL_BuscarRecetas",
        "type": "n8n-nodes-base.postgresTool",
        "position": [
          3008,
          1808
        ],
        "typeVersion": 2.6,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      },
      {
        "parameters": {
          "promptType": "define",
          "text": "=# ========================================\n# USER MESSAGE (TEXT) - COPIAR Y PEGAR EN n8n\n# ========================================\n# D\u00f3nde: Nodo \"\ud83e\udd16 Agente Luz v4\" \u2192 text\n# ========================================\n## CONTEXTO DEL CLIENTE\n- Nombre: {{ $json.clienteNombre }}\n- Saludo: {{ $json.saludo }}\n- Estado: {{ $json.clienteEstado }}\n- Total pedidos anteriores: {{ $json.clienteTotalPedidos }}\n- Carrito actual: {{ JSON.stringify($json.clienteCarrito) }}\n## \ud83d\udd0d PRODUCTOS ENCONTRADOS EN B\u00daSQUEDA\n{{ $json.productosTexto }}\n- Tel\u00e9fono: {{ $json.clienteTelefono }}\n- Direcci\u00f3n: {{ $json.clienteDireccion }}\n## MENSAJE DEL CLIENTE\n{{ $json.mensajeCliente }}\n## \ud83d\udcc5 FECHA Y ENTREGA\n- Fecha actual: {{ $json.fechaActual }}\n- Hora actual: {{ $json.horaActual }}\n- Pr\u00f3xima entrega: {{ $json.proximaEntrega }}\n- Dias hasta entrega: {{ $json.diasHastaEntrega }}\n- Hoy entregar: {{ $json.pasoCutoff ? 'NO (despu\u00e9s de 10AM)' : 'S\u00cd' }}\n- Es saludo simple: {{ $json.esSoloSaludo }}",
          "options": {
            "systemMessage": "# ========================================\n# SYSTEM MESSAGE - COPIAR Y PEGAR EN n8n\n# ========================================\n# D\u00f3nde: Nodo \"\ud83e\udd16 Agente Luz v4\" \u2192 options \u2192 systemMessage\n# ========================================\nEres Luz \ud83e\udd51, la asistente de ventas de Tus Aguacates. Tu misi\u00f3n: Ayudar a que compren, de forma r\u00e1pida y amable.\n## \ud83d\udd27 USO DE HERRAMIENTAS (SIEMPRE PRIMERO)\nAntes de responder, revisa si aplica alguna herramienta:\n| Situaci\u00f3n | Herramienta |\n|-----------|-------------|\n| Pregunta por producto | TOOL_BuscarProductos |\n| Dice \"quiero\", \"dame\", \"agregar\" | TOOL_AnadirAlCarrito |\n| Da su nombre | TOOL_GuardarNombreCliente |\n| Da su direcci\u00f3n | TOOL_GuardarDireccionCliente |\n| Confirma pedido | TOOL_CalcularTotalPrePedido + TOOL_CambiarEstadoCliente |\n| Est\u00e1 molesto/queja | TOOL_EscalarServicioCliente |\n| \"\u00bfCu\u00e1ndo llega?\" | TOOL_ConsultarEstadoPedido |\n## \ud83d\udcac ESTILO DE COMUNICACI\u00d3N\n\n- **BREVEDAD M\u00c1XIMA**: Si hay muchos productos, muestra solo los 3 m\u00e1s relevantes. Si el cliente quiere ver m\u00e1s, que pregunte. No listes todo el cat\u00e1logo de una vez.\n1. **USA EMOJIS** - Cada producto con su emoji (\ud83e\udd51\ud83c\udf3d\ud83c\udf4b\ud83c\udf53\ud83e\udd55\ud83c\udf45)\n2. **SALTOS DE L\u00cdNEA** - Despu\u00e9s de cada oraci\u00f3n\n3. **USA EL NOMBRE** - Menciona el nombre naturalmente\n4. **TONO C\u00c1LIDO** - Como un amigo colombiano, NO como robot\n5. **BREVE** - M\u00e1ximo 4-5 l\u00edneas por mensaje\n6. **S\u00c9 EXPRESIVA** - Usa \u00a1! y expresiones naturales\n## \ud83d\udea8 REGLAS CR\u00cdTICAS\n- **LINK DE TIENDA (Solo una vez)**: Env\u00eda el link de la tienda https://tus-aguacates.vercel.app/ \u00fanicamente en la bienvenida SI el cliente a\u00fan no ha interactuado mucho. No lo repitas en cada turno. Si el cliente pregunta por la tienda, env\u00edalo de nuevo.\n- **SALUDO = SOLO UNA VEZ** - Solo si el cliente es NUEVO. En mensajes siguientes responde directo\n- **NUNCA agregues sin confirmaci\u00f3n** - Solo cuando diga expl\u00edcitamente \"quiero\", \"dame\"\n- **SIEMPRE pregunta cantidad** antes de agregar\n- **NUNCA inventes precios**\n- **NUNCA confirmes pagos** - escala a humano\n- **USA EL NOMBRE del cliente** - si no lo tienes, p\u00eddelo amablemente\n## \ud83d\udce6 ENTREGAS\n- Solo martes y viernes\n- Precio fijo de env\u00edo: $7.400\n- Env\u00edo gratis: >$68.900\n- Hora l\u00edmite: 10:00 AM\n- Nunca digas que entregamos un d\u00eda que no es martes o viernes\n## \ud83d\udcb3 M\u00c9TODOS DE PAGO\n- Nequi/Daviplata: 320 306 2007\n- Efectivo contra entrega\n- Pagos online en tienda\n## \ud83c\udfaf FLUJO DE VENTA\n1. Cliente pregunta \u2192 Muestra opciones con precios\n2. Cliente confirma \u2192 Agrega al carrito\n3. \"Eso es todo\" \u2192 Muestra resumen con total\n4. Confirma direcci\u00f3n\n5. Muestra m\u00e9todos de pago\n6. TOOL_CambiarEstadoCliente(\"PEDIDO_CONFIRMADO\")\n## \ud83d\udea8 ESCALAMIENTO\nSi el cliente:\n- Pide hablar con humano \u2192 TOOL_EscalarServicioCliente inmediatamente\n- Est\u00e1 molesto \u2192 Disculpar, empatizar, escalar\n- Env\u00eda comprobante de pago \u2192 \"\u00a1Gracias! Lo revisaremos\" + escalar\n- Pregunta por recetas \u2192 Dirigir a la tienda online\n## \u26a0\ufe0f PEDIDOS DE PLATAFORMA (TIENDA ONLINE)\nSi el mensaje contiene \"Acabo de hacer un pedido\" + URL de la tienda:\n- NO agregues productos nuevos\n- Confirma recepci\u00f3n: \"\u00a1Gracias por tu pedido! Lo recibimos correctamente.\"\n- Cambia estado: TOOL_CambiarEstadoCliente(\"PEDIDO_ONLINE\")\n- Indica que se contactar\u00e1n para coordinar entrega\n## \u26a0\ufe0f QUEJAS DE PEDIDOS\nSi el cliente dice: \"no ped\u00ed\", \"no es mi pedido\", \"recib\u00ed algo diferente\", \"me enviaron mal\":\n- NO intentes confirmar el pedido\n- USA TOOL_EscalarServicioCliente INMEDIATAMENTE\n- Empatiza: \"\u00a1Qu\u00e9 pena! Vamos a revisar tu caso...\"\n\u00a1Luz, s\u00e9 directa y efectiva! \ud83e\udd51\n\n\n## \ud83d\udc4b SALUDO SIMPLE\n- Si el cliente solo saluda (hola, buenas tardes, etc.), responde con una bienvenida personalizada seg\u00fan si tienes o no el nombre:\n\n- SI NO TENGO EL NOMBRE (El nombre es \"Cliente\"): \n'\u00a1Hola! Soy Luz, tu asistente de ventas de Tus Aguacates. Bienvenido. \ud83e\udd51\n\nSi deseas mirar nuestra tienda en l\u00ednea, aqu\u00ed est\u00e1 el link:\nhttps://tus-aguacates.vercel.app/\n\nO si prefieres, yo te atiendo por ac\u00e1. Por cierto, \u00bfc\u00f3mo te llamas para atenderte mejor? \ud83d\ude0a'\n\n- SI YA TENGO EL NOMBRE (Ej: \"Mauricio\"): \n'\u00a1Hola, {{ $json.clienteNombre }}! Buenas tardes. Bienvenido a Tus Aguacates. \ud83e\udd51\n\nMira nuestro cat\u00e1logo en l\u00ednea aqu\u00ed:\nhttps://tus-aguacates.vercel.app/\n\nO si prefieres, estoy aqu\u00ed para atenderte personalmente. \u00bfEn qu\u00e9 puedo ayudarte hoy? \ud83d\ude0a'\n## \ud83d\udee1\ufe0f FASE DE VALIDACI\u00d3N OBLIGATORIA\n- ANTES de enviar el resumen final del pedido, VERIFICA:\n  1. \u00bfTengo el NOMBRE COMPLETO (nombre y al menos un apellido)?\n  2. \u00bfTengo la DIRECCI\u00d3N DE ENTREGA completa?\n- Si falta alguno de estos datos, NO generes el resumen. Pide amablemente al cliente: '\u00a1Casi listo! Para coordinar el env\u00edo, por favor conf\u00edrmame tu nombre completo y la direcci\u00f3n de entrega.'",
            "maxIterations": 10
          }
        },
        "id": "270c1ff8-e4b0-413b-82f3-1b3cf7083d24",
        "name": "\ud83e\udd16 Agente Luz v",
        "type": "@n8n/n8n-nodes-langchain.agent",
        "position": [
          2160,
          592
        ],
        "retryOnFail": true,
        "typeVersion": 3,
        "onError": "continueErrorOutput"
      },
      {
        "parameters": {
          "sessionIdType": "customKey",
          "sessionKey": "={{ $json.from || $(\"1. Pre-procesamiento YCloud\").first().json.from || \"default_session\" }}",
          "contextWindowLength": 30
        },
        "id": "52d5bb56-fefc-4c9f-8dd3-75b443091259",
        "name": "Postgres Chat Memory",
        "type": "@n8n/n8n-nodes-langchain.memoryPostgresChat",
        "position": [
          1440,
          1456
        ],
        "typeVersion": 1.3,
        "credentials": {
          "postgres": {
            "id": "R6hc0vEZJhKQSi3G",
            "name": "Mi PostgreSQL Docker"
          }
        }
      }
    ],
    "connections": {
      "\ud83d\udce5 Webhook YCloud": {
        "main": [
          [
            {
              "node": "\ud83d\udd00 \u00bfViene del Buffer?",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "1. Pre-procesamiento YCloud": {
        "main": [
          [
            {
              "node": "\u2753 \u00bfCliente Bloqueado?",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "\u2753 \u00bfEs Media?": {
        "main": [
          [
            {
              "node": "\ud83d\udcf1 Responder Media No Soportado",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "2. Obtener Cliente",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "2. Obtener Cliente": {
        "main": [
          [
            {
              "node": "Merge1",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "\u2753 \u00bfBusca Producto?": {
        "main": [
          [
            {
              "node": "3. B\u00fasqueda Autom\u00e1tica Productos",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "4. Merge Datos + Productos",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "3. B\u00fasqueda Autom\u00e1tica Productos": {
        "main": [
          [
            {
              "node": "4. Merge Datos + Productos",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "4. Merge Datos + Productos": {
        "main": [
          [
            {
              "node": "\ud83e\udd16 Agente Luz v",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_AnadirAlCarrito": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udd16 Agente Luz v",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_CalcularTotalPrePedido": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udd16 Agente Luz v",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_GuardarNombreCliente": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udd16 Agente Luz v",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_CambiarEstadoCliente": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udd16 Agente Luz v",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "\ud83d\udce4 Preparar Respuesta": {
        "main": [
          [
            {
              "node": "\ud83d\udcf1 Enviar WhatsApp YCloud",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_BuscarProductos": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udd16 Agente Luz v",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_EscalarServicioCliente": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udd16 Agente Luz v",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ConsultarEstadoPedido": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udd16 Agente Luz v",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_GuardarDireccionCliente": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udd16 Agente Luz v",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "Calculator": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udd16 Agente Luz v",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ObtenerVariantes": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udd16 Agente Luz v",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "Code - Formatear Producto": {
        "main": [
          [
            {
              "node": "Merge",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Buscar Producto del Bot\u00f3n": {
        "main": [
          [
            {
              "node": "Code - Formatear Producto",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Detectar Click de Bot\u00f3n": {
        "main": [
          [
            {
              "node": "Buscar Producto del Bot\u00f3n",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "Merge",
              "type": "main",
              "index": 1
            }
          ]
        ]
      },
      "Merge": {
        "main": [
          [
            {
              "node": "\u2753 \u00bfBusca Producto?",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Merge1": {
        "main": [
          [
            {
              "node": "Detectar Click de Bot\u00f3n",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "\ud83d\udd00 \u00bfEs Copiloto?": {
        "main": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "\u2753 \u00bfEs Media?",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "\ud83e\udde0 Memoria Copiloto": {
        "ai_memory": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "ai_memory",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ListarClientesSinNombre": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ADMIN_ActualizarNombre": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ADMIN_ConsultarCliente": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ADMIN_BuscarPorNombre": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ADMIN_ContarClientes": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ADMIN_VaciarCarrito": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ADMIN_VaciarTodosCarritos": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ADMIN_ListarCarritosActivos": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ADMIN_ResumenCarritos": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ADMIN_ConfirmarPedido": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "\ud83d\udce4 Preparar Respuesta Copiloto": {
        "main": [
          [
            {
              "node": "\ud83d\udcf1 Enviar WhatsApp YCloud",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "\ud83e\udde0 Agente Copiloto1": {
        "main": [
          [
            {
              "node": "\ud83d\udce4 Preparar Respuesta Copiloto",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ADMIN_CambiarEstadoCliente": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ADMIN_BorrarMemoriaCliente": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ADMIN_LimpiarCarritoCliente": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ConfirmarPedidoConEtiqueta": {
        "ai_tool": [
          [
            {
              "node": "\ud83e\udd16 Agente Luz v",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "OpenRouter Chat Model": {
        "ai_languageModel": [
          [
            {
              "node": "Model Selector",
              "type": "ai_languageModel",
              "index": 1
            }
          ]
        ]
      },
      "Model Selector": {
        "ai_languageModel": [
          [
            {
              "node": "\ud83e\udd16 Agente Luz v",
              "type": "ai_languageModel",
              "index": 0
            }
          ]
        ]
      },
      "Mistral Cloud Chat Model": {
        "ai_languageModel": [
          [
            {
              "node": "Model Selector",
              "type": "ai_languageModel",
              "index": 3
            }
          ]
        ]
      },
      "OpenRouter Chat Model2": {
        "ai_languageModel": [
          [
            {
              "node": "Model Selector",
              "type": "ai_languageModel",
              "index": 2
            }
          ]
        ]
      },
      "OpenRouter Chat Model3": {
        "ai_languageModel": [
          [
            {
              "node": "\ud83e\udde0 Agente Copiloto1",
              "type": "ai_languageModel",
              "index": 0
            }
          ]
        ]
      },
      "\u2753 \u00bfCliente Bloqueado?": {
        "main": [
          [
            {
              "node": "\ud83d\uded1 Fin - Cliente Escalado",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "\ud83d\udd00 \u00bfEs Copiloto?",
              "type": "main",
              "index": 0
            },
            {
              "node": "\ud83d\udcca Marcar Respuesta Campa\u00f1a",
              "type": "main",
              "index": 0
            },
            {
              "node": "Merge1",
              "type": "main",
              "index": 1
            }
          ]
        ]
      },
      "\ud83d\udd27 Init Webhook": {
        "main": [
          [
            {
              "node": "\ud83d\udd27 INIT - Crear tabla buffer",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "\ud83d\udd00 \u00bfViene del Buffer?": {
        "main": [
          [
            {
              "node": "1. Pre-procesamiento YCloud",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "\ud83d\udcbe Guardar en Buffer",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "\ud83d\udcbe Guardar en Buffer": {
        "main": [
          [
            {
              "node": "\u23f9\ufe0f Buffer guardado OK",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "2daa57db-e4ab-4696-a96a-4b4cad12b8fc": {
        "main": [
          [
            {
              "node": "4a51a2f9-c6d8-483e-a9dd-4a379d2ec130",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "OpenRouter Chat Model1": {
        "ai_languageModel": [
          [
            {
              "node": "Model Selector",
              "type": "ai_languageModel",
              "index": 0
            }
          ]
        ]
      },
      "37802349-d077-4ffe-a17d-acbf9652535d": {
        "main": [
          [
            {
              "node": "4a51a2f9-c6d8-483e-a9dd-4a379d2ec130",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "fb1e25a8-8f9b-417a-924f-5e713233d2f9": {
        "main": [
          [
            {
              "node": "4a51a2f9-c6d8-483e-a9dd-4a379d2ec130",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "211df281-543d-497a-b643-25dca132695c": {
        "main": [
          [
            {
              "node": "4a51a2f9-c6d8-483e-a9dd-4a379d2ec130",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "21c4122f-796a-4149-9524-9891a5a75928": {
        "main": [
          [
            {
              "node": "4a51a2f9-c6d8-483e-a9dd-4a379d2ec130",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "TOOL_ConsultarPrecio": {
        "ai_tool": [
          []
        ]
      },
      "TOOL_BuscarRecetas": {
        "ai_tool": [
          []
        ]
      },
      "\ud83e\udde0 Buffer Window Memory": {
        "ai_memory": [
          [
            {
              "node": "\ud83e\udd16 Agente Luz v4",
              "type": "ai_memory",
              "index": 0
            }
          ]
        ]
      },
      "\ud83e\udd16 Agente Luz v": {
        "main": [
          [
            {
              "node": "\ud83d\udce4 Preparar Respuesta",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Postgres Chat Memory": {
        "ai_memory": [
          [
            {
              "node": "\ud83e\udd16 Agente Luz v",
              "type": "ai_memory",
              "index": 0
            }
          ]
        ]
      }
    },
    "authors": "mauricio bustamante",
    "name": null,
    "description": null,
    "autosaved": false
  }
}