From 011d51d2584fcae84a138e2ba5151e69bd762a41 Mon Sep 17 00:00:00 2001 From: marsalva Date: Sat, 7 Mar 2026 22:34:27 +0000 Subject: [PATCH] Actualizar server.js --- server.js | 166 ++++++++++++++++++++++++++---------------------------- 1 file changed, 80 insertions(+), 86 deletions(-) diff --git a/server.js b/server.js index bfa45ef..ad1f669 100644 --- a/server.js +++ b/server.js @@ -434,85 +434,70 @@ async function registrarMovimiento(serviceId, userId, action, details) { async function procesarConIA(ownerId, mensajeCliente, datosExpediente) { try { - // 1. Extraemos TODO: nombre, settings y horarios reales de la ruta const userQ = await pool.query("SELECT wa_settings, full_name, portal_settings FROM users WHERE id=$1", [ownerId]); const userData = userQ.rows[0]; const settings = userData?.wa_settings || {}; const empresaNombre = userData?.full_name || "nuestra empresa"; - // Si no hay horario configurado, usa un estándar lógico const horarios = userData?.portal_settings || { m_start: "09:00", m_end: "14:00", a_start: "16:00", a_end: "19:00" }; if (!settings.wa_ai_enabled) return null; const ahora = new Date(); - const opciones = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }; - const fechaHoyTexto = ahora.toLocaleDateString('es-ES', opciones); + const fechaHoyTexto = ahora.toLocaleDateString('es-ES', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); + const esViernesOSabado = ahora.getDay() === 5 || ahora.getDay() === 6; - // 🧠 MEMORIA: Evita parecer tonto - const chatCheck = await pool.query(` - SELECT id FROM service_communications + // 🧠 MEMORIA TOTAL: Recuperamos los últimos 10 mensajes del expediente + const historyQ = await pool.query(` + SELECT sender_role, message + FROM service_communications WHERE scraped_id = $1 - AND created_at > NOW() - INTERVAL '60 minutes' LIMIT 1 + ORDER BY created_at ASC LIMIT 10 `, [datosExpediente.dbId]); - const yaSeHaPresentado = chatCheck.rowCount > 0; - // 📍 LOGÍSTICA: Buscar técnicos cerca - const poblacion = datosExpediente.poblacion || ""; - let fechasSugeridas = ""; - - if (poblacion) { - const rutasCercanas = await pool.query(` - SELECT raw_data->>'scheduled_date' as fecha - FROM scraped_services - WHERE owner_id = $1 AND raw_data->>'Población' ILIKE $2 - AND raw_data->>'scheduled_date' > CURRENT_DATE::text AND id != $3 - GROUP BY fecha ORDER BY fecha ASC LIMIT 2 - `, [ownerId, poblacion, datosExpediente.dbId]); + // Formateamos el historial para que ChatGPT lo entienda + const historialChat = historyQ.rows.map(row => ({ + role: (row.sender_role === 'ia' || row.sender_role === 'admin' || row.sender_role === 'operario') ? 'assistant' : 'user', + content: row.message + })); - if (rutasCercanas.rowCount > 0) { - fechasSugeridas = rutasCercanas.rows.map(r => { - const [y, m, d] = r.fecha.split('-'); - return `${d}/${m}`; - }).join(" y el "); - } - } + // Evaluamos cuántos mensajes hay para saber si es el primer saludo + const esPrimerMensaje = historialChat.length === 0; const promptSistema = ` - Eres el Asistente Humano de "${empresaNombre}". Tienes que ser MUY natural, cercano y resolutivo. Nada de sonar robótico. - - CONTEXTO Y HORARIOS: - - Hoy es: ${fechaHoyTexto}. Año 2026. - - HORARIO DE TRABAJO: Lunes a Viernes. Mañanas de ${horarios.m_start} a ${horarios.m_end}. Tardes de ${horarios.a_start} a ${horarios.a_end}. - - ⛔ LOS FINES DE SEMANA NO SE TRABAJA. Si el cliente pide un Sábado o Domingo, dile amablemente que descansamos el fin de semana y ofrécele un Viernes o Lunes dentro del horario. + Eres el Asistente de "${empresaNombre}". Habla de tú, de forma natural, sin parecer un robot. - DATOS EXPEDIENTE #${datosExpediente.ref}: - - Estado: ${datosExpediente.estado} - - Población: ${poblacion} + DATOS DEL EXPEDIENTE #${datosExpediente.ref}: + - Estado actual: ${datosExpediente.estado} + - Operario asignado: ${datosExpediente.operario || 'Pendiente de asignar'} + - Cita registrada: ${datosExpediente.cita || 'Ninguna'} + - Urgencia: ${datosExpediente.is_urgent ? 'SÍ (CRÍTICO)' : 'No'} + - Población: ${datosExpediente.poblacion} - LÓGICA DE SALUDO (MUY IMPORTANTE): - ${!yaSeHaPresentado - ? `- Es el inicio. Preséntate con empatía: "¡Hola! Soy el asistente de ${empresaNombre}. Te escribo sobre tu aviso #${datosExpediente.ref}..."` - : `- ⛔ YA HABÉIS HABLADO RECIENTEMENTE. PROHIBIDO decir "Hola" o volver a presentarte. Ve DIRECTO a la respuesta como harías en un chat real con un amigo.` - } + REGLAS SEGÚN LA SITUACIÓN (CUMPLE A RAJATABLA): + 1. SI ES URGENCIA: NO pidas cita. Diles que al ser un aviso de urgencia, el técnico (${datosExpediente.operario || 'de guardia'}) se pondrá en contacto y acudirá lo antes posible. + 2. SI YA ESTÁ CITADO: No intentes dar cita nueva a menos que pidan cambiarla. Recuérdales que su cita es el ${datosExpediente.cita || 'día acordado'} y que irá ${datosExpediente.operario || 'el técnico'}. + 3. SI YA ESTÁ FINALIZADO/ANULADO: Informa de que el expediente ya está cerrado. - ESTRATEGIA DE RUTA: - ${fechasSugeridas - ? `- Casualmente tenemos técnicos yendo a ${poblacion} el ${fechasSugeridas}. Sugiere esas fechas amigablemente para aprovechar el viaje.` - : `- Pregúntale qué día de Lunes a Viernes le viene bien y si prefiere mañana o tarde.` - } + REGLA PARA AGENDAR (SOLO SI NO ES URGENCIA Y NO ESTÁ CITADO): + - Horario: L-V de ${horarios.m_start} a ${horarios.m_end} y ${horarios.a_start} a ${horarios.a_end}. Fines de semana NO trabajamos. Hoy es ${fechaHoyTexto}. + - Si el cliente confirma día/hora, anótalo y pon al final de tu frase: [PROPUESTA:YYYY-MM-DD HH:mm] - REGLA PARA GUARDAR LA CITA: - - Si el cliente confirma un día y hora válidos (L-V), dile que lo dejas anotado para que el técnico lo confirme y añade AL FINAL tu respuesta este código oculto: [PROPUESTA:YYYY-MM-DD HH:mm] - - Ejemplo: "Perfecto, te dejo anotado para el viernes a las 10:00. El técnico te confirmará en breve. [PROPUESTA:2026-03-13 10:00]" + REGLAS DE CONVERSACIÓN: + ${esPrimerMensaje ? `- Es tu primer mensaje. Preséntate: "¡Hola! Soy el asistente de ${empresaNombre}..."` : `- Ya estás hablando con él. NO te presentes ni digas hola. Continúa la charla.`} + - Responde en máximo 2 frases. Corto y directo. `; + // Construimos el array final de mensajes: Prompt + Historial + Mensaje actual + const mensajesParaIA = [ + { role: "system", content: promptSistema }, + ...historialChat, + { role: "user", content: mensajeCliente } + ]; + const completion = await openai.chat.completions.create({ model: "gpt-4o-mini", - messages: [ - { role: "system", content: promptSistema }, - { role: "user", content: mensajeCliente } - ], - temperature: 0.4, // Controlado para no divagar + messages: mensajesParaIA, + temperature: 0.3, // Muy bajo para que sea estricto con las reglas de urgencia y estados }); return completion.choices[0].message.content; @@ -2969,6 +2954,7 @@ app.post("/services/:id/chat", authMiddleware, async (req, res) => { }); // 🤖 WEBHOOK CON ESCUDO DE INTERVENCIÓN HUMANA +// 🤖 WEBHOOK CON ESCUDO HUMANO, MEMORIA Y GESTIÓN DE URGENCIAS app.post("/webhook/evolution", async (req, res) => { try { const data = req.body; @@ -2981,13 +2967,19 @@ app.post("/webhook/evolution", async (req, res) => { if (!mensajeTexto) return res.sendStatus(200); + // Filtro de seguridad: Evitar procesar instancias del sistema como "ADMIN" + if (!instanceName || !instanceName.startsWith("cliente_")) { + return res.sendStatus(200); + } + const ownerId = instanceName.split("_")[1]; const cleanPhone = telefonoCliente.slice(-9); - // 1. BUSCAMOS EL SINIESTRO + // 1. BUSCAMOS EL SINIESTRO (Extrayendo urgencia y población) const svcQ = await pool.query(` - SELECT s.id, s.service_ref, u.full_name as worker_name, - st.name as status_name, s.raw_data->>'scheduled_date' as cita + SELECT s.id, s.service_ref, u.full_name as worker_name, s.is_urgent, + st.name as status_name, s.raw_data->>'scheduled_date' as cita, + s.raw_data->>'Población' as poblacion FROM scraped_services s LEFT JOIN users u ON s.assigned_to = u.id LEFT JOIN service_statuses st ON (s.raw_data->>'status_operativo')::text = st.id::text @@ -2998,7 +2990,7 @@ app.post("/webhook/evolution", async (req, res) => { if (svcQ.rowCount > 0) { const service = svcQ.rows[0]; - // 🛡️ 2. VERIFICAR INTERVENCIÓN HUMANA (NUEVO) + // 🛡️ 2. VERIFICAR INTERVENCIÓN HUMANA // Miramos si el último mensaje fue de un humano en las últimas 2 horas const checkHumanQ = await pool.query(` SELECT sender_role, created_at @@ -3018,52 +3010,54 @@ app.post("/webhook/evolution", async (req, res) => { } } - // 3. SI NO HAY HUMANO RECIENTE, LLAMAMOS A LA IA + // 3. SI NO HAY HUMANO RECIENTE, LLAMAMOS A LA IA CON TODO EL CONTEXTO const respuestaIA = await procesarConIA(ownerId, mensajeTexto, { + dbId: service.id, // 👈 CRÍTICO para que la IA lea el historial ref: service.service_ref, estado: service.status_name || "En proceso", operario: service.worker_name, - cita: service.cita + cita: service.cita, + poblacion: service.poblacion || "", // 👈 CRÍTICO para buscar rutas en la misma zona + is_urgent: service.is_urgent // 👈 CRÍTICO para que no pida cita en urgencias }); if (respuestaIA) { - // 1. --- MAGIA: DETECTAR SI LA IA RECOGIÓ UNA PROPUESTA DE CITA --- + // --- MAGIA: DETECTAR SI LA IA RECOGIÓ UNA PROPUESTA DE CITA --- const matchPropuesta = respuestaIA.match(/\[PROPUESTA:(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})\]/); if (matchPropuesta) { - const fechaSugerida = matchPropuesta[1]; - const horaSugerida = matchPropuesta[2]; + const fechaSugerida = matchPropuesta[1]; + const horaSugerida = matchPropuesta[2]; - console.log(`📅 PROPUESTA RECIBIDA (IA): ${fechaSugerida} a las ${horaSugerida} para exp #${service.service_ref}`); + console.log(`📅 PROPUESTA RECIBIDA (IA): ${fechaSugerida} a las ${horaSugerida} para exp #${service.service_ref}`); - // 🚀 LA MAGIA: Guardar como "requested_date" y "appointment_status = pending" - // Esto es lo que lee tu panel de control para que aparezca en "Citas Solicitadas" - await pool.query(` - UPDATE scraped_services - SET raw_data = raw_data || jsonb_build_object( - 'requested_date', $1::text, - 'requested_time', $2::text, - 'appointment_status', 'pending' - ) - WHERE id = $3 - `, [fechaSugerida, horaSugerida, service.id]); + // 🚀 Guardar como "requested_date" para que aparezca en el Panel en "Citas Solicitadas" + await pool.query(` + UPDATE scraped_services + SET raw_data = raw_data || jsonb_build_object( + 'requested_date', $1::text, + 'requested_time', $2::text, + 'appointment_status', 'pending' + ) + WHERE id = $3 + `, [fechaSugerida, horaSugerida, service.id]); - // Registramos el movimiento en el historial - await registrarMovimiento( - service.id, - null, - "Cita Solicitada (IA)", - `El cliente solicita cita vía Asistente IA para el ${fechaSugerida} a las ${horaSugerida}.` - ); - } + // Registramos el movimiento en el historial + await registrarMovimiento( + service.id, + null, + "Cita Solicitada (IA)", + `El cliente solicita cita vía Asistente IA para el ${fechaSugerida} a las ${horaSugerida}.` + ); + } - // 2. --- LIMPIEZA Y ENVÍO --- + // --- LIMPIEZA Y ENVÍO --- // Quitamos el código [PROPUESTA:...] del texto para que el cliente no lo vea const textoLimpio = respuestaIA.replace(/\[PROPUESTA:.*?\]/, "").trim(); await sendWhatsAppAuto(telefonoCliente, textoLimpio, instanceName, true); - // 3. --- REGISTRO DEL CHAT --- + // --- REGISTRO DEL CHAT --- await pool.query(`INSERT INTO service_communications (scraped_id, owner_id, sender_name, sender_role, message) VALUES ($1, $2, $3, $4, $5)`, [service.id, ownerId, "Asistente IA", "ia", textoLimpio]); }