La IA ha llegado para quedarse y con ella han llegado varias propuestas bastantes interesantes: una de estas propuestas es el protocolo MCP. Una de ellas es el modelo NLWeb propuesto por microsoft. NLWeb permite convertir nuestros sitios estáticos en una experiencia conversacional. Hoy se va a indagar como desplegar NLWeb en local y como implementarlo con un portal Drupal.
Como instalar NLWeb
Lo primero es acceder al repositorio de git: https://github.com/nlweb-ai/NLWeb y seguir el tutorial de Hello World.
A tener en cuenta que se necesita una API KEY de alguna cuenta de IA tipo Open AI, Azure Open AI, Elastic Search, etc. Para la base de datos en la que se almacenarán los datos se puede usar la base de datos qDrant local que ya trae el proyecto. Todo esto es configurable en el .env.
NLWeb es alimentado con un sistema de rss, por lo que en el siguiente tutorial se mostrará como instalar NLWeb de manera local paso a paso y como alimentar desde Drupal el servidor NLWeb.
Instalación paso a paso
Clonar el código del repositorio.
git clone https://github.com/microsoft/NLWeb
cd NLWebAbrir una terminal para crear un entorno virtual de Python y activarlo.
python -m venv myenv
source myenv/bin/activate # Or on Windows: myenv\Scripts\activateIr al directorio de code/python para instalar las dependencias. Hay que tener en cuenta que esto también instalará los requisitos de la base de datos vectorial local que se usará más adelante.
cd code/python
pip install -r requirements.txtAhora toca poner las variables en el .env. Copiar el archivo .env.template a un nuevo archivo .env dentro de la carpeta principal y actualizar la clave de API que se utilizará para el endpoint LLM elegido. Las variables de la base de datos local de Qdrant ya están configuradas para este ejercicio.
Ahora toca actualizar los archivos de configuración para seleccionar el proveedor que se va a usar. Se encuentra en el directorio config.
- Configurar el config_llm.yaml. Hay que cambiar la primera línea para seleccionar el proveedor que se esté usando actualmente.
- config_embedding.yaml. Lo mismo que el anterior.
- config_retrieval.yaml. Aquí se define que base de datos vectorial se va a usar, pro defecto la qdrant por defecto.
Una vez configurado todo lo idóneo sería verificar que se puede establecer conexión con el proveedor. Para ello se puede usar el script de testing que ya trae incorporado el ejemplo.
cd code/python
python testing/check_connectivity.pyCargar datos
Ahora toca cargar datos y con el script de python es bastante fácil.
# Run from code/python folder
python -m data_loading.db_load <RSS URL> <site-name>Pero antes vamos a preparar Drupal para que sirva estos datos.
Preparando Drupal
Gracias al sistema de de vistas de Drupal es muy fácil crear un canal de noticias que pueda alimentar el sistema. Para ello es tan fácil como crear una vista con el display "canal de noticias". El formato debe de ser RSS y luego asignar los campos.
Ojo! es muy importante tener cuidado a la hora de mostrar la vista verificar que no está saliendo los datos de debug (como el nombre de la template, etc.) sino no podrá ser cargado.
En nuestro caso para cargar los datos en el python es tan fácil como lanzar el siguiente comando:
python -m data_loading.db_load https://<miweb>/blog/rss NOMBRE_SISTEMAProbando en el chat local.
Si vamos a la URL que da el servidor (normalmente es 0.0.0.0:8000) podemos ver un chat y probar a conversar con él.
Insertar Chat en Drupal
En este caso se ha creado un módulo custom que genera un bloque con el chat, aunque realmente esto se podría hacer desde cualquier sitio. Lo realmente útil es usar las llamadas POST y GET de NLWeb para hacer consultar a nuestro servidor NLWeb.
Para hacer un pequeño ejemplo hemos creado una chatbox en el portal para poder consultar los artículos sin necesidad de usar la navegación de Drupal.
Para ello se ha creado un módulo custom que genera un bloque y emplaza una plantilla con su código JS. Cómo este no es el punto del artículo no se va a explicar como se genera el archivo y solo se va a mostrar el código JS para consultar al Servidor.
(function (Drupal, once, drupalSettings) {
Drupal.behaviors.myBlockBehavior = {
attach(context) {
once('init-block', '#ai-search-container', context).forEach((el) => {
const baseUrl = 'http://localhost:8000/ask';
const input = el.querySelector('#ai-search-input');
const site = 'SLX';
const searchInput = document.getElementById('ai-search-input');
const searchButton = document.getElementById('ai-search-button');
var chatContainer = document.getElementById('chat-container');
searchButton.addEventListener('click', handleSearch);
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleSearch();
}
});
async function handleSearch() {
const query = searchInput.value.trim();
const container = document.getElementById('chat-container');
const questionContainer = document.getElementById('question-container');
container.textContent = 'Loading, please wait...';
chatContainer.style.display = 'block';
searchInput.value = '';
questionContainer.textContent = `You asked: ${query}`;
questionContainer.classList.remove('hidden');
console.log('User query:', query);
const answer = await fetchAnswer(query) ?? {};
if (answer && answer[0]?.content) {
container.textContent = '';
answer[0].content.forEach(item => {
console.log(item);
const element = document.createElement('p');
element.classList.add('border-b', 'py-4');
const link = document.createElement('a');
link.href = item.url;
link.textContent = item.description;
element.appendChild(link);
container.appendChild(element);
});
}
}
async function fetchAnswer(query) {
const url = `${baseUrl}?site=${site}&query=${encodeURIComponent(query)}&streaming=false&mode=generate`;
console.log(url);
try {
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
}
});
return await response.json();
} catch (error) {
console.error('Error fetching answer:', error);
}
return null;
}
});
}
};
})(Drupal, once, drupalSettings);Básicamente se está consultando mediante una llamada GET en la que por la misma URL se le pasa los atributos.
Este es el resultado:
También se podría hacer con un request tipo POST emplazando en el body los campos de query y site. Es muy importante filtrar por el sitio, en el caso de tener varios.