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)
}