package models import ( "time" "github.com/sujit-baniya/fiber-boilerplate/app" "gorm.io/gorm" ) // UmindAgente es un agente de IA independiente dentro de un tenant — un // mismo negocio puede tener varios (ej. "Ventas", "Soporte"), cada uno con // su propia base de conocimiento, tools, canales y conexión de correo. Lo // único que sigue siendo del tenant (no del agente) son los dominios // permitidos y el nombre del negocio que se muestra al visitante — eso es // del sitio, no de un agente puntual. type UmindAgente struct { gorm.Model TenantID uint `json:"tenant_id" gorm:"column:tenant_id;index;not null"` Nombre string `json:"nombre" gorm:"column:nombre;size:150;not null"` // etiqueta interna, ej: "Ventas" SiteKey string `json:"site_key" gorm:"column:site_key;uniqueIndex;size:40;not null"` AiConfigID *uint `json:"ai_config_id" gorm:"column:ai_config_id"` Tono string `json:"tono" gorm:"column:tono;type:text"` MensajeBienvenida string `json:"mensaje_bienvenida" gorm:"column:mensaje_bienvenida;type:text"` Color string `json:"color" gorm:"column:color;size:7;default:'#8eb02f'"` Activo bool `json:"activo" gorm:"column:activo;default:true"` } func (UmindAgente) TableName() string { return "umind_agentes" } func CreateUmindAgente(a *UmindAgente) error { if a.SiteKey == "" { key, err := GenerarSiteKey() if err != nil { return err } a.SiteKey = key } if a.Color == "" { a.Color = "#8eb02f" } return app.Http.Database.DB.Create(a).Error } func GetUmindAgentesByTenant(tenantID uint) ([]UmindAgente, error) { var items []UmindAgente err := app.Http.Database.DB.Where("tenant_id = ?", tenantID).Order("id ASC").Find(&items).Error return items, err } func GetUmindAgenteByID(id uint) (*UmindAgente, error) { var a UmindAgente if err := app.Http.Database.DB.First(&a, id).Error; err != nil { return nil, err } return &a, nil } // GetUmindAgenteBySiteKey resuelve el agente a partir de la site_key pública // que manda el widget. Solo hace match si el agente está activo. func GetUmindAgenteBySiteKey(siteKey string) (*UmindAgente, error) { var a UmindAgente if err := app.Http.Database.DB.Where("site_key = ? AND activo = ?", siteKey, true).First(&a).Error; err != nil { return nil, err } return &a, nil } func UpdateUmindAgente(id uint, updates map[string]interface{}) error { return app.Http.Database.DB.Model(&UmindAgente{}).Where("id = ?", id).Updates(updates).Error } func DeleteUmindAgente(id uint) error { return app.Http.Database.DB.Delete(&UmindAgente{}, id).Error } // GetUmindAgentePorSiteKey resuelve el agente por site_key SIN filtrar por // activo, para que el llamador pueda distinguir "no existe" de "está // apagado" y decirlo. GetUmindAgenteBySiteKey (que sí filtra) queda para // quien solo necesita el camino feliz. func GetUmindAgentePorSiteKey(siteKey string) (*UmindAgente, error) { var a UmindAgente if err := app.Http.Database.DB.Where("site_key = ?", siteKey).First(&a).Error; err != nil { return nil, err } return &a, nil } // ResumenAgente son las señales que hacen falta para saber de un vistazo si // un agente está listo o le falta algo, sin entrar a configurarlo. type ResumenAgente struct { AgenteID uint `json:"agente_id"` Documentos int64 `json:"documentos"` Canales int64 `json:"canales"` Conversaciones7d int64 `json:"conversaciones_7d"` } // GetResumenAgentes agrega las tres señales en tres consultas agrupadas, no // en tres por agente: con 5 agentes la diferencia no se nota, pero el patrón // N+1 en una pantalla de listado es el que después no se puede sacar. func GetResumenAgentes(agenteIDs []uint) map[uint]*ResumenAgente { out := make(map[uint]*ResumenAgente, len(agenteIDs)) for _, id := range agenteIDs { out[id] = &ResumenAgente{AgenteID: id} } if len(agenteIDs) == 0 { return out } db := app.Http.Database.DB type fila struct { AgenteID uint Total int64 } var docs []fila db.Model(&UmindDocumento{}).Select("agente_id, COUNT(*) AS total"). Where("agente_id IN ? AND deleted_at IS NULL", agenteIDs).Group("agente_id").Scan(&docs) for _, f := range docs { if r, ok := out[f.AgenteID]; ok { r.Documentos = f.Total } } var canales []fila db.Model(&UmindCanal{}).Select("agente_id, COUNT(*) AS total"). Where("agente_id IN ? AND activo = ? AND deleted_at IS NULL", agenteIDs, true).Group("agente_id").Scan(&canales) for _, f := range canales { if r, ok := out[f.AgenteID]; ok { r.Canales = f.Total } } // Conversaciones, no mensajes: lo que importa es a cuánta gente atendió. var convs []fila db.Model(&UmindMensaje{}).Select("agente_id, COUNT(DISTINCT session_id) AS total"). Where("agente_id IN ? AND created_at >= ? AND deleted_at IS NULL", agenteIDs, time.Now().AddDate(0, 0, -7)). Group("agente_id").Scan(&convs) for _, f := range convs { if r, ok := out[f.AgenteID]; ok { r.Conversaciones7d = f.Total } } return out }