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>
69 lines
2.3 KiB
Go
69 lines
2.3 KiB
Go
package services
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/zlib"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// pdfDePrueba arma un PDF mínimo con el content stream comprimido, igual que
|
|
// los que genera cualquier programa que exporta a PDF.
|
|
func pdfDePrueba(t *testing.T, contenido string) []byte {
|
|
t.Helper()
|
|
var comp bytes.Buffer
|
|
zw := zlib.NewWriter(&comp)
|
|
if _, err := zw.Write([]byte(contenido)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
zw.Close()
|
|
|
|
var pdf bytes.Buffer
|
|
pdf.WriteString("%PDF-1.4\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n")
|
|
pdf.WriteString("2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n")
|
|
pdf.WriteString("3 0 obj<</Type/Page/Parent 2 0 R/Contents 4 0 R>>endobj\n")
|
|
pdf.WriteString("4 0 obj<</Length " + strconv.Itoa(comp.Len()) + "/Filter/FlateDecode>>stream\n")
|
|
pdf.Write(comp.Bytes())
|
|
pdf.WriteString("\nendstream endobj\ntrailer<</Root 1 0 R>>\n%%EOF")
|
|
return pdf.Bytes()
|
|
}
|
|
|
|
func TestExtraerTextoDeArchivoPDFDigital(t *testing.T) {
|
|
contenido := `BT /F1 12 Tf 72 720 Td (FACTURA DE VENTA No. 1042) Tj T* ` +
|
|
`(Cliente: Acme SAS NIT 900.123.456-7) Tj T* (Total: \$1.500.000 COP) Tj ET`
|
|
|
|
got, err := ExtraerTextoDeArchivo(0, "factura.pdf", pdfDePrueba(t, contenido))
|
|
if err != nil {
|
|
t.Fatalf("ExtraerTextoDeArchivo: %v", err)
|
|
}
|
|
for _, quiero := range []string{"FACTURA DE VENTA No. 1042", "Acme SAS", "NIT 900.123.456-7", "$1.500.000 COP"} {
|
|
if !strings.Contains(got, quiero) {
|
|
t.Errorf("falta %q en el texto extraído:\n%s", quiero, got)
|
|
}
|
|
}
|
|
// T* separa renglones: sin eso la factura llega al agente como un chorizo.
|
|
if lineas := strings.Count(strings.TrimSpace(got), "\n"); lineas < 2 {
|
|
t.Errorf("esperaba al menos 3 renglones, hay %d:\n%s", lineas+1, got)
|
|
}
|
|
}
|
|
|
|
func TestExtraerTextoDeArchivoTexto(t *testing.T) {
|
|
got, err := ExtraerTextoDeArchivo(0, "notas.txt", []byte("hola\nmundo"))
|
|
if err != nil || got != "hola\nmundo" {
|
|
t.Errorf("got %q, err %v", got, err)
|
|
}
|
|
if _, err := ExtraerTextoDeArchivo(0, "cosa.exe", []byte("x")); err == nil {
|
|
t.Error("una extensión desconocida debería devolver error")
|
|
}
|
|
}
|
|
|
|
func TestTextoDeArchivoParaAgente(t *testing.T) {
|
|
got := TextoDeArchivoParaAgente("factura.pdf", "me cobraron de más", "Total: 1000")
|
|
for _, quiero := range []string{"factura.pdf", "me cobraron de más", "Total: 1000"} {
|
|
if !strings.Contains(got, quiero) {
|
|
t.Errorf("falta %q en:\n%s", quiero, got)
|
|
}
|
|
}
|
|
}
|