feat(umind): cargar conocimiento de tres formas y que deje de quedar viejo en silencio
Hasta ahora la única forma de darle información a un agente era crawlear una
URL, una vez, para siempre. Tres cosas cambian:
Escribir a mano. Es la fuente más valiosa y la única que no está en ningún
documento: horarios, qué no hacen, la respuesta que dan quince veces por día.
También es la única que el dueño puede corregir en el momento en que ve al
agente contestar mal.
Subir un archivo. La lista de precios suele estar en un PDF, no en la web. Usa
el mismo extractor que los adjuntos de los canales, así que PDF, Word, texto e
imágenes entran sin código nuevo. El texto se extrae con la persona mirando la
pantalla: si el archivo no se puede leer, se dice ahí y no en un log.
Y lo importante: el conocimiento se congelaba el día que se cargaba. Si el
cliente cambiaba los precios en su sitio, el agente seguía dando los viejos con
total seguridad — sin error, sin aviso, nada. Ahora cada fuente muestra de
cuándo es ("leído hace 3 meses", en ámbar pasados dos meses), tiene botón de
actualizar, y las URLs pueden marcarse para releerse solas cada semana (cron a
las 4 AM). Reprocesar reemplaza los fragmentos en vez de sumarlos: si no,
quedaban las dos versiones compitiendo en la búsqueda y podía ganar la vieja.
De paso, el troceado dejaba fragmentos que arrancaban a mitad de palabra
("alabra…") porque el solape no se alineaba a un espacio.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3ee83c8534
commit
3ea17b0980
@@ -3,6 +3,7 @@ package controllers
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -297,9 +298,10 @@ func GetUmindDocumentosHandler(c *fiber.Ctx) error {
|
||||
// segundos/minutos según cuántas páginas tenga el sitio.
|
||||
func CreateUmindDocumentoHandler(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
AgenteID uint `json:"agente_id"`
|
||||
URL string `json:"url"`
|
||||
MaxPaginas int `json:"max_paginas"`
|
||||
AgenteID uint `json:"agente_id"`
|
||||
URL string `json:"url"`
|
||||
MaxPaginas int `json:"max_paginas"`
|
||||
AutoActualizar bool `json:"auto_actualizar"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
||||
@@ -318,10 +320,12 @@ func CreateUmindDocumentoHandler(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
doc := &models.UmindDocumento{
|
||||
AgenteID: req.AgenteID,
|
||||
Tipo: "url",
|
||||
Origen: strings.TrimSpace(req.URL),
|
||||
Estado: "procesando",
|
||||
AgenteID: req.AgenteID,
|
||||
Tipo: "url",
|
||||
Origen: strings.TrimSpace(req.URL),
|
||||
Estado: "procesando",
|
||||
MaxPaginas: req.MaxPaginas,
|
||||
AutoActualizar: req.AutoActualizar,
|
||||
}
|
||||
if err := models.CreateUmindDocumento(doc); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
@@ -859,3 +863,162 @@ func slugSimple(s string) string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ─── Fuentes de conocimiento que no son una URL ─────────────────────────────
|
||||
|
||||
// CreateUmindTextoHandler guarda como conocimiento un texto escrito a mano.
|
||||
// Es la fuente más valiosa y la que no está en ningún lado: horarios, qué no
|
||||
// hacen, la respuesta a la pregunta que les hacen todos los días.
|
||||
// POST /umind/documentos/texto
|
||||
func CreateUmindTextoHandler(c *fiber.Ctx) error {
|
||||
var req struct {
|
||||
AgenteID uint `json:"agente_id"`
|
||||
Titulo string `json:"titulo"`
|
||||
Contenido string `json:"contenido"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
||||
}
|
||||
if _, err := accesoAgente(c, req.AgenteID); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(req.Contenido) == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "escribí algo para que el agente lo aprenda"})
|
||||
}
|
||||
titulo := strings.TrimSpace(req.Titulo)
|
||||
if titulo == "" {
|
||||
titulo = "Nota"
|
||||
}
|
||||
|
||||
doc := &models.UmindDocumento{
|
||||
AgenteID: req.AgenteID,
|
||||
Tipo: "texto",
|
||||
Origen: titulo,
|
||||
Contenido: req.Contenido,
|
||||
Estado: "procesando",
|
||||
}
|
||||
if err := models.CreateUmindDocumento(doc); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
go services.IngestarTexto(req.AgenteID, doc.ID, req.Contenido)
|
||||
return c.Status(fiber.StatusAccepted).JSON(fiber.Map{"ok": true, "id": doc.ID, "estado": "procesando"})
|
||||
}
|
||||
|
||||
// CreateUmindArchivoHandler carga un archivo como conocimiento. Usa el mismo
|
||||
// extractor que los adjuntos de los canales: PDF, Word, texto e imágenes.
|
||||
// POST /umind/documentos/archivo (multipart: agente_id, archivo)
|
||||
func CreateUmindArchivoHandler(c *fiber.Ctx) error {
|
||||
agenteID, _ := strconv.ParseUint(c.FormValue("agente_id"), 10, 64)
|
||||
if _, err := accesoAgente(c, uint(agenteID)); err != nil {
|
||||
return err
|
||||
}
|
||||
archivo, err := c.FormFile("archivo")
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "subí un archivo"})
|
||||
}
|
||||
if archivo.Size > 20<<20 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "el archivo supera los 20 MB"})
|
||||
}
|
||||
f, err := archivo.Open()
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "no se pudo leer el archivo"})
|
||||
}
|
||||
defer f.Close()
|
||||
datos, err := io.ReadAll(io.LimitReader(f, 20<<20))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "no se pudo leer el archivo"})
|
||||
}
|
||||
|
||||
// El texto se extrae acá y no en segundo plano: si el archivo no se puede
|
||||
// leer, conviene decirlo mientras la persona está mirando la pantalla.
|
||||
texto, err := services.ExtraerTextoDeArchivo(uint(agenteID), archivo.Filename, datos)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
doc := &models.UmindDocumento{
|
||||
AgenteID: uint(agenteID),
|
||||
Tipo: "archivo",
|
||||
Origen: archivo.Filename,
|
||||
Contenido: texto,
|
||||
Estado: "procesando",
|
||||
}
|
||||
if err := models.CreateUmindDocumento(doc); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
go services.IngestarTexto(uint(agenteID), doc.ID, texto)
|
||||
return c.Status(fiber.StatusAccepted).JSON(fiber.Map{"ok": true, "id": doc.ID, "estado": "procesando"})
|
||||
}
|
||||
|
||||
// ReprocesarUmindDocumentoHandler vuelve a leer una fuente. Para una URL la
|
||||
// crawlea de nuevo —es la única forma de que el agente deje de contestar los
|
||||
// precios del año pasado— y para el resto rearma los fragmentos del texto
|
||||
// guardado.
|
||||
// POST /umind/documentos/:id/reprocesar
|
||||
func ReprocesarUmindDocumentoHandler(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 := accesoDocumento(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
doc, err := models.GetUmindDocumentoByID(uint(id))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "fuente no encontrada"})
|
||||
}
|
||||
if doc.Estado == "procesando" {
|
||||
return c.Status(409).JSON(fiber.Map{"error": "esta fuente ya se está procesando"})
|
||||
}
|
||||
services.ReprocesarDocumento(*doc)
|
||||
return c.Status(fiber.StatusAccepted).JSON(fiber.Map{"ok": true, "estado": "procesando"})
|
||||
}
|
||||
|
||||
// ActualizarUmindDocumentoHandler edita el texto de una fuente escrita a mano
|
||||
// y la vuelve a procesar.
|
||||
// PUT /umind/documentos/:id
|
||||
func ActualizarUmindDocumentoHandler(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 := accesoDocumento(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
doc, err := models.GetUmindDocumentoByID(uint(id))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "fuente no encontrada"})
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Titulo string `json:"titulo"`
|
||||
Contenido string `json:"contenido"`
|
||||
AutoActualizar *bool `json:"auto_actualizar"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
if req.AutoActualizar != nil {
|
||||
updates["auto_actualizar"] = *req.AutoActualizar
|
||||
}
|
||||
reprocesar := false
|
||||
if doc.Tipo == "texto" && strings.TrimSpace(req.Contenido) != "" {
|
||||
updates["contenido"] = req.Contenido
|
||||
if t := strings.TrimSpace(req.Titulo); t != "" {
|
||||
updates["origen"] = t
|
||||
}
|
||||
doc.Contenido = req.Contenido
|
||||
reprocesar = true
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
if err := models.UpdateUmindDocumento(uint(id), updates); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
}
|
||||
if reprocesar {
|
||||
services.ReprocesarDocumento(*doc)
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ func RegistrarRutasUmind(g fiber.Router, scope fiber.Handler, escritura fiber.Ha
|
||||
|
||||
g.Get("/umind/documentos", r(controllers.GetUmindDocumentosHandler)...)
|
||||
g.Post("/umind/documentos", w(controllers.CreateUmindDocumentoHandler)...)
|
||||
g.Post("/umind/documentos/texto", w(controllers.CreateUmindTextoHandler)...)
|
||||
g.Post("/umind/documentos/archivo", w(controllers.CreateUmindArchivoHandler)...)
|
||||
g.Put("/umind/documentos/:id", w(controllers.ActualizarUmindDocumentoHandler)...)
|
||||
g.Post("/umind/documentos/:id/reprocesar", w(controllers.ReprocesarUmindDocumentoHandler)...)
|
||||
g.Delete("/umind/documentos/:id", w(controllers.DeleteUmindDocumentoHandler)...)
|
||||
|
||||
g.Get("/umind/sesiones", r(controllers.GetUmindSesionesHandler)...)
|
||||
|
||||
Reference in New Issue
Block a user