JairoCesar commited on
Commit
4254f20
·
verified ·
1 Parent(s): 6f321a2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +168 -96
app.py CHANGED
@@ -1,44 +1,84 @@
1
- # Las librerias
 
 
2
  import os
3
  import hashlib
4
- import pickle
5
- import streamlit as st
6
- from google.generativeai import configure, GenerativeModel
7
  from sentence_transformers import SentenceTransformer
8
  from sklearn.metrics.pairwise import cosine_similarity
9
  import numpy as np
10
  import PyPDF2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- # Configuracion de la API de Google (Manejo una variable interna)
13
- configure(api_key=os.getenv('GOOGLE_API_KEY'))
14
 
15
- # Inicializar el modelo Gemini
16
- model = GenerativeModel('gemini-2.5-flash-lite')
17
- chat = model.start_chat()
 
18
 
19
- # Inicializar el modelo Sentence Transformer
20
- encoder = SentenceTransformer("all-mpnet-base-v2")
 
 
21
 
22
- # Función para calcular el hash del directorio
23
  def compute_directory_hash(directory):
24
  hash_md5 = hashlib.md5()
 
 
25
  for root, _, files in os.walk(directory):
26
  for file in sorted(files):
27
  file_path = os.path.join(root, file)
28
- with open(file_path, "rb") as f:
29
- for chunk in iter(lambda: f.read(4096), b""):
30
- hash_md5.update(chunk)
 
 
 
31
  return hash_md5.hexdigest()
32
 
33
- # Función para dividir texto en chunks
34
  def split_into_chunks(text, chunk_size=1000):
35
  words = text.split()
36
  return [" ".join(words[i:i+chunk_size]) for i in range(0, len(words), chunk_size)]
37
 
38
- # Función para cargar documentos y crear embeddings
39
- def load_documents_and_create_embeddings(directory):
 
 
 
 
 
40
  documents = []
41
  file_chunks = {}
 
 
 
 
 
42
  for root, _, files in os.walk(directory):
43
  for file in files:
44
  if file.endswith(".pdf"):
@@ -56,101 +96,133 @@ def load_documents_and_create_embeddings(directory):
56
  file_chunks[file] = len(chunks)
57
  documents.extend(chunks)
58
  else:
59
- print(f"Advertencia: No se pudo extraer texto del archivo {file_path}")
60
  except Exception as e:
61
- print(f"Error al procesar {file_path}: {e}")
62
 
63
  if not documents:
64
- return [], None, {} # No se encontraron documentos PDF válidos
65
 
 
 
66
  embeddings = encoder.encode(documents)
67
- return documents, embeddings, file_chunks
68
-
69
- # Función para cargar o actualizar caché
70
- def load_or_update_cache(directory):
71
- cache_file = "cache.pkl"
72
- dir_hash = compute_directory_hash(directory)
73
-
74
- if os.path.exists(cache_file):
75
- with open(cache_file, "rb") as f:
76
- cache = pickle.load(f)
77
- if cache["hash"] == dir_hash:
78
- return cache["documents"], cache["embeddings"], cache["file_chunks"]
79
-
80
- documents, embeddings, file_chunks = load_documents_and_create_embeddings(directory)
81
- if embeddings is not None: # Solo actualizar caché si se encontraron documentos válidos
82
- with open(cache_file, "wb") as f:
83
- pickle.dump({
84
- "hash": dir_hash,
85
- "documents": documents,
86
- "embeddings": embeddings,
87
- "file_chunks": file_chunks
88
- }, f)
89
 
 
90
  return documents, embeddings, file_chunks
91
 
92
- # Función para generar respuesta usando Gemini
93
- def generate_response(prompt, context=None):
94
- if context:
95
- full_prompt = f"""Contexto: {context}
96
-
97
- Pregunta: {prompt}
98
-
99
- Por favor, responde a la pregunta basándote en el contexto proporcionado."""
100
- else:
101
- full_prompt = f"""Pregunta: {prompt}
102
-
103
- Por favor, responde a la pregunta utilizando tu conocimiento general."""
104
-
105
- response_with_context = chat.send_message(full_prompt).text
106
- response_general = chat.send_message(f"Pregunta: {prompt}\n\nPor favor, responde a la pregunta utilizando tu conocimiento general.").text
107
 
108
- combined_response = f"{response_with_context}\n\nEn un contexto general: {response_general}"
109
- return combined_response
110
-
111
- # Función para reducir el contexto si es necesario
112
  def reduce_context(context, max_tokens=8000):
113
  words = context.split()
114
  if len(words) > max_tokens:
115
  return " ".join(words[:max_tokens])
116
  return context
117
 
118
- # Cargar documentos y crear embeddings
119
- directory = "./data"
120
- documents, embeddings, file_chunks = load_or_update_cache(directory)
121
-
122
- # Interfaz de Streamlit
123
- st.title("Pregúntale al Búho")
124
-
125
- # Entrada del usuario
126
- user_input = st.text_input("Tu duda:", key="user_input")
127
-
128
- # Generar respuesta
129
- if st.button("Enviar"):
130
- if user_input:
131
- # Buscar en los documentos si hay embeddings válidos
132
- if embeddings is not None and len(documents) > 0:
133
- question_embedding = encoder.encode([user_input])
134
- similarities = cosine_similarity(question_embedding, embeddings)
135
- most_similar_idx = np.argmax(similarities)
136
- retrieved_doc = reduce_context(documents[most_similar_idx])
137
 
138
- # Verificar si el documento recuperado es relevante
139
- if similarities[0][most_similar_idx] > 0.07: # Umbral de similitud
140
- response = generate_response(user_input, context=retrieved_doc)
141
- else:
142
- response = generate_response(user_input) # Usar conocimiento general
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  else:
144
- response = generate_response(user_input) # Usar conocimiento general
145
-
146
- st.text_area("Respuesta del Búho:", value=response, height=300)
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
- # Mostrar información sobre los chunks de archivos al final (comentado)
149
- # if file_chunks:
150
- # st.markdown("---") # Añadir una línea divisoria para separar visualmente
151
- # st.subheader("Información sobre los archivos procesados:")
152
- # for file, num_chunks in file_chunks.items():
153
- # st.write(f"- {file}: {num_chunks} chunks")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
  if __name__ == "__main__":
156
- pass
 
1
+ import streamlit as st
2
+ from google import genai
3
+ from google.genai import types
4
  import os
5
  import hashlib
 
 
 
6
  from sentence_transformers import SentenceTransformer
7
  from sklearn.metrics.pairwise import cosine_similarity
8
  import numpy as np
9
  import PyPDF2
10
+ import logging
11
+
12
+ # ==================== CONFIGURACIÓN DE LA PÁGINA ====================
13
+ st.set_page_config(
14
+ page_title="Pregúntale al Búho 🦉",
15
+ page_icon="🦉",
16
+ layout="centered"
17
+ )
18
+
19
+ # Configuración de Logging
20
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
21
+ logger = logging.getLogger("buho_app")
22
+
23
+ # ==================== CONFIGURACIÓN DE API GOOGLE ====================
24
+ try:
25
+ # Intenta obtener la API key de las variables de entorno o secrets de Streamlit
26
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
27
+
28
+ if not GOOGLE_API_KEY:
29
+ st.error("❌ No se encontró la variable de entorno GOOGLE_API_KEY. Por favor, configúrala en los Secrets del Space.")
30
+ st.stop()
31
+
32
+ except Exception as e:
33
+ st.error(f"❌ Error al configurar el entorno: {e}")
34
+ st.stop()
35
 
36
+ # ==================== FUNCIONES DE CACHÉ Y CARGA ====================
 
37
 
38
+ @st.cache_resource
39
+ def get_gemini_client():
40
+ """Inicializa el cliente de Google GenAI."""
41
+ return genai.Client(api_key=GOOGLE_API_KEY)
42
 
43
+ @st.cache_resource
44
+ def get_embedding_model():
45
+ """Carga el modelo de Sentence Transformer una sola vez."""
46
+ return SentenceTransformer("all-mpnet-base-v2")
47
 
48
+ # Función auxiliar para hashing (para invalidar caché si cambian los archivos)
49
  def compute_directory_hash(directory):
50
  hash_md5 = hashlib.md5()
51
+ if not os.path.exists(directory):
52
+ return "empty"
53
  for root, _, files in os.walk(directory):
54
  for file in sorted(files):
55
  file_path = os.path.join(root, file)
56
+ try:
57
+ with open(file_path, "rb") as f:
58
+ for chunk in iter(lambda: f.read(4096), b""):
59
+ hash_md5.update(chunk)
60
+ except Exception:
61
+ pass
62
  return hash_md5.hexdigest()
63
 
 
64
  def split_into_chunks(text, chunk_size=1000):
65
  words = text.split()
66
  return [" ".join(words[i:i+chunk_size]) for i in range(0, len(words), chunk_size)]
67
 
68
+ @st.cache_data(show_spinner=True)
69
+ def load_and_process_documents(directory, dir_hash):
70
+ """
71
+ Carga documentos PDF, extrae texto y crea embeddings.
72
+ El argumento 'dir_hash' asegura que si los archivos cambian, la función se re-ejecute.
73
+ """
74
+ logger.info("Procesando documentos PDF...")
75
  documents = []
76
  file_chunks = {}
77
+
78
+ if not os.path.exists(directory):
79
+ os.makedirs(directory, exist_ok=True)
80
+ return [], None, {}
81
+
82
  for root, _, files in os.walk(directory):
83
  for file in files:
84
  if file.endswith(".pdf"):
 
96
  file_chunks[file] = len(chunks)
97
  documents.extend(chunks)
98
  else:
99
+ logger.warning(f"Advertencia: No se pudo extraer texto del archivo {file_path}")
100
  except Exception as e:
101
+ logger.error(f"Error al procesar {file_path}: {e}")
102
 
103
  if not documents:
104
+ return [], None, {}
105
 
106
+ # Crear embeddings usando el modelo cargado en cache_resource
107
+ encoder = get_embedding_model()
108
  embeddings = encoder.encode(documents)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
+ logger.info(f"Procesados {len(documents)} fragmentos de texto.")
111
  return documents, embeddings, file_chunks
112
 
113
+ # ==================== LÓGICA DE IA ====================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
 
 
 
 
115
  def reduce_context(context, max_tokens=8000):
116
  words = context.split()
117
  if len(words) > max_tokens:
118
  return " ".join(words[:max_tokens])
119
  return context
120
 
121
+ def generate_response(client, prompt, context=None):
122
+ """Genera respuesta usando la nueva sintaxis de google-genai."""
123
+ model_id = 'gemini-2.5-flash-lite'
124
+
125
+ try:
126
+ if context:
127
+ full_prompt = f"""Usa el siguiente contexto para responder a la pregunta.
128
+
129
+ Contexto: {context}
130
+
131
+ Pregunta: {prompt}
132
+
133
+ Respuesta:"""
 
 
 
 
 
 
134
 
135
+ # Llamada al modelo con contexto
136
+ response_ctx = client.models.generate_content(
137
+ model=model_id,
138
+ contents=full_prompt
139
+ )
140
+ text_ctx = response_ctx.text
141
+ else:
142
+ text_ctx = "No se encontró contexto relevante en los documentos."
143
+
144
+ # Llamada para conocimiento general (opcional, como estaba en el original)
145
+ general_prompt = f"Pregunta: {prompt}\n\nResponde utilizando tu conocimiento general de forma concisa."
146
+ response_gen = client.models.generate_content(
147
+ model=model_id,
148
+ contents=general_prompt
149
+ )
150
+ text_gen = response_gen.text
151
+
152
+ combined_response = f"{text_ctx}\n\n---\n**Perspectiva General:**\n{text_gen}"
153
+ return combined_response
154
+
155
+ except Exception as e:
156
+ return f"Error al generar respuesta con Gemini: {e}"
157
+
158
+ # ==================== INTERFAZ PRINCIPAL ====================
159
+
160
+ def main():
161
+ col_img, col_text = st.columns([1, 5])
162
+ with col_img:
163
+ # Si tienes una imagen 'buho.png' en la carpeta, se mostrará
164
+ if os.path.exists("buho.png"):
165
+ st.image("buho.png", width=80)
166
  else:
167
+ st.write("🦉")
168
+ with col_text:
169
+ st.title("Pregúntale al Búho")
170
+ st.markdown("Sistema de consulta sobre documentos PDF usando **Gemini 2.5**.")
171
+
172
+ # Inicializar cliente y modelo
173
+ client = get_gemini_client()
174
+ encoder = get_embedding_model()
175
+
176
+ # Cargar datos
177
+ directory = "./data"
178
+ dir_hash = compute_directory_hash(directory) # Calcula hash para invalidar cache si hay cambios
179
+
180
+ with st.spinner("Cargando conocimiento del Búho..."):
181
+ documents, embeddings, file_chunks = load_and_process_documents(directory, dir_hash)
182
 
183
+ # Mostrar estado de la base de conocimiento
184
+ if not documents:
185
+ st.warning(f"No se encontraron documentos PDF en la carpeta '{directory}'. Sube archivos para empezar.")
186
+ else:
187
+ st.caption(f"📚 Base de conocimiento activa: {len(file_chunks)} documentos procesados.")
188
+
189
+ # Entrada del usuario
190
+ user_input = st.text_input("¿Cuál es tu duda?", key="user_input")
191
+
192
+ if st.button("Enviar Consulta", type="primary"):
193
+ if user_input:
194
+ with st.spinner("El Búho está pensando..."):
195
+ retrieved_doc = None
196
+
197
+ # Buscar en documentos si existen
198
+ if embeddings is not None and len(documents) > 0:
199
+ question_embedding = encoder.encode([user_input])
200
+ similarities = cosine_similarity(question_embedding, embeddings)
201
+ most_similar_idx = np.argmax(similarities)
202
+ score = similarities[0][most_similar_idx]
203
+
204
+ # Umbral de similitud (ajustado ligeramente)
205
+ if score > 0.15:
206
+ raw_context = documents[most_similar_idx]
207
+ retrieved_doc = reduce_context(raw_context)
208
+ # st.expander("Ver contexto recuperado").write(retrieved_doc) # Debug
209
+
210
+ # Generar respuesta
211
+ response = generate_response(client, user_input, context=retrieved_doc)
212
+
213
+ st.markdown("### Respuesta del Búho:")
214
+ st.write(response)
215
+ else:
216
+ st.warning("Por favor escribe una pregunta.")
217
+
218
+ # Sidebar con información
219
+ with st.sidebar:
220
+ st.header("Archivos Indexados")
221
+ if file_chunks:
222
+ for file, chunks in file_chunks.items():
223
+ st.text(f"📄 {file} ({chunks} fragmentos)")
224
+ else:
225
+ st.info("Carpeta vacía.")
226
 
227
  if __name__ == "__main__":
228
+ main()