diff --git a/resources/views/ai_config.html b/resources/views/ai_config.html index cdceaac..0498c98 100644 --- a/resources/views/ai_config.html +++ b/resources/views/ai_config.html @@ -73,9 +73,10 @@ + x-text="m.trim() === 'landing' ? 'Landing' : m.trim() === 'query_runner' ? 'Query Runner' : m.trim() === 'ia' ? 'IA / vCard' : m.trim()"> @@ -87,7 +88,8 @@ 'bg-purple-100 text-purple-700': item.provider === 'qwen', 'bg-green-100 text-green-700': item.provider === 'openai', 'bg-orange-100 text-orange-700': item.provider === 'anthropic', - 'bg-gray-100 text-gray-700': !['qwen','openai','anthropic'].includes(item.provider) + 'bg-blue-100 text-blue-700': item.provider === 'ollama', + 'bg-gray-100 text-gray-700': !['qwen','openai','anthropic','ollama'].includes(item.provider) }" x-text="item.provider"> @@ -143,6 +145,7 @@ + @@ -164,10 +167,16 @@
- + +

+ Interna (red Coolify): http://10.0.1.15:11434/v1 — Pública: https://ollama.u-s.app/v1 +

@@ -258,6 +267,7 @@ function aiConfigApp() { moduleOptions: [ { value: 'landing', label: 'Landing Generator' }, { value: 'query_runner', label: 'Query Runner SQL' }, + { value: 'ia', label: 'IA / vCard' }, ], async init() { await this.load() }, diff --git a/rest/controllers/api/ia_controller.go b/rest/controllers/api/ia_controller.go index 3087c4f..f345ee8 100644 --- a/rest/controllers/api/ia_controller.go +++ b/rest/controllers/api/ia_controller.go @@ -6,14 +6,17 @@ import ( "encoding/json" "fmt" "net/http" + "strings" + "time" "github.com/gofiber/fiber/v2" + "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) type DataIa struct { Prompt string `json:"prompt"` Model string `json:"model"` - Stream bool `json:"stream"` + Stream bool `json:"stream"` } type respuestaOllama struct { @@ -22,64 +25,72 @@ type respuestaOllama struct { } func GeneraTextoStream(c *fiber.Ctx) error { - var data DataIa - if err := c.BodyParser(&data); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Datos inválidos"}) - } + var data DataIa + if err := c.BodyParser(&data); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Datos inválidos"}) + } + data.Stream = true - // Forzar stream = true - data.Stream = true + config, err := models.GetAiConfigForService("ia") + if err != nil { + return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ + "error": "No hay configuración de IA activa. Configura una en /app/ai-config", + }) + } - jsonData, err := json.Marshal(data) - if err != nil { - return err - } + if data.Model == "" { + data.Model = config.ModelName + } + if data.Model == "" { + data.Model = "gemma3:1b" + } - req, err := http.NewRequest("POST", "https://n8n.u-s.app/webhook/c945b974-f534-419b-a6da-4ebd832f8cd9", bytes.NewBuffer(jsonData)) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer sk-43804e00f5294200ad1e599550fbee4f") + // Usar endpoint nativo de Ollama (/api/generate), no OpenAI compat (/v1/...) + baseURL := strings.TrimSuffix(strings.TrimRight(config.BaseURL, "/"), "/v1") + endpoint := baseURL + "/api/generate" - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return err - } + jsonData, _ := json.Marshal(data) + req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(jsonData)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + // Sin auth para red interna; api_key como token para URL pública + if config.ApiKey != "" && config.ApiKey != "ollama" { + req.SetBasicAuth("ollama", config.ApiKey) + } - // Configurar headers para streaming - c.Set("Content-Type", "text/plain; charset=utf-8") - c.Set("Cache-Control", "no-cache") - c.Set("Connection", "keep-alive") + client := &http.Client{Timeout: 120 * time.Second} + resp, err := client.Do(req) + if err != nil { + return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()}) + } - c.Context().SetBodyStreamWriter(func(w *bufio.Writer) { - defer resp.Body.Close() + c.Set("Content-Type", "text/plain; charset=utf-8") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") - scanner := bufio.NewScanner(resp.Body) + c.Context().SetBodyStreamWriter(func(w *bufio.Writer) { + defer resp.Body.Close() + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + line := scanner.Bytes() + if len(line) == 0 { + continue + } + var msg respuestaOllama + if err := json.Unmarshal(line, &msg); err != nil { + continue + } + if msg.Response != "" { + fmt.Fprint(w, msg.Response) + w.Flush() + } + if msg.Done { + break + } + } + }) - for scanner.Scan() { - line := scanner.Bytes() - if len(line) == 0 { - continue - } - - var msg respuestaOllama - if err := json.Unmarshal(line, &msg); err != nil { - continue - } - - if msg.Response != "" { - fmt.Fprint(w, msg.Response) - w.Flush() // <-- envía inmediatamente - } - - if msg.Done { - break - } - } - }) - - return nil + return nil } - diff --git a/rest/controllers/query_runner_controller.go b/rest/controllers/query_runner_controller.go index 66d32ae..70e71f5 100644 --- a/rest/controllers/query_runner_controller.go +++ b/rest/controllers/query_runner_controller.go @@ -552,6 +552,7 @@ SQL optimizado:`, sql) case "anthropic": baseURL = "https://api.anthropic.com/v1" default: + log.Printf("[AI_SQL] Provider '%s' requiere BaseURL configurada", config.Provider) return nil } } @@ -565,6 +566,8 @@ SQL optimizado:`, sql) modelName = "qwen2.5-72b-instruct" case "anthropic": modelName = "claude-3-haiku-20240307" + case "ollama": + modelName = "gemma3:1b" default: modelName = "gpt-4o-mini" } @@ -595,10 +598,16 @@ SQL optimizado:`, sql) req, _ := http.NewRequest("POST", url, bytes.NewReader(jsonBody)) req.Header.Set("Content-Type", "application/json") - if config.Provider == "anthropic" { + switch config.Provider { + case "anthropic": req.Header.Set(authHeader, config.ApiKey) req.Header.Set("anthropic-version", "2023-06-01") - } else { + case "ollama": + // Sin auth para red interna; para URL pública el api_key es "usuario:token" + if config.ApiKey != "" && config.ApiKey != "ollama" { + req.SetBasicAuth("ollama", config.ApiKey) + } + default: req.Header.Set(authHeader, "Bearer "+config.ApiKey) }