package controllers import ( "encoding/json" "math" "net/http" "strconv" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" "github.com/sujit-baniya/fiber-boilerplate/pkg/services" ) // AiConfigIndex renderiza la vista del panel de configuraciones de IA. func AiConfigIndex(c *fiber.Ctx) error { data := fiber.Map{ "user": c.Locals("user").(map[string]interface{}), "modules": c.Locals("userModules"), } return c.Render("ai_config", data, "layouts/main") } // GetAiConfigs devuelve la lista paginada en JSON. func GetAiConfigs(c *fiber.Ctx) error { page, _ := strconv.Atoi(c.Query("page", "1")) if page < 1 { page = 1 } limit := 20 offset := (page - 1) * limit search := c.Query("search", "") items, total, err := models.GetAllAiConfigs(limit, offset, search) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } // Ocultar la API key en el listado (mostrar solo últimos 4 chars) type safe struct { ID uint `json:"ID"` Nombre string `json:"nombre"` Provider string `json:"provider"` ApiKeyHint string `json:"api_key_hint"` BaseURL string `json:"base_url"` ModelName string `json:"model_name"` IsActive bool `json:"is_active"` Notes string `json:"notes"` Modulo string `json:"modulo"` EsAgenteBot bool `json:"es_agente_bot"` TelegramConfigID *uint `json:"telegram_config_id"` } safeItems := make([]safe, len(items)) for i, it := range items { hint := "••••" if len(it.ApiKey) > 4 { hint = "••••" + it.ApiKey[len(it.ApiKey)-4:] } safeItems[i] = safe{ ID: it.ID, Nombre: it.Nombre, Provider: it.Provider, ApiKeyHint: hint, BaseURL: it.BaseURL, ModelName: it.ModelName, IsActive: it.IsActive, Notes: it.Notes, Modulo: it.Modulo, EsAgenteBot: it.EsAgenteBot, TelegramConfigID: it.TelegramConfigID, } } return c.JSON(fiber.Map{ "items": safeItems, "total": total, "totalPages": int(math.Ceil(float64(total) / float64(limit))), "page": page, }) } // CreateAiConfigHandler crea una nueva configuración de IA. func CreateAiConfigHandler(c *fiber.Ctx) error { type Req struct { Nombre string `json:"nombre"` Provider string `json:"provider"` ApiKey string `json:"api_key"` BaseURL string `json:"base_url"` ModelName string `json:"model_name"` IsActive bool `json:"is_active"` Notes string `json:"notes"` Modulo string `json:"modulo"` EsAgenteBot bool `json:"es_agente_bot"` TelegramConfigID *uint `json:"telegram_config_id"` } var req Req if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"}) } if strings.TrimSpace(req.Nombre) == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "nombre es requerido"}) } if strings.TrimSpace(req.Provider) == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "provider es requerido"}) } if strings.TrimSpace(req.ApiKey) == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "api_key es requerido"}) } item := models.AiConfig{ Nombre: strings.TrimSpace(req.Nombre), Provider: strings.ToLower(strings.TrimSpace(req.Provider)), ApiKey: strings.TrimSpace(req.ApiKey), BaseURL: strings.TrimSpace(req.BaseURL), ModelName: strings.TrimSpace(req.ModelName), IsActive: req.IsActive, Notes: req.Notes, Modulo: models.JoinModulos(strings.Split(req.Modulo, ",")), EsAgenteBot: req.EsAgenteBot, TelegramConfigID: req.TelegramConfigID, } if err := models.CreateAiConfig(&item); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true, "id": item.ID}) } // UpdateAiConfigHandler actualiza una configuración de IA existente. func UpdateAiConfigHandler(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"}) } type Req struct { Nombre string `json:"nombre"` Provider string `json:"provider"` ApiKey string `json:"api_key"` // vacío = no cambiar BaseURL string `json:"base_url"` ModelName string `json:"model_name"` IsActive bool `json:"is_active"` Notes string `json:"notes"` Modulo string `json:"modulo"` EsAgenteBot bool `json:"es_agente_bot"` TelegramConfigID *uint `json:"telegram_config_id"` } var req Req if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"}) } updates := map[string]interface{}{ "nombre": strings.TrimSpace(req.Nombre), "provider": strings.ToLower(strings.TrimSpace(req.Provider)), "base_url": strings.TrimSpace(req.BaseURL), "model_name": strings.TrimSpace(req.ModelName), "is_active": req.IsActive, "notes": req.Notes, "modulo": models.JoinModulos(strings.Split(req.Modulo, ",")), "es_agente_bot": req.EsAgenteBot, "telegram_config_id": req.TelegramConfigID, } if strings.TrimSpace(req.ApiKey) != "" { updates["api_key"] = strings.TrimSpace(req.ApiKey) } if err := models.UpdateAiConfig(uint(id), updates); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } // GetAiConfigSelect devuelve lista simple para selects func GetAiConfigSelect(c *fiber.Ctx) error { items, err := models.GetAiConfigSelect() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"registros": items}) } // TestAiConfigHandler verifica conectividad con el provider de la config. func TestAiConfigHandler(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"}) } var item models.AiConfig if err := models.GetAiConfigByID(uint(id), &item); err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "config no encontrada"}) } client := &http.Client{Timeout: 10 * time.Second} var testURL string var req *http.Request switch item.Provider { case "ollama": base := strings.TrimSuffix(strings.TrimRight(item.BaseURL, "/"), "/v1") testURL = base + "/api/tags" req, _ = http.NewRequest("GET", testURL, nil) if item.ApiKey != "" && item.ApiKey != "ollama" { req.SetBasicAuth("ollama", item.ApiKey) } case "anthropic": // Anthropic no tiene /models; usamos /v1/models igual (devuelve 200 con lista) base := item.BaseURL if base == "" { base = "https://api.anthropic.com/v1" } testURL = base + "/models" req, _ = http.NewRequest("GET", testURL, nil) req.Header.Set("x-api-key", item.ApiKey) req.Header.Set("anthropic-version", "2023-06-01") default: // Mismo mapeo proveedor→URL que usa el chat/embeddings real // (services.ProviderDefaultURL) — antes esta prueba tenía su propia // copia desactualizada que no conocía proveedores nuevos como Gemini, // así que "funcionaba" en el chat pero la prueba de conexión fallaba. base := item.BaseURL if base == "" { base = services.ProviderDefaultURL(item.Provider) } testURL = base + "/models" req, _ = http.NewRequest("GET", testURL, nil) req.Header.Set("Authorization", "Bearer "+item.ApiKey) } resp, err := client.Do(req) if err != nil { return c.JSON(fiber.Map{"ok": false, "error": err.Error()}) } defer resp.Body.Close() if resp.StatusCode >= 400 { var body map[string]interface{} json.NewDecoder(resp.Body).Decode(&body) return c.JSON(fiber.Map{"ok": false, "status": resp.StatusCode, "error": body}) } // Para Ollama devolvemos la lista de modelos disponibles if item.Provider == "ollama" { var body map[string]interface{} json.NewDecoder(resp.Body).Decode(&body) return c.JSON(fiber.Map{"ok": true, "status": resp.StatusCode, "data": body}) } return c.JSON(fiber.Map{"ok": true, "status": resp.StatusCode}) } // DeleteAiConfigHandler elimina una configuración de IA. func DeleteAiConfigHandler(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"}) } if err := models.DeleteAiConfig(uint(id)); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) }