Files
soft_usite/pkg/services/umind_embeddings_service.go
T
Lizandro GuarnizoandClaude Sonnet 5 9dc44decd7 fix: unifica el mapeo proveedor→URL, el botón "Probar conexión" tenía su propia copia sin Gemini
El fix anterior de Gemini (22efb9c) solo tocó providerDefaultURL, que usan
el chat/embeddings/whisper reales. TestAiConfigHandler (el botón "Probar
conexión" del panel de AI Config) tenía una tercera copia independiente del
mismo mapeo, sin caso para Gemini ni Deepseek — por eso el chat ya
funcionaba con Gemini pero la prueba de conexión seguía fallando.

Se exporta providerDefaultURL a services.ProviderDefaultURL y el controller
la reusa, en vez de mantener una copia más que se desactualiza cada vez que
se agrega un proveedor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 21:38:16 -05:00

101 lines
2.8 KiB
Go

package services
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
var umindEmbeddingsHTTPClient = &http.Client{Timeout: 30 * time.Second}
// GenerarEmbeddings pide los vectores de una tanda de textos en una sola
// llamada (más barato y rápido que uno por uno durante la ingesta). Usa el
// endpoint de embeddings compatible con OpenAI — Anthropic no ofrece
// embeddings, por eso esta config debe ser una con provider "openai" (u otro
// compatible con ese formato de respuesta).
func GenerarEmbeddings(ai *models.AiConfig, textos []string) ([][]float32, error) {
if ai == nil {
return nil, fmt.Errorf("no hay una configuración de IA para generar embeddings")
}
if len(textos) == 0 {
return nil, nil
}
baseURL := ai.BaseURL
if baseURL == "" {
baseURL = ProviderDefaultURL(ai.Provider)
}
baseURL = strings.TrimRight(baseURL, "/")
model := ai.ModelName
if model == "" {
model = "text-embedding-3-small"
}
reqBody, _ := json.Marshal(map[string]interface{}{
"model": model,
"input": textos,
})
req, err := http.NewRequest("POST", baseURL+"/embeddings", bytes.NewReader(reqBody))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+ai.ApiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := umindEmbeddingsHTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("no se pudo conectar con el servicio de embeddings: %w", err)
}
defer resp.Body.Close()
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4*1024*1024))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
detalle := strings.TrimSpace(string(raw))
if len(detalle) > 300 {
detalle = detalle[:300]
}
return nil, fmt.Errorf("el servicio de embeddings respondió %d: %s", resp.StatusCode, detalle)
}
var out struct {
Data []struct {
Embedding []float32 `json:"embedding"`
Index int `json:"index"`
} `json:"data"`
}
if err := json.Unmarshal(raw, &out); err != nil {
return nil, fmt.Errorf("respuesta inesperada del servicio de embeddings")
}
if len(out.Data) != len(textos) {
return nil, fmt.Errorf("el servicio de embeddings devolvió %d vectores para %d textos", len(out.Data), len(textos))
}
vectores := make([][]float32, len(textos))
for _, d := range out.Data {
if d.Index < 0 || d.Index >= len(vectores) {
continue
}
vectores[d.Index] = d.Embedding
}
return vectores, nil
}
// GenerarEmbedding es el atajo para un solo texto (ej: la consulta del usuario en RAG).
func GenerarEmbedding(ai *models.AiConfig, texto string) ([]float32, error) {
vectores, err := GenerarEmbeddings(ai, []string{texto})
if err != nil {
return nil, err
}
if len(vectores) == 0 {
return nil, fmt.Errorf("no se generó ningún embedding")
}
return vectores[0], nil
}