velvet-api / models /rag_engine.py
Grilocas77's picture
Upload 25 files
e679b32 verified
# 📁 Ficheiro: rag_engine.py
# 👤 Autor: Duarte Grilo – 2201320
# 🧠 Projeto Final – Engenharia Informática
# 📚 Função: Motor de RAG (Retrieval-Augmented Generation)
# Este módulo gere a criação da base vetorial, recuperação semântica de documentos,
# rerank com CrossEncoder, limpeza/tradução do contexto e entrega ao gerador Velvet.
# 📦 Importações de bibliotecas externas e internas
from langchain_chroma import Chroma
from sentence_transformers import CrossEncoder
import unicodedata
from langchain_huggingface import HuggingFaceEmbeddings
from transformers import AutoTokenizer
from langchain_community.document_loaders import DirectoryLoader, TextLoader, PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
import re
from config import CHROMA_PATH, EMBEDDING_MODEL, K_SIMILARITY_SEARCH
from models.tradutor_local import traduzir
from config import CHUNK_SIZE, CHUNK_OVERLAP
# 🔧 Inicialização de modelos globais de embeddings e reranker
embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL)
tokenizer = AutoTokenizer.from_pretrained(EMBEDDING_MODEL)
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2") # usado para rerank dos documentos
# 🧪 Função auxiliar para testar se a tradução está funcional
def testar_traducao():
try:
print("PT ➜ EN:", traduzir("O que é a UML?", origem="pt", destino="en"))
print("EN ➜ PT:", traduzir("What is UML?", origem="en", destino="pt"))
except Exception as erro_trad:
print("⚠️ Erro ao testar tradução:", erro_trad)
# 📦 Aceder à base vetorial Chroma já persistida
def get_chroma_db():
return Chroma(persist_directory=CHROMA_PATH, embedding_function=embeddings)
# 📂 Carrega documentos (TXT ou PDF) de um diretório especificado
def carregar_documentos_por_tipo(path, extensao, loader_cls):
loader = DirectoryLoader(path, glob=f"**/*.{extensao}", loader_cls=loader_cls)
docs = loader.load()
print(f"📄 Documentos .{extensao} encontrados: {len(docs)}")
return docs
# 🏗️ Constrói a base vetorial Chroma a partir dos documentos
def build_chroma_db(documents_path="data/documents"):
print("🚀 A iniciar construção da base de embeddings...")
# Carregar documentos de texto e PDF
docs = (
carregar_documentos_por_tipo(documents_path, "txt", TextLoader) +
carregar_documentos_por_tipo(documents_path, "pdf", lambda path: PyPDFLoader(path))
)
if not docs:
print("❌ Nenhum documento foi encontrado.")
return
# Quebrar documentos em chunks com sobreposição
splitter = RecursiveCharacterTextSplitter(chunk_size=CHUNK_SIZE, chunk_overlap=CHUNK_OVERLAP)
chunks = splitter.split_documents(docs)
# Remover duplicados
unique_chunks = list({chunk.page_content: chunk for chunk in chunks}.values())
try:
db = Chroma.from_documents(unique_chunks, embeddings, persist_directory=CHROMA_PATH)
db.persist()
print(f"✅ Embeddings salvos em: {CHROMA_PATH}")
except Exception as erro_db:
print("❌ Erro ao criar a base ChromaDB:", erro_db)
# 🧹 Normaliza o texto: remove acentos, limita tamanho e limpa quebras de linha
def normalize_text(text: str, max_length: int = 1000) -> str:
text = unicodedata.normalize("NFKD", text).encode("ASCII", "ignore").decode("ASCII")
return text[:max_length].replace("\n", " ")
# 🧽 Remove ruído semântico do contexto: números soltos, palavras irrelevantes, etc.
def limpar_ruido_contexto(texto: str) -> str:
linhas = texto.splitlines()
filtradas = [
linha.strip()
for linha in linhas
if len(linha.strip()) > 30 # ignora linhas curtas
and not re.search(r"(FIGURA|FONTE|CONSIDERANDO|EXERC[ÍI]CIO|PAULO COELHO|INTRODUÇÃO)", linha, re.IGNORECASE)
and not re.fullmatch(r"\s*\d+\s*", linha) # ignora números sozinhos
and not re.search(r"\b\d{1,2}\b", linha) # ignora datas ou numerações soltas
]
texto_limpo = " ".join(filtradas)
return re.sub(r"\s{2,}", " ", texto_limpo).strip()
# 🔍 Função principal: recebe a query e retorna o melhor contexto (PT ou EN)
def retrieve_context(query: str, k: int = K_SIMILARITY_SEARCH, return_scores: bool = False, return_raw: bool = False):
debug_ctx = {}
# 🔍 Traduz a pergunta de PT para EN
try:
query_en = traduzir(query, origem="pt", destino="en")
debug_ctx["query_en"] = query_en
except Exception as erro_tq:
print("❌ Erro ao traduzir a query:", erro_tq)
query_en = query # fallback: usa a query original
debug_ctx["query_en"] = query_en
# 🔍 Pesquisa por similaridade no ChromaDB
db = get_chroma_db()
context_docs = db.similarity_search(query_en, k=k)
if not context_docs:
contexto_vazio = "Sem contexto relevante encontrado."
if return_scores and return_raw:
return contexto_vazio, [], "", "", debug_ctx
elif return_scores:
return contexto_vazio, []
else:
return contexto_vazio
# 🧠 Rerank com CrossEncoder
pairs = [(query_en, doc.page_content) for doc in context_docs]
scores = reranker.predict(pairs)
reranked = sorted(zip(context_docs, scores), key=lambda x: x[1], reverse=True)
# 🔹 Juntar os k melhores docs (PT)
top_docs = [doc.page_content for doc, _ in reranked[:k]]
context_pt = "\n\n".join(top_docs)
debug_ctx["context_pt"] = context_pt
# 🔁 Traduzir o contexto para EN (limitado a 3000 chars para evitar erro do modelo)
try:
context_pt_truncado = context_pt[:2000] # Limita para evitar erro de tokens (>512)
context_en = traduzir(context_pt_truncado, origem="pt", destino="en")
debug_ctx["context_en"] = context_en
except Exception as erro_ctx:
print("⚠️ Erro ao traduzir o contexto para EN:", erro_ctx)
context_en = context_pt[:500] # fallback seguro
debug_ctx["context_en"] = context_en
# 🔧 Limpar e normalizar (apenas para debug/métricas)
texto_limpo = limpar_ruido_contexto(context_pt)
texto_normalizado = normalize_text(texto_limpo[:800]) # corta para evitar excesso
debug_ctx["normalizado"] = texto_normalizado
# ✅ Retornar com os diferentes modos
if return_scores and return_raw:
return context_pt, reranked[:k], context_en, texto_normalizado, debug_ctx
elif return_scores:
return context_pt, reranked[:k]
else:
return context_pt
# 🧪 Execução direta para construir ou testar a base Chroma
if __name__ == "__main__":
build_chroma_db()
db = get_chroma_db()
print("🔁 Teste de busca:", db.similarity_search("Teste de contexto", k=1))