rag-model / app.py
Nicolas Gutierrez
Refactor QA model loading and response generation in app.py
58ea352
Raw
History Blame Contribute Delete
7.55 kB
import PyPDF2 as pypdf2
import os
import re
from pathlib import Path
from sentence_transformers import SentenceTransformer
import requests
from chromadb import Client
from transformers import AutoModelForQuestionAnswering, AutoTokenizer
import gradio as gr
import torch
# ==================== CONFIGURACIÓN ====================
DATA_DIR = "./"
CHUNK_SIZE = 500
OVERLAPPING = 100
N_SEARCH_RESULTS = 5
# ==================== FUNCIONES AUXILIARES ====================
def extraer_texto_pdf(pdf_path):
"""Extraer texto de un archivo PDF"""
try:
with open(pdf_path, "rb") as file:
reader = pypdf2.PdfReader(file)
text = ""
for page in reader.pages:
text += page.extract_text()
return text
except Exception as e:
print(f"Error extrayendo texto de {pdf_path}: {str(e)}")
return ""
def limpiar_texto(texto):
"""
Limpia el texto de espacios múltiples, saltos de línea y tabs
"""
# Remover tabs
texto = re.sub(r'\t', ' ', texto)
# Remover saltos de línea
texto = re.sub(r'\n+', ' ', texto)
# Remover espacios múltiples (3 o más)
texto = re.sub(r' {3,}', ' ', texto)
# Remover espacios dobles
texto = re.sub(r' +', ' ', texto)
# Remover espacios al inicio y final
texto = texto.strip()
# Remover espacios antes de puntos
texto = re.sub(r' \.', '.', texto)
# Remover puntos repetidos
texto = re.sub(r'\.{2,}', '.', texto)
return texto
def dividir_texto(text, chunk_size=CHUNK_SIZE, overlapping=OVERLAPPING):
"""
Dividir texto en fragmentos con superposición
"""
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
if end < len(text):
# Buscar el último espacio para no cortar palabras
last_space = text.rfind(' ', start, end)
if last_space > start:
end = last_space
chunk = text[start:end].strip()
if chunk:
chunks.append(chunk)
start = end - overlapping
if start >= len(text):
break
return chunks
def cargar_y_procesar_pdfs():
"""Cargar, procesar y crear embeddings de los PDFs"""
print("\n Leyendo los pdfs del reglamento")
# Leer PDFs
print("\nLeyendo archivos PDF...")
pdf_folder = DATA_DIR
pdf_files = [f for f in os.listdir(pdf_folder) if f.endswith('.pdf')]
if not pdf_files:
return None, None, None, None
pdf_texts = []
for pdf_file in pdf_files:
text = extraer_texto_pdf(os.path.join(pdf_folder, pdf_file))
if text:
pdf_texts.append(text)
# Limpiar textos
print("Limpiando textos")
pdf_texts_limpios = [limpiar_texto(text) for text in pdf_texts]
# Dividir en chunks
print("Dividiendo textos en fragmentos")
chunks = []
for texto in pdf_texts_limpios:
chunks.extend(dividir_texto(texto, chunk_size=CHUNK_SIZE, overlapping=OVERLAPPING))
print(f"✓ Total de fragmentos generados: {len(chunks)}")
# Crear embeddings
print("Generando embeddings")
embedding_model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
embeddings = embedding_model.encode(chunks)
print(f"✓ Dimensión de embeddings: {embeddings.shape}")
# Crear base de datos vectorial
print("Creando base de datos vectorial")
client = Client()
collection = client.create_collection(
name="documentos_pdf",
metadata={"hnsw:space": "cosine"}
)
# Agregar fragmentos a la colección
collection.add(
embeddings=embeddings,
metadatas=[{"source": "pdf", "chunk_id": i} for i in range(len(chunks))],
documents=chunks,
ids=[f"chunk_{i}" for i in range(len(chunks))]
)
print(f"Base de datos vectorial creada con {len(chunks)} fragmentos\n")
return embedding_model, collection, client, chunks
# ==================== INICIALIZACIÓN ====================
print("Cargando los documentos y los modelos necesarios")
embedding_model, collection, client, chunks = cargar_y_procesar_pdfs()
# Cargar modelo QA
print("Cargando modelo de Preguntas y Respuestas")
model_name = 'mrm8488/distill-bert-base-spanish-wwm-cased-finetuned-spa-squad2-es'
tokenizer = AutoTokenizer.from_pretrained(model_name)
qa_model = AutoModelForQuestionAnswering.from_pretrained(model_name)
print("Modelo QA cargado\n")
# ==================== FUNCIÓN PRINCIPAL ====================
def obtener_respuesta(pregunta):
if not pregunta.strip():
return "La pregunta no es valida."
if embedding_model is None or collection is None:
return "Los documentos no se cargaron correctamente."
try:
# Generar embedding de la pregunta
query_embedding = embedding_model.encode(pregunta)
# Buscar documentos similares
res = collection.query(
query_embeddings=[query_embedding],
n_results=N_SEARCH_RESULTS
)
# Combinar contexto
context = " ".join(res['documents'][0])
# Tokenizar pregunta y contexto
inputs = tokenizer(pregunta, context, return_tensors='pt', max_length=512, truncation=True)
# Obtener predicciones del modelo
with torch.no_grad():
outputs = qa_model(**inputs)
# Extraer índices de inicio y fin
start_idx = torch.argmax(outputs.start_logits)
end_idx = torch.argmax(outputs.end_logits) + 1
# Decodificar respuesta
answer_tokens = inputs.input_ids[0, start_idx:end_idx]
answer = tokenizer.decode(answer_tokens, skip_special_tokens=True)
return answer if answer.strip() else "No se encontró una respuesta clara."
except Exception as e:
return f"Error procesando la pregunta: {str(e)}"
# ==================== INTERFAZ GRADIO ====================
with gr.Blocks(title="Asistente de Reglamentos UNAB") as demo:
gr.Markdown("""
# Asistente de Reglamentos UNAB
Realiza preguntas sobre los reglamentos de la Universidad Autónoma de Bucaramanga
utilizando búsqueda semántica y procesamiento de lenguaje natural.
""")
with gr.Row():
with gr.Column():
question_input = gr.Textbox(
lines=3,
placeholder="Ejemplo: ¿Cuál es el proceso de inscripción?",
label="Tu pregunta"
)
submit_button = gr.Button("Buscar respuesta", variant="primary")
with gr.Column():
answer_output = gr.Textbox(
label="Respuesta",
lines=5,
interactive=False
)
submit_button.click(
fn=obtener_respuesta,
inputs=question_input,
outputs=answer_output
)
# Enter key support
question_input.submit(
fn=obtener_respuesta,
inputs=question_input,
outputs=answer_output
)
gr.Markdown("""
### Información
- Este asistente responde preguntas basadas en documentos de la UNAB
- Utiliza búsqueda semántica para encontrar fragmentos relevantes
- Las respuestas se generan automáticamente mediante IA
- Para mejores resultados, sé específico en tus preguntas
""")
if __name__ == "__main__":
demo.launch(share=False)