feat(soporte): el ticket de correo ya sabe de quién es, y llega con prioridad

Un ticket que entraba por correo guardaba la dirección como texto suelto: no se
podía cruzar con nada, el cliente no lo veía en su portal y no había forma de
pedir "todos los tickets de Acme". Ahora al crearlo se resuelve el remitente —
primero contra los usuarios del portal, si no contra el correo del cliente— y el
ticket queda atado a quien escribió. El que no matchea queda como contacto
externo, que también es información.

Solo coincidencia exacta de dirección, nunca por dominio: con gmail.com de por
medio, adivinar ata el ticket al cliente equivocado.

Y la llamada de IA que ya clasificaba el correo ahora devuelve también prioridad
y categoría en el mismo JSON: sin costo ni latencia extra. El campo prioridad
existía desde siempre y nadie lo llenaba. La urgencia la define el problema
descrito, no el tono del mensaje — está dicho en el prompt.

En la lista de tickets se ven los tres datos nuevos: cliente (o "externo"),
categoría y la prioridad que ya se mostraba.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-17 19:43:28 -05:00
co-authored by Claude Opus 5
parent 6afad025c2
commit bfcc320b7c
5 changed files with 112 additions and 29 deletions
+18 -5
View File
@@ -10,7 +10,8 @@ import (
// ClasificacionCorreo es el veredicto sobre un correo entrante.
type ClasificacionCorreo struct {
EsSoporte bool `json:"es_soporte"`
Categoria string `json:"categoria"` // soporte | comercial | factura | newsletter | notificacion | spam | otro
Categoria string `json:"categoria"` // error | facturacion | acceso | consulta | comercial | newsletter | spam | otro
Prioridad string `json:"prioridad"` // baja | media | alta | urgente
Motivo string `json:"motivo"`
}
@@ -29,6 +30,7 @@ func ClasificarCorreoSoporte(correo CorreoSoporte, contextoNegocio string) Clasi
return ClasificacionCorreo{
EsSoporte: false,
Categoria: "newsletter",
Prioridad: "baja",
Motivo: "el correo viene marcado como masivo o automático en sus encabezados",
}
}
@@ -40,13 +42,19 @@ func ClasificarCorreoSoporte(correo CorreoSoporte, contextoNegocio string) Clasi
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": "soporte|comercial|factura|newsletter|notificacion|spam|otro", "motivo": "una frase corta"}
{"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.`
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
@@ -57,17 +65,22 @@ Ante la duda, es_soporte = true: es peor ignorar a un cliente que abrir un ticke
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: "soporte", Motivo: "la clasificación falló"}
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: "soporte", Motivo: "respuesta ilegible del modelo"}
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
}