Puedes seguir el artículo y descargar el código en este notebook subido en GitHub.
En muchos casos, especialmente cuando tenemos que tratar con documentos largos, un chunk o fragmento de texto puede caracer del contexto necesario para eliminar la ambigüedad entre ese fragmento y otros del mismo Document.
Un método para abordar esto es etiquetar manualmente cada fragmento de nuestro conjunto de datos. No obstante, hay que tener en cuenta que esto puede requerir mucho tiempo si tratamos con una gran cantidad de documentos o documentos que se actualizan de manera constante.
Para solucionarlo, utilizamos LLMs para extraer cierta información contextual relevante para el documento y así ayudar con la recuperación de la información de manera más efectiva.
Se utiliza el módulo Metadata Extractor de LlamaIndex.
# Tienes que tener instalados los siguientes paquetes
%pip install llama-index-llms-openai
%pip install llama-index-extractors-entity
Si no has ejecutado nunca LlamaIndex también tendrás que instalarlo:
%pip install llama-index
Luego ejecuta este código:
import nest_asyncio
nest_asyncio.apply()
El módulo nest_asyncio se utiliza para permitir el uso de bucles de eventos asyncio anidados en un entorno Jupyter Notebook o en Google Colab. El módulo proporciona un marco para escribir código concurrente utilizando la sintaxis async/await y evitar errores de lógica inesperados.
Luego sigue con esto:
import os
import openai
# os.environ["OPENAI_API_KEY"] = "YOUR_API_KEY_HERE"
# O puedes utilizar la funcion load_dotenv del módulo dotenv, pero tendrás que tener creado un archivo .env con la clave OPENAI_API_KEY
from dotenv import load_dotenv
load_dotenv()
from llama_index.llms.openai import OpenAI
from llama_index.core.schema import MetadataMode
llm = OpenAI(temperature=0.1, model="gpt-3.5-turbo", max_tokens=512)
Ahora vamos a crear un analizador de nodos que extraerá el título del documento y hipotéticas preguntas relacionadas con el contenido de ese chunk. Instanciamos diferentes clases del módulo extractors como SummartyExtractor y KeywordExtractor, aunque también puedes crear el tuyo utilizando la clase BaseExtractor.
from llama_index.core.extractors import (
SummaryExtractor,
QuestionsAnsweredExtractor,
TitleExtractor,
KeywordExtractor,
BaseExtractor,
)
from llama_index.extractors.entity import EntityExtractor
from llama_index.core.node_parser import TokenTextSplitter
text_splitter = TokenTextSplitter(
separator=" ", chunk_size=512, chunk_overlap=128
)
class CustomExtractor(BaseExtractor):
def extract(self, nodes):
metadata_list = [
{
"custom": (
node.metadata["document_title"]
+ "\n"
+ node.metadata["excerpt_keywords"]
)
}
for node in nodes
]
return metadata_list
extractors = [
TitleExtractor(nodes=5, llm=llm),
QuestionsAnsweredExtractor(questions=3, llm=llm),
# EntityExtractor(prediction_threshold=0.5),
# SummaryExtractor(summaries=["prev", "self"], llm=llm),
# KeywordExtractor(keywords=10, llm=llm),
# CustomExtractor()
]
transformations = [text_splitter] + extractors
Utilizando la fuente de datos de Tesla que introducíamos en este artículo, y una vez cargada la información utilizando SimpleDirectoryReader, vamos a aplicar las transformaciones a los documentos que se han generado:
from llama_index.core import SimpleDirectoryReader
# Note the uninformative document file name, which may be a common scenario in a production setting
# tesla_docs = SimpleDirectoryReader(input_files=["data/TSLA-Q1-2024-Update.pdf"]).load_data()
tesla_docs = SimpleDirectoryReader("./data").load_data()
# Nuestro Document tiene un total de 31 objetos
# tesla_front_pages = tesla_docs[0:3]
# tesla_content = tesla_docs[3:31]
# tesla_docs = tesla_front_pages + tesla_content
from llama_index.core.ingestion import IngestionPipeline
pipeline = IngestionPipeline(transformations=transformations)
tesla_nodes = pipeline.run(documents=tesla_docs)
Para ver el resultado mejor vamos a utilizar una función auxiliar para ayudarnos a serializar, es decir, traducir una estructura compleja a un formato que Python pueda manejar, y luego imprimir el resultado más legible:
import json
def serialize_and_print_text_node(text_node):
# Convertir el objeto TextNode a un diccionario
text_node_dict = {
'id_': text_node.id_,
'embedding': text_node.embedding,
'metadata': text_node.metadata,
'excluded_embed_metadata_keys': text_node.excluded_embed_metadata_keys,
'excluded_llm_metadata_keys': text_node.excluded_llm_metadata_keys,
'relationships': {
str(relationship): {
'node_id': related_node_info.node_id,
'node_type': str(related_node_info.node_type),
'metadata': related_node_info.metadata,
'hash': related_node_info.hash
}
for relationship, related_node_info in text_node.relationships.items()
},
'text': text_node.text,
'start_char_idx': text_node.start_char_idx,
'end_char_idx': text_node.end_char_idx,
'text_template': text_node.text_template,
'metadata_template': text_node.metadata_template,
'metadata_seperator': text_node.metadata_seperator
}
# Serializar el diccionario a JSON con indentación para una mejor legibilidad
json_data = json.dumps(text_node_dict, indent=2)
# Imprimir el JSON serializado
print(json_data)
Si guardamos el primer objeto que se ha creado text_node = tesla_nodes[0] y lo mostramos a través de la función serialize_and_print_text_node(text_node):
serialize_and_print_text_node(tesla_nodes[0])
{
"id_": "b27f3ba9-a672-4ba3-842c-a7e2ff88832f",
"embedding": null,
"metadata": {
"page_label": "1",
"file_name": "TSLA-Q1-2024-Update.pdf",
"file_path": "c:\\Users\\Usuario\\Downloads\\medium\\data\\TSLA-Q1-2024-Update.pdf",
"file_type": "application/pdf",
"file_size": 7458951,
"creation_date": "2024-06-03",
"last_modified_date": "2024-06-03",
"document_title": "Q1 2024 Update: A Comprehensive Overview of Current Events and Developments",
"questions_this_excerpt_can_answer": "1. What is the title of the document that provides a comprehensive overview of current events and developments in Q1 2024 for Tesla (TSLA)?\n2. When was the Q1 2024 Update document created and last modified?\n3. Where can the detailed information about Q1 2024 events and developments for Tesla (TSLA) be found in the document?"
},
"excluded_embed_metadata_keys": [
"file_name",
"file_type",
"file_size",
"creation_date",
"last_modified_date",
"last_accessed_date"
],
"excluded_llm_metadata_keys": [
"file_name",
"file_type",
"file_size",
"creation_date",
"last_modified_date",
"last_accessed_date"
],
"relationships": {
"NodeRelationship.SOURCE": {
"node_id": "16a39240-345b-4434-9093-11d839905752",
"node_type": "ObjectType.DOCUMENT",
"metadata": {
"page_label": "1",
"file_name": "TSLA-Q1-2024-Update.pdf",
"file_path": "c:\\Users\\Usuario\\Downloads\\medium\\data\\TSLA-Q1-2024-Update.pdf",
"file_type": "application/pdf",
"file_size": 7458951,
"creation_date": "2024-06-03",
"last_modified_date": "2024-06-03"
},
"hash": "21032f05e68e627aa06c2618d8ba33b292cb3a554cf2beb63223bcfd9fbff2a0"
}
},
"text": "Q1 2024 Update\n1",
"start_char_idx": 0,
"end_char_idx": 16,
"text_template": "[Excerpt from document]\n{metadata_str}\nExcerpt:\n-----\n{content}\n-----\n",
"metadata_template": "{key}: {value}",
"metadata_seperator": "\n"
}
Te he marcado en negrita las partes interesantes. Fíjae como gracias a las clases TitleExtractor y QuestionsAnsweredExtractor extraemos contenido relevante del chunk y lo agregamos como Metadata dentro de nuestro objeto.
Ahora, si algun método de estas clases no nos convence y queremos controlar más el output del modelo, podemos hacer una aproximación diferente utilizando también técnicas de data mining pero de manera más artesana y programando nuestra propia función:
import logging
# Configuración de logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
from openai import OpenAI
# Crear una instancia del cliente de OpenAI
client = OpenAI()
# Definimos una función para extraer las Entities del contenido
def get_entity_extraction_prompt(content):
logger.info("Generando el prompt para la extracción de entidades.")
try:
completion = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "Extrae las entidades más importantes del texto que te proporcionará el usuario. Identifica entidades como personas, organizaciones, ubicaciones, fechas, cantidades, etc. Devuelve las entidades como una lista separada por comas, así: John Smith, Microsoft, Nueva York, 2023-05-20, $1 millón. Devuelve un máximo de 10 entidades. Si no se encuentran entidades poner: ' '"},
{"role": "user", "content": f"{content}"}
]
)
logger.info("Extracción de entidades completada con éxito.")
return completion.choices[0].message.content
except Exception as e:
logger.error("Error al generar la extracción de entidades: %s", e)
return None
Como queremos extraer las Entites de cada chunk hacemos una llamada al modelo con el prompt específico para cada Node o chunk del documento, así:
for node in tesla_nodes:
text = node.get_text()
try:
entities = get_entity_extraction_prompt(text)
logger.info("Entidades extraídas correctamente.")
node.metadata["entities"] = entities.split(", ")
except Exception as e:
logger.error(f"Error al extraer entidades: {e}")
node.metadata["entities"] = ["Error en la extracción"]
# Imprimir las entidades extraídas para cada nodo
for node in tesla_nodes:
print(f"Nodo: {node.node_id}")
print(f"Entidades: {node.metadata['entities']}")
print("---")
Y si volvemos a utilizar la función serialize_and_print_text_node(text_node) dónde text_node es text_node = tesla_nodes[1]:
serialize_and_print_text_node(tesla_nodes[2])
{
"id_": "b27f3ba9-a672-4ba3-842c-a7e2ff88832f",
"embedding": null,
"metadata": {
"page_label": "1",
"file_name": "TSLA-Q1-2024-Update.pdf",
"file_path": "c:\\Users\\Usuario\\Downloads\\medium\\data\\TSLA-Q1-2024-Update.pdf",
"file_type": "application/pdf",
"file_size": 7458951,
"creation_date": "2024-06-03",
"last_modified_date": "2024-06-03",
"document_title": "Q1 2024 Update: A Comprehensive Overview of Current Events and Developments",
"questions_this_excerpt_can_answer": "1. What is the title of the document that provides a comprehensive overview of current events and developments in Q1 2024 for Tesla (TSLA)?\n2. When was the Q1 2024 Update document created and last modified?\n3. Where can the detailed information about Q1 2024 events and developments for Tesla (TSLA) be found in the document?",
"entities": [
"'Financial Summary",
"Operational Summary",
"Vehicle Capacity",
"Core Technology",
"Outlook",
"Key Metrics",
"Financial Statements",
"Additional Information",
"Photos & Charts",
"'"
]
},
"excluded_embed_metadata_keys": [
"file_name",
"file_type",
"file_size",
"creation_date",
"last_modified_date",
"last_accessed_date"
],
"excluded_llm_metadata_keys": [
"file_name",
"file_type",
"file_size",
"creation_date",
"last_modified_date",
"last_accessed_date"
],
"relationships": {
"NodeRelationship.SOURCE": {
"node_id": "16a39240-345b-4434-9093-11d839905752",
"node_type": "ObjectType.DOCUMENT",
"metadata": {
"page_label": "1",
"file_name": "TSLA-Q1-2024-Update.pdf",
"file_path": "c:\\Users\\Usuario\\Downloads\\medium\\data\\TSLA-Q1-2024-Update.pdf",
"file_type": "application/pdf",
"file_size": 7458951,
"creation_date": "2024-06-03",
"last_modified_date": "2024-06-03"
},
"hash": "21032f05e68e627aa06c2618d8ba33b292cb3a554cf2beb63223bcfd9fbff2a0"
}
},
"text": "Q1 2024 Update\n1",
"start_char_idx": 0,
"end_char_idx": 16,
"text_template": "[Excerpt from document]\n{metadata_str}\nExcerpt:\n-----\n{content}\n-----\n",
"metadata_template": "{key}: {value}",
"metadata_seperator": "\n"
}