En la primera parte, abordamos cómo dialogar con un modelo para obtener un diálogo basado en información sobre la cual no ha sido entrenado. En nuestra segunda parte, examinamos la estrategia para crear nuestros índices y almacenarlos. En esta última parte, abordaremos, a través de un caso práctico, cómo crear un sistema RAG completo e iniciar un diálogo para dar a los usuarios la posibilidad de conversar con nuestro modelo enriquecido con nuestros propios datos.

Para recordar, toda la información está en la documentación disponible aquí. Por lo tanto, realmente no necesitas estos artículos. Sin embargo, siempre es más fácil comprender cuando se proporcionan explicaciones adicionales. Algunas informaciones pueden parecer aleatorias, lo cual es normal. Es muy difícil simplificar un campo tan amplio en unas pocas líneas. Además, el sector está en plena expansión, al igual que las herramientas disponibles. Hemos elegido deliberadamente detallar ciertos aspectos mientras que solo mencionamos otros. Depende de ti, que probablemente debes implementar tal sistema, profundizar en el tema según tus necesidades.

Así que vamos a empezar estableciendo un pequeño objetivo:

Nuestro sitio partitech.fr tiene un blog técnico. Publicamos diversos contenidos, siempre que puedan ser útiles para alguien. Habitualmente, un colega o un cliente nos hace una pregunta, y eso nos lleva a pensar: 'Hmm, si esta persona tiene esta duda, ¿por qué no redactar una pequeña nota resumen? Seguramente podría servirle a otros.' Para nosotros es la oportunidad de profundizar en un tema y convertirlo en un recordatorio personal. En resumen, tenemos un blog...

Podemos acceder a los archivos de nuestro blog a través del Sitemap. Es práctico, porque tú también podrías tener acceso a este recurso. Así que vamos a recorrer nuestro Sitemap e indexar su contenido para poder realizar consultas sobre él. ¡Genial!

Para empezar, necesitamos nuestra materia prima, por lo que vamos a desarrollar rápidamente un pequeño script para buscar nuestros contenidos. Lo vamos a llamar 'SonataExtraBlog'.

sonata_extra_blog.py:

import requests
import xml.etree.ElementTree as ET
from txtai.pipeline import Textractor
from urllib.parse import urlparse
from bs4 import BeautifulSoup
import tempfile
import os


class SonataExtraBlog:
    def __init__(self, sitemap_url):
        self.sitemap_url = sitemap_url
        self.textractor = Textractor(sentences=True)

    def is_valid_url(self, url):
        parsed = urlparse(url)
        return bool(parsed.netloc) and bool(parsed.scheme)

    def getData(self):
        # déclaration de notre retour de données
        data_list = []

        # Télécharger le fichier sitemap
        response = requests.get(self.sitemap_url)
        sitemap_content = response.content

        # Parser le contenu XML
        root = ET.fromstring(sitemap_content)

        # Extraire les URLs
        urls = [url.text for url in root.findall('.//{http://www.sitemaps.org/schemas/sitemap/0.9}loc')]

        # Parcourir chaque URL pour télécharger et traiter le contenu
        for url in urls:
            # Vérifier si l'URL est valide
            if not self.is_valid_url(url):
                print(f"URL invalide : {url}")
                continue

            try:
                print(f"Traitement de : {url}")
                # Télécharger le contenu HTML
                response = requests.get(url)
                html_content = response.content.decode('utf-8')

                # Utiliser BeautifulSoup pour extraire le contenu de la div avec la classe 'content'
                soup = BeautifulSoup(html_content, 'html.parser')
                content_div = soup.find('div', class_='site-content')
                if content_div:
                    # Créer un fichier temporaire pour le contenu HTML
                    with tempfile.NamedTemporaryFile(delete=False, suffix='.html', prefix='txtextractor_',
                                                     mode='w') as temp_file:
                        temp_file.write(str(content_div))
                        temp_file_path = temp_file.name

                    # Extraire le texte à l'aide de Textractor
                    text = self.textractor(temp_file_path)

                    # Supprimer le fichier temporaire
                    os.remove(temp_file_path)
                    # print(text)
                    data_list.append({"id": url, "text": text})
                else:
                    text = "Pas de contenu trouvé dans la div 'content'"


            except requests.RequestException as e:
                print(f"Erreur lors du téléchargement de {url}: {e}")
        return data_list

Los comentarios están en el código, así que no es necesario largas explicaciones. En resumen, recorremos los enlaces del sitemap, extraemos el área de contenido de cada una de las páginas, recogemos el contenido textual y luego lo ponemos en un arreglo que devolvemos. Realmente, sería difícil hacerlo más sencillo (aunque, de hecho, hemos hecho algo más sencillo en la parte 4, pero en realidad, ese no es el tema del artículo. Las consideraciones técnicas y la estética del código serán para otra ocasión).

Entonces, ya tenemos nuestra clase para recuperar nuestra información. Ahora, vamos a crear nuestro archivo que va a recoger estos datos y crear los índices.

No olvidamos lanzar nuestro servidor Postgresql:

services:
  db:
    hostname: db
    image: ankane/pgvector
    ports:
     - 5432:5432
    restart: always
    environment:
      - POSTGRES_DB=vectordb
      - POSTGRES_USER=testuser
      - POSTGRES_PASSWORD=testpwd
      - POSTGRES_HOST_AUTH_METHOD=trust
    volumes:
     - ./init.sql:/docker-entrypoint-initdb.d/init.sql

Ponemos en marcha todo

docker compose up -d
pipenv shell
python3 index.py

Nuestros archivos en formato FAISS están creados.

Y terminamos con 3 tablas creadas automáticamente y bien rellenadas.

Ahora, podemos lanzar nuestra pregunta pidiendo a txtai que amablemente nos añada un contexto obtenido en función de nuestra pregunta.

from txtai.embeddings import Embeddings
from llama_cpp import Llama

# on déclare notre sytème d'embeddings
embeddings = Embeddings(
    content="postgresql+psycopg2://testuser:testpwd@localhost:5432/vectordb",
    objects=True,
    backend="faiss"
)

# on charge les embeddings
embeddings.load("./index_blog_partitech")

llm = Llama(
    model_path="/Data/Projets/Llm/Models/openchat_3.5.Q2_K.gguf", n_ctx=90000
)


def execute(question, context):
    prompt = f"""GPT4 User: system You are a friendly assistant. You answer questions from users.
        user Answer the following question using only the context below. Only include information 
        specifically discussed or general AI and LLMs related subject. 
      question: {question}
      context: {context} <|end_of_turn|>
      GPT4 Assistant:
      """
    return llm(prompt,
               temperature=0,
               max_tokens=10000,
               top_p=0.2,
               top_k=10,
               repeat_penalty=1.2)


def rag(question):
    context = "\n".join(x["text"] for x in embeddings.search(question))
    return execute(question, context)


result = rag("What about sonata-extra ?")
print(result)

result = rag("Who wrote sonata-extra ?")
print(result)

Y aquí está la respuesta a la primera pregunta:

{
  "id": "cmpl-63ebabf2-ec6b-4a0e-a0ae-4433c2df6ece",
  "object": "text_completion",
  "created": 1702485091,
  "model": "/Data/Projets/Llm/Models/openchat_3.5.Q2_K.gguf",
  "choices": [
    {
      "text": "\nThe Sonata-Extra Bundle is an extension to Symfony that enhances your experience with additional functionalities such as Activity Log, Approval Workflow, Assets Management, Blog integration, Content Security Policy management, Header Redirect Manager, Language Switcher, Multisite and multilingual support for SonataPageBundle, Sitemap generation, Smart services (AI-powered), WordPress import, Cookie Consent Block, Gutenberg Editor Integration, FAQ manager, Article manager with Gutenberg editor, additional form types, and more.\n\nThe bundle provides features like automatic translation through smart service functionality, integration of the Gutenberg editor for content creation, cookie consent management in compliance with GDPR regulations, and efficient loading of assets only when necessary. It also offers a flexible way to manage CSS and JavaScript assets in Sonata blocks, allowing developers to include external files or inline styles and scripts easily.\n\nTo use these features, you need to inject the required services into your block service using autowireDependencies method. Then, add assets to your block by calling methods like addCss, addJs, addJsInline, and addCssInline. To render the assets in Twig templates, use the provided functions such as sonata_extra_get_blocks_css('default'), sonata_extra_get_blocks_js('default'), etc., with custom indexes for grouping assets when developing custom blocks.",
      "index": 0,
      "logprobs": "None",
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 2560,
    "completion_tokens": 299,
    "total_tokens": 2859
  }
}

La respuesta a la segunda pregunta:

{
  "id": "cmpl-cf77a2a2-de5b-45ca-905e-a11094a805aa",
  "object": "text_completion",
  "created": 1702485287,
  "model": "/Data/Projets/Llm/Models/openchat_3.5.Q2_K.gguf",
  "choices": [
    {
      "text": "\\nThe authors of the Sonata-extra bundle are Geraud Bourdin and Thomas Bourdin. They work for partITech, a company that specializes in Symfony, Sonata, and other technologies to enhance digital experiences. The context provided does not mention any specific individual who wrote \"sonata-extra.\"",
      "index": 0,
      "logprobs": "None",
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 1360,
    "completion_tokens": 67,
    "total_tokens": 1427
  }
}

Como podemos ver, las respuestas están completamente en línea con el contenido que hemos indexado. Por lo tanto, podemos imaginar muy fácilmente cómo tal sistema puede mejorar la documentación de una empresa. De hecho, tan fácilmente como esto, tenemos la posibilidad de indexar contenidos como imágenes, documentos Word, PDF... Mientras los datos estén bien organizados, es posible integrarlos fácilmente a su SI.

Txtai es realmente genial si consideramos las pocas líneas de código que fueron necesarias para producir esto. Ahora sólo queda acoplarlo con un sistema de chat. Por suerte, el desarrollador de txtai también escribió una herramienta, txtchat. Queda por ver cómo organizar todo. En otro artículo, muy probablemente.

En nuestra próxima sección, veremos cómo hacer lo mismo directamente con LangChain, con una base de datos Postgresql para albergar todos los embeddings. No más archivos FAISS. Lo haremos con Python y LangChain, y luego con JavaScript y LangChain. Y sí, este mundo también se está abriendo a los desarrolladores web.