File size: 6,205 Bytes
01fe139
4145718
01fe139
 
33e7d04
a41cc2d
f2c21b6
33e7d04
 
01fe139
 
33e7d04
 
01fe139
33e7d04
01fe139
 
 
4145718
01fe139
 
 
 
 
4145718
01fe139
 
 
 
 
 
 
 
 
 
 
 
 
33e7d04
01fe139
 
 
 
 
 
4145718
01fe139
4145718
01fe139
 
 
 
33e7d04
a41cc2d
01fe139
 
79fab4b
01fe139
 
 
4145718
01fe139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4145718
01fe139
 
 
 
 
 
 
 
 
 
 
 
4145718
01fe139
 
 
 
 
 
 
4145718
01fe139
 
 
 
 
 
 
 
 
 
 
 
 
4145718
 
 
 
 
01fe139
 
 
 
 
 
 
 
 
 
 
4145718
01fe139
 
 
 
 
 
 
 
 
 
 
 
 
 
4145718
01fe139
0b3a368
 
 
 
 
 
 
 
 
 
01fe139
4145718
01fe139
 
 
 
 
 
 
 
 
 
4145718
 
 
01fe139
 
 
 
 
 
 
 
 
 
 
 
4145718
01fe139
 
 
 
 
 
 
 
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
import os
import uuid
import gradio as gr
from langchain_openai import ChatOpenAI
# Mantenemos langchain_community como pediste (versiones legacy)
from langchain_community.vectorstores import Qdrant
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.chat_message_histories import ChatMessageHistory
# Clientes y Modelos
from qdrant_client import QdrantClient, models
from qdrant_client.http import models as rest_models
# Cadenas (Importación consolidada para evitar errores de ruta)
from langchain.chains import create_history_aware_retriever, create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
# Core
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory

# --- 1. CONFIGURACIÓN Y VARIABLES DE ENTORNO ---
QDRANT_URL = os.getenv("QDRANT_URL")
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
COLLECTION_NAME = "dgt_documents_qdrant_memory_filter_fixed_2"

# Categorías disponibles
OPCIONES_CATEGORIAS = [
    "Todas",
    "Documentos de la SUMA",
    "Manuales Técnicos y Procedimientos",
    "Inventarios y Activos SUMA",
    "Otros"
]

# --- 2. INICIALIZAR CLIENTES ---

# Cliente Qdrant
client = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY)

# Embeddings (Usando langchain_community antiguo)
embeddings_model = HuggingFaceEmbeddings(
    model_name="intfloat/e5-large-v2",
    model_kwargs={'device': 'cpu'}, 
    encode_kwargs={'normalize_embeddings': False}
)

# LLM
llm_openai = ChatOpenAI(
    model="gpt-4o-mini", 
    temperature=0.1,
    api_key=OPENAI_API_KEY
)

# Conexión a la VectorDB (Wrapper antiguo Qdrant)
vectordb = Qdrant(
    client=client,
    collection_name=COLLECTION_NAME,
    embeddings=embeddings_model,
    content_payload_key="content"
)

# --- 3. PROMPTS ---

contextualize_q_system_prompt = """Dado un historial de chat y la última pregunta del usuario \
que podría hacer referencia al contexto en el historial de chat, formula una pregunta independiente \
que pueda entenderse sin el historial de chat. NO respondas a la pregunta, \
solo reformúlala si es necesario y, si no, devuélvela tal cual."""

contextualize_q_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", contextualize_q_system_prompt),
        MessagesPlaceholder("chat_history"),
        ("human", "{input}"),
    ]
)

qa_system_prompt = """Eres un asistente especializado en los documentos sobre la Sociedad Musical de Alberic (SUMA). \
Utiliza los siguientes fragmentos de contexto recuperado para responder a la pregunta. \
Si no sabes la respuesta, di que no lo sabes. \
Menciona siempre de qué documentos has extraído la información (usando el metadato 'source'). \
Profundiza en la respuesta.
Contexto:
{context}"""

qa_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", qa_system_prompt),
        MessagesPlaceholder("chat_history"),
        ("human", "{input}"),
    ]
)

# --- 4. GESTIÓN DE MEMORIA EN RAM ---
store = {}

def get_session_history(session_id: str):
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

# --- 5. LÓGICA DEL CHAT ---

def build_qdrant_filter(category_name):
    if not category_name or category_name == "Todas":
        return None
    return rest_models.Filter(
        must=[
            rest_models.FieldCondition(
                key="category", 
                match=rest_models.MatchValue(value=category_name)
            )
        ]
    )

def chat_logic(message, history, selected_category, session_id):
    # Seguridad: Si por error session_id viene vacío, generamos uno temporal
    if not session_id:
        session_id = str(uuid.uuid4())

    # 1. Construir filtro
    qdrant_filter = build_qdrant_filter(selected_category)
    
    # 2. Retriever dinámico
    dynamic_retriever = vectordb.as_retriever(
        search_kwargs={
            "k": 4, 
            "filter": qdrant_filter 
        }
    )

    # 3. Cadenas LangChain
    history_aware_retriever = create_history_aware_retriever(
        llm_openai, dynamic_retriever, contextualize_q_prompt
    )
    question_answer_chain = create_stuff_documents_chain(llm_openai, qa_prompt)
    rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
    
    conversational_rag_chain = RunnableWithMessageHistory(
        rag_chain,
        get_session_history,
        input_messages_key="input",
        history_messages_key="chat_history",
        output_messages_key="answer",
    )

    # 4. Generar respuesta streaming usando el ID único del usuario
    full_response = ""
    try:
        for chunk in conversational_rag_chain.stream(
            {"input": message},
            config={"configurable": {"session_id": session_id}}
        ):
            if "answer" in chunk:
                full_response += chunk["answer"]
                yield full_response
    except Exception as e:
        yield f"Error al procesar la respuesta: {str(e)}"

# --- 6. INTERFAZ GRÁFICA ---

custom_css = """
footer {visibility: hidden}
.gradio-container {background-color: #f9fafb}
"""

tema_musical = gr.themes.Soft(primary_hue="indigo", secondary_hue="slate")

with gr.Blocks(theme=tema_musical, css=custom_css, title="Chatbot SUMA") as demo:
    
    # ESTADO: Genera un ID único cada vez que se carga la página
    session_state = gr.State(lambda: str(uuid.uuid4()))
    
    gr.Markdown("# 🎵 Asistente Virtual SUMA")
    gr.Markdown("Pregunta sobre normativas, manuales y documentos internos.")

    filtro_dropdown = gr.Dropdown(
        choices=OPCIONES_CATEGORIAS,
        value="Todas", 
        label="📂 Filtrar por Categoría",
        info="Acota la búsqueda a un tipo de documento específico."
    )

    chat_interface = gr.ChatInterface(
        fn=chat_logic,
        additional_inputs=[filtro_dropdown, session_state],
        examples=[
            ["¿Cuáles son los requisitos para ser socio?"], 
            ["Resumen del manual de procedimientos"],
        ]
    )

if __name__ == "__main__":
    demo.launch()