Nella prima parte, abbiamo affrontato come interagire con un modello per ottenere un dialogo basato su informazioni sulle quali non è stato addestrato. Nella nostra seconda parte, abbiamo esaminato la strategia per creare i nostri indici e memorizzarli. In questa ultima parte, attraverso un caso pratico, vedremo come creare un completo sistema RAG e inizializzare un dialogo, per dare agli utenti la possibilità di conversare con il nostro modello arricchito con i nostri dati personali.

Ricordiamo che tutte le informazioni si trovano nella documentazione disponibile qui. Quindi, non avete realmente bisogno di questi articoli. Tuttavia, è sempre più semplice comprendere quando vengono fornite ulteriori spiegazioni. Alcune informazioni possono sembrare casuali, il che è normale. È molto difficile semplificare un campo così ampio in poche righe. Inoltre, il settore è in costante espansione, così come gli strumenti disponibili. Abbiamo deliberatamente scelto di dettagliare alcuni aspetti mentre ne accenniamo altri. Sta a voi, che probabilmente dovete mettere in atto un tale sistema, approfondire l'argomento in base alle vostre esigenze.

Inizieremo quindi ponendoci un piccolo obiettivo:

Il nostro sito partitech.fr ha un blog tecnico. Pubblichiamo vari contenuti, purché possano essere utili a qualcuno. Di solito, un collega o un cliente ci pone una domanda, e ciò ci porta a pensare: 'Ehi, se questa persona si pone questa domanda, perché non scrivere un piccolo promemoria? Potrebbe sicuramente servire a qualcuno.' Questa è per noi l'opportunità di approfondire un argomento e farne un promemoria personale. In breve, abbiamo un blog...

Possiamo accedere ai file del nostro blog tramite il Sitemap. Questo è pratico, perché anche voi potreste avere accesso a questa risorsa. Quindi, scorreremo il nostro Sitemap e indicizzeremo il suo contenuto per poter effettuare ricerche su di esso. Ottimo!

Per cominciare, abbiamo bisogno della nostra materia prima, quindi svilupperemo rapidamente un piccolo script per cercare i nostri contenuti. Lo chiameremo '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

I commenti sono nel codice, quindi non c'è bisogno di lunghe spiegazioni. Fondamentalmente, scansioniamo i link del sitemap, estrarre l'area di contenuto di ciascuna delle pagine, recuperiamo il contenuto testuale e poi lo mettiamo in un array che restituiamo. Davvero, sarebbe difficile fare più semplice (anche se, in realtà, abbiamo fatto più semplice nella parte 4, ma in verità, questo non è l'argomento dell'articolo. Le considerazioni tecniche e l'estetica del codice saranno per un'altra volta).

Abbiamo quindi la nostra classe per recuperare le nostre informazioni. Ora, creeremo il nostro file che recupererà queste informazioni e creerà gli indici.

Non dimentichiamo di avviare il nostro server 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

Avviamo il tutto

docker compose up -d
pipenv shell
python3 index.py

I nostri file in formato FAISS sono stati creati.

E ci troviamo con 3 tabelle create automaticamente e ben riempite.

Adesso possiamo lanciare la nostra domanda chiedendo a txtai di aggiungerci gentilmente un contesto recuperato in base alla nostra domanda.

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)

Ecco la risposta alla prima domanda:

{
  "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 risposta alla seconda domanda:

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

Come si può vedere, le risposte sono assolutamente in linea con i contenuti che abbiamo indicizzato. Quindi, si può immaginare molto facilmente come un tale sistema possa migliorare la documentazione aziendale. Infatti, semplicemente così, abbiamo la possibilità di indicizzare contenuti come immagini, documenti Word, PDF... Finché i dati sono ben organizzati, è possibile integrarli facilmente nel vostro SI.

Txtai è quindi davvero fantastico se si considera il numero limitato di righe di codice che sono state necessarie per produrre ciò. Ora rimane da abbinarlo con un sistema di chat. Bene, lo sviluppatore di txtai ha anche scritto uno strumento, txtchat. Resta da vedere come organizzare il tutto. In un altro articolo, molto probabilmente.

Nella nostra prossima parte, vedremo come fare la stessa cosa direttamente con LangChain, con un database Postgresql per ospitare tutti gli embeddings. Niente più file FAISS. Lo faremo con Python e LangChain, e poi con JavaScript e LangChain. E sì, questo mondo si apre anche agli sviluppatori web.