El agente estaba dando https://escuelametropolitana.com/login.php, una
ruta que no existe: el prompt decía "no inventes datos" pero no decía
nada de URLs, teléfonos ni correos, que es justo lo que un modelo
confabula con más confianza porque "sabe" que un login suele estar en
/login.php. Un enlace inventado deja a la persona sin poder completar el
trámite, peor que no responder.
- Regla explícita: URLs, teléfonos, WhatsApp y correos solo si aparecen
LITERALMENTE en los resultados de buscar_conocimiento, copiados carácter
por carácter, sin completar rutas ni cambiar el dominio.
- Widget: los enlaces se vuelven clickeables. El contenido sale de páginas
crawleadas, así que NO se inserta como HTML: se parte por URL y cada
trozo va con createTextNode o un <a> con rel=noopener noreferrer
nofollow. Verificado que un <img onerror> o un <script> en la respuesta
quedan escapados como texto inerte.
- WhatsApp: preview_url=true, así el primer enlace se ve como tarjeta
clickeable en vez de texto pelado.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Es la base del cobro por uso: hasta ahora no había ninguna medición de
consumo en todo el repo.
- UmindUso registra cada evento facturable con el costo YA calculado al
precio vigente del plan. Congelarlo evita que subir un precio revalúe
consumo pasado, que haría indefendible una factura ante un reclamo.
- callAI devuelve los tokens que reportó el proveedor (campo usage, igual
en todos los OpenAI-compatibles; input+output en Anthropic). Se mide
cada ronda de tool-calling, no solo la última: todas gastan tokens.
- ExtraerTextoOCR y TranscribirAudioSelfHosted reciben agenteID; 0 = no
medir, que es lo que pasan los botones "Probar" del panel de staff.
- Aviso al superar el tope del plan, una vez por mes y sin cortar el
servicio. El flag de "ya avisé" es en memoria a propósito.
- GET /app/umind/uso con filtros de fecha: resumen por tipo + detalle.
- Test del cálculo de costo por tipo, incluida fracción de 1k tokens y
tenant sin plan.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nueva tabla umind_eventos_log + tab "Auditoría" en el panel: errores y
eventos que hasta ahora solo se podían ver pidiendo los logs del servidor
(fallos al llamar al AI, respuestas sin texto usable, tools/webhooks que
fallan, tokens de correo que no se pueden refrescar, firmas de WhatsApp
inválidas, errores procesando mensajes de Telegram/WhatsApp) quedan
registrados por agente, visibles directo en el panel con el JSON crudo
expandible para diagnosticar sin acceso al servidor.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Una sesión de widget que pegó contra un bug ya arreglado (ej. el
thought_signature de Gemini) podía quedar con un turno "assistant" vacío
guardado en umind_mensajes — como el widget mantiene la misma session_id en
sessionStorage mientras no se cierra la pestaña, cada mensaje nuevo
reenviaba ese historial roto y el modelo volvía a responder vacío en
cascada. El chat de prueba del panel no lo sufría porque arranca session_id
nueva en cada carga de página.
Dos cambios: no se guarda un mensaje "assistant" con contenido vacío de acá
en más, y al armar el prompt se descartan los que ya hayan quedado guardados
así — una sesión vieja rota se autorepara sola en el próximo mensaje, sin
que el visitante tenga que abrir una pestaña nueva.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cuando el AI respondía sin error pero con un content que no era string
(o vacío), ProcessWidgetMessage caía en silencio al mensaje genérico de
"dame más detalle" sin dejar ningún rastro en el log — no había forma de
saber qué pasó. Ahora loguea el JSON crudo (agentMessage.Raw) en ese caso,
y también cuando se agotan las 3 rondas de tool-calling sin llegar a una
respuesta final.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Un tenant (negocio/sitio, dueño de los dominios permitidos) puede tener
varios UmindAgente independientes (ej. "Ventas", "Soporte"), cada uno con
su propia config de IA, tono, base de conocimiento, tools, canales y
conexión de correo. El site_key también pasa a ser por agente, así cada
uno tiene su propio <script> de widget embebible y su propio color.
Backend:
- Nuevo modelo UmindAgente (pkg/models/umind_agente.go), con SiteKey,
AiConfigID, Tono, MensajeBienvenida y Color — campos que antes vivían en
UmindTenant y se sacan de ahí (las columnas viejas quedan huérfanas sin
usar, no se hace DROP COLUMN).
- UmindDocumento, UmindChunk, UmindHerramienta, UmindCanal, UmindConexion y
UmindMensaje pasan de TenantID a AgenteID. El campo se agrega sin
"not null" para no romper el ALTER TABLE en Postgres sobre tablas que ya
tienen filas (ej. emetropolitana).
- migrations.MigrarUmindAgentes(): idempotente, crea un agente "Principal"
por cada tenant existente heredando lo que ya tenía configurado, y mueve
sus datos de tenant_id a agente_id. Corre en cada arranque normal, mismo
criterio que los Seed* — nada se rompe para los tenants ya en producción.
- Motor del agente, widget, canales (Telegram/WhatsApp) y OAuth de correo
ahora operan sobre UmindAgente; el tenant solo se consulta para el
chequeo de dominio permitido y el nombre del negocio que ve el visitante.
Frontend: nueva jerarquía de navegación tenant → lista de agentes
(TenantAgentes.vue) → detalle de un agente (AgenteDetail.vue, antes
TenantDetail.vue) con las mismas 6 tabs de siempre, ahora por agente. El
modal de tenant en el sidebar se achica a nombre/dominios/activo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ese historial se reenvía completo en cada turno junto con el system prompt
y las tools — con Gemini como proveedor principal, sin acceso fácil a
caching de contexto (su API de caching no pasa por la capa de
compatibilidad OpenAI que usamos), la forma más directa de bajar el costo
por mensaje en charlas largas es mandar menos historial repetido.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- providerDefaultURL no tenía caso para "gemini": sin Base URL manual caía
al default (la URL de OpenAI) y fallaba siempre con la API key de Google.
Se agrega la capa de compatibilidad OpenAI de Gemini
(generativelanguage.googleapis.com/v1beta/openai).
- El widget muestra el texto del agente tal cual (textContent, no innerHTML
— deliberado, evita XSS si algo del contenido crawleado se cuela en una
respuesta). El modelo respondía con **negrita** y listas en Markdown que
se veían como asteriscos/guiones sueltos en vez de renderizarse. Se
instruye al system prompt a no usar Markdown, en vez de sumar un parser
al widget. Aplica a los tres canales (widget, Telegram, WhatsApp) y al
chat de prueba del panel, porque todos comparten el mismo prompt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Backend:
- UmindConexion: cuenta de correo conectada por tenant vía OAuth2
(golang.org/x/oauth2, promovida de indirecta a directa), tokens cifrados
en reposo con el mismo AES-GCM+APP_KEY que ya usan tools/canales.
- Flujo completo: /app/umind/conexiones/conectar redirige a Google/Microsoft,
/callback/:proveedor intercambia el code (state autoverificable por HMAC,
sin tabla de estados pendientes), refresh on-demand antes de cada uso.
- Dos tools nuevas para el agente (enviar_correo/leer_bandeja) que aparecen
solo si el tenant tiene una conexión activa, vía Gmail API / Microsoft
Graph directo (sin el SDK pesado de Google).
- Requiere que el dueño del proyecto cree las apps OAuth en Google Cloud
Console / Azure y cargue GOOGLE_OAUTH_CLIENT_ID/SECRET y
MS_OAUTH_CLIENT_ID/SECRET — sin eso los botones de conectar fallan con un
mensaje claro, no en silencio.
Frontend: rediseño del orquestador — layout de sidebar fijo (reemplaza el
navbar + lista de página completa), modo oscuro vía prefers-color-scheme,
tabs en pill, y la nueva tab "Conexiones".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SPA nueva en /orchestrator (Vue 3 + Vite, servida por el mismo binario Go
bajo /orchestrator para que la cookie de sesión funcione sin tocar CORS),
reemplaza al panel Alpine.js como punto de entrada del menú.
Backend, todo aditivo sobre el motor de uMind ya existente:
- UmindHerramienta: tools custom por tenant que llaman un webhook HTTP,
integradas al loop de function-calling existente. Cliente HTTP con
guardas SSRF (bloqueo de IPs privadas/loopback/link-local resuelto en el
momento de conectar, no antes, para cerrar la ventana de DNS rebinding)
que no existían en el proyecto.
- UmindCanal: Telegram y WhatsApp Business Cloud API como canales
adicionales del mismo agente que ya atiende el widget web, ambos
reusando ProcessWidgetMessage. WhatsApp valida X-Hub-Signature-256.
Credenciales cifradas en reposo con el mismo AES-GCM+APP_KEY que ya usa
el proyecto para la contraseña SMTP (primer uso para secretos de uMind).
- Se conecta middlewares.Limit() (rate limiter que existía pero no se
usaba en ningún lado) al widget público y a los webhooks nuevos.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Multi-tenant dentro de soft_usite, reutilizando la infraestructura ya
existente (AiConfig, motor de function-calling del agente de Telegram)
en vez de un servicio nuevo aparte:
- UmindTenant: sitio/cliente con dominios permitidos, config de IA para
el chat y personalidad/tono.
- Ingesta: crawler simple (mismo dominio, N páginas) + chunking +
embeddings (config global con módulo "umind_embeddings", pensada
para OpenAI ya que Claude no ofrece embeddings) guardados como JSON,
con búsqueda por similitud coseno en memoria (sin pgvector todavía).
- Agente acotado: única herramienta buscar_conocimiento, sin acceso a
nada interno — si no encuentra la respuesta, lo dice en vez de
inventar.
- Widget público (/widget/umind.js + /widget/:site_key/*), autenticado
por site_key + validación de dominio (Origin/Referer), no por
secreto, ya que la key viaja en el HTML público del sitio instalado.
- Panel /app/umind: tenants, estado de ingesta, historial de
conversaciones por sesión.