hermes-agent

Le puse dos cerebros a mi IA (III) - Publicación

Tocaba abrirlo para que mi socio también pudiera usarlo: Telegram fue fácil, pero meterlo en el chat interno de la empresa exigió un traductor de protocolo, tres rediseños hasta encontrar un componente ya escrito, y una persecución de bugs silenciosos antes de publicarlo en producción.

Le puse dos cerebros a mi IA (III) - Publicación
En la primera y la segunda parte monté y configuré el asistente. Esta es la de abrirlo para que alguien más lo use — mi socio, el otro admin de la app, sin depender de mí para nada.
💡
En resumen, para quien tenga prisa

Metí a Hermes en dos sitios: Telegram (rápido, sin sustos) y dentro de nuestra propia app de empresa, como una pestaña de chat de verdad para cualquier empleado. Lo segundo llevó su tiempo: Hermes solo habla un protocolo con estado, así que hizo falta un puente aparte; el diseño lo rehice tres veces hasta encontrar un componente de chat que ya existía en el código, olvidado, sin conectar nunca a nada; y un bug de "las respuestas se guardan bien pero nunca aparecen en pantalla" resultó ser primo hermano de uno que ya había cazado antes en las notificaciones del panel — dos piezas del sistema escuchando el mismo canal, cerrándose la una a la otra sin querer. Al final, para poder publicarlo de verdad en producción (nuestra app vive en la nube, Hermes en el servidor de casa) hizo falta un túnel, cuidando bien la seguridad antes de darlo por publicado. Y de propina, fusioné sin querer todo el trabajo en la rama equivocada — se deshizo limpio, sin perder nada, y se volvió a hacer en la buena.

📱 Telegram: la parte fácil

Empezar por Telegram fue casi un trámite comparado con lo que vino después. Un bot nuevo con @BotFather, el token en el .env del contenedor de Hermes, un ID de usuario para que solo yo (de momento) pudiera hablarle, y listo — funcionando a la primera.


🧩 El problema de fondo: Hermes no habla como habla todo el mundo

Meter a Hermes dentro de nuestra propia app —para que cualquier empleado, empezando por mi socio, pudiera hablarle desde donde ya vive el resto de herramientas internas, sin tocar terminal ni saber qué es un contenedor— chocó enseguida con algo: Hermes no tiene ningún sitio al que mandarle "aquí tienes un mensaje, dame la respuesta" y ya. Habla un protocolo con sesión y memoria propia, pensado para una conexión que se queda abierta todo el rato, no para "pregunta y respuesta" sueltas.

Nuestra app, en cambio, ya tiene un patrón muy asentado para hablar con servicios de IA externos: uno le manda el aviso a un servicio aparte, ese servicio hace el trabajo largo, y cuando termina, llama de vuelta él solo a avisar que ya está. Ese patrón no encajaba con "sesión abierta todo el rato" de ninguna manera directa.

La solución fue montar una pieza nueva, pequeña, dedicada solo a esto: un traductor entre los dos mundos. Por un lado habla el protocolo raro de Hermes, con su sesión persistente; por el otro, expone la forma sencilla de "mándame un mensaje, ya te aviso yo cuando tenga la respuesta" que el resto de la app entiende. Ese traductor también decide, por su cuenta, ignorar todo el "pensar en voz alta" que Hermes va soltando mientras trabaja — las herramientas que prueba, los borradores a medias — y solo entrega la respuesta final, ya limpia. Nadie quiere ver a la IA dudar en directo delante de un cliente.


🔑 Encontrar la puerta correcta

Para que ese traductor pudiera hablar con Hermes hacía falta autenticarse. Mi primer intento fue el "correcto" sobre el papel: un usuario y contraseña propios. Configurarlo, probarlo... y nada, seguía rechazando la conexión como si no hubiera ningún usuario.

🐛
El descubrimiento

Resultó que ese sistema de usuario/contraseña solo se activa cuando Hermes está expuesto hacia fuera — en modo "solo accesible desde el propio servidor" (que es como estaba, todavía sin publicar) ni siquiera se molesta en mirarlo. En su lugar usa algo mucho más simple: una única clave fija, pensada exactamente para este caso, un programa hablando con otro programa dentro de la misma máquina. Toda la vuelta con usuario y contraseña sobraba por completo.

Con la clave simple configurada, la conexión funcionó a la primera... por HTTP normal. Por el protocolo especial de sesión seguía fallando, sin ningún motivo aparente.

🐛
El culpable: un carácter de más

La clave la había generado con un método que a veces mete símbolos como + o =. Por una cabecera normal esos símbolos viajan sin problema. Metidos dentro de la dirección de conexión del protocolo especial, en cambio, el + se interpreta como un espacio — la clave llegaba corrompida sin que nadie se enterase. La solución fue generarla de otra forma, pensada específicamente para ir dentro de una dirección. Un cambio de una función, y la conexión se abrió del todo: sesión creada, mensaje mandado, respuesta real llegando de Hermes.

🎨 Tres intentos de diseño, y el que ya existía escondido

Con la parte técnica funcionando, tocaba que se viera bien. El primer diseño, hecho a mano, mezclando estilos por mi cuenta: "feo". El segundo, con avatares, colores propios, más cuidado: "sigue pareciendo horrendo". A la tercera empecé a mirar mejor por dentro del propio código de la app, en vez de inventar desde cero — y ahí apareció algo que no esperaba: un componente de chat genérico, ya escrito, ya diseñado, con los colores y el estilo exactos del resto del panel... que nadie había conectado nunca a ningún sitio. Un diseño terminado y abandonado, esperando.

Copiar su mismo lenguaje visual —los mismos colores, la misma forma de las burbujas, los mismos iconos— resolvió en un intento lo que tres rediseños desde cero no habían conseguido. La lección no fue de diseño, fue de dónde buscar primero: antes de inventar un estilo nuevo, merece la pena mirar si ya existe uno hecho, guardado en algún rincón del propio proyecto.

Quedaron, además, dos detalles pequeños que le dieron mucha vida al asunto: avatares de verdad (con reserva automática a un avatar generado si la foto de perfil falla al cargar, que en algún momento del propio proceso pasó de verdad) y una barra encima del cuadro de texto con el modelo que está respondiendo y cuánta "memoria de conversación" lleva usada — información que Hermes ya mandaba por su cuenta y que hasta entonces se estaba tirando sin usar.


🔇 El aviso que se guardaba bien pero nunca se veía

Con todo funcionando y bonito, apareció un fallo que ya había visto antes, en otra entrega: la respuesta de Hermes se guardaba perfectamente — se podía comprobar en la base de datos, ahí estaba, completa — pero nunca aparecía en pantalla en directo. Sin ningún error, en ningún sitio.

🐛
Otra vez lo mismo, en otro disfraz

Dos partes distintas de la pantalla necesitaban escuchar el mismo canal en tiempo real: una para pintar la conversación mientras la tienes abierta, otra para poder avisarte aunque estés mirando otra cosa. Al cambiar de pestaña, la primera se desconectaba de forma educada... cerrando sin querer el canal entero, incluida la suscripción de la segunda, que se suponía que tenía que seguir viva. Exactamente la misma familia de fallo silencioso de un lío parecido de hace poco: algo que se cierra "bien" y de paso apaga algo que no debería. El arreglo fue que cada una quitase solo su propia parte, sin tocar la del otro.

🚧 Dos guardias, no uno

Para que el traductor pudiera avisar a la app cuando Hermes terminaba de responder, hacía falta una puerta trasera sin necesidad de haber iniciado sesión — un aviso automático, no una persona con usuario y contraseña. La primera comprobación de seguridad se ajustó sin problema. Aun así, la respuesta seguía sin llegar bien: en vez del aviso real, llegaba la página de bienvenida pública de la empresa, disfrazada de "todo correcto".

🐛
La segunda puerta, escondida

Había una segunda comprobación, completamente aparte de la primera, pensada para otra cosa —mandar a cualquier visitante sin sesión iniciada a la página pública en vez de enseñarle un error feo— que también se aplicaba aquí sin que nadie se lo pidiera. Dos guardias distintos, cada uno con su propia lista de excepciones, y solo se había avisado a uno de los dos. Añadir el aviso automático también a la lista del segundo guardia arregló el problema del todo.

🌐 Publicarlo de verdad: un túnel, y revisar bien la seguridad

Nuestra app vive en la nube; Hermes, en el servidor de casa. Para que la una pudiera hablar con el otro de verdad en producción, sin ninguna ruta directa entre ambos sitios, hizo falta el mismo truco que ya usamos para otras herramientas internas: un túnel que expone el traductor bajo un nombre público propio, sin abrir ningún puerto directamente al servidor.

Publicar algo hacia fuera obliga a mirar la seguridad con otros ojos: lo que es razonable dentro de las cuatro paredes de la red de casa deja de serlo en cuanto tiene un nombre público. Antes de dar el paso final, se revisó a fondo quién podía hablar con el traductor y en qué condiciones, y se dejó todo cerrado como corresponde a algo expuesto a internet.

La comprobación final

Con todo bien cerrado, una llamada sin credenciales válidas: rechazada. Con las credenciales correctas: adelante, funcionando de punta a punta, exactamente por el mismo camino público que usará producción de verdad.

😅 El último susto: la rama equivocada

Con todo probado, fusioné el trabajo... en la rama principal, en vez de en la rama de la próxima versión, que era donde tenía que ir. Deshacerlo fue, por suerte, la parte fácil: un "deshacer" limpio del propio sistema de control de versiones, sin tocar nada a mano ni perder ni una línea, y todo el trabajo se volvió a abrir, completo, en la rama correcta. Ningún dato tocado, ninguna migración corrida en el sitio equivocado — solo un cable cruzado, diez minutos para arreglarlo.


📋 El resumen de daños

Problema Causa real Arreglo
Usuario/contraseña propio rechazado siempre Ese sistema solo se activa expuesto hacia fuera; en local usa una clave fija distinta Cambiar al mecanismo de clave fija pensado para esto
La clave fija fallaba solo en la conexión especial Generada con símbolos (+, =) que esa conexión interpreta mal Generarla con un método pensado para ir en una dirección
Tres rediseños seguidos y seguía feo Se estaba inventando un estilo nuevo en vez de mirar si ya había uno Copiar un componente de chat ya existente en el código, nunca conectado
Respuestas guardadas bien, nunca visibles en directo Dos partes de la pantalla compartían un canal; una cerraba el canal entero al desconectarse Que cada una quite solo su propia parte, no el canal completo
El aviso automático llegaba vacío / como página pública Una segunda comprobación de seguridad, aparte de la primera, interceptaba la llamada Añadir la excepción también en esa segunda comprobación
El traductor pasaba a estar público Publicar hacia fuera exige otro nivel de revisión de seguridad Repasar y cerrar bien los accesos antes de publicar
Todo fusionado en la rama equivocada Prisa al fusionar Deshacer limpio + reabrir en la rama correcta

🎓 La moraleja

Ninguno de los fallos de esta entrega fue difícil de arreglar una vez encontrado — todos cupieron en una o dos líneas de código. Lo que costó, otra vez, fue encontrarlos: casi todos eran de la misma familia, cosas que fallaban en silencio absoluto, sin un solo error que señalara por dónde mirar. Una clave que se corrompía sin avisar. Un canal que se cerraba "correctamente" y de paso apagaba algo que no debía. Una segunda comprobación de seguridad que nadie recordaba que existía.

Hay un patrón que se repite entre esta entrega y la de las notificaciones del panel, y merece la pena decirlo en voz alta: cuando dos partes de un sistema comparten un mismo canal de comunicación, cerrar tu parte a veces cierra la de otro sin que nadie se entere. No es un fallo de un sitio concreto — es una forma de romperse que va a volver a aparecer, en otra ropa, la próxima vez que dos piezas compartan algo sin coordinarse del todo.

Y luego está lo otro, lo que no es técnico: publicar algo hacia fuera cambia las reglas. Lo que es perfectamente seguro dentro de las cuatro paredes de la red de casa deja de serlo en el momento en que le pones un nombre público y una dirección de internet. Revisar la seguridad antes de publicar no es un capricho de última hora — es la diferencia entre "funciona" y "funciona de forma segura", y solo se nota la diferencia si te acuerdas de mirarla antes de darle al botón de publicar, no después.

Con esto, Hermes ya no depende de mí. Mi socio puede entrar, escribirle, y que le conteste — desde el propio sitio donde ya trabaja, sin preguntarme nada. Que es, al final, todo lo que se buscaba desde el principio de esta serie.

Jose Maria Gomez Lobato
Escrito por

Jose Maria Gomez Lobato

Desarrollador web y cofundador de Nebula Systems. Trabajo creando soluciones donde la inteligencia artificial se convierte en herramientas reales, combinando desarrollo, automatización y curiosidad por entender cómo funciona todo por dentro.