feat: uMind pasa a multi-agente por tenant
Un tenant (negocio/sitio, dueño de los dominios permitidos) puede tener varios UmindAgente independientes (ej. "Ventas", "Soporte"), cada uno con su propia config de IA, tono, base de conocimiento, tools, canales y conexión de correo. El site_key también pasa a ser por agente, así cada uno tiene su propio <script> de widget embebible y su propio color. Backend: - Nuevo modelo UmindAgente (pkg/models/umind_agente.go), con SiteKey, AiConfigID, Tono, MensajeBienvenida y Color — campos que antes vivían en UmindTenant y se sacan de ahí (las columnas viejas quedan huérfanas sin usar, no se hace DROP COLUMN). - UmindDocumento, UmindChunk, UmindHerramienta, UmindCanal, UmindConexion y UmindMensaje pasan de TenantID a AgenteID. El campo se agrega sin "not null" para no romper el ALTER TABLE en Postgres sobre tablas que ya tienen filas (ej. emetropolitana). - migrations.MigrarUmindAgentes(): idempotente, crea un agente "Principal" por cada tenant existente heredando lo que ya tenía configurado, y mueve sus datos de tenant_id a agente_id. Corre en cada arranque normal, mismo criterio que los Seed* — nada se rompe para los tenants ya en producción. - Motor del agente, widget, canales (Telegram/WhatsApp) y OAuth de correo ahora operan sobre UmindAgente; el tenant solo se consulta para el chequeo de dominio permitido y el nombre del negocio que ve el visitante. Frontend: nueva jerarquía de navegación tenant → lista de agentes (TenantAgentes.vue) → detalle de un agente (AgenteDetail.vue, antes TenantDetail.vue) con las mismas 6 tabs de siempre, ahora por agente. El modal de tenant en el sidebar se achica a nombre/dominios/activo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8eba3ab97f
commit
f3f2f421d6
@@ -119,11 +119,13 @@ func Migrate() {
|
||||
&models.TelegramStaffToken{},
|
||||
// uMind: chat con IA embebible por tenant (widget web + RAG)
|
||||
&models.UmindTenant{},
|
||||
&models.UmindAgente{},
|
||||
&models.UmindDocumento{},
|
||||
&models.UmindChunk{},
|
||||
&models.UmindMensaje{},
|
||||
&models.UmindHerramienta{},
|
||||
&models.UmindCanal{},
|
||||
&models.UmindConexion{},
|
||||
// API Keys de /api/v2 (token + IP obligatoria + scopes)
|
||||
&models.ApiKey{},
|
||||
}
|
||||
@@ -1364,3 +1366,87 @@ func SeedApiKeys() {
|
||||
}
|
||||
log.Println("[SEED] Seed de API Keys completado.")
|
||||
}
|
||||
|
||||
// MigrarUmindAgentes crea un agente "Principal" por cada UmindTenant que
|
||||
// todavía no tenga ninguno, heredando lo que antes vivía en el tenant
|
||||
// (site_key, config de IA, tono, mensaje de bienvenida, color) y mueve los
|
||||
// datos que ya tenía (documentos, chunks, tools, canales, conexiones,
|
||||
// mensajes) de tenant_id a agente_id. Idempotente: un tenant que ya tiene
|
||||
// al menos un agente se salta — así corre sola en cada arranque sin
|
||||
// duplicar nada, mismo criterio que los Seed* de este archivo.
|
||||
func MigrarUmindAgentes() {
|
||||
db := app.Http.Database.DB
|
||||
|
||||
var tenants []models.UmindTenant
|
||||
if err := db.Find(&tenants).Error; err != nil {
|
||||
log.Printf("[MIGRACION] Error leyendo umind_tenants: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tablasConAgenteID := []string{
|
||||
"umind_documentos", "umind_chunks", "umind_herramientas",
|
||||
"umind_canales", "umind_conexiones", "umind_mensajes",
|
||||
}
|
||||
|
||||
for _, t := range tenants {
|
||||
var yaTieneAgente int64
|
||||
if err := db.Model(&models.UmindAgente{}).Where("tenant_id = ?", t.ID).Count(&yaTieneAgente).Error; err != nil {
|
||||
log.Printf("[MIGRACION] Error contando agentes del tenant %d: %v", t.ID, err)
|
||||
continue
|
||||
}
|
||||
if yaTieneAgente > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Las columnas viejas (site_key, ai_config_id, tono,
|
||||
// mensaje_bienvenida, color) ya no están en el struct Go UmindTenant,
|
||||
// pero AutoMigrate nunca las borró de la tabla — se leen directo.
|
||||
var viejo struct {
|
||||
SiteKey string
|
||||
AiConfigID *uint
|
||||
Tono string
|
||||
MensajeBienvenida string
|
||||
Color string
|
||||
}
|
||||
if err := db.Table("umind_tenants").
|
||||
Select("site_key, ai_config_id, tono, mensaje_bienvenida, color").
|
||||
Where("id = ?", t.ID).Scan(&viejo).Error; err != nil {
|
||||
log.Printf("[MIGRACION] Error leyendo datos viejos del tenant %d: %v", t.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
agente := models.UmindAgente{
|
||||
TenantID: t.ID,
|
||||
Nombre: "Principal",
|
||||
SiteKey: viejo.SiteKey,
|
||||
AiConfigID: viejo.AiConfigID,
|
||||
Tono: viejo.Tono,
|
||||
MensajeBienvenida: viejo.MensajeBienvenida,
|
||||
Color: viejo.Color,
|
||||
Activo: true,
|
||||
}
|
||||
if agente.SiteKey == "" {
|
||||
key, err := models.GenerarSiteKey()
|
||||
if err != nil {
|
||||
log.Printf("[MIGRACION] Error generando site_key para el agente del tenant %d: %v", t.ID, err)
|
||||
continue
|
||||
}
|
||||
agente.SiteKey = key
|
||||
}
|
||||
if agente.Color == "" {
|
||||
agente.Color = "#8eb02f"
|
||||
}
|
||||
if err := db.Create(&agente).Error; err != nil {
|
||||
log.Printf("[MIGRACION] Error creando agente Principal del tenant %d: %v", t.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, tabla := range tablasConAgenteID {
|
||||
sql := fmt.Sprintf("UPDATE %s SET agente_id = ? WHERE tenant_id = ? AND (agente_id IS NULL OR agente_id = 0)", tabla)
|
||||
if err := db.Exec(sql, agente.ID, t.ID).Error; err != nil {
|
||||
log.Printf("[MIGRACION] Error moviendo %s del tenant %d al agente %d: %v", tabla, t.ID, agente.ID, err)
|
||||
}
|
||||
}
|
||||
log.Printf("[MIGRACION] Tenant %d (%s): agente 'Principal' creado (id=%d), datos migrados", t.ID, t.Nombre, agente.ID)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user