Feat: descripcion de averia limpia con IA para el Portal del cliente (con cache)
This commit is contained in:
57
server.js
57
server.js
@@ -2280,6 +2280,52 @@ app.post("/public/portal/:token/interest", async (req, res) => {
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// 🔗 PORTAL PÚBLICO DEL CLIENTE
|
// 🔗 PORTAL PÚBLICO DEL CLIENTE
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
// 🧽 LIMPIEZA DE AVERÍA PARA EL CLIENTE (con IA + caché en raw_data)
|
||||||
|
// El campo "Descripción" va acumulando avisos automáticos, cambios de cita, códigos internos, etc.
|
||||||
|
// Aquí lo convertimos en una frase breve y sin ruido para que la vea el cliente en el Portal.
|
||||||
|
async function getClientFriendlyDescription(service) {
|
||||||
|
const rawDesc = (service.raw_data?.["Descripción"] || service.raw_data?.["DESCRIPCION"] || "").toString().trim();
|
||||||
|
if (!rawDesc) return "Aviso de reparación";
|
||||||
|
|
||||||
|
const rawHash = crypto.createHash('md5').update(rawDesc).digest('hex');
|
||||||
|
|
||||||
|
// Si ya la limpiamos antes y el texto original no ha cambiado, reutilizamos la versión guardada
|
||||||
|
if (service.raw_data?.portal_desc_hash === rawHash && service.raw_data?.portal_desc_clean) {
|
||||||
|
return service.raw_data.portal_desc_clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const completion = await openai.chat.completions.create({
|
||||||
|
model: OPENAI_MODEL || "gpt-4o-mini",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: "Redactas, para el CLIENTE de una empresa de reparaciones del hogar, una frase muy breve (máximo 18 palabras) que describa SOLO el problema técnico o avería en su domicilio. No menciones fechas, horas, citas, cambios de cita, nombres de operarios o agentes, teléfonos, códigos de expediente, aseguradoras, ni el estado del trámite administrativo. Si el texto no describe ningún problema técnico claro, responde exactamente: \"Revisión técnica en el domicilio.\". Responde en español, en una sola frase, sin comillas ni markdown."
|
||||||
|
},
|
||||||
|
{ role: "user", content: rawDesc.slice(0, 2000) }
|
||||||
|
],
|
||||||
|
temperature: 0.2,
|
||||||
|
max_tokens: 60
|
||||||
|
});
|
||||||
|
|
||||||
|
const clean = completion.choices?.[0]?.message?.content?.trim();
|
||||||
|
if (!clean) throw new Error("La IA no devolvió texto");
|
||||||
|
|
||||||
|
// Guardamos en caché de forma asíncrona (no bloqueamos la respuesta al cliente ni si falla)
|
||||||
|
pool.query(`
|
||||||
|
UPDATE scraped_services
|
||||||
|
SET raw_data = jsonb_set(jsonb_set(COALESCE(raw_data, '{}'::jsonb), '{portal_desc_clean}', $1::jsonb), '{portal_desc_hash}', $2::jsonb)
|
||||||
|
WHERE id = $3
|
||||||
|
`, [JSON.stringify(clean), JSON.stringify(rawHash), service.id]).catch(e => console.error("⚠️ No se pudo cachear la avería limpia:", e.message));
|
||||||
|
|
||||||
|
return clean;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("❌ Error limpiando avería con IA:", e.message);
|
||||||
|
// Si la IA falla, devolvemos algo razonable en lugar del volcado crudo completo
|
||||||
|
return rawDesc.length > 150 ? rawDesc.slice(0, 150).trim() + "..." : rawDesc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app.get("/public/portal/:token", async (req, res) => {
|
app.get("/public/portal/:token", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { token } = req.params;
|
const { token } = req.params;
|
||||||
@@ -2331,20 +2377,21 @@ app.get("/public/portal/:token", async (req, res) => {
|
|||||||
`, [ownerId, `%${phoneMatch}%`]);
|
`, [ownerId, `%${phoneMatch}%`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedServices = qServices.rows.map(s => {
|
const formattedServices = await Promise.all(qServices.rows.map(async s => {
|
||||||
return {
|
return {
|
||||||
id: s.id,
|
id: s.id,
|
||||||
title: s.is_urgent ? `🚨 URGENTE: #${s.service_ref}` : `Expediente #${s.service_ref}`,
|
title: s.is_urgent ? `🚨 URGENTE: #${s.service_ref}` : `Expediente #${s.service_ref}`,
|
||||||
description: s.raw_data?.["Descripción"] || s.raw_data?.["DESCRIPCION"] || "Aviso de reparación",
|
description: s.raw_data?.["Descripción"] || s.raw_data?.["DESCRIPCION"] || "Aviso de reparación",
|
||||||
status_name: s.real_status_name || "En gestión",
|
description_clean: await getClientFriendlyDescription(s),
|
||||||
is_final: s.is_status_final || false,
|
status_name: s.real_status_name || "En gestión",
|
||||||
|
is_final: s.is_status_final || false,
|
||||||
scheduled_date: s.raw_data?.scheduled_date || "",
|
scheduled_date: s.raw_data?.scheduled_date || "",
|
||||||
scheduled_time: s.raw_data?.scheduled_time || "",
|
scheduled_time: s.raw_data?.scheduled_time || "",
|
||||||
assigned_worker: s.worker_name || null,
|
assigned_worker: s.worker_name || null,
|
||||||
worker_phone: s.worker_phone || null,
|
worker_phone: s.worker_phone || null,
|
||||||
raw_data: s.raw_data
|
raw_data: s.raw_data
|
||||||
};
|
};
|
||||||
});
|
}));
|
||||||
|
|
||||||
// 4. BUSCAR PRESUPUESTOS CON SUS ARTÍCULOS
|
// 4. BUSCAR PRESUPUESTOS CON SUS ARTÍCULOS
|
||||||
let cleanPhoneSearch = String(client.phone || "").replace(/[^0-9]/g, "");
|
let cleanPhoneSearch = String(client.phone || "").replace(/[^0-9]/g, "");
|
||||||
|
|||||||
Reference in New Issue
Block a user