package services import ( "encoding/json" "fmt" "log" "strings" ) // ClasificacionCorreo es el veredicto sobre un correo entrante. type ClasificacionCorreo struct { EsSoporte bool `json:"es_soporte"` Categoria string `json:"categoria"` // error | facturacion | acceso | consulta | comercial | newsletter | spam | otro Prioridad string `json:"prioridad"` // baja | media | alta | urgente Motivo string `json:"motivo"` } // ClasificarCorreoSoporte decide si un correo entrante es una solicitud de // soporte o ruido (newsletters, notificaciones automáticas, facturas de // proveedores, spam). // // Falla abierto a propósito: si la IA no está configurada, se cae o devuelve // cualquier cosa, el correo se trata como soporte. Perder el mensaje de un // cliente porque el modelo estaba caído es mucho peor que abrir un ticket de // más. func ClasificarCorreoSoporte(correo CorreoSoporte, contextoNegocio string) ClasificacionCorreo { // Los correos masivos se reconocen por sus propios encabezados; no hace // falta gastar una llamada de IA para saber que un newsletter no es soporte. if correo.Automatico { return ClasificacionCorreo{ EsSoporte: false, Categoria: "newsletter", Prioridad: "baja", Motivo: "el correo viene marcado como masivo o automático en sus encabezados", } } cuerpo := strings.TrimSpace(correo.Texto) if len([]rune(cuerpo)) > 3000 { cuerpo = string([]rune(cuerpo)[:3000]) } sistema := `Clasificás correos que llegan a la casilla de soporte de una empresa. Respondé SOLO con este JSON, sin explicaciones ni bloques de código: {"es_soporte": true|false, "categoria": "...", "prioridad": "...", "motivo": "una frase corta"} es_soporte = true cuando una persona pide ayuda, reporta un problema, hace una consulta sobre un servicio contratado o responde una conversación de soporte. es_soporte = false para newsletters, promociones, notificaciones automáticas de plataformas, facturas de proveedores y spam. Ante la duda, es_soporte = true: es peor ignorar a un cliente que abrir un ticket de más. categoria: error | facturacion | acceso | consulta | comercial | newsletter | spam | otro prioridad: urgente si algo está caído, si hay plata o datos en riesgo, o si el cliente dice que está bloqueado; alta si no puede trabajar pero tiene cómo seguir; media para el resto; baja para consultas sin apuro y para lo que no es soporte. La urgencia la da el problema, no el tono del mensaje.` if c := strings.TrimSpace(contextoNegocio); c != "" { sistema += "\n\nContexto del negocio:\n" + c } usuario := fmt.Sprintf("De: %s\nAsunto: %s\n\n%s", correo.From, correo.Subject, cuerpo) salida, err := CompletarTextoIA("ia", sistema, usuario) if err != nil { log.Printf("[Soporte] No se pudo clasificar el correo, se trata como soporte: %v", err) return ClasificacionCorreo{EsSoporte: true, Categoria: "otro", Prioridad: "media", Motivo: "la clasificación falló"} } var out ClasificacionCorreo if err := json.Unmarshal([]byte(soloJSON(salida)), &out); err != nil { log.Printf("[Soporte] Clasificación ilegible (%q), se trata como soporte", recortar(salida, 120)) return ClasificacionCorreo{EsSoporte: true, Categoria: "otro", Prioridad: "media", Motivo: "respuesta ilegible del modelo"} } if out.Categoria == "" { out.Categoria = "otro" } switch out.Prioridad { case "baja", "media", "alta", "urgente": default: out.Prioridad = "media" } return out } // soloJSON recorta lo que rodea al objeto JSON: los modelos agregan cercas de // código o una frase antes aunque se les pida que no. func soloJSON(s string) string { i := strings.Index(s, "{") j := strings.LastIndex(s, "}") if i < 0 || j <= i { return s } return s[i : j+1] }