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:
co-authored by
Claude Opus 5
parent
e681f41639
commit
c4b0305112
@@ -1,12 +1,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -24,66 +19,10 @@ var variablesPorTipo = map[string]string{
|
||||
|
||||
const variablesComunes = `{{.Fecha}}, {{.EmpresaNombre}}, {{.EmpresaWeb}}`
|
||||
|
||||
// ExtraerTextoDePlantilla saca el texto de un archivo subido para usarlo como
|
||||
// referencia. Los formatos de texto se leen directo; el .docx es un zip con XML
|
||||
// adentro (stdlib alcanza) y las imágenes pasan por OCR.
|
||||
// ExtraerTextoDePlantilla lee el archivo que subió el admin como referencia.
|
||||
// agenteID 0: es una acción del staff, no se le cobra a ningún cliente.
|
||||
func ExtraerTextoDePlantilla(nombreArchivo string, datos []byte) (string, error) {
|
||||
ext := strings.ToLower(filepath.Ext(nombreArchivo))
|
||||
switch ext {
|
||||
case ".html", ".htm", ".txt", ".md":
|
||||
return string(datos), nil
|
||||
case ".docx":
|
||||
return textoDeDocx(datos)
|
||||
case ".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tif", ".tiff":
|
||||
mime := "image/png"
|
||||
if ext == ".jpg" || ext == ".jpeg" {
|
||||
mime = "image/jpeg"
|
||||
} else if ext != ".png" {
|
||||
mime = "image/" + strings.TrimPrefix(ext, ".")
|
||||
}
|
||||
// agenteID 0: es una acción del staff, no se le cobra a ningún cliente.
|
||||
return ExtraerTextoOCR(0, datos, mime)
|
||||
case ".pdf":
|
||||
return "", fmt.Errorf("el PDF todavía no se puede leer acá; exportalo a .docx o subí una captura de pantalla del documento")
|
||||
default:
|
||||
return "", fmt.Errorf("formato %s no soportado: subí .docx, .html, .txt o una imagen del documento", ext)
|
||||
}
|
||||
}
|
||||
|
||||
var etiquetaXML = regexp.MustCompile(`<[^>]+>`)
|
||||
|
||||
// textoDeDocx lee word/document.xml del .docx y lo aplana a texto. No pretende
|
||||
// conservar el formato: la IA solo necesita el contenido y el orden.
|
||||
func textoDeDocx(datos []byte) (string, error) {
|
||||
zr, err := zip.NewReader(bytes.NewReader(datos), int64(len(datos)))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("el .docx no se pudo abrir: %w", err)
|
||||
}
|
||||
for _, f := range zr.File {
|
||||
if f.Name != "word/document.xml" {
|
||||
continue
|
||||
}
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer rc.Close()
|
||||
xmlBytes, err := io.ReadAll(io.LimitReader(rc, 8<<20))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s := string(xmlBytes)
|
||||
// Un párrafo y un salto de línea explícito valen como salto de línea;
|
||||
// el resto de las etiquetas se descarta.
|
||||
s = strings.ReplaceAll(s, "</w:p>", "\n")
|
||||
s = strings.ReplaceAll(s, "<w:br/>", "\n")
|
||||
s = strings.ReplaceAll(s, "</w:tr>", "\n")
|
||||
s = strings.ReplaceAll(s, "</w:tc>", "\t")
|
||||
s = etiquetaXML.ReplaceAllString(s, "")
|
||||
s = strings.NewReplacer("&", "&", "<", "<", ">", ">", """, `"`, "'", "'").Replace(s)
|
||||
return strings.TrimSpace(s), nil
|
||||
}
|
||||
return "", fmt.Errorf("el archivo no parece un .docx (no tiene word/document.xml)")
|
||||
return ExtraerTextoDeArchivo(0, nombreArchivo, datos)
|
||||
}
|
||||
|
||||
// ConvertirEnPlantilla le pide a la IA que rearme el documento como plantilla
|
||||
|
||||
Reference in New Issue
Block a user