From 6b212d2e65e8669fcb24a2d0e8e24c5b9fd32ba2 Mon Sep 17 00:00:00 2001 From: marsalva Date: Fri, 3 Apr 2026 10:59:22 +0000 Subject: [PATCH] Actualizar server.js --- server.js | 146 ++++++++++++++++++++++++------------------------------ 1 file changed, 64 insertions(+), 82 deletions(-) diff --git a/server.js b/server.js index 8e6ef1a..12f55bb 100644 --- a/server.js +++ b/server.js @@ -904,38 +904,23 @@ async function registrarMovimiento(serviceId, userId, action, details) { } // ========================================== -// 🧠 CEREBRO IA (WHATSAPP) CON MODO DEBUG +// 🧠 CEREBRO IA (WHATSAPP) // ========================================== async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { - // 🎚️ INTERRUPTOR DE LOGS (Cambia a false para apagar los mensajes en la consola) - const DEBUG_IA = true; - - if (DEBUG_IA) { - console.log("\n======================================================="); - console.log(`🤖 [DEBUG IA] INICIANDO CEREBRO PARA EXP: #${datosExpediente.ref}`); - console.log(`💬 Mensaje del cliente: "${mensajeCliente}"`); - console.log("=======================================================\n"); - } - try { const userQ = await pool.query( "SELECT wa_settings, full_name, portal_settings FROM users WHERE id=$1", [ownerId] ); - if (userQ.rowCount === 0) return null; // 🛡️ Protección de seguridad - const userData = userQ.rows[0]; - const settings = userData.wa_settings || {}; + const settings = userData?.wa_settings || {}; const instruccionesExtra = settings.ai_custom_prompt || ""; - const empresaNombre = userData.full_name || "nuestra empresa"; + const empresaNombre = userData?.full_name || "nuestra empresa"; - if (!settings.wa_ai_enabled) { - if (DEBUG_IA) console.log("❌ [DEBUG IA] La IA está apagada en los ajustes del usuario. Abortando."); - return null; - } + if (!settings.wa_ai_enabled) return null; - const pSettings = userData.portal_settings || {}; + const pSettings = userData?.portal_settings || {}; const horarios = { m_start: pSettings.m_start || "09:00", m_end: pSettings.m_end || "14:00", @@ -951,7 +936,9 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { day: "numeric" }).format(new Date()); - // 👇 Historial del chat + // 👇 IMPORTANTE: + // Aquí leemos el historial REAL, ya incluyendo el mensaje actual del cliente + // que el webhook guardará antes de llamar a OpenAI. const historyQ = await pool.query(` SELECT sender_role, message FROM service_communications @@ -968,9 +955,10 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { content: row.message })); + // Si solo existe el mensaje actual del cliente, es primer mensaje const esPrimerMensaje = historialRows.length <= 1; - let agendaOcupadaTexto = "AGENDA LIBRE. Puedes proponer cualquier hora dentro del horario laboral."; + let agendaOcupadaTexto = "✅ El técnico tiene la agenda libre en horario laboral."; if (datosExpediente.worker_id) { const agendaQ = await pool.query(` @@ -978,6 +966,7 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { COALESCE(NULLIF(raw_data->>'scheduled_date', ''), raw_data->>'requested_date') as date, COALESCE(NULLIF(raw_data->>'scheduled_time', ''), raw_data->>'requested_time') as time, raw_data->>'duration_minutes' as duration, + COALESCE(raw_data->>'Población', raw_data->>'POBLACION-PROVINCIA') as pob, provider FROM scraped_services WHERE owner_id = $1 @@ -1001,32 +990,33 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { let [h, m] = r.time.split(':').map(Number); let dur = parseInt(r.duration || 60, 10); - - // 🚀 TRUCO: 45 min extra de desplazamiento - let startMin = h * 60 + m; - let endMin = startMin + dur + 45; - - let endH = Math.floor(endMin / 60) % 24; - let endM = endMin % 60; + let endMin = (h * 60 + m) + dur; + let endH = String(Math.floor(endMin / 60) % 24).padStart(2, '0'); + let endM = String(endMin % 60).padStart(2, '0'); - let horasAfectadas = []; - for(let i = h; i <= endH; i++) { - horasAfectadas.push(`${String(i).padStart(2,'0')}:00`); - horasAfectadas.push(`${String(i).padStart(2,'0')}:30`); - } + const tipo = r.provider === 'SYSTEM_BLOCK' ? 'BLOQUEO/AUSENCIA' : 'CITA'; + const lugar = r.pob || 'Otra zona'; - ocupaciones[r.date].push(`De ${r.time} a ${String(endH).padStart(2,'0')}:${String(endM).padStart(2,'0')} (Afecta a: ${horasAfectadas.join(", ")})`); + ocupaciones[r.date].push(`❌ OCUPADO de ${r.time} a ${endH}:${endM} (${tipo} en ${lugar})`); } }); const lineas = Object.keys(ocupaciones).sort().map(d => { const [y, m, day] = d.split('-').map(Number); - const fechaHumana = new Date(y, m - 1, day, 12, 0, 0).toLocaleDateString('es-ES', { weekday: 'long', day: 'numeric', month: 'long' }); - return `FECHA: ${d} (${fechaHumana})\nBLOQUEOS:\n - ${ocupaciones[d].join("\n - ")}`; + const fechaHumana = new Date(y, m - 1, day, 12, 0, 0).toLocaleDateString('es-ES', { + weekday: 'long', + day: 'numeric', + month: 'long' + }); + return `- Día ${fechaHumana} (${d}):\n * ${ocupaciones[d].join("\n * ")}`; }); if (lineas.length > 0) { - agendaOcupadaTexto = lineas.join("\n\n"); + agendaOcupadaTexto = + "⚠️ ¡ATENCIÓN! LA SIGUIENTE LISTA SON LOS HORARIOS QUE YA ESTÁN OCUPADOS Y NO PUEDES OFRECER:\n" + + lineas.join("\n") + + "\n\n👉 INSTRUCCIÓN VITAL: Revisa bien la lista de arriba. Tienes que proponerle al cliente CUALQUIER OTRA HORA que esté totalmente libre y no pise esos tramos." + + "\n🚨 REGLA LOGÍSTICA: Si la zona de la avería actual no es la misma que la de la cita anterior o posterior, DEBES dejar al menos 45-60 minutos libres para el viaje."; } } } @@ -1070,53 +1060,53 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { let directivaEstricta = ""; if (esEstadoFinal) { - directivaEstricta = `ESTADO: CERRADO. Informa al cliente que el servicio está finalizado. NO AGENDAR.`; + directivaEstricta = `🛑 ESTADO ACTUAL: SERVICIO CERRADO. Informa al cliente que el servicio por su avería (${datosExpediente.averia}) está finalizado. NO AGENDES NADA.`; } else if (noTieneTecnico) { - directivaEstricta = `ESTADO: SIN TÉCNICO. Solo informa que hemos recibido el aviso y estamos buscando técnico. PROHIBIDO AGENDAR.`; + directivaEstricta = `🛑 ESTADO ACTUAL: SIN TÉCNICO ASIGNADO.\nTU ÚNICO OBJETIVO: Informar al cliente que hemos recibido el aviso de su avería (${datosExpediente.averia}) y que estamos coordinando para asignarle un técnico en su zona.\n⛔ PROHIBICIÓN ABSOLUTA: NO ofrezcas citas, NO des horas, NO agendes nada hasta que se le asigne un técnico.`; } else if (citaYaPaso) { - directivaEstricta = `ESTADO: CITA PASADA. Informa que estamos tramitando su avería. NO AGENDAR.`; + directivaEstricta = `🛑 ESTADO ACTUAL: LA CITA YA PASÓ (${datosExpediente.cita}). Informa que estamos tramitando su avería (${datosExpediente.averia}). NO AGENDES NADA.`; } else if (esUrgencia) { - directivaEstricta = `ESTADO: URGENCIA. Tranquiliza al cliente, el técnico está avisado. NO PROPONGAS HORAS.`; + directivaEstricta = `🛑 ESTADO ACTUAL: URGENCIA. Tranquiliza al cliente sobre su avería (${datosExpediente.averia}) y dile que el técnico está avisado. NO PROPONGAS HORAS.`; } else if (hayCitaPendiente) { - directivaEstricta = `ESTADO: CITA PENDIENTE DE APROBACIÓN el día ${datosExpediente.cita_pendiente_fecha} ${tramoPendiente}. Informa que esperamos confirmación del técnico. Si el cliente quiere cambiar, ofrece otra hora.`; + directivaEstricta = `🛑 ESTADO ACTUAL: CITA PENDIENTE DE APROBACIÓN.\n📅 Propuesta actual: El día ${datosExpediente.cita_pendiente_fecha} ${tramoPendiente}.\nTU OBJETIVO: Informar que esperamos confirmación del técnico para reparar su avería (${datosExpediente.averia}).\n⚠️ EXCEPCIÓN: Si el cliente pide CAMBIAR o CANCELAR, ofrécele un hueco nuevo.`; } else if (tieneCitaConfirmada) { - directivaEstricta = `ESTADO: CITA CONFIRMADA el ${datosExpediente.cita} ${tramoConfirmado}. Recuerda la cita. Si el cliente quiere cambiar, ofrece otra hora.`; + directivaEstricta = `🛑 ESTADO ACTUAL: CITA CONFIRMADA para el ${datosExpediente.cita} ${tramoConfirmado}. Recuerda la cita para su avería (${datosExpediente.averia}).\n⚠️ EXCEPCIÓN: Si el cliente pide CAMBIARLA o CANCELARLA, ofrécele un hueco nuevo.`; } else { - directivaEstricta = `ESTADO: PENDIENTE DE AGENDAR. Acuerda fecha y hora para reparar su avería (${datosExpediente.averia}). Nunca ofrezcas horas ocupadas.`; + directivaEstricta = `🟢 ESTADO ACTUAL: PENDIENTE DE AGENDAR CITA.\nTU OBJETIVO: Acordar fecha y hora para reparar su avería (${datosExpediente.averia}). NUNCA ofrezcas horas ocupadas. Fines de semana solo URGENCIAS.\n⚠️ MUY IMPORTANTE: Cuando el cliente elija un hueco, NO le digas que la cita está confirmada. Dile que le pasas la nota al técnico para que él lo valide.`; } - const promptSistema = \` -Eres el coordinador de la empresa "\${empresaNombre}". Hablas de tú, de forma amable y directa por WhatsApp (máximo 2 frases). + const promptSistema = ` +Eres el coordinador humano de "${empresaNombre}". Hablas de tú, de forma muy natural, empática y con buen tono por WhatsApp. - -- Fecha de Hoy: \${fechaHoyTexto} -- Horario de Trabajo: L-V de \${horarios.m_start} a \${horarios.m_end} y de \${horarios.a_start} a \${horarios.a_end}. NUNCA propongas horas fuera de aquí ni en fin de semana. - +--- 📋 CONTEXTO BÁSICO --- +- Hoy es: ${fechaHoyTexto}. +- Horario de la empresa: L-V de ${horarios.m_start} a ${horarios.m_end} y de ${horarios.a_start} a ${horarios.a_end}. +- ⛔ REGLA DE ORO DEL HORARIO: NUNCA propongas horas fuera de ese horario ni pises el tramo de comer. Fines de semana prohibidos salvo urgencias. +- Localidad del cliente actual: ${datosExpediente.poblacion || 'Localidad no especificada'}. - -\${agendaOcupadaTexto} - +--- 📅 REVISIÓN DE AGENDA (EVITAR SOLAPES) --- +${agendaOcupadaTexto} - -\${directivaEstricta} - +--- 🎯 DIRECTIVA ESTRICTA PARA ESTE MENSAJE --- +${directivaEstricta} - -1. LEE LA AGENDA PROHIBIDA: Si el cliente propone una hora que coincide o choca con las horas prohibidas, DÍSELO ("A esa hora el técnico está ocupado") y propón una alternativa libre. -2. NUNCA propongas una fecha y hora que esté dentro de la AGENDA PROHIBIDA. -3. Si llegáis a un acuerdo y tú o el cliente proponéis una hora LIBRE, añade al FINAL de tu respuesta exactamente: [PROPUESTA:YYYY-MM-DD HH:mm] -4. JAMÁS uses la etiqueta [PROPUESTA] si la hora solicitada choca con la AGENDA PROHIBIDA. -5. Nunca digas "¿En qué te puedo ayudar?". Ve al grano. -\${esPrimerMensaje ? '6. Es tu primer mensaje: saluda, di quién eres y menciona el aviso (#' + datosExpediente.ref + ').' : ''} -\${instruccionesExtra ? '7. REGLA EXTRA: ' + instruccionesExtra : ''} - -\`; +--- ⚡ REGLA CRÍTICA DE AGENDA (COMANDO SECRETO) --- +Si (y solo si) has propuesto un hueco y el cliente ACEPTA FIRMEMENTE, DEBES añadir AL FINAL ABSOLUTO de tu respuesta este texto literal: +[PROPUESTA:YYYY-MM-DD HH:mm] - if (DEBUG_IA) { - console.log("📝 [DEBUG IA] PROMPT GENERADO Y ENVIADO A OPENAI:"); - console.log("\x1b[36m%s\x1b[0m", promptSistema); // Se imprime en color cian para verlo mejor - console.log("-------------------------------------------------------\n"); - } +Ejemplo: +"Perfecto, le paso la nota al técnico para que te confirme el miércoles entre las 10:00 y las 11:00 aprox. ¡Te decimos algo pronto! [PROPUESTA:2026-03-25 10:00]" + +⛔ PROHIBICIÓN: NUNCA le digas "te agendo" ni "cita confirmada". El cliente debe saber que dependemos del técnico. NUNCA menciones las palabras "código" o "etiqueta". + +--- ⚙️ REGLAS DE COMUNICACIÓN --- +1. MÁXIMO 2 FRASES. Mensajes cortos y directos. +2. NUNCA uses fechas frías si puedes decir "el martes". NUNCA des una hora exacta si puedes decir "entre las 10:00 y las 11:00 aprox". +3. NO TE PRESENTES si ya habéis intercambiado mensajes. +4. ⛔ MULETILLAS PROHIBIDAS: NUNCA digas "¿En qué más te puedo ayudar?". +${esPrimerMensaje ? `5. Primer mensaje: preséntate y menciona el aviso (#${datosExpediente.ref}).` : ''} +${instruccionesExtra ? `6. Instrucción extra de la empresa: ${instruccionesExtra}` : ''} + `; const completion = await openai.chat.completions.create({ model: OPENAI_MODEL || "gpt-4o-mini", @@ -1124,18 +1114,10 @@ Eres el coordinador de la empresa "\${empresaNombre}". Hablas de tú, de forma a { role: "system", content: promptSistema }, ...historialChat ], - temperature: 0.0 // 🚀 Máxima obediencia + temperature: 0.1 }); - const respuestaFinal = completion.choices?.[0]?.message?.content || null; - - if (DEBUG_IA) { - console.log("💡 [DEBUG IA] RESPUESTA RECIBIDA DE OPENAI:"); - console.log("\x1b[32m%s\x1b[0m", respuestaFinal); // Se imprime en color verde - console.log("=======================================================\n"); - } - - return respuestaFinal; + return completion.choices?.[0]?.message?.content || null; } catch (e) { console.error("❌ Error OpenAI:", e.message); return null;