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;