Dans une première partie, nous avons abordé la manière de dialoguer avec un modèle pour obtenir un dialogue basé sur des informations sur lesquelles il n'a pas été entraîné. Dans notre seconde partie, nous avons examiné la stratégie pour créer nos index et les stocker. Dans cette dernière partie, nous aborderons, à travers un cas pratique, comment créer un système RAG complet et initialiser un dialogue, afin de donner aux utilisateurs la possibilité de converser avec notre modèle enrichi de nos propres données.

Pour rappel, toutes les informations sont dans la documentation disponible ici. Vous n'avez donc pas réellement besoin de ces articles. Néanmoins, il est toujours plus facile de comprendre lorsque des explications supplémentaires sont fournies. Certaines informations peuvent sembler aléatoires, ce qui est normal. Il est très difficile de simplifier un domaine aussi vaste en quelques lignes. De plus, le secteur est en pleine expansion, tout comme les outils disponibles. Nous avons délibérément choisi de détailler certains aspects tout en effleurant d'autres. C'est à vous, qui devez probablement mettre en place un tel système, d'approfondir le sujet en fonction de vos besoins.

On va donc commencer par poser un petit objectif :

Notre site partitech.fr possède un blog technique. Nous y publions divers contenus, à condition qu'ils puissent être utiles à quelqu'un. Habituellement, un collègue ou un client nous pose une question, et cela nous amène à penser : 'Tiens, si cette personne se pose cette question, pourquoi ne pas rédiger une petite note de synthèse ? Cela pourrait sûrement servir à d'autres.' C'est pour nous l'opportunité d'approfondir un sujet et d'en faire un pense-bête personnel. En bref, nous avons un blog...

Nous pouvons accéder aux fichiers de notre blog via le Sitemap. C'est pratique, car vous pourriez également avoir accès à cette ressource. Nous allons donc parcourir notre Sitemap et indexer son contenu pour pouvoir effectuer des requêtes dessus. Super !

Pour commencer, nous avons besoin de notre matière première, alors nous allons rapidement développer un petit script pour chercher nos contenus. Nous allons l'appeler '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

Les commentaires sont dans le code, donc pas besoin de longues explications. En gros, on parcourt les liens du sitemap, on extrait la zone de contenu de chacune des pages, on récupère le contenu textuel, puis on le met dans un tableau qu'on retourne. Vraiment, il serait difficile de faire plus simple (bien que, en fait, nous ayons fait plus simple dans la partie 4, mais en vérité, ce n'est pas le sujet de l'article. Les considérations techniques et l'esthétique du code seront pour une autre fois).

Nous avons donc notre classe pour récupérer nos informations. Maintenant, nous allons créer notre fichier qui va récupérer ces infos et créer les index.

On oubli pas de lancer notre serveur 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

On lance le tout

docker compose up -d
pipenv shell
python3 index.py

Nos fichiers au format FAISS sont créés.

Et nous nous retrouvons avec 3 tables créées automatiquement et bien remplies.

À présent, nous pouvons lancer notre question en demandant à txtai de bien vouloir nous ajouter un contexte récupéré en fonction de notre question.

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)

Et voici la réponse à la première question :

{
  "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 réponse à la seconde question :

{
  "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
  }
}

Comme on peut le voir, les réponses sont tout à fait en adéquation avec le contenu que nous avons indexé. On peut donc imaginer très facilement comment un tel système peut améliorer une documentation d'entreprise. En effet, aussi facilement que cela, nous avons la possibilité d'indexer des contenus tels que des images, des documents Word, des PDF... Tant que les données sont bien organisées, il est possible de les intégrer facilement à votre SI.

Txtai est donc vraiment super si l’on considère le peu de lignes de code qui ont été nécessaires pour produire cela. Reste à présent à le coupler avec un système de chat. Ça tombe bien, le développeur de txtai a aussi écrit un outil, txtchat. Reste à voir comment organiser le tout. Dans un autre article, très certainement.

Dans notre prochaine partie, nous allons voir comment faire la même chose directement avec LangChain, avec une base de données Postgresql pour héberger tous les embeddings. Plus de fichiers FAISS. On le fera avec Python et LangChain, et ensuite avec JavaScript et LangChain. Et oui, ce monde s'ouvre aussi aux développeurs web.