feat(umind): el correo entra al agente por dos puertas distintas — y son distintas a propósito
En "Dónde atiende", el canal correo: el agente lee una casilla y responde solo los correos que llegan, con su base de conocimiento, igual que atiende WhatsApp. Como el correo no tiene webhook, se revisa por intervalo — y el intervalo lo elige el cliente por canal (2 a 60 minutos): una inmobiliaria quiere 2, a un estudio contable con 30 le sobra. El cron corre cada minuto pero cada casilla se revisa solo cuando le toca, con un pool de 8 para que 100 casillas no salgan a la red en el mismo instante. Las guardas que separan "asistente" de "incidente", cada una con su test: nunca responde correo automático ni se responde a sí mismo (el bucle con otro autoresponder); la revisión se marca ANTES de conectar, así una contraseña cambiada no martilla el login cada minuto hasta que el host del cliente nos bloquea; y el correo se marca leído recién cuando la respuesta salió — si el envío falla, queda sin leer y se reintenta. En "Lo que sabe", las cuentas de correo: casillas que el agente consulta a pedido — "revisame los correos de hoy y haceme un resumen" — sin nada de fondo. Solo lectura en serio: Peek, INBOX en read-only, y un test que falla si alguien le agrega un marcado. Ahora se pueden conectar varias por agente; con más de una, el modelo pregunta cuál en vez de adivinar — resumirle a alguien la casilla que no pidió no es un error menor. El alta pide correo y contraseña: el host se deduce (mail.<dominio>) y el campo técnico aparece recién si eso falla. Se prueba la conexión antes de guardar, con la persona mirando. Enviar por una cuenta conectada está bloqueado a propósito: para responder correos está el canal, con sus guardas. Una tool de envío sin límites es una máquina de spam con el dominio del cliente. La navegación acompaña: "Lo que sabe" agrupa Información, Cuentas de correo y Herramientas — tres formas de saber, no tres pantallas sueltas — y Avanzado queda solo con Problemas. El SMTP se unificó en una sola implementación que comparten soporte y el canal: el bug de STARTTLS que abría dos conexiones ya se pagó una vez. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ffa24a068f
commit
eddec45857
@@ -119,13 +119,14 @@ func umindEmailTools() []agentTool {
|
||||
Type: "function",
|
||||
Function: agentToolFunc{
|
||||
Name: "leer_bandeja",
|
||||
Description: "Busca correos recibidos en la bandeja conectada del negocio (ej. revisar si llegó un comprobante o la respuesta de un cliente).",
|
||||
Description: "Busca o resume correos recibidos en las casillas conectadas del negocio (ej. revisar si llegó un comprobante, resumir los correos de hoy).",
|
||||
Parameters: agentToolParam{
|
||||
Type: "object",
|
||||
Properties: map[string]agentToolParam{
|
||||
"consulta": {Type: "string", Description: "Qué buscar: remitente, palabras clave del asunto o del cuerpo"},
|
||||
"consulta": {Type: "string", Description: "Qué buscar: remitente o palabras clave. Vacío = los correos más recientes"},
|
||||
"cuenta": {Type: "string", Description: "Dirección de la casilla a revisar, si hay más de una conectada"},
|
||||
},
|
||||
Required: []string{"consulta"},
|
||||
Required: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -188,18 +189,61 @@ func executeUmindTool(agenteID uint, name string, args map[string]interface{}) s
|
||||
// Microsoft Graph según el proveedor de la conexión activa del agente,
|
||||
// refrescando el token primero si hace falta.
|
||||
func executeUmindEmailTool(agenteID uint, name string, args map[string]interface{}) string {
|
||||
conexion, err := models.GetUmindConexionActiva(agenteID)
|
||||
if err != nil {
|
||||
conexiones, err := models.GetUmindConexionesActivas(agenteID)
|
||||
if err != nil || len(conexiones) == 0 {
|
||||
return `{"error": "no hay ninguna cuenta de correo conectada"}`
|
||||
}
|
||||
if err := RefrescarSiVence(conexion); err != nil {
|
||||
log.Printf("[UMIND] Error refrescando token OAuth (conexión %d): %v", conexion.ID, err)
|
||||
models.RegistrarEventoUmind(agenteID, "error", "email", "Error refrescando el token de la cuenta de correo conectada", err.Error())
|
||||
return `{"error": "no se pudo usar la cuenta de correo conectada, intenta más tarde"}`
|
||||
|
||||
// Con varias casillas, el modelo elige por dirección; con una sola, esa.
|
||||
// Si hay varias y no dijo cuál, se le listan para que pregunte o elija —
|
||||
// adivinar la casilla equivocada sería resumirle a alguien el correo que
|
||||
// no pidió.
|
||||
cuenta, _ := args["cuenta"].(string)
|
||||
cuenta = strings.ToLower(strings.TrimSpace(cuenta))
|
||||
var conexion *models.UmindConexion
|
||||
if len(conexiones) == 1 {
|
||||
conexion = &conexiones[0]
|
||||
} else if cuenta != "" {
|
||||
for i := range conexiones {
|
||||
if strings.Contains(strings.ToLower(conexiones[i].Email), cuenta) {
|
||||
conexion = &conexiones[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if conexion == nil {
|
||||
return fmt.Sprintf(`{"error": "no hay ninguna cuenta que coincida con %q"}`, cuenta)
|
||||
}
|
||||
} else {
|
||||
emails := make([]string, len(conexiones))
|
||||
for i, cx := range conexiones {
|
||||
emails[i] = cx.Email
|
||||
}
|
||||
b, _ := json.Marshal(map[string]interface{}{
|
||||
"error": "hay varias cuentas conectadas: indicá cuál en el parámetro cuenta",
|
||||
"cuentas": emails,
|
||||
})
|
||||
return string(b)
|
||||
}
|
||||
|
||||
esIMAP := conexion.Proveedor == "imap"
|
||||
|
||||
// El refresco de token es cosa de OAuth; una cuenta IMAP no vence.
|
||||
if !esIMAP {
|
||||
if err := RefrescarSiVence(conexion); err != nil {
|
||||
log.Printf("[UMIND] Error refrescando token OAuth (conexión %d): %v", conexion.ID, err)
|
||||
models.RegistrarEventoUmind(agenteID, "error", "email", "Error refrescando el token de la cuenta de correo conectada", err.Error())
|
||||
return `{"error": "no se pudo usar la cuenta de correo conectada, intenta más tarde"}`
|
||||
}
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "enviar_correo":
|
||||
// Las cuentas IMAP son de solo lectura a propósito: se conectan en "Lo
|
||||
// que sabe" para consultar. Para que el agente RESPONDA correos está el
|
||||
// canal de correo en "Dónde atiende", con sus guardas propias.
|
||||
if esIMAP {
|
||||
return `{"error": "esta cuenta es de solo lectura; para enviar correos configurá el canal de correo en Dónde atiende"}`
|
||||
}
|
||||
destinatario, _ := args["destinatario"].(string)
|
||||
asunto, _ := args["asunto"].(string)
|
||||
cuerpo, _ := args["cuerpo"].(string)
|
||||
@@ -223,9 +267,12 @@ func executeUmindEmailTool(agenteID uint, name string, args map[string]interface
|
||||
consulta, _ := args["consulta"].(string)
|
||||
var resultados []CorreoResumen
|
||||
var lecErr error
|
||||
if conexion.Proveedor == UmindOAuthGoogle {
|
||||
switch {
|
||||
case esIMAP:
|
||||
resultados, lecErr = LeerBandejaIMAP(conexion, consulta, 8)
|
||||
case conexion.Proveedor == UmindOAuthGoogle:
|
||||
resultados, lecErr = LeerBandejaGoogle(conexion, consulta, 5)
|
||||
} else {
|
||||
default:
|
||||
resultados, lecErr = LeerBandejaMicrosoft(conexion, consulta, 5)
|
||||
}
|
||||
if lecErr != nil {
|
||||
@@ -233,7 +280,7 @@ func executeUmindEmailTool(agenteID uint, name string, args map[string]interface
|
||||
models.RegistrarEventoUmind(agenteID, "error", "email", "Error leyendo la bandeja de correo", lecErr.Error())
|
||||
return `{"error": "no se pudo leer la bandeja"}`
|
||||
}
|
||||
b, _ := json.Marshal(map[string]interface{}{"resultados": resultados})
|
||||
b, _ := json.Marshal(map[string]interface{}{"cuenta": conexion.Email, "resultados": resultados})
|
||||
return string(b)
|
||||
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user