diff --git a/pkg/models/ai_config.go b/pkg/models/ai_config.go
index 83ce715..b7989b6 100644
--- a/pkg/models/ai_config.go
+++ b/pkg/models/ai_config.go
@@ -19,7 +19,7 @@ type AiConfig struct {
ApiKey string `gorm:"type:text;not null" json:"api_key"` // Clave de API
BaseURL string `gorm:"type:text" json:"base_url"` // URL base (override), vacío = default del provider
ModelName string `gorm:"size:100" json:"model_name"` // ej: qwen2.5-72b-instruct
- IsActive bool `gorm:"default:true" json:"is_active"` // Solo uno activo a la vez
+ IsActive bool `gorm:"default:true" json:"is_active"` // varias pueden estar activas: una por módulo
Notes string `gorm:"type:text" json:"notes"`
// Modulo indica a qué servicio pertenece esta config.
// "" = global (disponible para todos como fallback)
@@ -108,21 +108,6 @@ func GetAiConfigByID(id uint, out *AiConfig) error {
return app.Http.Database.DB.First(out, id).Error
}
-// GetActiveAiConfig retorna la primera configuración activa del provider indicado.
-// Si provider está vacío, retorna cualquier config activa.
-func GetActiveAiConfig(provider string) (*AiConfig, error) {
- var item AiConfig
- db := app.Http.Database.DB.Where("is_active = ?", true)
- if provider != "" {
- db = db.Where("provider = ?", provider)
- }
- if err := db.First(&item).Error; err != nil {
- log.Printf("[AI_CONFIG] No se encontró config activa para provider '%s': %v", provider, err)
- return nil, err
- }
- return &item, nil
-}
-
// SplitModulos parte el campo Modulo (comma-separated) en un slice limpio.
// "" → [] (config global), "landing,query_runner" → ["landing","query_runner"]
func SplitModulos(modulo string) []string {
@@ -171,6 +156,15 @@ func GetAiConfigSelectPorTenants(tenantIDs []uint) ([]AiConfig, error) {
return items, nil
}
+// QuitarAgenteBotSalvo deja como cerebro del agente solo a la config indicada.
+// GetAgenteBotAiConfig hace First() sobre es_agente_bot: con dos marcadas, cuál
+// gana depende del orden de la tabla, que no es una forma de elegir nada.
+func QuitarAgenteBotSalvo(id uint) {
+ app.Http.Database.DB.Model(&AiConfig{}).
+ Where("id <> ? AND es_agente_bot = ?", id, true).
+ Update("es_agente_bot", false)
+}
+
// GetAgenteBotConfig retorna la config marcada como agente Telegram, con su TelegramConfig cargada.
func GetAgenteBotConfig() (*AiConfig, *TelegramConfig, error) {
var ai AiConfig
@@ -230,9 +224,29 @@ func GetAiConfigForService(service string) (*AiConfig, error) {
}
}
- // 3. Cualquier config activa como último recurso
- log.Printf("[AI_CONFIG] No se encontró config para servicio '%s', usando cualquier activa", service)
- return &items[0], nil
+ // 3. Cualquier config activa como último recurso, pero nunca una que esté
+ // dedicada a un servicio que no sabe conversar: la de embeddings devuelve
+ // vectores y la de Whisper transcribe audio. Caer ahí daba errores del
+ // proveedor imposibles de relacionar con esta elección.
+ for i := range items {
+ if esConfigDeUsoEspecial(items[i].Modulo) {
+ continue
+ }
+ log.Printf("[AI_CONFIG] Sin config para %q, se usa %q (que no la declara)", service, items[i].Nombre)
+ return &items[i], nil
+ }
+ return nil, fmt.Errorf("no hay ninguna configuración de IA para %q: asignale ese módulo a una config en /app/ai-config", service)
+}
+
+// esConfigDeUsoEspecial marca los módulos cuyo endpoint no es de chat, así que
+// no sirven como comodín para otra cosa.
+func esConfigDeUsoEspecial(modulo string) bool {
+ for _, m := range SplitModulos(modulo) {
+ if m == "whisper" || m == "umind_embeddings" {
+ return true
+ }
+ }
+ return false
}
// HayAiConfigParaModulo dice si alguna config activa declara ese módulo.
diff --git a/pkg/models/ai_config_test.go b/pkg/models/ai_config_test.go
new file mode 100644
index 0000000..0b1a90b
--- /dev/null
+++ b/pkg/models/ai_config_test.go
@@ -0,0 +1,23 @@
+package models
+
+import "testing"
+
+// El fallback de GetAiConfigForService puede terminar usando una config que no
+// declara el servicio pedido. Lo que no puede es agarrar una dedicada a
+// embeddings o a Whisper: esos endpoints no conversan, y el error del proveedor
+// no se parece en nada a la causa real.
+func TestConfigsDeUsoEspecialNoSirvenDeComodin(t *testing.T) {
+ casos := map[string]bool{
+ "whisper": true,
+ "umind_embeddings": true,
+ "landing,umind_embeddings": true,
+ "": false,
+ "ia": false,
+ "landing,query_runner": false,
+ }
+ for modulo, want := range casos {
+ if got := esConfigDeUsoEspecial(modulo); got != want {
+ t.Errorf("esConfigDeUsoEspecial(%q) = %v, want %v", modulo, got, want)
+ }
+ }
+}
diff --git a/resources/views/ai_config.html b/resources/views/ai_config.html
index a2f9972..0713eef 100644
--- a/resources/views/ai_config.html
+++ b/resources/views/ai_config.html
@@ -200,6 +200,26 @@
+
+
+
+ Solo una config puede serlo: al marcar esta, se desmarca la anterior.
+