feat(umind): los agentes también leen archivos, no solo audios e imágenes

Faltaba la tercera pata: audio pasaba por Whisper, imagen por OCR, y un PDF o
un Word adjunto se ignoraba en silencio. Ahora hay un interruptor por canal
—"Leer archivos adjuntos"— y los documentos que llegan por Telegram o WhatsApp
se convierten a texto antes de pasar al agente.

PDF se lee con stdlib cuando el documento es digital (facturas, cotizaciones,
lo exportado por cualquier programa) y cae al OCR si es un escaneo. Word .docx
es un zip con XML adentro; texto plano, CSV y JSON van directo.

El agente recibe el contenido con contexto ("el cliente adjuntó X, y escribió
Y"), no el chorizo pelado: sin eso el modelo contesta como si el cliente
hubiera tipeado una factura.

De paso la importación de plantillas gana PDF, que usa el mismo extractor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-17 18:50:59 -05:00
co-authored by Claude Opus 5
parent e681f41639
commit c4b0305112
14 changed files with 384 additions and 124 deletions
@@ -16,15 +16,18 @@ type umindTgChat struct {
}
type umindTgFileRef struct {
FileID string `json:"file_id"`
FileID string `json:"file_id"`
FileName string `json:"file_name"` // solo lo traen los documentos
}
type umindTgMessage struct {
Chat umindTgChat `json:"chat"`
Text string `json:"text"`
Voice *umindTgFileRef `json:"voice"`
Audio *umindTgFileRef `json:"audio"`
Photo []umindTgFileRef `json:"photo"`
Chat umindTgChat `json:"chat"`
Text string `json:"text"`
Voice *umindTgFileRef `json:"voice"`
Audio *umindTgFileRef `json:"audio"`
Photo []umindTgFileRef `json:"photo"`
Document *umindTgFileRef `json:"document"`
Caption string `json:"caption"`
}
type umindTgUpdate struct {
@@ -55,6 +58,8 @@ func UmindTelegramWebhook(c *fiber.Ctx) error {
err = services.ProcesarMediaTelegramUmind(canal, msg.Chat.ID, msg.Audio.FileID, "audio")
case len(msg.Photo) > 0:
err = services.ProcesarMediaTelegramUmind(canal, msg.Chat.ID, msg.Photo[len(msg.Photo)-1].FileID, "image")
case msg.Document != nil && msg.Document.FileID != "":
err = services.ProcesarMediaTelegramUmindConNombre(canal, msg.Chat.ID, msg.Document.FileID, "document", msg.Document.FileName, msg.Caption)
default:
texto := strings.TrimSpace(msg.Text)
if texto == "" {
@@ -81,8 +86,13 @@ type umindWaMessage struct {
Text struct {
Body string `json:"body"`
} `json:"text"`
Image umindWaMediaRef `json:"image"`
Audio umindWaMediaRef `json:"audio"`
Image umindWaMediaRef `json:"image"`
Audio umindWaMediaRef `json:"audio"`
Document struct {
ID string `json:"id"`
Filename string `json:"filename"`
Caption string `json:"caption"`
} `json:"document"`
}
type umindWaValue struct {
@@ -172,6 +182,11 @@ func UmindWhatsAppWebhook(c *fiber.Ctx) error {
continue
}
err = services.ProcesarMediaWhatsAppUmind(canal, msg.From, msg.Image.ID, "image")
case "document":
if msg.Document.ID == "" {
continue
}
err = services.ProcesarMediaWhatsAppUmindConNombre(canal, msg.From, msg.Document.ID, "document", msg.Document.Filename, msg.Document.Caption)
default:
continue
}
+4 -1
View File
@@ -547,6 +547,7 @@ func GetUmindCanalesHandler(c *fiber.Ctx) error {
"ID": canal.ID, "agente_id": canal.AgenteID, "tipo": canal.Tipo, "activo": canal.Activo,
"webhook_url": webhookURL, "ultimo_error": canal.UltimoError,
"usar_whisper_audio": canal.UsarWhisperAudio, "usar_ocr_imagenes": canal.UsarOcrImagenes,
"usar_archivos_docs": canal.UsarArchivosDocs,
}
}
return c.JSON(fiber.Map{"items": out})
@@ -559,6 +560,7 @@ type umindCanalReq struct {
Activo bool `json:"activo"`
UsarWhisperAudio bool `json:"usar_whisper_audio"`
UsarOcrImagenes bool `json:"usar_ocr_imagenes"`
UsarArchivosDocs bool `json:"usar_archivos_docs"`
}
func CreateUmindCanalHandler(c *fiber.Ctx) error {
@@ -594,6 +596,7 @@ func CreateUmindCanalHandler(c *fiber.Ctx) error {
canal := &models.UmindCanal{
AgenteID: req.AgenteID, Tipo: req.Tipo, Activo: true, CredencialesEnc: credencialesEnc,
UsarWhisperAudio: req.UsarWhisperAudio, UsarOcrImagenes: req.UsarOcrImagenes,
UsarArchivosDocs: req.UsarArchivosDocs,
}
if err := models.CreateUmindCanal(canal); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
@@ -620,7 +623,7 @@ func UpdateUmindCanalHandler(c *fiber.Ctx) error {
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"})
}
updates := map[string]interface{}{"activo": req.Activo, "usar_whisper_audio": req.UsarWhisperAudio, "usar_ocr_imagenes": req.UsarOcrImagenes}
updates := map[string]interface{}{"activo": req.Activo, "usar_whisper_audio": req.UsarWhisperAudio, "usar_ocr_imagenes": req.UsarOcrImagenes, "usar_archivos_docs": req.UsarArchivosDocs}
if len(req.Credenciales) > 0 {
enc, err := services.CifrarCredencialesCanal(req.Credenciales)
if err != nil {