¡Descubramos cómo podemos crear una conversación auténtica con un asistente virtual basado en un modelo de IA similar a ChatGPT, sin tener que comunicarnos con un servidor, sino completamente dentro del navegador!
¿Es realmente posible hacer esto sin costo alguno completamente del lado del cliente con JavaScript? ¿Realmente llegaremos a algo "similar a ChatGPT"? ¿Nos dirigimos hacia un futuro con asistentes virtuales fuera de línea y control total de la privacidad?
Responderemos a estas preguntas con este tutorial especializado en Transformers.js, centrándonos en crear un chatbot basado en un modelo LLM real de Hugging Face.
Guía práctica para un Chatbot basado en IA integrado en una página web
Requisitos
¡Esta guía, al igual que la anterior, está diseñada para ser clara y accesible!
Por ejemplo, no usaremos ningún agrupador, sino una simple carpeta /public que se servirá con tu servidor web favorito.
Ingredientes
Para nuestra aplicación web, necesitaremos tres ingredientes principales correspondientes a los cuatro archivos del proyecto en los que trabajaremos:
./public/
index.html
worker.js
app.js
style.css
Interfaz Minimalista para el Chat:
Dentro de nuestro index.html
tendremos:
#chat-messages
donde aparecerán los mensajes del usuario, asistente virtual y del sistema (con diferentes estilos).#chat-input-container
para enviar un mensaje desde el teclado y con un botón.
Un web worker (cargado como módulo): contendrá nuestro modelo de IA y nos permitirá consultarlo sin bloquear la interfaz de usuario del hilo principal.
La lógica de la aplicación de un chat clásico: el usuario puede enviar sus mensajes, y también lo hará el modelo de IA.
Preparación
Un chat simple
Para este experimento, nos limitaremos a un contenedor para albergar todo el chat con un #chat-header
, #chat-messages
y #chat-input-container
dentro.
Agreguemos todo lo que necesitamos a nuestro index.html
:
<div id="container">
<div id="chat-container">
<div id="chat-header">
<h2>Mi primer LLM</h2>
</div>
<div id="chat-messages" class="chat-messages">
<!-- los mensajes aparecerán aquí -->
</div>
<div id="chat-input-container">
<input type="text" id="chat-input" placeholder="Escribe tu mensaje...">
<button id="send-button" disabled>Enviar</button>
</div>
</div>
</div>
Enviando y recibiendo mensajes
Ahora añadamos a nuestro app.js
el sistema para enviar mensajes escritos por el usuario en su caja. Nos conectaremos a su envío para enviar el mensaje a nuestra IA… pero primero, creemos un sistema simple para enviar y recibir mensajes.
document.addEventListener("DOMContentLoaded", () => {
const sendButton = document.getElementById("send-button");
const chatInput = document.getElementById("chat-input");
const chatMessages = document.getElementById("chat-messages");
const disableUI = () => {
sendButton.setAttribute("disabled", true);
sendButton.innerText = "...";
};
const enableUI = () => {
sendButton.removeAttribute("disabled");
sendButton.innerText = "Enviar";
};
const chat = (text) => {
setTimeout(() => {
addMessage("Hola mundo", "assistant");
}, 1000);
};
const download = (modelURL) => {
disableUI();
setTimeout(() => {
addMessage(
'<small id="downloading-message">Descargando modelo...</small>',
"system"
);
}, 1000);
setTimeout(() => {
addMessage(
`<small>¡Modelo listo! Más información aquí <a href="https://huggingface.co/${modelURL}" target="_blank">${modelURL}</a></small>`,
"system"
);
enableUI();
}, 2000);
};
const addMessage = (message, role) => {
const newMessageElement = document.createElement("div");
newMessageElement.classList.add("chat-message");
newMessageElement.classList.add(role);
newMessageElement.innerHTML = message;
chatMessages.appendChild(newMessageElement);
chatMessages.scrollTop = chatMessages.scrollHeight;
return newMessageElement;
};
const sendMessage = () => {
disableUI();
const question = chatInput.value;
addMessage(question, "user");
chat(question);
chatInput.value = "";
};
sendButton.addEventListener("click", sendMessage);
chatInput.addEventListener("keypress", (event) => {
if (event.key === "Enter") {
sendMessage();
}
});
download("HF_USER/HF_MODEL");
});
Con un toque de CSS, ¡tomará la apariencia y el comportamiento de un chat clásico!
body {
margin: 0;
font-family: system-ui;
}
a, a:visited, a:focus {
color: #ff5c00;
}
#container {
display: flex;
width: 100lvw;
height: 100lvh;
justify-content: center;
align-items: center;
background-color: #333;
}
#chat-container {
display: flex;
width: 60vw;
flex-direction: column;
max-width: 80%;
max-height: 80%;
background-color: white;
padding: 1rem;
border: 3px solid #666;
}
#chat-header {
display: flex;
justify-content: space-between;
}
#chat-header button {
color: #ff5c00;
background-color: transparent;
border: 0;
font-size: 3rem;
padding: 0;
margin: 0;
}
#chat-messages {
height: 50vh;
overflow-y: auto;
display: flex;
flex-direction: column;
}
#chat-input-container {
display: flex;
}
#chat-input {
width: 100%;
}
input[type=text], button {
font-size: 1rem;
border: 1px solid #ff5c00;
padding: 1rem;
margin: 1rem;
}
button {
background-color: #ff5c00;
color: white;
cursor: pointer;
}
button:disabled {
background-color: white;
cursor: wait;
color: #ff5c00;
border: 1px dashed #ff5c00;
}
input[type=text]:disabled {
background-color: white;
cursor: wait;
color: #ff5c00;
border: 1px dashed #ff5c00;
}
div.chat-message {
padding: 1rem;
margin-bottom: 1rem;
white-space: break-spaces;
width: 80%;
}
div.chat-message.user {
background-color: antiquewhite;
align-self: flex-end;
}
div.chat-message.assistant {
background-color: rgb(249, 205, 147);
align-self: flex-start;
}
div.chat-message.system {
margin: 0;
color: #666;
font-family: monospace;
padding: 0.5rem;
}
¡La parte exterior está lista! ¿No puedes ya saborear el resultado? 🤤
¡Estamos listos para el sistema de comunicación real con nuestro modelo de IA!
Ejecutando un Modelo de IA Dentro de un Web Worker 🌶️ 🌶️ 🌶️
Hemos llegado a la parte más emocionante de la receta: crear un web worker para descargar y ejecutar el modelo LLM en el navegador sin bloquear el hilo principal.
¿No conoces los Web Workers? ¡Es una gran oportunidad para probarlos!
En el archivo app.js
, incluiremos el archivo worker.js
como un módulo:
var aiWorker = new Worker('worker.js', {
type: "module"
});
Implementemos las dos funciones que nos permitirán enviar mensajes al web worker utilizando el método postMessage()
, reemplazando los falsos:
// Para enviar mensajes a la IA
const chat = (message) => {
aiWorker.postMessage({
action: "chat",
content: message,
});
};
// Para cargar el modelo de IA: sucede solo la primera vez
const download = (modelURL) => {
addMessage(
'<small id="downloading-message">Descargando modelo...</small>',
"system"
);
aiWorker.postMessage({
action: "download",
model: modelURL,
});
};
Para escuchar las respuestas del Web Worker, simplemente necesitamos agregar un listener de eventos que nos informará de cada mensaje. Ten en cuenta que el evento siempre es «message», pero el contenido pasado al callback contendrá un objeto que definirás: en la práctica, ¡puedes inventar tu propio protocolo formado por parámetros y banderas!
Para esta receta, solo necesitamos recibir dos tipos de mensajes:
- Si la respuesta contiene la propiedad de estado, entonces es la señal de que el modelo está listo (es decir, la respuesta al mensaje con action: ‘download’ enviado por nosotros una vez que se carga app.js).
- De lo contrario, es el texto generado por el modelo y contenido en la propiedad result.
Veamos nuestro sistema completo de recepción de mensajes con las reacciones correspondientes de nuestra interfaz de usuario:
aiWorker.addEventListener("message", (event) => {
const aiResponse = event.data;
if (aiResponse.status == "ready") {
addMessage(
`<small>¡Modelo listo! Más información aquí <a href="https://huggingface.co/${aiResponse.modelURL}" target="_blank">${aiResponse.modelURL}</a></small>`,
"system"
);
} else {
const result = aiResponse.result;
addMessage(result, 'assistant');
enableUI();
}
});
// ¡todo comienza con esta solicitud!
download('Felladrin/onnx-Pythia-31M-Chat-v1');
¡Sí, lo leíste bien: tenemos un parámetro modelURL! ¡Unos pocos pasos y descubriremos para qué sirve, pero puedes imaginarlo! 🤓
Web Worker Repleto de un Chatbot Real
¡Todo está listo para dar inteligencia a nuestro asistente virtual basado en modelos de generación de texto (LLM) cargados completamente en el navegador gracias a Transformers.js!
Primero, carguemos la última versión de Transformers.js directamente desde un servicio CDN. ¡Atención! 🔥 No te quemes: podemos cargar esta u otras bibliotecas de esta manera exclusivamente porque cargamos el Web Worker con la opción type: «module»! 🙂↕️
import {
pipeline,
env,
} from "https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.1";
env.allowLocalModels = false; // ¡usaremos modelos remotos!
Ahora, todo lo que tenemos que hacer es implementar nuestra pipeline como en los ejemplos de la documentación oficial, pero dentro del web worker y cuando la aplicación lo solicite.
La función downloadModel descargará los archivos del modelo desde Hugging Face y finalmente creará nuestro generador, que es una pipeline de generación de texto.
¡Seguramente has notado async
y await
! Cuando descargamos el modelo, el Web Worker esperará a que la descarga se complete y luego notificará a nuestra aplicación que todo está listo con self.postMessage()
con la propiedad de estado: «ready» (que es exactamente lo que nuestra lógica de aplicación está esperando para activar la interfaz de usuario y así poder usar el chat).
var generator;
const downloadModel = async (modelURL) => {
generator = await pipeline("text-generation", modelURL);
self.postMessage({
status: "ready",
task: "text-generation",
modelURL: modelURL,
});
};
Aquí es donde ocurre la magia de los modelos de generación de texto y su capacidad para parecer «inteligentes»: ¡qué emocionante!
const generateResponse = async (content) => {
// los modelos de generación de texto para chatbots toman un chat como entrada
const messages = [
{
role: "system",
content: "Eres un asistente muy conocedor y amigable.",
},
{
role: "user",
content: content,
},
];
// Los mensajes del chat con sus roles se alimentan a un
// tokenizador especial específico de ese modelo que los transformará en vectores (embedding)
const textInput = generator.tokenizer.apply_chat_template(messages, {
tokenize: false,
add_generation_prompt: true,
});
// ¡la pipeline en acción! Aquí es donde podemos pasar muchos parámetros para cambiar el resultado de la generación de texto
const output = await generator(textInput, {
max_new_tokens: 64,
do_sample: true,
});
// La conversación se nos devuelve en un formato específico del modelo
// pero al profundizar en la tarjeta en Hugging Face encontraremos toda la información
// y podemos extraer el contenido de la última respuesta.
// En este momento, aún no hay consenso sobre cómo debería ser una plantilla de chat, pero para extraer la última frase (es decir, la respuesta de la IA) simplemente corta lo que sigue a la última ocurrencia de la cadena `"assistant\n"` por ejemplo así:
const conversation = output[0].generated_text;
const start = conversation.lastIndexOf("assistant\n");
const lastMessage = conversation
.substr(start)
.replace("assistant\n", "");
// todo listo para enviar la respuesta generada por la IA
self.postMessage({
result: lastMessage,
});
};
Todo lo que queda es amalgamar el Web Worker con las solicitudes de nuestra app.js.
Hemos preparado todo previamente para enviar dos mensajes action: ‘download’ y action: ‘chat’, y aquí no hacemos más que recibirlos y reaccionar en consecuencia.
self.addEventListener("message", (event) => {
const userRequest = event.data;
if (userRequest.action == "download") {
const modelURL = userRequest.modelURL;
downloadModel(modelURL);
} else if (userRequest.action == "chat") {
const content = userRequest.content;
generateResponse(content);
}
});
Resultado Final y Observaciones
Cuando interactúas por primera vez con tu chatbot, es posible que encuentres que sus respuestas son un poco limitadas o extrañas. Esto es normal para un modelo pequeño, y es importante entender por qué:
El Tamaño del Modelo Importa: El modelo que usamos (Felladrin/onnx-Pythia-31M-Chat-v1) es muy pequeño, solo tiene alrededor de 31 millones de parámetros. Si bien esto hace que sea rápido de cargar y ejecutar en un navegador, limita significativamente sus capacidades.
Mejorando Respuestas: Puedes ajustar algunos parámetros para potencialmente mejorar las salidas:
const output = await generator(textInput, {
max_new_tokens: 1024, // Permite respuestas más largas
repetition_penalty: 1.2, // Reduce la repetición de palabras
do_sample: true,
});
Modelos Más Grandes, Mejores Resultados: Para respuestas más coherentes y capaces, considera usar modelos más grandes:
- Felladrin/onnx-TinyMistral-248M-Chat-v2 (248 millones de parámetros)
- Xenova/Qwen1.5-0.5B-Chat (500 millones de parámetros)
Estos modelos más grandes tardarán más en cargar, pero ofrecerán un rendimiento significativamente mejorado.
Siguientes Pasos y Desafíos
Ahora que has construido un chatbot de IA básico, aquí hay algunas formas de expandir tu proyecto:
- Animaciones Suaves: Implementa una animación de escritura para las respuestas del chatbot para que la interacción se sienta más natural.
- Integración del Lado del Servidor: Crea un backend en Python para interactuar con modelos de lenguaje aún más grandes (7-8B parámetros) para capacidades más avanzadas.
- Asistentes Especializados: Adapta el chatbot para propósitos específicos, como crear un NPC (Personaje No Jugador) para un juego.
- Explora Otras Tareas de IA: Prueba a implementar modelos de visión por computadora o reconocimiento de voz usando Transformers.js. Busca modelos del autor de la biblioteca en Hugging Face para opciones compatibles.
- Mejoras en la UI: Mejora la interfaz de chat con características como historial de mensajes, perfiles de usuario o personalización de temas.
- Manejo de Errores y Robustez: Implementa un mejor manejo de errores para fallos en la carga del modelo o problemas de red.
Recuerda que el campo de la IA y el procesamiento de lenguaje natural está evolucionando rápidamente. Sigue experimentando, aprendiendo y manteniéndote actualizado con los últimos desarrollos en modelos de transformadores y aplicaciones de IA basadas en el navegador.
Esperamos que hayas disfrutado de este tutorial y que lo encuentres valioso para entender cómo crear chatbots impulsados por IA directamente en el navegador. ¡Feliz codificación!