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
+23 -19
View File
@@ -198,7 +198,7 @@ async function copiarWidget() {
function canalVacio() {
return {
tipo: 'telegram', bot_token: '', phone_number_id: '', access_token: '', app_secret: '', verify_token: '',
usar_whisper_audio: false, usar_ocr_imagenes: false,
usar_whisper_audio: false, usar_ocr_imagenes: false, usar_archivos_docs: false,
}
}
@@ -226,6 +226,7 @@ async function guardarCanal() {
await api.post(apiUmind('/umind/canales'), {
agente_id: agenteIdNum.value, tipo: canalForm.value.tipo, credenciales, activo: true,
usar_whisper_audio: canalForm.value.usar_whisper_audio, usar_ocr_imagenes: canalForm.value.usar_ocr_imagenes,
usar_archivos_docs: canalForm.value.usar_archivos_docs,
})
showCanalForm.value = false
await cargarCanales()
@@ -234,29 +235,24 @@ async function guardarCanal() {
}
}
async function toggleCanal(c) {
// El PUT de canales manda los tres interruptores siempre: si alguno faltara, el
// backend lo tomaría como false y lo apagaría sin que nadie lo pidiera.
async function guardarInterruptores(c, cambios) {
await api.put(apiUmind(`/umind/canales/${c.ID}`), {
activo: !c.activo, credenciales: {},
usar_whisper_audio: c.usar_whisper_audio, usar_ocr_imagenes: c.usar_ocr_imagenes,
activo: c.activo,
credenciales: {},
usar_whisper_audio: c.usar_whisper_audio,
usar_ocr_imagenes: c.usar_ocr_imagenes,
usar_archivos_docs: c.usar_archivos_docs,
...cambios,
})
await cargarCanales()
}
async function toggleCanalWhisper(c) {
await api.put(apiUmind(`/umind/canales/${c.ID}`), {
activo: c.activo, credenciales: {},
usar_whisper_audio: !c.usar_whisper_audio, usar_ocr_imagenes: c.usar_ocr_imagenes,
})
await cargarCanales()
}
async function toggleCanalOcr(c) {
await api.put(apiUmind(`/umind/canales/${c.ID}`), {
activo: c.activo, credenciales: {},
usar_whisper_audio: c.usar_whisper_audio, usar_ocr_imagenes: !c.usar_ocr_imagenes,
})
await cargarCanales()
}
const toggleCanal = (c) => guardarInterruptores(c, { activo: !c.activo })
const toggleCanalWhisper = (c) => guardarInterruptores(c, { usar_whisper_audio: !c.usar_whisper_audio })
const toggleCanalOcr = (c) => guardarInterruptores(c, { usar_ocr_imagenes: !c.usar_ocr_imagenes })
const toggleCanalArchivos = (c) => guardarInterruptores(c, { usar_archivos_docs: !c.usar_archivos_docs })
async function eliminarCanal(c) {
if (!confirm(`¿Eliminar el canal ${c.tipo}?`)) return
@@ -585,6 +581,10 @@ watch(
<input type="checkbox" :checked="c.usar_ocr_imagenes" @change="toggleCanalOcr(c)" class="rounded border-borde text-brand focus:ring-brand" />
Leer texto de imágenes (OCR)
</label>
<label class="flex items-center gap-1.5 text-texto cursor-pointer">
<input type="checkbox" :checked="c.usar_archivos_docs" @change="toggleCanalArchivos(c)" class="rounded border-borde text-brand focus:ring-brand" />
Leer archivos adjuntos (PDF, Word, texto)
</label>
</div>
<p class="label mt-1 break-all">
Webhook: <code class="bg-elevado px-1 rounded">{{ c.webhook_url }}</code>
@@ -640,6 +640,10 @@ watch(
<input type="checkbox" v-model="canalForm.usar_ocr_imagenes" class="rounded border-borde text-brand focus:ring-brand" />
Leer texto de imágenes con OCR
</label>
<label class="flex items-center gap-2 text-sm text-texto cursor-pointer">
<input type="checkbox" v-model="canalForm.usar_archivos_docs" class="rounded border-borde text-brand focus:ring-brand" />
Leer archivos adjuntos (PDF, Word, texto)
</label>
</div>
<div class="flex justify-end gap-2 pt-2">
<button type="button" class="btn-ghost" @click="showCanalForm = false">Cancelar</button>