En "Dónde atiende", el canal correo: el agente lee una casilla y responde solo
los correos que llegan, con su base de conocimiento, igual que atiende WhatsApp.
Como el correo no tiene webhook, se revisa por intervalo — y el intervalo lo
elige el cliente por canal (2 a 60 minutos): una inmobiliaria quiere 2, a un
estudio contable con 30 le sobra. El cron corre cada minuto pero cada casilla
se revisa solo cuando le toca, con un pool de 8 para que 100 casillas no salgan
a la red en el mismo instante.
Las guardas que separan "asistente" de "incidente", cada una con su test:
nunca responde correo automático ni se responde a sí mismo (el bucle con otro
autoresponder); la revisión se marca ANTES de conectar, así una contraseña
cambiada no martilla el login cada minuto hasta que el host del cliente nos
bloquea; y el correo se marca leído recién cuando la respuesta salió — si el
envío falla, queda sin leer y se reintenta.
En "Lo que sabe", las cuentas de correo: casillas que el agente consulta a
pedido — "revisame los correos de hoy y haceme un resumen" — sin nada de fondo.
Solo lectura en serio: Peek, INBOX en read-only, y un test que falla si alguien
le agrega un marcado. Ahora se pueden conectar varias por agente; con más de
una, el modelo pregunta cuál en vez de adivinar — resumirle a alguien la
casilla que no pidió no es un error menor. El alta pide correo y contraseña:
el host se deduce (mail.<dominio>) y el campo técnico aparece recién si eso
falla. Se prueba la conexión antes de guardar, con la persona mirando.
Enviar por una cuenta conectada está bloqueado a propósito: para responder
correos está el canal, con sus guardas. Una tool de envío sin límites es una
máquina de spam con el dominio del cliente.
La navegación acompaña: "Lo que sabe" agrupa Información, Cuentas de correo y
Herramientas — tres formas de saber, no tres pantallas sueltas — y Avanzado
queda solo con Problemas.
El SMTP se unificó en una sola implementación que comparten soporte y el canal:
el bug de STARTTLS que abría dos conexiones ya se pagó una vez.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Una fuente crawleada decía "9 fragmentos" y nada más. Las notas y archivos al
menos tienen Editar, que muestra su texto; un sitio no tiene nada — era una
caja negra, sin forma de saber si el crawler leyó los precios o el pie de
página. Y cuando el agente contesta mal, la primera pregunta es justamente
qué tiene cargado de verdad.
Cada fuente gana un "Ver" que abre sus fragmentos tal como quedaron indexados,
numerados y en orden, con la advertencia que importa: esto es exactamente lo
que el asistente puede consultar — si acá falta algo, eso mismo le va a faltar
en las respuestas.
El botón se apaga cuando la fuente no tiene fragmentos todavía (procesando o
con error): abrir un visor vacío no informa nada.
El endpoint pasa por accesoDocumento como todos los sub-recursos, y devuelve
solo el contenido — el embedding no viaja: son miles de números que no le
sirven a nadie en pantalla.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El modelo soportaba una config de IA por espacio desde hace varias fases, pero
no había forma de cargarla: ni el cliente desde el portal, ni el staff por él.
Quedaba como una columna que solo se podía llenar tocando la base a mano.
Ahora hay una pantalla — "Tu IA", desde la lista de agentes — donde se conecta
una cuenta de OpenAI, Anthropic, Gemini, Groq, DeepSeek, Qwen u Ollama. Con el
botón "Probar", que manda una consulta real: una clave vencida se descubre ahí
y no cuando un cliente escribe y no le contestan.
El aislamiento es la parte delicada, y va en tres capas:
Las configs globales son del staff. Un cliente puede usarlas —le aparecen en el
selector— pero no editarlas ni borrarlas; el acceso corta antes de mirar
permisos, así que ni siquiera se le confirma que existen. Sin eso, cualquiera
podría cambiarle el proveedor de IA a todos los demás o dejarlos sin servicio.
El tenant sale del alcance ya validado, nunca del body. Si viniera del cuerpo
de la petición, bastaría con cambiar el número para colgarle una config a otro
cliente, y con ella una clave que no es suya.
El tenant no se puede mover en una edición, por lo mismo al revés: sería
regalarle la propia.
La clave se guarda cifrada y no vuelve nunca al navegador — ni al dueño. Solo
sus últimos cuatro caracteres, que alcanzan para reconocer cuál cargó.
Borrar está bloqueado si hay agentes usándola: si no, quedarían apuntando a
algo inexistente y cayendo al proveedor global sin que nadie se entere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El catálogo de rubros da un arranque genérico, pero el mejor punto de partida
para el segundo restaurante es el primero — el que ya tiene el conocimiento
real, el tono ajustado y las herramientas andando. Ahora se puede copiar: se
lleva conocimiento, tono, bienvenida, color, config de IA y herramientas.
Tres cosas no se copian, y es a propósito:
Los canales. Llevan las credenciales de una cuenta concreta de WhatsApp o
Telegram; copiarlas haría que dos agentes contesten por el mismo número.
Las claves de las herramientas. Son secretos de un tercero, atados a una
cuenta. La copia llega sin ellas y, si la herramienta las necesitaba, llega
desactivada — para que la falta se note al configurarla y no cuando un cliente
recibe un error.
La site_key. Es la identidad pública del widget y tiene índice único:
compartirla sería servir dos agentes distintos bajo el mismo nombre.
El conocimiento se rehace desde cero en vez de copiar los vectores. Es más
lento, pero los chunks viejos pueden venir de otro modelo de embeddings, y
mezclar vectores de modelos distintos rompe la comparación por similitud —
la búsqueda devolvería cualquier cosa.
Permisos: se verifica el acceso al agente de origen y también al tenant
destino. Copiar es crear, y crear en un tenant ajeno tampoco corresponde.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Logo. Hasta ahora no había: era el texto "uM" en un cuadrado, con tamaño y
esquinas distintas en cada pantalla. Ahora hay un mark — la "u" del nombre con
el punto de pulso integrado — probado a 16px, 20px y 32px sobre claro y oscuro,
que es donde se cae la mayoría de los logos. Reemplaza el texto en el portal,
el header, el sidebar del Studio y la promo, y va también de favicon.
Plantillas por rubro. Un agente recién creado no sabía nada, y escribir el
conocimiento desde cero frente a un campo en blanco es donde la mayoría
abandona. Al crear uno se puede elegir un punto de partida —tienda,
restaurante, consultorio, servicios, peluquería o la base genérica— y arranca
con sus notas ya escritas, listas para editar.
Los valores de ejemplo van entre corchetes a propósito, para que se vea que hay
que reemplazarlos: una nota que parezca dato real termina en boca del asistente.
Un test lo verifica nota por nota. Y la plantilla de salud abre con el límite —
el asistente no da consejo médico ni interpreta síntomas — porque es el único
rubro donde una respuesta inventada hace daño de verdad.
El catálogo vive en código, no en base: es contenido editorial que mejoramos
nosotros, no configuración que cada instalación toca por su lado. Las notas se
cargan en segundo plano porque cada una necesita sus embeddings, y hacer
esperar el alta sería castigar justo al que eligió plantilla.
Con las opciones nuevas el formulario pasaba de largo la pantalla y el botón de
guardar quedaba abajo del borde: el modal ahora tiene su propio scroll.
Y la página promocional de uMind queda servida en /umind.html.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Cuatro cosas que ya estaban a medio construir y no se usaban como canal.
1. El badge del widget ahora es un enlace con UTM. Cada cliente ya te
estaba dando exposición en su sitio y no se capitalizaba. El host sale
de la URL del propio script, así funciona igual en cualquier entorno.
2. Reporte del mes por agente en Excel: conversaciones atendidas, con qué
las abrió el visitante, y el consumo desglosado. Es lo que el cliente
necesita para justificar el gasto puertas adentro — un panel al que hay
que entrar no sirve para eso, un archivo que se reenvía sí. Reusa el
generador de xlsx del cronograma.
3. Aviso automático de agentes sin base de conocimiento (cron diario). Un
agente sin fuentes responde de memoria e inventa datos, el cliente
concluye que el producto no sirve y se va. Es el punto donde más gente
se cae y se detecta solo. Se avisa UNA vez por agente, usando el log de
auditoría como registro de envío para no necesitar tabla nueva ni
convertir el recordatorio en spam.
4. Tarjeta de uMind en el dashboard del portal: atajo si el cliente ya lo
tiene, oferta si no. Es el punto de contacto más barato que hay — ya
entró, ya confía, y el cobro se suma a la factura que ya recibe. El
mensaje lidera con notas de voz y fotos, que es lo más difícil de
copiar de lo que tenemos.
La consulta de conversaciones agrupa en SQL en vez de traerse el historial
entero para agrupar en Go.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Acá es donde uMind deja de ser una herramienta interna: el cliente entra
a /portal/studio con su sesión de portal y gestiona lo suyo.
- UmindScopePortal/UmindScopeStaff es el ÚNICO punto donde se decide el
alcance. El del cliente sale de GetClienteIDsForPortalUser, el mismo
que ya autoriza el resto del portal. nil = staff sin restricción,
slice vacío = no ve nada; una ruta sin scope también cae en "no ve
nada" para que olvidarse el middleware falle visible y no abra todo.
- Un solo set de handlers montado bajo /app/umind y /portal/umind
(RegistrarRutasUmind). Duplicarlos sería duplicar las chances de
olvidar un chequeo.
- Guarda de acceso en TODOS los handlers, incluidos los sub-recursos que
llegan por :id (documento, tool, canal, conexión): hay que cargarlos
para saber de quién son, si no un cliente podría borrar el canal de
otro adivinando el id. Responden 404, no 403: un 403 confirmaría que
el recurso existe.
- Cierra un bug preexistente: las lecturas GET /app/umind/* no tenían
SoloAdmin ni pasaban por MenuMiddleware, así que cualquier usuario de
staff podía leer los tenants de todos los clientes.
- Límite de agentes por plan (409 con mensaje claro). Un tenant sin plan
no tiene límite: cortarles de golpe sería peor que dejarlos como estaban.
- /umind/ai-configs reemplaza con alcance a /app/api/ai-config/select,
que devolvía TODAS las configs del sistema.
- El SPA deduce por la URL si es staff o cliente (base del router,
prefijo de API y URL de login) y oculta lo que es solo de staff.
- Test de aislamiento entre clientes: 7 casos, incluido que un scope
vacío no se confunda con staff.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>