Spaces:
Sleeping
Sleeping
import os | |
from typing import Dict, List | |
import numpy as np | |
import torch | |
from transformers import AutoTokenizer, AutoModel | |
from huggingface_hub import InferenceClient | |
class RAGEngine: | |
def __init__( | |
self, | |
documents: List[Dict[str, str]], | |
embedding_model: str = "BAAI/bge-m3", | |
llm_model: str = "meta-llama/Llama-3.1-8B-Instruct", | |
batch_size: int = 64, | |
): | |
""" | |
Initialise le moteur RAG avec les documents (contenant chacun 'url' et 'text'), | |
les paramètres de configuration et les clients nécessaires. | |
Args: | |
documents: Liste de documents, chacun un dictionnaire avec les clés 'url' et 'text'. | |
embedding_model: Nom du modèle pour calculer les embeddings en local. | |
llm_model: Nom du modèle LLM pour les complétions. | |
batch_size: Nombre de documents à traiter par lot. | |
""" | |
self.documents = documents | |
self.embedding_model = embedding_model # Nom du modèle pour embeddings (local) | |
self.llm_model = llm_model | |
self.batch_size = batch_size | |
self.embeddings: List[List[float]] = [] | |
# Filtrer les documents dont le texte est vide pour éviter les erreurs | |
self.indexed_documents = [doc for doc in self.documents if doc["text"].strip()] | |
# Initialiser le modèle et le tokenizer en local pour le calcul des embeddings | |
self.embedding_tokenizer = AutoTokenizer.from_pretrained(self.embedding_model) | |
self.embedding_model_local = AutoModel.from_pretrained(self.embedding_model) | |
# Initialiser le client pour le LLM (l'inférence reste à distance pour le LLM) | |
self._init_client_hf() | |
def _init_client_hf(self) -> None: | |
self.client = InferenceClient( | |
model=self.llm_model, | |
token=os.environ.get("HF_TOKEN"), | |
) | |
def index_documents(self) -> None: | |
"""Calcule les embeddings par lots en local avec le modèle Hugging Face.""" | |
texts = [doc["text"] for doc in self.indexed_documents] | |
for i in range(0, len(texts), self.batch_size): | |
batch = texts[i:i + self.batch_size] | |
if not batch: | |
continue | |
# Tokenisation et préparation des tenseurs | |
inputs = self.embedding_tokenizer(batch, padding=True, truncation=True, return_tensors="pt") | |
with torch.no_grad(): | |
outputs = self.embedding_model_local(**inputs) | |
# Calcul du pooling moyen sur la dernière couche | |
batch_embeddings_tensor = outputs.last_hidden_state.mean(dim=1) | |
batch_embeddings = batch_embeddings_tensor.cpu().tolist() | |
self.embeddings.extend(batch_embeddings) | |
print(f"Batch {i//self.batch_size + 1} traité, {len(batch_embeddings)} embeddings obtenus") | |
def cosine_similarity(query_vec: np.ndarray, matrix: np.ndarray) -> np.ndarray: | |
""" | |
Calcule la similarité cosinus entre un vecteur de requête et chaque vecteur d'une matrice. | |
""" | |
query_norm = np.linalg.norm(query_vec) | |
query_normalized = query_vec / (query_norm + 1e-10) | |
matrix_norm = np.linalg.norm(matrix, axis=1, keepdims=True) | |
matrix_normalized = matrix / (matrix_norm + 1e-10) | |
return np.dot(matrix_normalized, query_normalized) | |
def search(self, query_embedding: List[float], top_k: int = 5) -> List[Dict]: | |
""" | |
Recherche des documents sur la base de la similarité cosinus. | |
Args: | |
query_embedding: L'embedding de la requête. | |
top_k: Nombre de résultats à renvoyer. | |
Returns: | |
Une liste de dictionnaires avec les clés "url", "text" et "score". | |
""" | |
query_vec = np.array(query_embedding) | |
emb_matrix = np.array(self.embeddings) | |
scores = self.cosine_similarity(query_vec, emb_matrix) | |
top_indices = np.argsort(scores)[::-1][:top_k] | |
results = [] | |
for idx in top_indices: | |
doc = self.indexed_documents[idx] | |
results.append( | |
{"url": doc["url"], "text": doc["text"], "score": float(scores[idx])} | |
) | |
return results | |
def ask_llm(self, prompt: str) -> str: | |
""" | |
Appelle le LLM avec l'invite construite et renvoie la réponse générée. | |
""" | |
messages = [{"role": "user", "content": prompt}] | |
response = self.client.chat.completions.create( | |
model=self.llm_model, messages=messages | |
) | |
return response.choices[0].message.content | |
def rag(self, question: str, top_k: int = 4) -> Dict[str, str]: | |
""" | |
Effectue une génération augmentée par récupération (RAG) pour une question donnée. | |
Args: | |
question: La question posée. | |
top_k: Nombre de documents de contexte à inclure. | |
Returns: | |
Un dictionnaire avec les clés "response", "prompt" et "urls". | |
""" | |
# 1. Calculer l'embedding de la question en local. | |
inputs = self.embedding_tokenizer(question, return_tensors="pt") | |
with torch.no_grad(): | |
outputs = self.embedding_model_local(**inputs) | |
question_embedding_tensor = outputs.last_hidden_state.mean(dim=1)[0] | |
question_embedding = question_embedding_tensor.cpu().tolist() | |
# 2. Récupérer les documents les plus similaires. | |
results = self.search(query_embedding=question_embedding, top_k=top_k) | |
context = "\n\n".join([f"URL: {res['url']}\n{res['text']}" for res in results]) | |
# 3. Construire l'invite. | |
prompt = ( | |
"You are a highly capable, thoughtful, and precise assistant. Your goal is to deeply understand the user's intent, ask clarifying questions when needed, think step-by-step through complex problems, provide clear and accurate answers, and proactively anticipate helpful follow-up information. " | |
"Based on the following context, answer the question precisely and concisely. " | |
"If you do not know the answer, do not make it up.\n\n" | |
f"Context:\n{context}\n\n" | |
f"Question: {question}\n\n" | |
"Answer:" | |
) | |
urls = [res['url'] for res in results] | |
# 4. Appeler le LLM avec l'invite construite. | |
llm_response = self.ask_llm(prompt) | |
return {"response": llm_response, "prompt": prompt, "urls": urls} | |