feat: log de auditoría técnica por agente
Nueva tabla umind_eventos_log + tab "Auditoría" en el panel: errores y eventos que hasta ahora solo se podían ver pidiendo los logs del servidor (fallos al llamar al AI, respuestas sin texto usable, tools/webhooks que fallan, tokens de correo que no se pueden refrescar, firmas de WhatsApp inválidas, errores procesando mensajes de Telegram/WhatsApp) quedan registrados por agente, visibles directo en el panel con el JSON crudo expandible para diagnosticar sin acceso al servidor. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
5febbd24d5
commit
997e3cc790
@@ -0,0 +1,51 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UmindEventoLog es el registro de auditoría técnica de un agente — errores
|
||||
// y eventos que hoy solo quedaban en el log del servidor (invisibles desde
|
||||
// el panel): fallos al llamar al AI, tools/webhooks que fallan, canales que
|
||||
// no logran procesar un mensaje, etc. No es el historial de conversación
|
||||
// (eso es UmindMensaje) ni el estado de ingesta (eso ya se ve por
|
||||
// documento en UmindDocumento.Estado/Error) — es específicamente lo que
|
||||
// antes solo se podía ver pidiendo los logs del servidor.
|
||||
type UmindEventoLog struct {
|
||||
gorm.Model
|
||||
AgenteID uint `json:"agente_id" gorm:"column:agente_id;index"`
|
||||
Nivel string `json:"nivel" gorm:"column:nivel;size:10"` // error | warn
|
||||
Origen string `json:"origen" gorm:"column:origen;size:40"` // ai | tool | email | canal_telegram | canal_whatsapp
|
||||
Mensaje string `json:"mensaje" gorm:"column:mensaje;type:text"`
|
||||
Detalle string `json:"detalle" gorm:"column:detalle;type:text"` // JSON crudo opcional, para diagnosticar sin pedir logs
|
||||
}
|
||||
|
||||
func (UmindEventoLog) TableName() string { return "umind_eventos_log" }
|
||||
|
||||
// RegistrarEventoUmind guarda un evento de auditoría. No devuelve error a
|
||||
// propósito — es un side-channel de diagnóstico, nunca debe interrumpir el
|
||||
// flujo principal (responderle al visitante) si la escritura falla.
|
||||
func RegistrarEventoUmind(agenteID uint, nivel, origen, mensaje, detalle string) {
|
||||
e := &UmindEventoLog{AgenteID: agenteID, Nivel: nivel, Origen: origen, Mensaje: mensaje, Detalle: detalle}
|
||||
if err := app.Http.Database.DB.Create(e).Error; err != nil {
|
||||
log.Printf("[UMIND] No se pudo guardar el evento de auditoría (agente %d): %v", agenteID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetUmindEventosByAgente retorna los últimos eventos del agente, más
|
||||
// reciente primero — acotado para no crecer sin límite en la respuesta.
|
||||
func GetUmindEventosByAgente(agenteID uint, limit int) ([]UmindEventoLog, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 100
|
||||
}
|
||||
var items []UmindEventoLog
|
||||
err := app.Http.Database.DB.
|
||||
Where("agente_id = ?", agenteID).
|
||||
Order("id DESC").
|
||||
Limit(limit).
|
||||
Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
@@ -59,12 +59,14 @@ func umindTools(agenteID uint) []agentTool {
|
||||
herramientas, err := models.GetUmindHerramientasActivas(agenteID)
|
||||
if err != nil {
|
||||
log.Printf("[UMIND] Error leyendo tools custom del agente %d: %v", agenteID, err)
|
||||
models.RegistrarEventoUmind(agenteID, "error", "tool", "Error leyendo tools custom", err.Error())
|
||||
return tools
|
||||
}
|
||||
for _, h := range herramientas {
|
||||
params, err := models.ParametrosFromJSON(h.ParametrosJSON)
|
||||
if err != nil {
|
||||
log.Printf("[UMIND] Tool %q del agente %d tiene parametros_json inválido, se omite: %v", h.Nombre, agenteID, err)
|
||||
models.RegistrarEventoUmind(agenteID, "warn", "tool", fmt.Sprintf("Tool %q tiene parametros_json inválido, se omitió", h.Nombre), err.Error())
|
||||
continue
|
||||
}
|
||||
props := map[string]agentToolParam{}
|
||||
@@ -168,12 +170,14 @@ func executeUmindTool(agenteID uint, name string, args map[string]interface{}) s
|
||||
authValor, err = DescifrarSecretoUmind(herramienta.AuthHeaderValorEnc)
|
||||
if err != nil {
|
||||
log.Printf("[UMIND] Error descifrando credencial de tool %q: %v", name, err)
|
||||
models.RegistrarEventoUmind(agenteID, "error", "tool", fmt.Sprintf("Error descifrando credencial de la tool %q", name), err.Error())
|
||||
return `{"error": "la tool no está configurada correctamente"}`
|
||||
}
|
||||
}
|
||||
resultado, err := LlamarHerramientaWebhook(herramienta.URL, herramienta.AuthHeaderNombre, authValor, args)
|
||||
if err != nil {
|
||||
log.Printf("[UMIND] Error llamando tool %q del agente %d: %v", name, agenteID, err)
|
||||
models.RegistrarEventoUmind(agenteID, "error", "tool", fmt.Sprintf("Error llamando la tool %q", name), err.Error())
|
||||
return fmt.Sprintf(`{"error": %q}`, "no se pudo completar la acción, intenta de nuevo")
|
||||
}
|
||||
return resultado
|
||||
@@ -189,6 +193,7 @@ func executeUmindEmailTool(agenteID uint, name string, args map[string]interface
|
||||
}
|
||||
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"}`
|
||||
}
|
||||
|
||||
@@ -208,6 +213,7 @@ func executeUmindEmailTool(agenteID uint, name string, args map[string]interface
|
||||
}
|
||||
if envErr != nil {
|
||||
log.Printf("[UMIND] Error enviando correo (agente %d): %v", agenteID, envErr)
|
||||
models.RegistrarEventoUmind(agenteID, "error", "email", "Error enviando correo", envErr.Error())
|
||||
return `{"error": "no se pudo enviar el correo"}`
|
||||
}
|
||||
return `{"ok": true}`
|
||||
@@ -223,6 +229,7 @@ func executeUmindEmailTool(agenteID uint, name string, args map[string]interface
|
||||
}
|
||||
if lecErr != nil {
|
||||
log.Printf("[UMIND] Error leyendo bandeja (agente %d): %v", agenteID, lecErr)
|
||||
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})
|
||||
@@ -279,6 +286,7 @@ func ProcessWidgetMessage(agente *models.UmindAgente, sessionID, userText string
|
||||
aiMsg, err := callAI(&ai, messages, tools)
|
||||
if err != nil {
|
||||
log.Printf("[UMIND] Error llamando AI (agente %d) round %d: %v", agente.ID, round, err)
|
||||
models.RegistrarEventoUmind(agente.ID, "error", "ai", fmt.Sprintf("Error contactando el AI (ronda %d)", round), err.Error())
|
||||
return "", fmt.Errorf("error al contactar el sistema de IA")
|
||||
}
|
||||
|
||||
@@ -293,6 +301,7 @@ func ProcessWidgetMessage(agente *models.UmindAgente, sessionID, userText string
|
||||
// "dame más detalle" sin ninguna pista de qué pasó. Con .Raw (ver
|
||||
// agentMessage.UnmarshalJSON) queda el JSON crudo para diagnosticar.
|
||||
log.Printf("[UMIND] Respuesta del AI sin texto usable (agente %d) round %d: %s", agente.ID, round, string(aiMsg.Raw))
|
||||
models.RegistrarEventoUmind(agente.ID, "warn", "ai", fmt.Sprintf("El AI respondió sin texto usable (ronda %d)", round), string(aiMsg.Raw))
|
||||
}
|
||||
finalResponse = content
|
||||
if strings.TrimSpace(content) != "" {
|
||||
@@ -317,6 +326,7 @@ func ProcessWidgetMessage(agente *models.UmindAgente, sessionID, userText string
|
||||
|
||||
if finalResponse == "" {
|
||||
log.Printf("[UMIND] Agente %d: se agotaron las rondas de tool-calling sin una respuesta final", agente.ID)
|
||||
models.RegistrarEventoUmind(agente.ID, "warn", "ai", "Se agotaron las rondas de tool-calling sin una respuesta final", "")
|
||||
finalResponse = "Un momento, por favor — dame un poco más de detalle sobre lo que necesitas."
|
||||
}
|
||||
return finalResponse, nil
|
||||
|
||||
Reference in New Issue
Block a user