diff --git a/pkg/models/users.go b/pkg/models/users.go index 71b6e26..5586003 100755 --- a/pkg/models/users.go +++ b/pkg/models/users.go @@ -30,6 +30,7 @@ type Users struct { NumeroMedidor string `json:"numero_medidor" gorm:"numero_medidor"` TelefonoFijo string `json:"telefono_fijo" gorm:"telefono_fijo"` Celular string `json:"celular" gorm:"celular"` + TelegramChatID string `json:"telegram_chat_id" gorm:"column:telegram_chat_id"` AceptoInfo bool `json:"acepto_info" gorm:"acepto_info"` } @@ -103,16 +104,15 @@ func CreateUser(User Users) error { } // UpdateUser updates an existing user -func UpdateUser(userID uint, Name string, NombreUsuario string, Email string, RoleID int) error { - // Crea un mapa con los campos a actualizar +func UpdateUser(userID uint, Name string, NombreUsuario string, Email string, RoleID int, TelegramChatID string) error { updates := map[string]interface{}{ - "name": Name, - "nombre_usuario": NombreUsuario, - "email": Email, - "role_id": RoleID, + "name": Name, + "nombre_usuario": NombreUsuario, + "email": Email, + "role_id": RoleID, + "telegram_chat_id": TelegramChatID, } - // Realiza la actualización de los campos en la base de datos if err := app.Http.Database.DB.Model(&Users{}).Where("id = ?", userID).Updates(updates).Error; err != nil { return err } diff --git a/rest/controllers/api/ia_controller.go b/rest/controllers/api/ia_controller.go index ae34a22..b5f1c55 100644 --- a/rest/controllers/api/ia_controller.go +++ b/rest/controllers/api/ia_controller.go @@ -27,6 +27,28 @@ type respuestaOllama struct { Error string `json:"error"` } +type openaiChatMsg struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type openaiChatReq struct { + Model string `json:"model"` + Messages []openaiChatMsg `json:"messages"` + Stream bool `json:"stream"` +} + +type openaiChatChoice struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + FinishReason *string `json:"finish_reason"` +} + +type openaiChatResp struct { + Choices []openaiChatChoice `json:"choices"` +} + func GeneraTextoStream(c *fiber.Ctx) error { var data DataIa if err := c.BodyParser(&data); err != nil { @@ -34,16 +56,13 @@ func GeneraTextoStream(c *fiber.Ctx) error { } data.Stream = true - hasDBConfig := true config, err := models.GetAiConfigForService("ia") if err != nil { - hasDBConfig = false - config = &models.AiConfig{ - BaseURL: "http://72.60.24.97:8080/ollama", - ApiKey: "", - ModelName: "gemma3:1b", - } + return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ + "error": "No hay configuración de IA activa. Configura una en /app/ai-config", + }) } + if data.Model == "" { data.Model = config.ModelName } @@ -51,37 +70,64 @@ func GeneraTextoStream(c *fiber.Ctx) error { data.Model = "gemma3:1b" } - baseURL := strings.TrimSuffix(strings.TrimRight(config.BaseURL, "/"), "/v1") - endpoint := baseURL + "/api/generate" + provider := strings.ToLower(config.Provider) + baseURL := strings.TrimRight(config.BaseURL, "/") + hasV1 := strings.Contains(baseURL, "/v1") - if !hasDBConfig { - log.Printf("[IA] Usando config default: endpoint=%s model=%s", endpoint, data.Model) + var endpoint string + var bodyReader io.Reader + useOpenAI := provider != "ollama" || hasV1 + + // OpenAI-compatible (openai, anthropic, qwen, o llameo con /v1) + if !useOpenAI { + // Native Ollama API + endpoint = baseURL + "/api/generate" + jsonData, _ := json.Marshal(data) + bodyReader = bytes.NewBuffer(jsonData) } else { - log.Printf("[IA] Usando config BD: endpoint=%s model=%s", endpoint, data.Model) + // OpenAI-compatible (/v1/chat/completions) + baseURL = strings.TrimSuffix(baseURL, "/v1") + endpoint = baseURL + "/v1/chat/completions" + chatReq := openaiChatReq{ + Model: data.Model, + Messages: []openaiChatMsg{ + {Role: "user", Content: data.Prompt}, + }, + Stream: true, + } + jsonData, _ := json.Marshal(chatReq) + bodyReader = bytes.NewBuffer(jsonData) } - jsonData, _ := json.Marshal(data) - req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(jsonData)) + log.Printf("[IA] endpoint=%s model=%s provider=%s", endpoint, data.Model, provider) + + req, err := http.NewRequest("POST", endpoint, bodyReader) if err != nil { return err } req.Header.Set("Content-Type", "application/json") - if config.ApiKey != "" && config.ApiKey != "ollama" { + + // Auth + if provider == "ollama" && config.ApiKey != "" && config.ApiKey != "ollama" { req.SetBasicAuth("ollama", config.ApiKey) + } else if config.ApiKey != "" { + req.Header.Set("Authorization", "Bearer "+config.ApiKey) } client := &http.Client{Timeout: 120 * time.Second} resp, err := client.Do(req) if err != nil { - log.Printf("[IA] Error conectando a Ollama: %v", err) - return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()}) + log.Printf("[IA] Error conectando: %v", err) + return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{ + "error": fmt.Sprintf("Error conectando a %s: %v", provider, err), + }) } if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) resp.Body.Close() - log.Printf("[IA] Ollama status %d: %s", resp.StatusCode, string(bodyBytes)) + log.Printf("[IA] %s status %d: %s", provider, resp.StatusCode, string(bodyBytes)) return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{ - "error": fmt.Sprintf("Ollama respondió con status %d", resp.StatusCode), + "error": fmt.Sprintf("%s respondió con status %d", provider, resp.StatusCode), "details": string(bodyBytes), }) } @@ -94,25 +140,46 @@ func GeneraTextoStream(c *fiber.Ctx) error { defer resp.Body.Close() dec := json.NewDecoder(resp.Body) for { - var msg respuestaOllama - if err := dec.Decode(&msg); err != nil { - if err != io.EOF { - log.Printf("[IA] Error decodificando: %v", err) + if useOpenAI { + var msg openaiChatResp + if err := dec.Decode(&msg); err != nil { + if err != io.EOF { + log.Printf("[IA] Error decodificando respuesta OpenAI: %v", err) + } + break + } + if len(msg.Choices) == 0 { + continue + } + content := msg.Choices[0].Delta.Content + if content != "" { + fmt.Fprint(w, content) + w.Flush() + } + if msg.Choices[0].FinishReason != nil { + break + } + } else { + var msg respuestaOllama + if err := dec.Decode(&msg); err != nil { + if err != io.EOF { + log.Printf("[IA] Error decodificando respuesta Ollama: %v", err) + } + break + } + if msg.Error != "" { + log.Printf("[IA] Ollama error: %s", msg.Error) + fmt.Fprintf(w, "[Error: %s]", msg.Error) + w.Flush() + break + } + if msg.Response != "" { + fmt.Fprint(w, msg.Response) + w.Flush() + } + if msg.Done { + break } - break - } - if msg.Error != "" { - log.Printf("[IA] Ollama error: %s", msg.Error) - fmt.Fprintf(w, "[Error: %s]", msg.Error) - w.Flush() - break - } - if msg.Response != "" { - fmt.Fprint(w, msg.Response) - w.Flush() - } - if msg.Done { - break } } }) diff --git a/rest/controllers/users_controller.go b/rest/controllers/users_controller.go index 1274e5e..e1de706 100755 --- a/rest/controllers/users_controller.go +++ b/rest/controllers/users_controller.go @@ -240,8 +240,10 @@ func UpdateUser(c *fiber.Ctx) error { updates["role_id"] = m.RoleID } + updates["telegram_chat_id"] = m.TelegramChatID + // Realiza la actualización de los campos seleccionados - if err := models.UpdateUser(m.ID, m.Name, m.NombreUsuario, m.Email, int(m.RoleID)); err != nil { + if err := models.UpdateUser(m.ID, m.Name, m.NombreUsuario, m.Email, int(m.RoleID), m.TelegramChatID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "message": err.Error(), "error": true,