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
|
||||
// ==========================================
|
||||
// 🧽 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) => {
|
||||
try {
|
||||
const { token } = req.params;
|
||||
@@ -2331,20 +2377,21 @@ app.get("/public/portal/:token", async (req, res) => {
|
||||
`, [ownerId, `%${phoneMatch}%`]);
|
||||
}
|
||||
|
||||
const formattedServices = qServices.rows.map(s => {
|
||||
const formattedServices = await Promise.all(qServices.rows.map(async s => {
|
||||
return {
|
||||
id: s.id,
|
||||
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",
|
||||
status_name: s.real_status_name || "En gestión",
|
||||
is_final: s.is_status_final || false,
|
||||
description_clean: await getClientFriendlyDescription(s),
|
||||
status_name: s.real_status_name || "En gestión",
|
||||
is_final: s.is_status_final || false,
|
||||
scheduled_date: s.raw_data?.scheduled_date || "",
|
||||
scheduled_time: s.raw_data?.scheduled_time || "",
|
||||
assigned_worker: s.worker_name || null,
|
||||
worker_phone: s.worker_phone || null,
|
||||
worker_phone: s.worker_phone || null,
|
||||
raw_data: s.raw_data
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
// 4. BUSCAR PRESUPUESTOS CON SUS ARTÍCULOS
|
||||
let cleanPhoneSearch = String(client.phone || "").replace(/[^0-9]/g, "");
|
||||
|
||||
Reference in New Issue
Block a user