Files
soft_usite/rest/controllers/umind_admin_controller.go
T
Lizandro GD 5ee5880dc2 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.
2026-08-07 16:18:16 +00:00

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