secciones

Asistente IA con "personalidad"

A partir de una artículo que estuve leyendo (Stevens: a hackable AI assistant using a single SQLite table and a handful of cron jobs) se me ocurrió la idea de hacer un "asistente con personalidad".

El concepto es el siguiente:

  1. Se puede acceder al asistente a través de Telegram.
  2. Cuando accedes a él te pregunta algunas cosas sobre ti, para poder personalizar sus respuestas.
  3. A partir de ahí, cada día se presenta un nuevo asistente con una nueva "personalidad", con el que puedes hablar durante todo el día.

Lo puedes probar siguiendo este enlace (lo voy a dejar unos días funcionando) y si llegas tarde puedes ver cómo funciona en este vídeo:

Además puedes descargar los fuentes y montarlo tú mismo desde este repositorio de Github.

A continuación tienes algunos detalles técnicos y cosas aprendidas.

Stack tecnológico

Todo el desarrollo está realizado en Spring Boot (versión 3.4.4) con JDK 21.

  • Para el acceso a la IA de Anthropic estoy usando Spring Boot AI.
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-anthropic-spring-boot-starter</artifactId>
    <version>1.0.0-M6</version>
</dependency>
  • Para la gestión del bot se utiliza una librería de org.telegram. Hay dos librerías para esto, una que utiliza webhooks y otra que utiliza una conexión continua. La versión que yo he utilizado es esta última ya que no requiere exponer un servidor en internet:
<dependency>
    <groupId>org.telegram</groupId>
    <artifactId>telegrambots-spring-boot-starter</artifactId>
    <version>6.9.7.1</version>
</dependency>

Creación del Bot en Telegram

La creación del bot en Telegram es curiosa: En lugar de conectarte a alguna página web con un panel de control, utilizas el propio Telegram, hablando con una cuenta/bot llamada @BotFather, desde donde puedes crear y configurar el Bot y obtener los dos datos que necesitas para configurar en el fichero de properties:

telegram.bot.token=${TELEGRAM_BOT_TOKEN}
telegram.bot.name=${TELEGRAM_BOT_NAME}

En código, tienes que definir un bean que extienda TelegramLongPollingBot y que implemente un método:

public void onUpdateReceived(Update update) {

Ese método es llamado cada vez que el bot recibe un mensaje. En el objeto recibido Update se tiene toda la información sobre el mensaje: Quién lo envía. Qué tipo es (texto, imagen, ...). La clase tiene disponibles una serie de métodos execute que permite enviar un mensaje al usuario. Una cosa importante: Sólo puedes enviar un mensaje a un usuario si ese usuario ha escrito al bot previamente (bien por Telegram).

Proceso de onboarding

La parte más interesante del bot desde el punto de vista de Telegram es la gestión del proceso de onboarding. Cuando se recibe un mensaje de un usuario nuevo (no está en nuestra bbdd o lo está con un estado "ONBOARDING") se le pasa el mensaje (junto con todo el historial hasta ese momento) utilizando el siguiente prompt:

Eres un asistente de onboarding. Tu misión es ayudar a los usuarios a completar su onboarding.
Primero debes presentarte y decir que eres "su asistente" y que tienes que hacerle unas
preguntas para ser más útil.
Para ello debes preguntarle su nombre, su profesión, nombres de los familiares.
Haz una pregunta para cada uno de estos datos, pero solo una pregunta cada vez.
La pregunta de los familiares repitela hasta que te diga que no quiere añadir a nadie más.
Finalmente pregúntale si hay algo más sobre él que le gustaría que supieses.
Esta pregunta también repitela hasta que te diga que no quiere añadir nada más.
Si el usuario prefiere no contestar a algún dato, no vuelvas a preguntar.
Cuando hayas terminado esta entrevista y tengas los datos necesarios
registra sus datos haciendo uso de la herramienta disponible
y dale las gracias simplemente, no le preguntes nada más.

Llamada a la IA

Llamar a un LLM desde Spring Boot AI es bastante sencillo. Por ejemplo, utilizando el prompt anterior puedo utilizar este api fluent:

String mensaje = chat.prompt()
        .system(u -> u.text("""
            Fecha actual: {fecha}
            Eres un asistente de onboarding. Tu misión es ayudar a los usuarios a completar su onboarding.
            [...]
            y dale las gracias simplemente, no le preguntes nada más.""")
                .param("fecha", fechaActual))
        .messages(mensajes.stream().map(m -> {
            if (m.getSender().equals(MensajeChat.SenderType.USER)) {
                return (Message) new UserMessage(m.getMessage());
            } else {
                return (Message) new AssistantMessage(m.getMessage());
            }
        }).toList())
        .tools(toolIA)
        .call().content();

Como se puede ver, se le pasa el prompt (en este caso de tipo system) y se pueden interpolar textos (que luego se sustituyen con el método param). También se puede pasar el historial del chat previo (necesario en este caso para que sepa qué preguntas te ha hecho ya) y finalmente se llama al método call que realiza la llamada a la IA. Aquí me quedo simplemente con el texto de la respuesta (content()) pero se podría obtener a toda la información de la respuesta.

Herramienta de registro de datos

En la llamada previa, se puede ver que le pasamos una "herramienta" al prompt para que haga uso de ella para guardar los datos recolectados del usuario:

.tools(toolIA)

Este objeto toolIA es la instanciación de una clase con la herramienta.

Las herramientas en Spring Boot AI son métodos o clases que definen un "callback" al cual llama el LLM para realizar distintas acciones:

@Tool(description = "Registra la información del usuario")
public void registraDatosUsuario(List<HechoUsuario> informaciones) {
    System.out.println(informaciones);

    onboardingDone = true;

    List<Map<String, String>> infoMap = (List) informaciones;
    infoMap.forEach(map -> {

Aquí se utiliza la anotación @Tool para describir la herramienta. Esta información, junto con otras (como el nombre del método o información del objeto HechoUsuario también con sus propias anotaciones) le llega al LLM y el motor de Spring Boot IA lo utiliza para llamar al método.

Yo lo que hago aquí es coger el listado de "hechos" sobre el usuario y guardarlo en la BBDD para futura referencia. También marco la variable onboardingDone a true. Esa variable la utilizamos más adelante para dar por finalizado el proceso de onboarding y marcar el usuario ya como activo.

Problema con el tipo de datos de la herramienta

Una cosa que me dio trabajo fue el objeto informaciones que recibo en la herramienta. Aunque teóricamente debería ser una lista de HechoUsuario realmente me está llegando una lista de Map<String, String>, donde las claves del mapa se corresponden con los atributos de la clase HechoUsuario, por eso tengo que realizar ese feo cast sin tipo:

List<Map<String, String>> infoMap = (List) informaciones;

Entiendo que el problema viene porque los genéricos en Java hacen un type erasure en tiempo de ejecución, de tal manera que la librería Spring Boot AI no sabe realmente el tipo de los objetos de la lista, pero es algo que tendré que investigar porque esta solución es de todo menos elegante.

Envío de mensaje de saludo

Una vez que el proceso de onboarding ha terminado, el sistema genera un mensaje de saludo con la personalidad del asistente del día. Para generar este mensaje, se utiliza el siguiente prompt:

Fecha actual: {fecha}
Eres un asistente con personalidad. Tu misión es dar un mensaje de buenos días para una agenda.
Se te asignará un rol, debes hablar como si fueses ese personaje.
Se te darán una serie de hechos. Cada hecho relevante tendrá una fecha o el texto "sin fecha" si es un hecho
general sin fecha establecida.
Se te dará también una frase diaria.
Debes basar tu mensaje de buenos días saludando personalmente al usuario y comentando los hechos
que sean relevantes para hoy y los próximos 7 días. IMPORTANTE: Sólo cosas de hoy y de los próximos 7 días.
Después debes mostrar la frase diaria y su autor con un breve comentario sobre ella.
Utiliza markdown para dar formato al mensaje, pero sólo *negritas* e _itálicas_.
Si creas acotaciones, hazlo usando _(itálicas y entre paréntesis)_.
# [ROL]
{rol}
# [HECHOS]
{hechos}
# [FRASE DIARIA]
{fraseTexto}
# [AUTOR FRASE DIARIA]
{fraseAutor}

Utilizamos varios parámetros que después sustituiremos por los datos personalizados:

  • {rol} -- El rol del asistente del día, sacado de la bbdd de asistentes.
  • {hechos} - Los hechos sobre el usuario que el asistente ha registrado durante el proceso de onboarding o en las conversaciones posteriores.
  • {fraseTexto} y {fraseAutor} - Para darle un poco de color al mensaje diario, incluimos una frase que va variando cada día.

Este mensaje también se envía todos los días a las 10 de la mañana, una vez que se ha fijado el nuevo asistente diario. Esto simplemente se hace desde un método utilizando el sistema de cron que Spring Boot tiene incorporado:

@Scheduled(cron = "0 0 10 * * *")
public void avanzarDia() {

Conversación con el asistente una vez finalizado el proceso de onboarding

Cuando se recibe un mensaje del usuario se comprueba si este usuario es nuevo (o todavía está en el proceso de onboarding). Si es así, se genera la respuesta con el prompt ya descrito más arriba.

Por el contrario, si el usuario ya está activo (ya se ha marcado en la bbdd que ha finalizado su proceso de onboarding) se genera la respuesta con un prompt distinto:

Fecha actual: {fecha}
Eres un asistente con personalidad. Tu misión es dar una respuesta al usuario siendo útil.
Se te asignará un rol, debes hablar como si fueses ese personaje.
Se te darán una serie de hechos sobre el usuario. Cada hecho relevante tendrá una fecha o el texto "sin fecha" si es un hecho
general sin fecha establecida.
Puedes usar estos hechos para personalizar tu respuesta si es adecuado, pero no respondas a esos hechos.
Responde brevemente a la pregunta del usuario.
Si el usuario te pide que registres algún dato o menciona algún dato personal relevante,
registralo haciendo uso de la herramienta disponible.
# [ROL]
{rol}
# [HECHOS]
{hechos}

Aquí también le pasamos la herramienta de registro de datos, de tal manera que se pueden registrar informaciones que mencione el usuario y que pueden ser útiles para conversaciones futuras.

Mejoras posibles

Existen varias mejoras que se podrían realizar sobre este sistema básico:

  • Usar Whatsapp además de Telegram. Whatsapp también tiene un API para crear bots, aunque en este caso creo que es necesario darse de alta como negocio.
  • Obtención de informaciones distintas para el mensaje de saludo diario. Se pueden incorporar a la bbdd distintas informaciones útiles, igual que ahora se está incorporando la frase diaria. Por ejemplo, se podría incorporar la previsión del clima, noticias del día, etc.
  • Del mismo modo, se podría realizar una integración con Google Calendar o Apple Calendar de tal manera que en el saludo diario se tenga en cuenta la agenda del usuario. En el prompt actual ya se apunta esta funcionalidad, si bien ahora sólo utiliza los eventos registrados en la conversación con el usuario.
  • Otras integraciones interesantes. Por ejemplo, se podría integrar con Strava para estar informado de las actividades deportivas del usuario. Con TMDB para avisar de los episodios de las series que el usuario está siguiendo. Y así hasta el infinito.
  • Bajando más a la tierra se podrían crear comandos para que el usuario pudiese cambiar de asistente (en lugar de esperar al cambio diario), poder activar o desactivar el mensaje diario, etc.
21 abril 2025 —
Anterior: Demo de editor con IA