Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -6,7 +6,7 @@ import PyPDF2
|
|
| 6 |
from langchain.embeddings.openai import OpenAIEmbeddings
|
| 7 |
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 8 |
from langchain.vectorstores import Chroma
|
| 9 |
-
from langchain.chains import RetrievalQAWithSourcesChain
|
| 10 |
from langchain.chat_models import ChatOpenAI
|
| 11 |
from langchain.prompts.chat import (
|
| 12 |
ChatPromptTemplate,
|
|
@@ -36,34 +36,52 @@ if not OPENAI_API_KEY:
|
|
| 36 |
"No se encontró la variable de entorno 'OPENAI_API_KEY'. Defínela en tu entorno o en los secrets."
|
| 37 |
)
|
| 38 |
|
| 39 |
-
# Configuración del text splitter (
|
| 40 |
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
|
| 41 |
|
| 42 |
-
#
|
| 43 |
-
system_template = """Utiliza las siguientes piezas de contexto para responder la pregunta del usuario de manera breve y concisa.
|
| 44 |
-
Si no sabes la respuesta, simplemente di que no lo sabes, no intentes inventarla.
|
| 45 |
-
SIEMPRE incluye una parte "FUENTES" en tu respuesta, donde se indique el documento del cual obtuviste la información.
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
----------------
|
| 52 |
-
{summaries}
|
|
|
|
| 53 |
|
| 54 |
-
|
| 55 |
SystemMessagePromptTemplate.from_template(system_template),
|
| 56 |
HumanMessagePromptTemplate.from_template("{question}")
|
| 57 |
]
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
# --- EVENTO AL INICIAR EL CHAT ---
|
| 62 |
@cl.on_chat_start
|
| 63 |
async def on_chat_start():
|
| 64 |
-
await cl.Message(
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
| 67 |
pdf_paths = [
|
| 68 |
"gestios de conflictos.pdf",
|
| 69 |
"Managing Conflict with Your Boss .pdf"
|
|
@@ -82,78 +100,121 @@ async def on_chat_start():
|
|
| 82 |
text = page.extract_text()
|
| 83 |
if text:
|
| 84 |
pdf_text += text
|
|
|
|
| 85 |
chunks = text_splitter.split_text(pdf_text)
|
| 86 |
all_texts.extend(chunks)
|
| 87 |
all_metadatas.extend([{"source": base_name} for _ in chunks])
|
| 88 |
|
| 89 |
# Crear la base vectorial usando nuestra clase personalizada de embeddings
|
| 90 |
-
# Al no especificar persist_directory se utiliza el modo in-memory, evitando la necesidad de configurar un tenant
|
| 91 |
embeddings = CustomOpenAIEmbeddings(openai_api_key=OPENAI_API_KEY)
|
| 92 |
docsearch = await cl.make_async(Chroma.from_texts)(
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
)
|
| 98 |
|
| 99 |
-
#
|
| 100 |
-
|
| 101 |
-
ChatOpenAI(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
chain_type="stuff",
|
| 103 |
retriever=docsearch.as_retriever(),
|
| 104 |
-
chain_type_kwargs=
|
| 105 |
)
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
# Guardar en la sesión de usuario
|
| 108 |
-
cl.user_session.set("
|
|
|
|
| 109 |
cl.user_session.set("metadatas", all_metadatas)
|
| 110 |
cl.user_session.set("texts", all_texts)
|
| 111 |
|
| 112 |
-
await cl.Message(content="¡Listo!
|
|
|
|
| 113 |
|
| 114 |
-
# --- EVENTO AL RECIBIR UN MENSAJE DEL USUARIO ---
|
| 115 |
@cl.on_message
|
| 116 |
async def main(message: cl.Message):
|
| 117 |
query = message.content
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
cb = cl.AsyncLangchainCallbackHandler(
|
| 120 |
stream_final_answer=True,
|
| 121 |
answer_prefix_tokens=["FINAL", "ANSWER"]
|
| 122 |
)
|
| 123 |
cb.answer_reached = True
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
| 127 |
answer = res["answer"]
|
| 128 |
sources = res["sources"].strip()
|
| 129 |
-
source_elements = []
|
| 130 |
-
|
| 131 |
-
metadatas = cl.user_session.get("metadatas")
|
| 132 |
-
all_sources = [m["source"] for m in metadatas]
|
| 133 |
-
texts = cl.user_session.get("texts")
|
| 134 |
-
|
| 135 |
-
if sources:
|
| 136 |
-
found_sources = []
|
| 137 |
-
for src in sources.split(","):
|
| 138 |
-
source_name = src.strip().replace(".", "")
|
| 139 |
-
try:
|
| 140 |
-
index = all_sources.index(source_name)
|
| 141 |
-
except ValueError:
|
| 142 |
-
continue
|
| 143 |
-
found_sources.append(source_name)
|
| 144 |
-
source_elements.append(cl.Text(content=texts[index], name=source_name))
|
| 145 |
-
if found_sources:
|
| 146 |
-
answer += f"\nFUENTES: {', '.join(found_sources)}"
|
| 147 |
-
else:
|
| 148 |
-
answer += "\nNo se encontraron fuentes."
|
| 149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
if cb.has_streamed_final_answer:
|
| 151 |
-
|
| 152 |
-
await cb.final_stream.update()
|
| 153 |
else:
|
| 154 |
-
await cl.Message(content=answer
|
|
|
|
| 155 |
|
| 156 |
-
# --- EJECUCIÓN ---
|
| 157 |
if __name__ == "__main__":
|
| 158 |
from chainlit.cli import run_chainlit
|
| 159 |
file_name = __file__ if '__file__' in globals() else "app.py"
|
|
|
|
| 6 |
from langchain.embeddings.openai import OpenAIEmbeddings
|
| 7 |
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 8 |
from langchain.vectorstores import Chroma
|
| 9 |
+
from langchain.chains import RetrievalQAWithSourcesChain, LLMChain
|
| 10 |
from langchain.chat_models import ChatOpenAI
|
| 11 |
from langchain.prompts.chat import (
|
| 12 |
ChatPromptTemplate,
|
|
|
|
| 36 |
"No se encontró la variable de entorno 'OPENAI_API_KEY'. Defínela en tu entorno o en los secrets."
|
| 37 |
)
|
| 38 |
|
| 39 |
+
# Configuración del text splitter (puedes ajustar chunk_size y chunk_overlap según tus necesidades)
|
| 40 |
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
|
| 41 |
|
| 42 |
+
# --- PROMPTS Y PLANTILLAS ---
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
+
# Plantilla del sistema para consultas basadas en PDF + Conocimiento General
|
| 45 |
+
system_template = """\
|
| 46 |
+
Eres un asistente en español basado en ChatGPT-4 con grandes capacidades de razonamiento y análisis.
|
| 47 |
+
Tienes acceso a los siguientes documentos, y también cuentas con conocimientos generales para responder
|
| 48 |
+
toda clase de preguntas, tanto del contexto provisto como de tu conocimiento general.
|
| 49 |
+
|
| 50 |
+
- Si la pregunta está claramente respondida por el contenido de los textos, proporciona la información relevante y cita tus fuentes.
|
| 51 |
+
- Si no está respondida por los textos, utiliza tu conocimiento general y responde de forma analítica, extensa y detallada.
|
| 52 |
+
- Siempre que utilices información proveniente de los PDFs, al final de tu respuesta indica las fuentes de la forma:
|
| 53 |
+
FUENTES: nombre_del_pdf
|
| 54 |
|
| 55 |
----------------
|
| 56 |
+
{summaries}
|
| 57 |
+
"""
|
| 58 |
|
| 59 |
+
messages_pdf = [
|
| 60 |
SystemMessagePromptTemplate.from_template(system_template),
|
| 61 |
HumanMessagePromptTemplate.from_template("{question}")
|
| 62 |
]
|
| 63 |
+
pdf_prompt = ChatPromptTemplate.from_messages(messages_pdf)
|
| 64 |
+
|
| 65 |
+
# Cadena/prompt para conocimiento general (fallback), en caso de que no haya nada relevante en los PDF
|
| 66 |
+
fallback_system_template = """\
|
| 67 |
+
Eres ChatGPT-4, un modelo de lenguaje altamente analítico y con amplio conocimiento.
|
| 68 |
+
Responde en español de manera extensa, detallada y muy analítica.
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
messages_fallback = [
|
| 72 |
+
SystemMessagePromptTemplate.from_template(fallback_system_template),
|
| 73 |
+
HumanMessagePromptTemplate.from_template("{question}")
|
| 74 |
+
]
|
| 75 |
+
fallback_prompt = ChatPromptTemplate.from_messages(messages_fallback)
|
| 76 |
+
|
| 77 |
|
|
|
|
| 78 |
@cl.on_chat_start
|
| 79 |
async def on_chat_start():
|
| 80 |
+
await cl.Message(
|
| 81 |
+
content="¡Bienvenido! Estoy listo para ayudarte con gestión de conflictos y cualquier otra pregunta que tengas."
|
| 82 |
+
).send()
|
| 83 |
+
|
| 84 |
+
# Rutas de los PDFs
|
| 85 |
pdf_paths = [
|
| 86 |
"gestios de conflictos.pdf",
|
| 87 |
"Managing Conflict with Your Boss .pdf"
|
|
|
|
| 100 |
text = page.extract_text()
|
| 101 |
if text:
|
| 102 |
pdf_text += text
|
| 103 |
+
|
| 104 |
chunks = text_splitter.split_text(pdf_text)
|
| 105 |
all_texts.extend(chunks)
|
| 106 |
all_metadatas.extend([{"source": base_name} for _ in chunks])
|
| 107 |
|
| 108 |
# Crear la base vectorial usando nuestra clase personalizada de embeddings
|
|
|
|
| 109 |
embeddings = CustomOpenAIEmbeddings(openai_api_key=OPENAI_API_KEY)
|
| 110 |
docsearch = await cl.make_async(Chroma.from_texts)(
|
| 111 |
+
all_texts,
|
| 112 |
+
embeddings,
|
| 113 |
+
metadatas=all_metadatas,
|
| 114 |
+
persist_directory="./chroma_db" # Directorio de persistencia (ajústalo si necesitas)
|
| 115 |
+
)
|
| 116 |
|
| 117 |
+
# Cadena para preguntas que sí tengan match en los PDFs
|
| 118 |
+
pdf_chain = RetrievalQAWithSourcesChain.from_chain_type(
|
| 119 |
+
llm=ChatOpenAI(
|
| 120 |
+
temperature=0.7,
|
| 121 |
+
model_name="gpt-4", # Asegúrate de que tu cuenta tenga acceso a GPT-4
|
| 122 |
+
openai_api_key=OPENAI_API_KEY,
|
| 123 |
+
max_tokens=2000
|
| 124 |
+
),
|
| 125 |
chain_type="stuff",
|
| 126 |
retriever=docsearch.as_retriever(),
|
| 127 |
+
chain_type_kwargs={"prompt": pdf_prompt}
|
| 128 |
)
|
| 129 |
|
| 130 |
+
# Cadena de fallback para preguntas fuera de contexto PDF (conocimiento general)
|
| 131 |
+
fallback_chain = LLMChain(
|
| 132 |
+
llm=ChatOpenAI(
|
| 133 |
+
temperature=0.7,
|
| 134 |
+
model_name="gpt-4", # Asegúrate de que tu cuenta tenga acceso a GPT-4
|
| 135 |
+
openai_api_key=OPENAI_API_KEY,
|
| 136 |
+
max_tokens=2000
|
| 137 |
+
),
|
| 138 |
+
prompt=fallback_prompt
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
# Guardar en la sesión de usuario
|
| 142 |
+
cl.user_session.set("pdf_chain", pdf_chain)
|
| 143 |
+
cl.user_session.set("fallback_chain", fallback_chain)
|
| 144 |
cl.user_session.set("metadatas", all_metadatas)
|
| 145 |
cl.user_session.set("texts", all_texts)
|
| 146 |
|
| 147 |
+
await cl.Message(content="¡Listo! Puedes comenzar a hacer tus preguntas.").send()
|
| 148 |
+
|
| 149 |
|
|
|
|
| 150 |
@cl.on_message
|
| 151 |
async def main(message: cl.Message):
|
| 152 |
query = message.content
|
| 153 |
+
pdf_chain = cl.user_session.get("pdf_chain")
|
| 154 |
+
fallback_chain = cl.user_session.get("fallback_chain")
|
| 155 |
+
metadatas = cl.user_session.get("metadatas")
|
| 156 |
+
texts = cl.user_session.get("texts")
|
| 157 |
+
|
| 158 |
+
# Callback para hacer streaming de la respuesta
|
| 159 |
cb = cl.AsyncLangchainCallbackHandler(
|
| 160 |
stream_final_answer=True,
|
| 161 |
answer_prefix_tokens=["FINAL", "ANSWER"]
|
| 162 |
)
|
| 163 |
cb.answer_reached = True
|
| 164 |
|
| 165 |
+
# 1) Intentar obtener respuesta del PDF chain
|
| 166 |
+
res = await pdf_chain.acall(query, callbacks=[cb])
|
| 167 |
answer = res["answer"]
|
| 168 |
sources = res["sources"].strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
+
# Verificamos si la respuesta indica que no se encontró nada relevante
|
| 171 |
+
# o si la cadena devolvió algo muy corto que parezca "No lo sé".
|
| 172 |
+
# Ajusta la condición según tu preferencia.
|
| 173 |
+
if ("no lo sé" in answer.lower()) or ("no sé" in answer.lower()) or (len(answer) < 30):
|
| 174 |
+
# 2) Fallback a la cadena de conocimiento general
|
| 175 |
+
res_fallback = await fallback_chain.acall({"question": query})
|
| 176 |
+
answer = res_fallback["text"]
|
| 177 |
+
# En fallback no tenemos "FUENTES", pues responde con conocimiento general
|
| 178 |
+
sources = ""
|
| 179 |
+
else:
|
| 180 |
+
# Agregar fuentes si las hay
|
| 181 |
+
if sources:
|
| 182 |
+
# Buscamos los fragmentos correspondientes
|
| 183 |
+
found_sources = []
|
| 184 |
+
source_elements = []
|
| 185 |
+
|
| 186 |
+
all_sources = [m["source"] for m in metadatas]
|
| 187 |
+
for src in sources.split(","):
|
| 188 |
+
src_name = src.strip().replace(".", "")
|
| 189 |
+
try:
|
| 190 |
+
index = all_sources.index(src_name)
|
| 191 |
+
except ValueError:
|
| 192 |
+
continue
|
| 193 |
+
found_sources.append(src_name)
|
| 194 |
+
source_elements.append(cl.Text(content=texts[index], name=src_name))
|
| 195 |
+
|
| 196 |
+
if found_sources:
|
| 197 |
+
answer += f"\n\nFUENTES: {', '.join(found_sources)}"
|
| 198 |
+
|
| 199 |
+
# Si estamos haciendo streaming, actualizamos el mensaje con los elementos
|
| 200 |
+
if cb.has_streamed_final_answer:
|
| 201 |
+
cb.final_stream.elements = source_elements
|
| 202 |
+
await cb.final_stream.update()
|
| 203 |
+
return
|
| 204 |
+
else:
|
| 205 |
+
# Si no hubo streaming, mandamos el mensaje completo al final
|
| 206 |
+
await cl.Message(content=answer, elements=source_elements).send()
|
| 207 |
+
return
|
| 208 |
+
|
| 209 |
+
# Si llegamos aquí, simplemente enviamos la respuesta (sea PDF o fallback)
|
| 210 |
+
# y no hay fuentes que mostrar (o ya se procesaron).
|
| 211 |
if cb.has_streamed_final_answer:
|
| 212 |
+
# Si fue streaming, actualizamos el mensaje final sin fuentes
|
| 213 |
+
await cb.final_stream.update(content=answer)
|
| 214 |
else:
|
| 215 |
+
await cl.Message(content=answer).send()
|
| 216 |
+
|
| 217 |
|
|
|
|
| 218 |
if __name__ == "__main__":
|
| 219 |
from chainlit.cli import run_chainlit
|
| 220 |
file_name = __file__ if '__file__' in globals() else "app.py"
|