Saltar a contenido

Text Document Chat

Resumen del proceso completo

Este documento detalla el desarrollo y funcionamiento de un sistema avanzado diseñado para la consulta interactiva de documentos en formato PDF. El sistema abarca varios procesos clave, incluyendo la limpieza de documentos, la generación de embeddings, y la gestión de una base de datos vectorial. A continuación, se describen cada uno de estos procesos en detalle.

Proceso de Limpieza de Documentos

Inicialmente, el sistema realiza una limpieza exhaustiva de los documentos PDF. Este paso es crucial para optimizar la eficiencia del modelo, reduciendo el número de tokens necesarios para la creación de embeddings. La limpieza implica la eliminación de elementos no esenciales y la estandarización del texto para asegurar una entrada de datos coherente y de alta calidad.

Generación y Almacenamiento de Embeddings

Una vez limpiados los documentos, se procede a la generación de embeddings. Este proceso transforma el texto en vectores numéricos que representan el contenido semántico de los documentos. Estos embeddings se almacenan posteriormente en una base de datos vectorial, diseñada para facilitar una recuperación eficiente y precisa del contenido.

Interacción del Usuario y Búsqueda de Similitud Contextual

El usuario interactúa con el sistema a través de preguntas formuladas en lenguaje natural. Estas preguntas se transforman en embeddings utilizando un procedimiento similar al aplicado a los documentos PDF. Seguidamente, el sistema realiza una búsqueda en la base de datos vectorial para encontrar los documentos cuyos embeddings son más similares en contexto a la pregunta del usuario.

Respuesta a la Consulta mediante el Modelo de Lenguaje de IA (GPT-4)

Finalmente, basándose en los documentos seleccionados por su relevancia contextual, el modelo de lenguaje de inteligencia artificial GPT-4 genera una respuesta a la consulta del usuario. Este proceso se lleva a cabo aprovechando la capacidad avanzada del modelo para comprender y sintetizar información, proporcionando respuestas precisas y contextualmente relevantes.

A continuación se muestra un esquema completo del proceso anteriormente descrito.

Proceso completo

Explicación del código empleado

Parte 1: Declaraciones de Importación

import uuid
import openai
import streamlit as st
from langchain.chat_models import AzureChatOpenAI
from langchain.embeddings import OpenAIEmbeddings
from helpers.CustomOpenAIEmbeddings import CustomOpenAIEmbeddings
from langchain.text_splitter import TokenTextSplitter
from PyPDF2 import PdfReader
import docx
import re
from langchain.vectorstores import FAISS
from langchain.chains import RetrievalQA
from streamlit_chat import message
from langchain.callbacks import get_openai_callback
from helpers.LogGeneartor import LogGenerator
from PIL import Image
import tiktoken

Este fragmento del script incluye declaraciones de importación, esenciales para usar bibliotecas y módulos externos en el script. Aquí hay una breve descripción de las bibliotecas y sus propósitos:

  • uuid: Genera identificadores únicos, útil para crear referencias distintas.
  • openai: Interfaz con la API de OpenAI, probablemente para interacciones con modelos de lenguaje.
  • streamlit: Un marco para crear aplicaciones web, sugiriendo un componente de interfaz de usuario.
  • langchain.chat_models: Relacionado con modelos de chatbot, posiblemente para manejar aspectos conversacionales.
  • langchain.embeddings: Para generar y manejar incrustaciones de texto.
  • helpers.CustomOpenAIEmbeddings: Un módulo personalizado, probablemente una extensión o modificación de las incrustaciones estándar de OpenAI.
  • langchain.text_splitter: Potencialmente para dividir textos grandes en partes manejables.
  • PyPDF2: Una biblioteca para leer y manipular archivos PDF.
  • docx: Para manejar documentos de Microsoft Word.
  • re: Biblioteca de expresiones regulares para el procesamiento de texto.
  • langchain.vectorstores: Para almacenar y gestionar incrustaciones vectoriales.
  • langchain.chains: Posiblemente para configurar cadenas de procesamiento o flujos de trabajo.
  • streamlit_chat: Integración con Streamlit para funcionalidades de chat.
  • langchain.callbacks: Manejo de callbacks, potencialmente para características interactivas o respuestas de API.

Parte 2: Carga del Modelo OpenAI con Caché de Streamlit

@st.cache_resource
def load_OpenAI(temperature=0.7):
    llm = AzureChatOpenAI(model_name="gpt-3.5-turbo",deployment_name="GPTTurbo35",
                    openai_api_base=st.secrets.openai.api_base,
                    openai_api_key=st.secrets.openai.api_key,
                    openai_api_type=st.secrets.openai.api_type,
                    openai_api_version=st.secrets.openai.api_version,temperature=temperature)
    return llm

Esta parte del script define una función load_OpenAI con un parámetro opcional temperature. La función está diseñada para inicializar y devolver un modelo de lenguaje de OpenAI. Aquí está el desglose del código:

  • Decorador de Caché de Streamlit (@st.cache_resource): Este decorador se utiliza para almacenar en caché la salida de la función. En Streamlit, el almacenamiento en caché es beneficioso para el rendimiento, ya que previene la carga repetida de recursos (como un modelo de lenguaje) cada vez que se accede o se actualiza la aplicación.

  • Definición de la Función (load_OpenAI):

  • Parámetro: temperature=0.7 - Esto establece el comportamiento predeterminado del modelo de lenguaje. El parámetro de temperatura en los modelos de lenguaje típicamente controla la aleatoriedad de la salida. Una temperatura más alta resulta en respuestas más variadas, mientras que una temperatura más baja produce respuestas más predecibles y conservadoras.

  • Inicialización del Modelo de Lenguaje (AzureChatOpenAI):

    • Esto crea una instancia de un modelo de lenguaje usando AzureChatOpenAI, lo que sugiere que el modelo está alojado en Azure, la plataforma en la nube de Microsoft.
    • model_name="gpt-3.5-turbo": Especifica el uso del modelo GPT-3.5 Turbo de OpenAI.
    • deployment_name, openai_api_base, openai_api_key, openai_api_type, openai_api_version: Estos parámetros están configurados con ajustes y credenciales, probablemente almacenados en la gestión de secretos de Streamlit. Esta es una forma segura de manejar información sensible como las claves de API.
    • temperature=temperature: Pasa el parámetro de temperatura al modelo, influyendo en su estilo de respuesta.
  • Declaración de Retorno: La función devuelve el objeto del modelo de lenguaje inicializado, que se puede utilizar en otras partes del script para diversas tareas de procesamiento de lenguaje natural o generación.

Parte 3: Carga de Embeddings con Caché de Streamlit

@st.cache_resource
def load_Embeddings():
    embeddings = CustomOpenAIEmbeddings(deployment="PdfEmbedding", chunk_size=1,
                            openai_api_base=st.secrets.openai.api_base,
                            openai_api_key=st.secrets.openai.api_key,
                            openai_api_type=st.secrets.openai.api_type,
                            openai_api_version=st.secrets.openai.api_version)
    return embeddings

Esta parte del script define una función load_Embeddings. La función está diseñada para inicializar y devolver incrustaciones personalizadas de OpenAI. Aquí está el desglose del código:

  • Decorador de Caché de Streamlit (@st.cache_resource): Este decorador se utiliza para almacenar en caché la salida de la función, mejorando el rendimiento en Streamlit.

  • Definición de la Función (load_Embeddings):

  • Inicialización de Embedding (CustomOpenAIEmbeddings):

    • deployment="PdfEmbedding": Especifica un despliegue personalizado para las incrustaciones, posiblemente centrado en documentos PDF.
    • chunk_size=1: Define el tamaño del fragmento para procesar los textos, lo que podría influir en cómo se dividen los textos para la incrustación.
    • openai_api_base, openai_api_key, openai_api_type, openai_api_version: Estos parámetros están configurados con ajustes y credenciales de la API de OpenAI.
  • Declaración de Retorno: La función devuelve el objeto de incrustaciones personalizadas, que se puede utilizar para generar incrustaciones de texto para su uso en el sistema de preguntas y respuestas y otras tareas de procesamiento de lenguaje.

Parte 4: Función para Evaluar la Objetividad de una Pregunta

def question_objetivity(question,context):
    auxcontext = ""
    enc = tiktoken.get_encoding("p50k_base")
    for doc in context:
        base = len(enc.encode(auxcontext))
        new_content = len(enc.encode(doc.page_content))
        if base+new_content < 4000:
            auxcontext += doc.page_content
        else:
            break
    PROMPT = """\
    Given the next context and a question made by user, please rate the objectivity of the question based on the given context in a rate of 1 to 10, return the number you decide.
    QUESTION CONTEXT: {context}
    QUESTION MADE BY USER: {question}
    """
    enc = tiktoken.get_encoding("p50k_base")
    prompt = PROMPT.format(context=auxcontext,question=question)
    response = openai.Completion.create(engine="Davi",
                                        prompt=prompt,
                                        temperature=0.1,
                                        max_tokens=10,
                                        top_p=0.5,
                                        frequency_penalty=0,
                                        presence_penalty=0,
                                        best_of=1,
                                        stop=None)
    LogGenerator().store_entry("text-document-chat", "text-davinci-003", response.usage.total_tokens,prompt)
    return response.choices[0].text

Esta parte del script define una función question_objetivity, que evalúa la objetividad de una pregunta basada en un contexto dado. Aquí está el desglose del código:

  • Definición de la Función (question_objetivity):
  • Parámetros: question, context - La función toma una pregunta y un contexto (un conjunto de documentos o textos) como entradas.

  • Proceso de Creación del Contexto:

  • Se inicia un contexto auxiliar vacío (auxcontext).
  • Se utiliza una codificación (probablemente una forma de tokenización) para manejar el texto.
  • Se itera sobre los documentos en context, agregando su contenido al auxcontext. Hay una comprobación para asegurarse de que la longitud total del texto no exceda un límite específico (4000 tokens), para mantener la entrada manejable para el modelo de lenguaje.

  • Construcción del Prompt:

  • Se crea un prompt estructurado que solicita al modelo de lenguaje evaluar la objetividad de la pregunta en una escala del 1 al 10, basándose en el contexto proporcionado.

  • Consulta al Modelo de Lenguaje:

  • Se utiliza openai.Completion.create para generar una respuesta del modelo de lenguaje.
  • Se configuran varios parámetros como engine, temperature, max_tokens, que controlan el estilo y la longitud de la respuesta.
  • La respuesta se genera en función del prompt que combina la pregunta y el contexto.

  • Registro de la Actividad:

  • Se registra la actividad (incluyendo el número de tokens utilizados y el prompt) usando LogGenerator().store_entry. Esto sugiere un mecanismo de seguimiento o registro para la actividad del modelo de lenguaje.

  • Retorno de la Respuesta:

  • La función devuelve el texto de la respuesta, que representa la evaluación de la objetividad de la pregunta.

Esta función es crucial para evaluar preguntas en términos de su relevancia y objetividad con respecto a un conjunto de documentos o contexto dado, lo cual es importante en un sistema de preguntas y respuestas.

Parte 5: Función para Normalizar Texto

def normalize_text(s, sep_token = " \n "):
    s = re.sub(r'\s+',  ' ', s).strip()
    s = re.sub(r". ,","",s)
    # remove all instances of multiple spaces
    s = s.replace("..",".")
    s = s.replace(". .",".")
    s = s.replace("\n", "")
    s = s.strip()

    return s

Esta sección del script define una función normalize_text para normalizar el texto. La normalización es un paso importante en el procesamiento de texto, ya que uniformiza el formato del texto para su posterior análisis o procesamiento. Aquí está el desglose del código:

  • Definición de la Función (normalize_text):
  • Parámetros: s (el texto a normalizar), sep_token (un token separador, por defecto es " \n ").

  • Proceso de Normalización:

  • Se utiliza re.sub para reemplazar espacios múltiples y consecutivos con un solo espacio. Esto elimina la inconsistencia en el espaciado.
  • Se eliminan instancias específicas de puntuación y caracteres, como múltiples comas y puntos, y se reemplazan los saltos de línea (\\n) con un espacio vacío. Esto ayuda a crear un flujo de texto más coherente.
  • Se eliminan espacios al principio y al final del texto con strip(). Esto asegura que no haya espacios en blanco innecesarios alrededor del texto.

  • Retorno del Texto Normalizado:

  • La función devuelve el texto normalizado, listo para ser utilizado en otras operaciones de procesamiento de texto en el script.

La función normalize_text juega un papel clave en la preparación del texto para el análisis, asegurando que esté en un formato consistente y limpio antes de ser procesado por modelos de lenguaje u otras funciones.

Parte 6: Función para Extraer Texto de Documentos DOCX

def extract_text_from_docx(document):
    doc = docx.Document(document)
    text = '\n'.join([paragraph.text for paragraph in doc.paragraphs])
    text = normalize_text(text)
    return text

En esta parte del script, se define una función extract_text_from_docx para extraer texto de documentos Word (DOCX). Aquí está el desglose del código:

  • Definición de la Función (extract_text_from_docx):
  • Parámetro: document - La función toma un documento Word (DOCX) como entrada.

  • Proceso de Extracción de Texto:

  • Se utiliza docx.Document para cargar el documento Word.
  • Se extrae el texto de cada párrafo del documento. Esto se hace iterando sobre doc.paragraphs y concatenando el texto de cada párrafo, separados por saltos de línea ('\\n').
  • El texto extraído se pasa a la función normalize_text para normalizarlo, lo que implica limpiar y estandarizar el formato del texto.

  • Retorno del Texto Extraído:

  • La función devuelve el texto extraído y normalizado del documento Word.

Esta función es esencial para procesar documentos Word en el sistema. Permite extraer y limpiar el texto de estos documentos, preparándolo para su incrustación y análisis posterior en el sistema de preguntas y respuestas.

Parte 7: Función para Extraer Texto de Documentos y Crear Base de Datos de Vectores

@st.cache_resource
def extract_text_from_document(documents):
    chunks = []
    for document in documents:
        text = ""
        if document.type == "application/pdf":
            pages = PdfReader(document).pages
            text = " \n ".join([page.extract_text() for page in pages])
            text = normalize_text(text)
        elif document.type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
            doc = docx.Document(document)
            text = '\n'.join([paragraph.text for paragraph in doc.paragraphs])
            text = normalize_text(text)
        text_splitter = TokenTextSplitter(chunk_size=1000, chunk_overlap=0)
        chunks.extend(text_splitter.split_text(text))

    if len(chunks) == 0:
        st.error("No text found in document")
        return None

    with st.spinner("Processing Document..."):
        db = FAISS.from_texts(chunks, load_Embeddings())
        st.success("Document processed!")
        st.session_state.textdocument_chat_messages = []
    return db

Esta sección del script define una función extract_text_from_document para extraer texto de documentos y luego crear una base de datos vectorial utilizando FAISS. Aquí está el desglose del código:

  • Decorador de Caché de Streamlit (@st.cache_resource): Al igual que antes, este decorador se utiliza para mejorar el rendimiento almacenando en caché los resultados de la función.

  • Definición de la Función (extract_text_from_document):

  • Parámetro: documents - La función toma una lista de documentos (PDF o Word) como entrada.

  • Procesamiento de Documentos:

    • Inicializa una lista chunks para almacenar fragmentos de texto.
    • Recorre cada document en documents.
    • Verifica el tipo de documento (PDF o Word) y extrae el texto correspondiente:
    • Para PDF: Usa PdfReader para leer las páginas y extraer texto.
    • Para Word: Usa docx.Document para leer los párrafos y extraer texto.
    • Normaliza el texto extraído con normalize_text (una función no mostrada en este fragmento).
    • Divide el texto en fragmentos más pequeños usando TokenTextSplitter, con un tamaño de fragmento de 1000 tokens.
  • Manejo de Documentos sin Texto: Si no se encuentra texto (len(chunks) == 0), muestra un error y devuelve None.

  • Procesamiento del Documento:

    • Muestra un indicador de procesamiento con st.spinner.
    • Crea una base de datos vectorial (FAISS) a partir de los fragmentos de texto. Utiliza la función load_Embeddings para obtener las incrustaciones necesarias para la base de datos.
    • Muestra un mensaje de éxito una vez que el documento ha sido procesado.
    • Inicializa st.session_state.textdocument_chat_messages como una lista vacía, probablemente para almacenar mensajes en la interfaz de Streamlit.
  • Declaración de Retorno: La función devuelve la base de datos db creada con FAISS, que contiene las incrustaciones de los fragmentos de texto. Esta base de datos puede ser utilizada luego para tareas de recuperación y respuesta a preguntas basadas en los documentos procesados.

Parte 8: Función para Limpiar el Chat

def on_change_clear_chat():
    st.session_state.textdocument_chat_messages = []

Esta parte del script define una función simple llamada on_change_clear_chat. Aquí está el desglose del código:

  • Definición de la Función (on_change_clear_chat):
  • Esta función no toma parámetros y su propósito es limpiar el chat en la interfaz de Streamlit.
  • Dentro de la función, se restablece st.session_state.textdocument_chat_messages a una lista vacía.
  • st.session_state es una característica de Streamlit que permite almacenar y manejar el estado de la aplicación durante la interacción del usuario. En este caso, se utiliza para mantener los mensajes del chat.
  • Al asignar una lista vacía a textdocument_chat_messages, se borran todos los mensajes anteriores del chat, posiblemente para iniciar una nueva conversación o para refrescar la interfaz.

Parte 9: Inicialización del ID de Usuario en Streamlit

if 'user_id' not in st.session_state:
    st.session_state.user_id = str(uuid.uuid4())

Esta parte del script es totalmente necesaria para el funcionamiento completo de la aplicación completa de streamlit.

  • Comprobación y Asignación de ID de Usuario:
  • if 'user_id' not in st.session_state: Esta línea verifica si la clave user_id existe en st.session_state. Si no existe, significa que es la primera vez que el usuario accede a la aplicación o que la sesión se ha reiniciado.
  • st.session_state.user_id = str(uuid.uuid4()): Si user_id no existe, el script genera un nuevo identificador único utilizando uuid.uuid4() y lo asigna a st.session_state.user_id.
  • uuid.uuid4() genera un UUID (Identificador Único Universal) aleatorio, lo que garantiza que cada usuario tenga un identificador único. Esto es útil para rastrear sesiones de usuario o para personalizar la experiencia del usuario en la aplicación.

Parte 10: Input y lógica base de la aplicación

st.set_page_config(page_title="Document Chat", page_icon="📖",layout="centered")
sogetilabs_logo = Image.open('images/SogetiLabs_Logo.png')
st.image(sogetilabs_logo, width=200)
st.title("Ask your Text Documents 📃")

documents = st.file_uploader("Upload your document", type=['pdf','docx'],on_change=on_change_clear_chat,accept_multiple_files=True)

if documents is not None and len(documents) > 0:
    db = extract_text_from_document(documents)
    if db is not None:
        temp = st.slider("Temperature (AI creativity in response)", min_value=0.1, max_value=1.0, value=0.1, step=0.01)
        for message in st.session_state.textdocument_chat_messages:
            with st.chat_message(message["role"]):
                st.markdown(message["content"])
        if prompt := st.chat_input("Ask me something about documents"):
            st.session_state.textdocument_chat_messages.append({"role": "user", "content": prompt})
            with st.chat_message("user"):
                st.markdown(prompt)

            with st.chat_message("assistant"):
                message_placeholder = st.empty()
                llm = load_OpenAI(temperature=temp)
                qa = RetrievalQA.from_chain_type(llm=llm, chain_type="stuff", retriever=db.as_retriever(search_type="similarity", search_kwargs={"k":6}), return_source_documents=False)
                context = qa._get_docs(prompt)
                objetivity = question_objetivity(prompt,context).replace("\n","")
                with get_openai_callback() as cb:
                    result = qa({"query": prompt})
                    LogGenerator().store_entry("text-document-chat", llm.model_name, cb.total_tokens,prompt)
                ai_result = f"\t**Specificity level: {objetivity}**\n\n{result['result']}"
                message_placeholder.markdown(ai_result)
            st.session_state.textdocument_chat_messages.append({"role": "assistant", "content": ai_result})
    else:
        st.warning("Please upload a document for start chatting")
else:
    st.warning("Please upload a document for start chatting")
  • Configuración de la Página con Streamlit:
  • st.set_page_config(page_title="Document Chat", page_icon="📖", layout="centered"): Esta línea configura la página web utilizando Streamlit. Establece el título de la página como "Document Chat", el icono de la página como un emoji de libro, y el diseño de la página como centrado.

  • Carga del Logo y Título de la Página:

  • sogetilabs_logo = Image.open('images/SogetiLabs_Logo.png'): Carga una imagen del logo de SogetiLabs desde una ruta de archivo especificada.
  • st.image(sogetilabs_logo, width=200): Muestra la imagen del logo en la página web con un ancho de 200 píxeles.
  • st.title("Ask your Text Documents 📃"): Añade un título a la página web con el texto "Ask your Text Documents 📃".

  • Cargador de Archivos:

  • documents = st.file_uploader("Upload your document", type=['pdf','docx'], on_change=on_change_clear_chat, accept_multiple_files=True): Crea un cargador de archivos en la página web, permitiendo a los usuarios subir documentos en formato PDF o DOCX. La función on_change_clear_chat se activa al cambiar el estado del cargador de archivos, y se permite la subida de múltiples archivos.

  • Procesamiento de Documentos Subidos:

  • if documents is not None and len(documents) > 0: Esta condición verifica si se han subido documentos.

    • db = extract_text_from_document(documents): Llama a la función extract_text_from_document para procesar los documentos subidos y extraer su texto.
    • if db is not None: Verifica si la base de datos resultante no es nula.
    • temp = st.slider(...): Crea un control deslizante en la interfaz para ajustar la "temperatura" (creatividad) de la respuesta de la IA.
  • Interfaz de Chat:

  • Se recorren los mensajes almacenados en st.session_state.textdocument_chat_messages y se muestran en la interfaz de chat.
  • if prompt := st.chat_input("Ask me something about documents"): Si el usuario introduce una pregunta, se almacena y se muestra en el chat.
  • with st.chat_message("assistant"): Prepara la interfaz para mostrar la respuesta del asistente.
    • message_placeholder = st.empty(): Crea un espacio en la interfaz para la respuesta del asistente.
    • llm = load_OpenAI(temperature=temp): Carga un modelo de OpenAI con la temperatura especificada.
    • qa = RetrievalQA.from_chain_type(...): Crea una instancia de un sistema de preguntas y respuestas que utiliza el modelo de OpenAI y la base de datos de documentos.
    • context = qa._get_docs(prompt): Obtiene documentos relevantes para la pregunta.
    • objetivity = question_objetivity(prompt, context): Evalúa la objetividad de la pregunta.
    • with get_openai_callback() as cb: Envía la pregunta al modelo de OpenAI y recibe la respuesta.
    • message_placeholder.markdown(ai_result): Muestra la respuesta del asistente en la interfaz de chat.
  • Los mensajes del usuario y del asistente se almacenan en st.session_state.textdocument_chat_messages para mantener un historial del chat.

  • Manejo de Situaciones sin Documentos Subidos:

  • else: st.warning(...): Si no se han subido documentos, se muestra una advertencia en la interfaz pidiendo al usuario que suba un documento.