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.
216 lines
7.7 KiB
Go
216 lines
7.7 KiB
Go
package controllers
|
|
|
|
import (
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
|
)
|
|
|
|
// UmindIndex renderiza el panel de administración de uMind.
|
|
func UmindIndex(c *fiber.Ctx) error {
|
|
return c.Render("umind", fiber.Map{
|
|
"user": c.Locals("user"),
|
|
"modules": c.Locals("userModules"),
|
|
}, "layouts/main")
|
|
}
|
|
|
|
// ─── Tenants ─────────────────────────────────────────────────────────────────
|
|
|
|
func GetUmindTenants(c *fiber.Ctx) error {
|
|
page, _ := strconv.Atoi(c.Query("page", "1"))
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
limit := 20
|
|
offset := (page - 1) * limit
|
|
items, total, err := models.GetAllUmindTenants(limit, offset)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{
|
|
"items": items,
|
|
"total": total,
|
|
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
|
|
"page": page,
|
|
})
|
|
}
|
|
|
|
type umindTenantReq struct {
|
|
Nombre string `json:"nombre"`
|
|
DominiosPermitidos []string `json:"dominios_permitidos"`
|
|
AiConfigID *uint `json:"ai_config_id"`
|
|
Tono string `json:"tono"`
|
|
MensajeBienvenida string `json:"mensaje_bienvenida"`
|
|
Activo bool `json:"activo"`
|
|
}
|
|
|
|
func (r umindTenantReq) dominiosLimpios() []string {
|
|
var out []string
|
|
for _, d := range r.DominiosPermitidos {
|
|
d = strings.ToLower(strings.TrimSpace(d))
|
|
if d != "" {
|
|
out = append(out, d)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func CreateUmindTenantHandler(c *fiber.Ctx) error {
|
|
var req umindTenantReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
|
}
|
|
if strings.TrimSpace(req.Nombre) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "nombre es requerido"})
|
|
}
|
|
dominios := req.dominiosLimpios()
|
|
if len(dominios) == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "agrega al menos un dominio permitido"})
|
|
}
|
|
|
|
tenant := &models.UmindTenant{
|
|
Nombre: strings.TrimSpace(req.Nombre),
|
|
DominiosPermitidos: strings.Join(dominios, ","),
|
|
AiConfigID: req.AiConfigID,
|
|
Tono: req.Tono,
|
|
MensajeBienvenida: req.MensajeBienvenida,
|
|
Activo: true,
|
|
CreadoPorID: extraerUserID(c),
|
|
}
|
|
if err := models.CreateUmindTenant(tenant); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"ok": true, "id": tenant.ID, "site_key": tenant.SiteKey})
|
|
}
|
|
|
|
func UpdateUmindTenantHandler(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
|
}
|
|
var req umindTenantReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
|
}
|
|
dominios := req.dominiosLimpios()
|
|
updates := map[string]interface{}{
|
|
"nombre": strings.TrimSpace(req.Nombre),
|
|
"dominios_permitidos": strings.Join(dominios, ","),
|
|
"ai_config_id": req.AiConfigID,
|
|
"tono": req.Tono,
|
|
"mensaje_bienvenida": req.MensajeBienvenida,
|
|
"activo": req.Activo,
|
|
}
|
|
if err := models.UpdateUmindTenant(uint(id), updates); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
func DeleteUmindTenantHandler(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
|
}
|
|
if err := models.DeleteUmindTenant(uint(id)); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
// ─── Documentos / ingesta ────────────────────────────────────────────────────
|
|
|
|
func GetUmindDocumentosHandler(c *fiber.Ctx) error {
|
|
tenantID, err := strconv.ParseUint(c.Query("tenant_id"), 10, 64)
|
|
if err != nil || tenantID == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
|
|
}
|
|
items, err := models.GetUmindDocumentosByTenant(uint(tenantID))
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"items": items})
|
|
}
|
|
|
|
// CreateUmindDocumentoHandler registra una nueva fuente (por ahora solo URL a
|
|
// crawlear) y dispara la ingesta en segundo plano — puede tardar varios
|
|
// segundos/minutos según cuántas páginas tenga el sitio.
|
|
func CreateUmindDocumentoHandler(c *fiber.Ctx) error {
|
|
var req struct {
|
|
TenantID uint `json:"tenant_id"`
|
|
URL string `json:"url"`
|
|
MaxPaginas int `json:"max_paginas"`
|
|
}
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
|
}
|
|
if req.TenantID == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
|
|
}
|
|
if strings.TrimSpace(req.URL) == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "url requerida"})
|
|
}
|
|
if _, err := models.GetUmindTenantByID(req.TenantID); err != nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant no encontrado"})
|
|
}
|
|
|
|
doc := &models.UmindDocumento{
|
|
TenantID: req.TenantID,
|
|
Tipo: "url",
|
|
Origen: strings.TrimSpace(req.URL),
|
|
Estado: "procesando",
|
|
}
|
|
if err := models.CreateUmindDocumento(doc); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
go services.IngestarTenant(req.TenantID, doc.ID, doc.Origen, req.MaxPaginas)
|
|
|
|
return c.Status(fiber.StatusAccepted).JSON(fiber.Map{"ok": true, "id": doc.ID, "estado": "procesando"})
|
|
}
|
|
|
|
func DeleteUmindDocumentoHandler(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
|
}
|
|
if err := models.DeleteUmindDocumento(uint(id)); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true})
|
|
}
|
|
|
|
// ─── Conversaciones ───────────────────────────────────────────────────────────
|
|
|
|
func GetUmindSesionesHandler(c *fiber.Ctx) error {
|
|
tenantID, err := strconv.ParseUint(c.Query("tenant_id"), 10, 64)
|
|
if err != nil || tenantID == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
|
|
}
|
|
items, err := models.GetUmindSesiones(uint(tenantID), 50)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"items": items})
|
|
}
|
|
|
|
func GetUmindHistorialHandler(c *fiber.Ctx) error {
|
|
tenantID, err := strconv.ParseUint(c.Query("tenant_id"), 10, 64)
|
|
if err != nil || tenantID == 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant_id requerido"})
|
|
}
|
|
sessionID := c.Query("session_id")
|
|
if sessionID == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "session_id requerido"})
|
|
}
|
|
items, err := models.GetUmindHistorial(uint(tenantID), sessionID, 200)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"items": items})
|
|
}
|