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"
|