diff --git a/server.js b/server.js index 12f55bb..8e6ef1a 100644 --- a/server.js +++ b/server.js @@ -904,23 +904,38 @@ async function registrarMovimiento(serviceId, userId, action, details) { } // ========================================== -// 🧠 CEREBRO IA (WHATSAPP) +// 🧠 CEREBRO IA (WHATSAPP) CON MODO DEBUG // ========================================== 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) return null; + if (!settings.wa_ai_enabled) { + if (DEBUG_IA) console.log("❌ [DEBUG IA] La IA está apagada en los ajustes del usuario. Abortando."); + 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", @@ -936,9 +951,7 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { day: "numeric" }).format(new Date()); - // 👇 IMPORTANTE: - // Aquí leemos el historial REAL, ya incluyendo el mensaje actual del cliente - // que el webhook guardará antes de llamar a OpenAI. + // 👇 Historial del chat const historyQ = await pool.query(` SELECT sender_role, message FROM service_communications @@ -955,10 +968,9 @@ 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 = "✅ El técnico tiene la agenda libre en horario laboral."; + let agendaOcupadaTexto = "AGENDA LIBRE. Puedes proponer cualquier hora dentro del horario laboral."; if (datosExpediente.worker_id) { const agendaQ = await pool.query(` @@ -966,7 +978,6 @@ 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 @@ -990,33 +1001,32 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { let [h, m] = r.time.split(':').map(Number); let dur = parseInt(r.duration || 60, 10); - 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'); + + // 🚀 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; - const tipo = r.provider === 'SYSTEM_BLOCK' ? 'BLOQUEO/AUSENCIA' : 'CITA'; - const lugar = r.pob || 'Otra zona'; + 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`); + } - ocupaciones[r.date].push(`❌ OCUPADO de ${r.time} a ${endH}:${endM} (${tipo} en ${lugar})`); + ocupaciones[r.date].push(`De ${r.time} a ${String(endH).padStart(2,'0')}:${String(endM).padStart(2,'0')} (Afecta a: ${horasAfectadas.join(", ")})`); } }); 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 `- Día ${fechaHumana} (${d}):\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 `FECHA: ${d} (${fechaHumana})\nBLOQUEOS:\n - ${ocupaciones[d].join("\n - ")}`; }); if (lineas.length > 0) { - 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."; + agendaOcupadaTexto = lineas.join("\n\n"); } } } @@ -1060,53 +1070,53 @@ async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { let directivaEstricta = ""; if (esEstadoFinal) { - directivaEstricta = `🛑 ESTADO ACTUAL: SERVICIO CERRADO. Informa al cliente que el servicio por su avería (${datosExpediente.averia}) está finalizado. NO AGENDES NADA.`; + directivaEstricta = `ESTADO: CERRADO. Informa al cliente que el servicio está finalizado. NO AGENDAR.`; } else if (noTieneTecnico) { - 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.`; + directivaEstricta = `ESTADO: SIN TÉCNICO. Solo informa que hemos recibido el aviso y estamos buscando técnico. PROHIBIDO AGENDAR.`; } else if (citaYaPaso) { - directivaEstricta = `🛑 ESTADO ACTUAL: LA CITA YA PASÓ (${datosExpediente.cita}). Informa que estamos tramitando su avería (${datosExpediente.averia}). NO AGENDES NADA.`; + directivaEstricta = `ESTADO: CITA PASADA. Informa que estamos tramitando su avería. NO AGENDAR.`; } else if (esUrgencia) { - directivaEstricta = `🛑 ESTADO ACTUAL: URGENCIA. Tranquiliza al cliente sobre su avería (${datosExpediente.averia}) y dile que el técnico está avisado. NO PROPONGAS HORAS.`; + directivaEstricta = `ESTADO: URGENCIA. Tranquiliza al cliente, el técnico está avisado. NO PROPONGAS HORAS.`; } else if (hayCitaPendiente) { - 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.`; + 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.`; } else if (tieneCitaConfirmada) { - 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.`; + directivaEstricta = `ESTADO: CITA CONFIRMADA el ${datosExpediente.cita} ${tramoConfirmado}. Recuerda la cita. Si el cliente quiere cambiar, ofrece otra hora.`; } else { - 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.`; + directivaEstricta = `ESTADO: PENDIENTE DE AGENDAR. Acuerda fecha y hora para reparar su avería (${datosExpediente.averia}). Nunca ofrezcas horas ocupadas.`; } - const promptSistema = ` -Eres el coordinador humano de "${empresaNombre}". Hablas de tú, de forma muy natural, empática y con buen tono por WhatsApp. + const promptSistema = \` +Eres el coordinador de la empresa "\${empresaNombre}". Hablas de tú, de forma amable y directa por WhatsApp (máximo 2 frases). ---- 📋 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'}. + +- 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. + ---- 📅 REVISIÓN DE AGENDA (EVITAR SOLAPES) --- -${agendaOcupadaTexto} + +\${agendaOcupadaTexto} + ---- 🎯 DIRECTIVA ESTRICTA PARA ESTE MENSAJE --- -${directivaEstricta} + +\${directivaEstricta} + ---- ⚡ 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] + +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 : ''} + +\`; -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}` : ''} - `; + 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"); + } const completion = await openai.chat.completions.create({ model: OPENAI_MODEL || "gpt-4o-mini", @@ -1114,10 +1124,18 @@ ${instruccionesExtra ? `6. Instrucción extra de la empresa: ${instruccionesExtr { role: "system", content: promptSistema }, ...historialChat ], - temperature: 0.1 + temperature: 0.0 // 🚀 Máxima obediencia }); - return completion.choices?.[0]?.message?.content || null; + 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; } catch (e) { console.error("❌ Error OpenAI:", e.message); return null;