magomerob commited on
Commit
31aeee3
·
verified ·
1 Parent(s): 9bab919

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +13 -0
  2. app.py +229 -0
  3. requirements.txt +12 -0
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ RUN apt-get update && apt-get install -y curl zstd lshw
4
+ RUN curl -fsSL https://ollama.com/install.sh | sh
5
+
6
+ WORKDIR /app
7
+ COPY requirements.txt .
8
+ RUN pip install --no-cache-dir -r requirements.txt
9
+
10
+ COPY app.py .
11
+
12
+ EXPOSE 7860
13
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ import subprocess
4
+ import time
5
+ import threading
6
+ import gradio as gr
7
+
8
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
9
+ from langchain_community.vectorstores import Chroma
10
+ from langchain_huggingface import HuggingFaceEmbeddings
11
+ from langchain_community.document_loaders import PyPDFLoader
12
+ from langchain_ollama import ChatOllama
13
+ from langchain import hub
14
+ from langchain_core.output_parsers import StrOutputParser
15
+ from rerankers import Reranker
16
+
17
+ # ──────────────────────────────────────────────
18
+ # 1. Arrancar Ollama en background
19
+ # ──────────────────────────────────────────────
20
+ def start_ollama():
21
+ subprocess.Popen(["ollama", "serve"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
22
+ time.sleep(5) # Esperar a que el servidor esté listo
23
+ subprocess.run(["ollama", "pull", "gemma3:4b"], check=True)
24
+
25
+ print("Iniciando Ollama...")
26
+ ollama_thread = threading.Thread(target=start_ollama)
27
+ ollama_thread.start()
28
+ ollama_thread.join()
29
+ print("Ollama listo.")
30
+
31
+ # ──────────────────────────────────────────────
32
+ # 2. Descargar y procesar el PDF
33
+ # ──────────────────────────────────────────────
34
+ PDF_URL = "https://escueladepacientes.es/images/Pdfs/Guia_Informativa_Diabetes_1.pdf"
35
+ PDF_PATH = "Guia_Informativa_Diabetes_1.pdf"
36
+
37
+ if not os.path.exists(PDF_PATH):
38
+ print("Descargando PDF...")
39
+ response = requests.get(PDF_URL)
40
+ with open(PDF_PATH, "wb") as f:
41
+ f.write(response.content)
42
+
43
+ print("Cargando y procesando documento...")
44
+ loader = PyPDFLoader(PDF_PATH)
45
+ documents = loader.load()
46
+
47
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=20)
48
+ all_splits = text_splitter.split_documents(documents)
49
+
50
+ # ──────────────────────────────────────────────
51
+ # 3. Embeddings y base de datos vectorial
52
+ # ──────────────────────────────────────────────
53
+ print("Creando embeddings...")
54
+ model_name = "sentence-transformers/paraphrase-multilingual-mpnet-base-v2"
55
+ model_kwargs = {"device": "cuda" if os.environ.get("CUDA_VISIBLE_DEVICES") else "cpu"}
56
+
57
+ embeddings = HuggingFaceEmbeddings(model_name=model_name, model_kwargs=model_kwargs)
58
+ vectordb = Chroma.from_documents(
59
+ documents=all_splits,
60
+ embedding=embeddings,
61
+ persist_directory="chroma_db"
62
+ )
63
+ print("Base de datos vectorial lista.")
64
+
65
+ # ──────────────────────────────────────────────
66
+ # 4. LLM y reranker
67
+ # ──────────────────────────────────────────────
68
+ llm = ChatOllama(model="gemma3:4b", temperature=0, top_k=50, top_p=0.95)
69
+ ranker = Reranker("answerdotai/answerai-colbert-small-v1", model_type="colbert")
70
+
71
+ # ──────────────────────────────────────────────
72
+ # 5. Funciones RAG
73
+ # ──────────────────────────────────────────────
74
+ def rag_sin_reranking(query: str) -> tuple[str, str]:
75
+ docs = vectordb.similarity_search_with_score(query)
76
+ prompt = hub.pull("rlm/rag-prompt")
77
+ rag_chain = prompt | llm | StrOutputParser()
78
+
79
+ context = []
80
+ for doc, score in docs:
81
+ if score < 7:
82
+ context.append(doc.to_json()["kwargs"]["page_content"])
83
+
84
+ if context:
85
+ answer = rag_chain.invoke({"context": "\n\n".join(context), "question": query})
86
+ sources = "\n\n---\n\n".join(
87
+ f"📄 Página {doc.to_json()['kwargs']['metadata'].get('page', '?')} "
88
+ f"(score: {score:.2f})\n{doc.to_json()['kwargs']['page_content'][:300]}..."
89
+ for doc, score in docs if score < 7
90
+ )
91
+ return answer, sources
92
+ return "No tengo información para responder a esta pregunta.", ""
93
+
94
+
95
+ def rag_con_reranking(query: str) -> tuple[str, str]:
96
+ docs = vectordb.similarity_search_with_score(query)
97
+ prompt = hub.pull("rlm/rag-prompt")
98
+ rag_chain = prompt | llm | StrOutputParser()
99
+
100
+ context = []
101
+ for doc, score in docs:
102
+ if score < 7:
103
+ context.append(doc.to_json()["kwargs"]["page_content"])
104
+
105
+ if context:
106
+ ranking = ranker.rank(query=query, docs=context)
107
+ best_context = ranking[0].text
108
+ answer = rag_chain.invoke({"context": best_context, "question": query})
109
+ return answer, f"📄 Contexto seleccionado por reranking:\n\n{best_context}"
110
+ return "No tengo información para responder a esta pregunta.", ""
111
+
112
+
113
+ # ──────────────────────────────────────────────
114
+ # 6. Lógica del chat
115
+ # ──────────────────────────────────────────────
116
+ def chat(message: str, history: list, mode: str, temperature: float, top_k: int, top_p: float):
117
+ # Actualizar parámetros del LLM si han cambiado
118
+ global llm
119
+ llm = ChatOllama(model="gemma3:4b", temperature=temperature, top_k=top_k, top_p=top_p)
120
+
121
+ if mode == "LLM base (sin RAG)":
122
+ chain = llm | StrOutputParser()
123
+ answer = chain.invoke(message)
124
+ sources = "_Sin recuperación de documentos._"
125
+
126
+ elif mode == "RAG sin reranking":
127
+ answer, sources = rag_sin_reranking(message)
128
+
129
+ else: # RAG con reranking
130
+ answer, sources = rag_con_reranking(message)
131
+
132
+ # Añadir fuentes al final de la respuesta
133
+ full_response = answer
134
+ if sources:
135
+ full_response += f"\n\n---\n**📚 Fuentes utilizadas:**\n{sources}"
136
+
137
+ history.append((message, full_response))
138
+ return history, history, "" # history, state, limpiar input
139
+
140
+
141
+ # ──────────────────────────────────────────────
142
+ # 7. Interfaz Gradio
143
+ # ──────────────────────────────────────────────
144
+ with gr.Blocks(title="RAG - Guía de Diabetes", theme=gr.themes.Soft()) as demo:
145
+
146
+ gr.Markdown("""
147
+ # 🩺 Sistema de Question Answering sobre Diabetes
148
+ Basado en la [Guía Informativa de Diabetes](https://escueladepacientes.es/mi-enfermedad/diabetes)
149
+ de la **Escuela de Pacientes**.
150
+ Puedes elegir entre tres modos de respuesta y ajustar los parámetros de generación.
151
+ """)
152
+
153
+ with gr.Row():
154
+ with gr.Column(scale=3):
155
+ chatbot = gr.Chatbot(
156
+ label="Conversación",
157
+ height=500,
158
+ bubble_full_width=False,
159
+ )
160
+ with gr.Row():
161
+ msg_input = gr.Textbox(
162
+ placeholder="Escribe tu pregunta aquí...",
163
+ label="Pregunta",
164
+ scale=4,
165
+ autofocus=True,
166
+ )
167
+ send_btn = gr.Button("Enviar", variant="primary", scale=1)
168
+
169
+ clear_btn = gr.Button("🗑️ Limpiar conversación", variant="secondary")
170
+
171
+ with gr.Column(scale=1):
172
+ gr.Markdown("### ⚙️ Configuración")
173
+
174
+ mode = gr.Radio(
175
+ choices=["LLM base (sin RAG)", "RAG sin reranking", "RAG con reranking"],
176
+ value="RAG con reranking",
177
+ label="Modo de respuesta",
178
+ )
179
+
180
+ gr.Markdown("### 🎛️ Parámetros de generación")
181
+
182
+ temperature = gr.Slider(
183
+ minimum=0.0, maximum=2.0, value=0.0, step=0.1,
184
+ label="Temperature",
185
+ info="0 = determinista, 2 = muy aleatorio"
186
+ )
187
+ top_k = gr.Slider(
188
+ minimum=1, maximum=100, value=50, step=1,
189
+ label="Top-k",
190
+ info="Número de tokens candidatos"
191
+ )
192
+ top_p = gr.Slider(
193
+ minimum=0.1, maximum=1.0, value=0.95, step=0.05,
194
+ label="Top-p",
195
+ info="Nucleus sampling threshold"
196
+ )
197
+
198
+ gr.Markdown("### 💡 Preguntas de ejemplo")
199
+ examples = gr.Examples(
200
+ examples=[
201
+ ["¿Qué es la glucosa?"],
202
+ ["¿Qué tratamiento tiene la diabetes tipo 1?"],
203
+ ["¿Cuáles son los síntomas de la hipoglucemia?"],
204
+ ["¿Qué diferencia hay entre diabetes tipo 1 y tipo 2?"],
205
+ ["¿Cuál es la receta de la tarta de queso?"],
206
+ ],
207
+ inputs=msg_input,
208
+ )
209
+
210
+ # Estado para mantener el historial
211
+ state = gr.State([])
212
+
213
+ # Eventos
214
+ send_btn.click(
215
+ fn=chat,
216
+ inputs=[msg_input, state, mode, temperature, top_k, top_p],
217
+ outputs=[chatbot, state, msg_input],
218
+ )
219
+ msg_input.submit(
220
+ fn=chat,
221
+ inputs=[msg_input, state, mode, temperature, top_k, top_p],
222
+ outputs=[chatbot, state, msg_input],
223
+ )
224
+ clear_btn.click(
225
+ fn=lambda: ([], [], ""),
226
+ outputs=[chatbot, state, msg_input],
227
+ )
228
+
229
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ langchain_community
2
+ langchain
3
+ langchain-huggingface
4
+ langchain-text-splitters
5
+ langchain_ollama
6
+ chromadb
7
+ pypdf
8
+ rerankers[transformers]
9
+ requests
10
+ gradio
11
+ sentence-transformers
12
+ huggingface_hub