Un tenant (negocio/sitio, dueño de los dominios permitidos) puede tener varios UmindAgente independientes (ej. "Ventas", "Soporte"), cada uno con su propia config de IA, tono, base de conocimiento, tools, canales y conexión de correo. El site_key también pasa a ser por agente, así cada uno tiene su propio <script> de widget embebible y su propio color. Backend: - Nuevo modelo UmindAgente (pkg/models/umind_agente.go), con SiteKey, AiConfigID, Tono, MensajeBienvenida y Color — campos que antes vivían en UmindTenant y se sacan de ahí (las columnas viejas quedan huérfanas sin usar, no se hace DROP COLUMN). - UmindDocumento, UmindChunk, UmindHerramienta, UmindCanal, UmindConexion y UmindMensaje pasan de TenantID a AgenteID. El campo se agrega sin "not null" para no romper el ALTER TABLE en Postgres sobre tablas que ya tienen filas (ej. emetropolitana). - migrations.MigrarUmindAgentes(): idempotente, crea un agente "Principal" por cada tenant existente heredando lo que ya tenía configurado, y mueve sus datos de tenant_id a agente_id. Corre en cada arranque normal, mismo criterio que los Seed* — nada se rompe para los tenants ya en producción. - Motor del agente, widget, canales (Telegram/WhatsApp) y OAuth de correo ahora operan sobre UmindAgente; el tenant solo se consulta para el chequeo de dominio permitido y el nombre del negocio que ve el visitante. Frontend: nueva jerarquía de navegación tenant → lista de agentes (TenantAgentes.vue) → detalle de un agente (AgenteDetail.vue, antes TenantDetail.vue) con las mismas 6 tabs de siempre, ahora por agente. El modal de tenant en el sidebar se achica a nombre/dominios/activo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
84 lines
2.2 KiB
Go
84 lines
2.2 KiB
Go
package services
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
)
|
|
|
|
// resultadoRAG empareja un chunk con su similitud a la consulta, para poder ordenar.
|
|
type resultadoRAG struct {
|
|
chunk models.UmindChunk
|
|
similitud float64
|
|
}
|
|
|
|
// BuscarConocimiento embebe la consulta del usuario y devuelve los topK
|
|
// fragmentos más parecidos de la base de conocimiento del agente, por
|
|
// similitud coseno calculada en memoria. Sin pgvector por ahora: para el
|
|
// volumen de un piloto (un agente, unos cientos de chunks) esto es
|
|
// suficientemente rápido; si el volumen crece, se reemplaza por una consulta
|
|
// pgvector sin cambiar la firma de esta función.
|
|
func BuscarConocimiento(agenteID uint, consulta string, topK int) ([]models.UmindChunk, error) {
|
|
if topK <= 0 {
|
|
topK = 4
|
|
}
|
|
|
|
ai, err := models.GetUmindEmbeddingsConfig()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
consultaVec, err := GenerarEmbedding(ai, consulta)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("no se pudo generar el embedding de la consulta: %w", err)
|
|
}
|
|
|
|
chunks, err := models.GetUmindChunksByAgente(agenteID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(chunks) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
resultados := make([]resultadoRAG, 0, len(chunks))
|
|
for _, c := range chunks {
|
|
vec, err := models.EmbeddingFromJSON(c.EmbeddingJSON)
|
|
if err != nil || len(vec) == 0 {
|
|
continue
|
|
}
|
|
resultados = append(resultados, resultadoRAG{chunk: c, similitud: similitudCoseno(consultaVec, vec)})
|
|
}
|
|
|
|
sort.Slice(resultados, func(i, j int) bool { return resultados[i].similitud > resultados[j].similitud })
|
|
|
|
if topK > len(resultados) {
|
|
topK = len(resultados)
|
|
}
|
|
out := make([]models.UmindChunk, topK)
|
|
for i := 0; i < topK; i++ {
|
|
out[i] = resultados[i].chunk
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// similitudCoseno calcula la similitud coseno entre dos vectores del mismo
|
|
// tamaño. Vectores de tamaño distinto (embeddings de modelos diferentes) dan 0.
|
|
func similitudCoseno(a, b []float32) float64 {
|
|
if len(a) != len(b) || len(a) == 0 {
|
|
return 0
|
|
}
|
|
var punto, normaA, normaB float64
|
|
for i := range a {
|
|
ai, bi := float64(a[i]), float64(b[i])
|
|
punto += ai * bi
|
|
normaA += ai * ai
|
|
normaB += bi * bi
|
|
}
|
|
if normaA == 0 || normaB == 0 {
|
|
return 0
|
|
}
|
|
return punto / (math.Sqrt(normaA) * math.Sqrt(normaB))
|
|
}
|