Habla con tus documentos en formato PDF, txt e incluso páginas web
Habla con tus documentos en PDF, txt y páginas web.
Guía completa para crear una web y la inteligencia que te permite hacer preguntas a documentos como PDFs, TXTs, e incluso páginas web utilizando LLMs.
Tabla de contenido
· Introducción· ¿Cómo funciona· Pasos (parte 1) 👣· Descanso: Recapitulando (parte 1) 🌪️· Pasos (parte 2) 👣· Descanso: Recapitulando (parte 2) 🌪️· Aplicación web· Para los impacientes (código)· Conclusiones· Referencias
Introducción
Todos tenemos que leer documentos eternos para obtener dos frases que contengan el valor/información que necesitamos.
¿Alguna vez has deseado poder extraer información interesante de un documento sin perderte en un mar de palabras?
¡No busques más! Bienvenido al proyecto “Talking to Documents Using LLM”. Es como recuperar tesoros de archivos PDF, TXT o páginas web sin fatiga cerebral. Y no te preocupes, no necesitas un título en hechicería tecnológica para divertirte. Hemos creado una interfaz fácil de usar con Streamlit, para que incluso tus amigos no expertos en tecnología, como los asistentes de marketing, puedan unirse.
Este artículo profundizará en la teoría y el código detrás de la inteligencia de la aplicación, arrojando luz sobre cómo funcionan las aplicaciones web. Para darte una visión general rápida de las tecnologías que utilizaremos, echa un vistazo a la siguiente imagen que muestra las cuatro herramientas principales en acción.
- Interpretable AI con SHAP
- 2024 Bola de cristal de gestión de datos principales 4 tendencias e...
- Científicos de la Computación de la Universidad de Massachusetts Am...

Y por supuesto, puedes encontrar el código en mi repositorio de GitHub, o si quieres probar el código directamente, puedes ir a la sección “Para los impacientes (código)”.
¿Cómo funciona?
Echemos un vistazo tras el velo de “Talking to Documents Using LLM”. Este proyecto es como un dúo interesante: un jugador inteligente (backend) y un sitio web fácil de usar (frontend).
- Lector inteligente: El cerebro de IA lee y comprende documentos como un amigo súper inteligente, manejando excelentemente PDFs, TXTs y contenido web.
- Sitio web fácil de usar: Interfaz web fácil de usar para configurar e interactuar con el modelo. Consta de dos páginas principales, cada una con un propósito específico: Paso 1️⃣ Crear una base de datos y Paso 2️⃣ Solicitar documentos.
El esquema de nuestro proyecto se ve algo así:

Aunque pueda parecer complicado, simplificaremos cada paso esencial para que sea funcional. Al centrarnos en el funcionamiento de la inteligencia, descubriremos cómo funciona. Estos pasos son compatibles con la clase TalkDocument de Python y demostraremos cada paso con el código correspondiente para darle vida.
Pasos (parte 1) 👣
Prepárate para explorar cada paso del camino mientras descubrimos qué hace la magia detrás de escena! 🚀🔍🎩
1- Importar documentos
Este paso es obvio, ¿verdad? Aquí es donde decides qué tipo de material proporcionas. Ya sea PDF, texto plano, URL de la web o incluso formato de cadena cruda, todo está en el menú.
# Dentro de la función __init__, he comentado las variables# en las que no estamos interesados en este momento.def __init__(self, HF_API_TOKEN, data_source_path=None, data_text=None, OPENAI_KEY=None) -> None: # Puedes ingresar la ruta del archivo. self.data_source_path = data_source_path # Puedes ingresar el archivo en formato de cadena directamente self.data_text = data_text self.document = None # self.document_splited = None # self.embedding_model = None # self.embedding_type = None # self.OPENAI_KEY = OPENAI_KEY # self.HF_API_TOKEN = HF_API_TOKEN # self.db = None # self.llm = None # self.chain = None # self.repo_id = Nonedef get_document(self, data_source_type="TXT"):# DS_TYPE_LIST= ["WEB", "PDF", "TXT"] data_source_type = data_source_type if data_source_type.upper() in DS_TYPE_LIST else DS_TYPE_LIST[0] if data_source_type == "TXT": if self.data_text: self.document = self.data_text elif self.data_source_path: loader = dl.TextLoader(self.data_source_path) self.document = loader.load() elif data_source_type == "PDF": if self.data_text: self.document = self.data_text elif self.data_source_path: loader = dl.PyPDFLoader(self.data_source_path) self.document = loader.load() elif data_source_type == "WEB": loader = dl.WebBaseLoader(self.data_source_path) self.document = loader.load() return self.document
Dependiendo del tipo de archivo que cargues, el documento se leerá mediante diferentes métodos. Una mejora interesante podría implicar agregar detección automática de formato a la mezcla.
2- Tipo de división
Ahora, es posible que te preguntes por qué existe el término “tipo”. Bueno, en la aplicación, puedes elegir el método de división. Pero antes de adentrarnos en este tema, expliquemos qué es la división de documentos.
Piénsalo así: al igual que los humanos necesitan capítulos, párrafos y oraciones para estructurar la información (imagina leer un libro con un párrafo interminable, ¡sí!), las máquinas también necesitan estructura. Necesitamos dividir el documento en varias partes más pequeñas para entenderlo mejor. Esta división se puede hacer por carácter o por token.
# SPLIT_TYPE_LIST = ["CHARACTER", "TOKEN"]def get_split(self, split_type="character", chunk_size=200, chunk_overlap=10): split_type = split_type.upper() if split_type.upper() in SPLIT_TYPE_LIST else SPLIT_TYPE_LIST[0] if self.document: if split_type == "CHARACTER": text_splitter = ts.RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap) elif split_type == "TOKEN": text_splitter = ts.TokenTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap) # Si ingresas una cadena como documento, realizaremos una división de texto. if self.data_text: try: self.document_splited = text_splitter.split_text(text=self.document) except Exception as error: print( error) # Si cargas un documento, realizaremos una división de documentos. elif self.data_source_path: try: self.document_splited = text_splitter.split_documents(documents=self.document) except Exception as error: print( error) return self.document_splited
3- Tipo de incrustación
Los humanos podemos entender palabras e imágenes fácilmente, pero las máquinas necesitan un poco más de orientación. Esto se vuelve evidente cuando:
- Intentamos convertir variables categóricas en un conjunto de datos en números
- Gestionamos imágenes en redes neuronales. Por ejemplo, antes de que una imagen se alimente a un modelo de red neuronal, se somete a transformaciones para convertirse en un tensor numérico.
Como podemos ver, los modelos matemáticos tienen el lenguaje de los números. Este fenómeno también se observa en el campo de NLP, donde se promueve el concepto de word embedding.
En esencia, lo que estamos haciendo en este paso es convertir las divisiones de la etapa anterior (fragmentos de documentos) en vectores numéricos.
Esta conversión o codificación se realiza utilizando algoritmos especializados. Es importante tener en cuenta que este proceso convierte las oraciones en vectores digitales y que esta codificación no es aleatoria; sigue un método estructurado.
Este código es muy simple: instanciamos el objeto responsable de la integración. Es importante tener en cuenta que en este punto, solo estamos creando un objeto de integración. En los siguientes pasos, realizaremos la conversión real.
def get_embedding(self, embedding_type="HF", OPENAI_KEY=None): if not self.embedding_model: embedding_type = embedding_type.upper() if embedding_type.upper() in EMBEDDING_TYPE_LIST else EMBEDDING_TYPE_LIST[0] # Si elegimos usar el modelo de Hugging Face para el embedding if embedding_type == "HF": self.embedding_model = embeddings.HuggingFaceEmbeddings() # Si optamos por el modelo de OpenAI para el embedding elif embedding_type == "OPENAI": self.OPENAI_KEY = self.OPENAI_KEY if self.OPENAI_KEY else OPENAI_KEY if self.OPENAI_KEY: self.embedding_model = embeddings.OpenAIEmbeddings(openai_api_key=OPENAI_KEY) else: print("Necesitas introducir una clave de API de OPENAI") # El objeto self.embedding_type = embedding_type return self.embedding_model
Descanso: Vamos a recapitular (parte 1) 🌪️
Para comprender los primeros tres pasos, consideremos el siguiente ejemplo:

Empezamos con un texto de entrada.
- Hacemos una división basada en el número de caracteres (aproximadamente 50 caracteres en este ejemplo).
- Hacemos integraciones, convirtiendo fragmentos de texto en vectores digitales.
¡Qué maravilloso! 🚀 Ahora, comencemos el emocionante viaje a través de los niveles restantes. ¡Abróchate los cinturones porque estamos a punto de desbloquear algo de magia tecnológica real! 🔥🔓
Pasos (parte 2) 👣
Continuaremos viendo los pasos a seguir.
4- Tipo de Almacenamiento de Vectores del Modelo
Ahora que hemos convertido nuestro texto en código (incrustado), necesitamos un lugar para almacenarlos. Aquí es donde entra en juego el concepto de “almacenamiento de vectores”. Es como una biblioteca inteligente de estos códigos, lo que facilita encontrar y recuperar códigos similares al hacer una pregunta.
¡Piénsalo como un espacio de almacenamiento ordenado que te permite volver rápidamente a lo que necesitas!
La creación de este tipo de base de datos es gestionada por algoritmos especializados diseñados para este propósito, como FAISS (Facebook AI Similarity Search). También hay otras opciones, y actualmente, esta clase admite CHROMA y SVM.
Este paso y el siguiente comparten código. En este paso, eliges el tipo de repositorio de vectores que deseas crear, mientras que el siguiente paso es donde se realiza la creación real.
5. Modelo VectoreStore (Creación)
Este tipo de base de datos maneja dos aspectos principales:
- Almacenamiento de vectores: Almacena los vectores generados por la integración.
- Cálculo de similitud: Calcula la similitud entre vectores.
Pero, ¿qué es exactamente la similitud entre estos vectores y por qué importa?
Bueno, ¿recuerdas que mencioné que la integración no es aleatoria? Está diseñada para que palabras o frases con significados similares tengan vectores similares. De esta manera, podemos calcular la distancia entre los vectores (como usar la distancia euclidiana) y esto nos proporciona una medida de su “similitud”.
Para visualizar esto con un ejemplo, imagina que tenemos tres oraciones.

Dos están relacionadas con recetas, mientras que la tercera está relacionada con motocicletas. Al representarlas como vectores (gracias a la integración), podemos calcular la distancia entre estos puntos u oraciones. Esta distancia sirve como medida de su similitud.
En términos de código, echemos un vistazo a los requisitos requeridos:
- El texto dividido
- El tipo de incrustación
- El modelo de almacenamiento de vectores
# VECTORSTORE_TYPE_LIST = ["FAISS", "CHROMA", "SVM"]def get_storage(self, vectorstore_type = "FAISS", embedding_type="HF", OPENAI_KEY=None): self.embedding_type = self.embedding_type if self.embedding_type else embedding_type vectorstore_type = vectorstore_type.upper() if vectorstore_type.upper() in VECTORSTORE_TYPE_LIST else VECTORSTORE_TYPE_LIST[0] # Aquí hacemos la llamada al algoritmo # que realiza la incrustación y creamos el objeto self.get_embedding(embedding_type=self.embedding_type, OPENAI_KEY=OPENAI_KEY) # Aquí elegimos el tipo de almacenamiento de vectores que queremos usar if vectorstore_type == "FAISS": model_vectorstore = vs.FAISS elif vectorstore_type == "CHROMA": model_vectorstore = vs.Chroma elif vectorstore_type == "SVM": model_vectorstore = retrievers.SVMRetriever # Aquí creamos el almacenamiento de vectores. En este caso, # el documento proviene de texto sin formato. if self.data_text: try: self.db = model_vectorstore.from_texts(self.document_splited, self.embedding_model) except Exception as error: print( error) # Aquí creamos el almacenamiento de vectores. En este caso, # el documento proviene de un documento como pdf txt... elif self.data_source_path: try: self.db = model_vectorstore.from_documents(self.document_splited, self.embedding_model) except Exception as error: print( error) return self.db
Para ilustrar lo que sucede en este paso, podemos visualizar la siguiente imagen. Muestra cómo los fragmentos de texto codificados se almacenan en el almacenamiento de vectores, lo que nos permite calcular la distancia/similitud entre vectores/puntos.

Descanso: Repasemos (parte 2) 🌪️
¡Genial! Ahora tenemos una base de datos capaz de almacenar nuestros documentos y calcular la similitud entre fragmentos de texto encriptados. Imagina una situación en la que queramos codificar una oración externa y almacenarla en nuestro almacenamiento de vectores. Esto nos permitirá calcular la distancia entre el nuevo vector y la división del documento. (Ten en cuenta que aquí se debe usar la misma integración para crear el almacenamiento de vectores). Podemos visualizar esto a través de la siguiente imagen.

Recuerda la imagen anterior… tenemos dos preguntas y las convertimos en números usando la integración. Luego medimos la distancia y encontramos las frases más cercanas a nuestra pregunta. ¡Estas frases son como combinaciones perfectas! ¡Es como encontrar la mejor pieza de un rompecabezas al instante! 🚀🧩🔍
6- Pregunta
Recuerda que nuestro objetivo es hacer una pregunta sobre un documento y obtener una respuesta. En este paso, recopilamos las preguntas como entrada proporcionada por el usuario.
7 y 8- Divisiones relevantes
Aquí es donde insertamos nuestra pregunta en el repositorio de vectores, lo incorporamos para convertir la frase en un vector digital. Luego calculamos la distancia entre nuestra pregunta y las partes del documento, determinando qué partes están más cerca de nuestra pregunta. El código es:
# Dependiendo del tipo de almacenamiento de vectores que hayamos construido, # utilizaremos una función específica. Todas ellas devuelven # una lista de las divisiones más relevantes.def obtener_busqueda(self, pregunta, con_puntuacion=False): documentos_relevantes = None if self.db and "SVM" not in str(type(self.db)): if con_puntuacion: documentos_relevantes = self.db.busqueda_similitud_con_puntuacion(pregunta) else: documentos_relevantes = self.db.busqueda_similitud(pregunta) elif self.db: documentos_relevantes = self.db.obtener_documentos_relevantes(pregunta) return documentos_relevantes
Pruébalo con el siguiente código. Aprende cómo responder como una lista de 4 elementos. Y cuando miramos dentro, en realidad son partes separadas del documento que ingresaste. ¡Es como la aplicación que te presenta las 4 mejores piezas de rompecabezas que se ajustan perfectamente a tu pregunta! 🧩💬
9- Respuesta (Lenguaje Natural)
Genial, ahora tenemos el texto más relevante para nuestra pregunta. Pero no podemos simplemente entregar esas partes al usuario y terminar ahí. Necesitamos una respuesta concisa y precisa a su pregunta. ¡Y ahí es donde entra en juego nuestro Modelo de Lenguaje Inteligente (LLM)! Hay muchos sabores de LLM. En el código, establecemos “flan-alpaca-large” como predeterminado. ¡No dudes en elegir a la persona que más te conmueve! 🚀🎉
Aquí está el plan:
- Restauramos las partes más importantes (divisiones) relacionadas con la pregunta.
- Preparamos un estímulo que incluya la pregunta, cómo queremos la respuesta y esos elementos de texto.
- Pasamos este estímulo a nuestro Modelo de Lenguaje Inteligente (LLM). Él conoce la pregunta y la información contenida en estos elementos y nos da respuestas naturales.️
Esta última parte se muestra en la siguiente imagen: 🖼️

Sin duda, este flujo exacto es el que se ejecuta en el código. Te darás cuenta de que en el código hay un paso adicional involucrado en la creación de un “estímulo”. De hecho, podemos llegar a una respuesta final que sea una combinación de las respuestas encontradas por el LLM y otras opciones. Para simplificar, hagámoslo de la manera más sencilla: “stuff”. Esto significa que la respuesta es la primera solución encontrada por el LLM. ¡Aquí no te pierdes en la teoría! 🌟
def hacer_pregunta(self, pregunta, repo_id="declare-lab/flan-alpaca-large", tipo_cadena="stuff", documentos_relevantes=None, con_puntuacion=False, temperatura=0, longitud_maxima=300): # Obtenemos las divisiones más relevantes. documentos_relevantes = self.obtener_busqueda(pregunta, con_puntuacion=con_puntuacion) # Definimos el LLM que queremos usar, # debemos introducir el id del repositorio ya que estamos utilizando HuggingFace. self.repo_id = self.repo_id if self.repo_id is not None else repo_id tipo_cadena = tipo_cadena.lower() if tipo_cadena.lower() in LISTA_TIPO_CADENA else LISTA_TIPO_CADENA[0] # Esta comprobación es necesaria ya que podemos llamar a la función varias veces, # pero no tendría sentido crear un LLM cada vez que se realiza la llamada. # Por lo tanto, comprueba si ya existe un llm dentro de la clase # o si ha cambiado el repo_id (el tipo de llm). if (self.repo_id != repo_id ) or (self.llm is None): self.repo_id = repo_id # Creamos el LLM. self.llm = HuggingFaceHub(repo_id=self.repo_id,huggingfacehub_api_token=self.HF_API_TOKEN, model_kwargs= {"temperature":temperatura, "max_length": longitud_maxima}) # Creamos el estímulo plantilla_estimulo = """Utiliza los siguientes elementos de contexto para responder la pregunta al final. Si no sabes la respuesta, simplemente di que no lo sabes, no intentes inventar una respuesta. Si la pregunta es similar a [Háblame sobre el documento], la respuesta debe ser un resumen comentando los puntos más importantes sobre el documento {contexto} Pregunta: {pregunta} """ ESTIMULO = PromptTemplate( template=plantilla_estimulo, input_variables=["contexto", "pregunta"] ) # Creamos la cadena, tipo_cadena = "stuff". self.cadena = self.cadena if self.cadena is not None else load_qa_chain(self.llm, tipo_cadena=tipo_cadena, prompt = ESTIMULO) # Hacemos la consulta al LLM utilizando el estímulo # Comprobamos si ya hay una cadena definida, # si no existe, se crea respuesta = self.cadena({"documentos_entrada": documentos_relevantes, "pregunta": pregunta}, solo_salidas=True) return respuesta
Lo has logrado, has llegado al final y has entendido cómo funciona el cerebro de nuestra aplicación web. ¡Choca esos cinco si estás aquí! Ahora, adentrémonos en la etapa final del artículo, donde exploraremos las páginas de la aplicación web. 🎉🕵️♂️
Aplicación web
Esta interfaz está diseñada para personas sin conocimientos técnicos, para que puedas aprovechar al máximo esta tecnología sin problemas. 🚀👩💻
Usar el sitio es extremadamente fácil y potente. Básicamente, el usuario solo necesita proporcionar el documento. Si no estás listo para aprovechar los parámetros adicionales, en el siguiente paso, ya puedes comenzar a hacer preguntas. 🌟🤖
Veamos los pasos a seguir:
- Proporciona el documento o enlace web. Por página Paso 1️⃣ Crear Base de Datos.
- Configura la configuración (opcional). Por página Paso 1️⃣ Crear Base de Datos.
- ¡Haz tus preguntas y obtén respuestas! 📚🔍🚀 Por página Paso 2️⃣ Preguntar al documento.
1. Proporciona el documento o enlace web.
En este paso, agregas el documento subiéndolo a la web. Recuerda que si no tienes la clave de la API de Face Hugging como variable de entorno, la pestaña “Inicio” te pedirá que la ingreses. 📂🔑

Una vez que hayas adjuntado el documento y configurado tus preferencias, se mostrará una tabla de resumen de tu configuración. El botón “Crear Base de Datos” se desbloqueará. Al hacer clic en el botón, se creará la base de datos o el almacén de vectores, y serás redirigido a la sección de preguntas. 📑🔒🚀

2. Configura la configuración (opcional).
Como mencioné antes, podemos configurar el almacén de vectores según nuestras preferencias. Hay diferentes métodos para dividir documentos, incrustar, y más. Esta pestaña te permite personalizarlo como desees. Viene con una configuración predeterminada para una configuración más rápida. ⚙️🛠️

3. ¡Haz tus preguntas y obtén respuestas!
En este punto, puedes comenzar a hacer todas las consultas que desees. ¡Ahora es el momento de disfrutar e interactuar con la herramienta todo lo que quieras! 🤗🔍💬

Para los impacientes (código)
Para aquellos que están ansiosos por sumergirse, pueden tomar directamente la clase TalkDocument y pegarla en un cuaderno Jupyter para empezar a experimentar. Es posible que necesiten instalar algunas dependencias, pero estoy seguro de que no será un desafío para ustedes. ¡Diviértanse explorando y experimentando! ¡Feliz codificación! 🚀📚😄
La clase TalkDocument en código para jugar (Código por el autor)
Conclusiones
¡Felicitaciones por llegar tan lejos en nuestro emocionante viaje! Hemos profundizado en cómo funciona la inteligencia detrás de esta aplicación web. ¡Desde cargar documentos hasta obtener respuestas, has cubierto mucho terreno! Si te sientes inspirado, el código está disponible en mi repositorio de GitHub. ¡No dudes en colaborar y contactarme en LinkedIn para cualquier pregunta o sugerencia! ¡Diviértete explorando y experimentando con esta poderosa herramienta!
Si quieres, puedes consultar mi GitHub
damiangilgonzalez1995 – Resumen
Apasionado por los datos, pasé de la física a la ciencia de datos. Trabajé en Telefónica, HP y ahora soy CTO en…
github.com
Referencias
- Documentación de Langchain
- Documentación de Hugging Face
- Introducción a la búsqueda de similitud de Facebook AI (Faiss)
- Documentación de Steamlit