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)