Agrega uMind: chat con IA embebible por tenant (F1 — widget web + RAG)
Multi-tenant dentro de soft_usite, reutilizando la infraestructura ya existente (AiConfig, motor de function-calling del agente de Telegram) en vez de un servicio nuevo aparte: - UmindTenant: sitio/cliente con dominios permitidos, config de IA para el chat y personalidad/tono. - Ingesta: crawler simple (mismo dominio, N páginas) + chunking + embeddings (config global con módulo "umind_embeddings", pensada para OpenAI ya que Claude no ofrece embeddings) guardados como JSON, con búsqueda por similitud coseno en memoria (sin pgvector todavía). - Agente acotado: única herramienta buscar_conocimiento, sin acceso a nada interno — si no encuentra la respuesta, lo dice en vez de inventar. - Widget público (/widget/umind.js + /widget/:site_key/*), autenticado por site_key + validación de dominio (Origin/Referer), no por secreto, ya que la key viaja en el HTML público del sitio instalado. - Panel /app/umind: tenants, estado de ingesta, historial de conversaciones por sesión.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user