Spaces:
Sleeping
Sleeping
import os | |
import inspect | |
import chainlit as cl | |
import PyPDF2 | |
from langchain.embeddings.openai import OpenAIEmbeddings | |
from langchain.text_splitter import RecursiveCharacterTextSplitter | |
from langchain.vectorstores import Chroma | |
from langchain.chains import RetrievalQAWithSourcesChain, LLMChain | |
from langchain.chat_models import ChatOpenAI | |
from langchain.prompts.chat import ( | |
ChatPromptTemplate, | |
SystemMessagePromptTemplate, | |
HumanMessagePromptTemplate, | |
) | |
# Clase personalizada que cumple con la nueva interfaz de EmbeddingFunction de Chroma | |
class CustomOpenAIEmbeddings(OpenAIEmbeddings): | |
def __call__(self, input): | |
# Llama al método embed_documents para generar las embeddings a partir de una lista de textos | |
return self.embed_documents(input) | |
# Forzamos la firma de __call__ para que tenga exactamente ("self", "input") | |
CustomOpenAIEmbeddings.__call__.__signature__ = inspect.Signature( | |
parameters=[ | |
inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD), | |
inspect.Parameter("input", inspect.Parameter.POSITIONAL_OR_KEYWORD) | |
] | |
) | |
# --- CONFIGURACIÓN --- | |
# Obtenemos la API key de OpenAI desde las variables de entorno | |
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") | |
if not OPENAI_API_KEY: | |
raise ValueError( | |
"No se encontró la variable de entorno 'OPENAI_API_KEY'. Defínela en tu entorno o en los secrets." | |
) | |
# Configuración del text splitter (puedes ajustar chunk_size y chunk_overlap según tus necesidades) | |
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100) | |
# --- PROMPTS Y PLANTILLAS --- | |
# Plantilla del sistema para consultas basadas en PDF + Conocimiento General | |
system_template = """\ | |
Eres un asistente en español basado en ChatGPT-4 con grandes capacidades de razonamiento y análisis. | |
Tienes acceso a los siguientes documentos, y también cuentas con conocimientos generales para responder | |
toda clase de preguntas, tanto del contexto provisto como de tu conocimiento general. | |
- Si la pregunta está claramente respondida por el contenido de los textos, proporciona la información relevante y cita tus fuentes. | |
- Si no está respondida por los textos, utiliza tu conocimiento general y responde de forma analítica, extensa y detallada. | |
- Siempre que utilices información proveniente de los PDFs, al final de tu respuesta indica las fuentes de la forma: | |
FUENTES: nombre_del_pdf | |
---------------- | |
{summaries} | |
""" | |
messages_pdf = [ | |
SystemMessagePromptTemplate.from_template(system_template), | |
HumanMessagePromptTemplate.from_template("{question}") | |
] | |
pdf_prompt = ChatPromptTemplate.from_messages(messages_pdf) | |
# Cadena/prompt para conocimiento general (fallback), en caso de que no haya nada relevante en los PDF | |
fallback_system_template = """\ | |
Eres ChatGPT-4, un modelo de lenguaje altamente analítico y con amplio conocimiento. | |
Responde en español de manera extensa, detallada y muy analítica. | |
""" | |
messages_fallback = [ | |
SystemMessagePromptTemplate.from_template(fallback_system_template), | |
HumanMessagePromptTemplate.from_template("{question}") | |
] | |
fallback_prompt = ChatPromptTemplate.from_messages(messages_fallback) | |
async def on_chat_start(): | |
await cl.Message( | |
content="¡Bienvenido! Estoy listo para ayudarte con gestión de conflictos y cualquier otra pregunta que tengas." | |
).send() | |
# Rutas de los PDFs | |
pdf_paths = [ | |
"gestios de conflictos.pdf", | |
"Managing Conflict with Your Boss .pdf" | |
] | |
all_texts = [] | |
all_metadatas = [] | |
# Procesar cada PDF: extraer texto, dividirlo en fragmentos y asignar metadata | |
for path in pdf_paths: | |
base_name = os.path.basename(path) | |
with open(path, "rb") as f: | |
reader = PyPDF2.PdfReader(f) | |
pdf_text = "" | |
for page in reader.pages: | |
text = page.extract_text() | |
if text: | |
pdf_text += text | |
chunks = text_splitter.split_text(pdf_text) | |
all_texts.extend(chunks) | |
all_metadatas.extend([{"source": base_name} for _ in chunks]) | |
# Crear la base vectorial usando nuestra clase personalizada de embeddings | |
embeddings = CustomOpenAIEmbeddings(openai_api_key=OPENAI_API_KEY) | |
docsearch = await cl.make_async(Chroma.from_texts)( | |
all_texts, | |
embeddings, | |
metadatas=all_metadatas, | |
persist_directory="./chroma_db" # Directorio de persistencia (ajústalo si necesitas) | |
) | |
# Cadena para preguntas que sí tengan match en los PDFs | |
pdf_chain = RetrievalQAWithSourcesChain.from_chain_type( | |
llm=ChatOpenAI( | |
temperature=0.7, | |
model_name="gpt-4", # Asegúrate de que tu cuenta tenga acceso a GPT-4 | |
openai_api_key=OPENAI_API_KEY, | |
max_tokens=2000 | |
), | |
chain_type="stuff", | |
retriever=docsearch.as_retriever(), | |
chain_type_kwargs={"prompt": pdf_prompt} | |
) | |
# Cadena de fallback para preguntas fuera de contexto PDF (conocimiento general) | |
fallback_chain = LLMChain( | |
llm=ChatOpenAI( | |
temperature=0.7, | |
model_name="gpt-4", # Asegúrate de que tu cuenta tenga acceso a GPT-4 | |
openai_api_key=OPENAI_API_KEY, | |
max_tokens=2000 | |
), | |
prompt=fallback_prompt | |
) | |
# Guardar en la sesión de usuario | |
cl.user_session.set("pdf_chain", pdf_chain) | |
cl.user_session.set("fallback_chain", fallback_chain) | |
cl.user_session.set("metadatas", all_metadatas) | |
cl.user_session.set("texts", all_texts) | |
await cl.Message(content="¡Listo! Puedes comenzar a hacer tus preguntas.").send() | |
async def main(message: cl.Message): | |
query = message.content | |
pdf_chain = cl.user_session.get("pdf_chain") | |
fallback_chain = cl.user_session.get("fallback_chain") | |
metadatas = cl.user_session.get("metadatas") | |
texts = cl.user_session.get("texts") | |
# Callback para hacer streaming de la respuesta | |
cb = cl.AsyncLangchainCallbackHandler( | |
stream_final_answer=True, | |
answer_prefix_tokens=["FINAL", "ANSWER"] | |
) | |
cb.answer_reached = True | |
# 1) Intentar obtener respuesta del PDF chain | |
res = await pdf_chain.acall(query, callbacks=[cb]) | |
answer = res["answer"] | |
sources = res["sources"].strip() | |
# Verificamos si la respuesta indica que no se encontró nada relevante | |
# o si la cadena devolvió algo muy corto que parezca "No lo sé". | |
# Ajusta la condición según tu preferencia. | |
if ("no lo sé" in answer.lower()) or ("no sé" in answer.lower()) or (len(answer) < 30): | |
# 2) Fallback a la cadena de conocimiento general | |
res_fallback = await fallback_chain.acall({"question": query}) | |
answer = res_fallback["text"] | |
# En fallback no tenemos "FUENTES", pues responde con conocimiento general | |
sources = "" | |
else: | |
# Agregar fuentes si las hay | |
if sources: | |
# Buscamos los fragmentos correspondientes | |
found_sources = [] | |
source_elements = [] | |
all_sources = [m["source"] for m in metadatas] | |
for src in sources.split(","): | |
src_name = src.strip().replace(".", "") | |
try: | |
index = all_sources.index(src_name) | |
except ValueError: | |
continue | |
found_sources.append(src_name) | |
source_elements.append(cl.Text(content=texts[index], name=src_name)) | |
if found_sources: | |
answer += f"\n\nFUENTES: {', '.join(found_sources)}" | |
# Si estamos haciendo streaming, actualizamos el mensaje con los elementos | |
if cb.has_streamed_final_answer: | |
cb.final_stream.elements = source_elements | |
await cb.final_stream.update() | |
return | |
else: | |
# Si no hubo streaming, mandamos el mensaje completo al final | |
await cl.Message(content=answer, elements=source_elements).send() | |
return | |
# Si llegamos aquí, simplemente enviamos la respuesta (sea PDF o fallback) | |
# y no hay fuentes que mostrar (o ya se procesaron). | |
if cb.has_streamed_final_answer: | |
# Si fue streaming, actualizamos el mensaje final sin fuentes | |
await cb.final_stream.update(content=answer) | |
else: | |
await cl.Message(content=answer).send() | |
if __name__ == "__main__": | |
from chainlit.cli import run_chainlit | |
file_name = __file__ if '__file__' in globals() else "app.py" | |
run_chainlit(file_name) | |