Spaces:
Sleeping
Sleeping
File size: 8,697 Bytes
907fa7a fa6d2db 910c97a a7804e8 907fa7a 1478636 907fa7a 9dd220c 907fa7a 910c97a 907fa7a 910c97a 1478636 907fa7a 910c97a 1478636 910c97a 1478636 910c97a 907fa7a 1478636 910c97a 1478636 907fa7a 1478636 a7804e8 fa6d2db 910c97a 1478636 907fa7a 1478636 907fa7a 1478636 907fa7a 1478636 907fa7a 1478636 907fa7a 1478636 907fa7a 1478636 907fa7a 1478636 356ecf6 fa6d2db 907fa7a 1478636 907fa7a 1478636 907fa7a 1478636 907fa7a 1478636 907fa7a 1478636 907fa7a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
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)
@cl.on_chat_start
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()
@cl.on_message
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)
|