JairoCesar commited on
Commit
1e4041a
·
verified ·
1 Parent(s): 538ede7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +83 -89
app.py CHANGED
@@ -1,5 +1,5 @@
1
- # ==================== El Detective de Alimentos (Versión 5.0 - con Profundización Científica) =====================================
2
- # Mejoras: Integración de la base de datos FoodB para una capa de evidencia bioquímica.
3
 
4
  import streamlit as st
5
  import google.generativeai as genai
@@ -10,6 +10,7 @@ import logging
10
  import re
11
  import pandas as pd
12
  import altair as alt
 
13
 
14
  st.set_page_config(
15
  page_title="El Detective de Alimentos",
@@ -17,33 +18,28 @@ st.set_page_config(
17
  layout="wide"
18
  )
19
 
20
- # ... (El código de configuración de Gemini no cambia) ...
21
  # Configurar logging
22
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
23
  logger = logging.getLogger("food_detective_app")
24
-
25
  # --- CONFIGURACIÓN DE GEMINI ---
26
  try:
27
  if 'GEMINI_API_KEY' in st.secrets:
28
  GEMINI_API_KEY = st.secrets['GEMINI_API_KEY']
29
  else:
30
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
31
-
32
  if not GEMINI_API_KEY:
33
  st.error("No se encontró la GEMINI_API_KEY. Por favor, configúrala en los Secrets de Streamlit.")
34
  logger.error("GEMINI_API_KEY no encontrada.")
35
  st.stop()
36
-
37
  genai.configure(api_key=GEMINI_API_KEY)
38
  logger.info("✅ Configuración de Gemini API realizada.")
39
  except Exception as e:
40
  st.error(f"❌ Error al configurar Gemini API: {e}")
41
  logger.error(f"Error al configurar Gemini API: {e}")
42
  st.stop()
43
-
44
  @st.cache_resource
45
  def get_gemini_model():
46
- """Carga el modelo generativo de Gemini."""
47
  logger.info("🔄 Cargando modelo Gemini (gemini-1.5-flash)...")
48
  try:
49
  return genai.GenerativeModel("gemini-1.5-flash")
@@ -51,26 +47,15 @@ def get_gemini_model():
51
  st.error(f"❌ No se pudo cargar el modelo Gemini: {e}")
52
  logger.error(f"Error cargando modelo Gemini: {e}")
53
  return None
54
-
55
  model = get_gemini_model()
56
-
57
-
58
- # --- FUNCIÓN DE CARGA DE DATOS MODIFICADA ---
59
  @st.cache_data
60
  def load_data():
61
- """
62
- Carga todas las bases de conocimiento: datos enriquecidos y el índice de FoodB.
63
- """
64
  try:
65
  path_alimentos = os.path.join('DATOS', 'alimentos_enriquecido.json')
66
  path_foodb_index = os.path.join('DATOS', 'foodb_index.json')
67
-
68
  with open(path_alimentos, 'r', encoding='utf-8') as f:
69
  data_alimentos = json.load(f)
70
-
71
  lista_condiciones = sorted(list(set(item['condicion_asociada'] for item in data_alimentos)))
72
-
73
- # Cargar el índice de FoodB (si existe)
74
  foodb_index = {}
75
  if os.path.exists(path_foodb_index):
76
  with open(path_foodb_index, 'r', encoding='utf-8') as f:
@@ -78,21 +63,17 @@ def load_data():
78
  logger.info(f"✅ Índice científico de FoodB cargado con {len(foodb_index)} alimentos.")
79
  else:
80
  logger.warning("Advertencia: No se encontró 'foodb_index.json'. La funcionalidad de profundización científica estará desactivada.")
81
-
82
  logger.info(f"✅ Base de datos principal cargada con {len(data_alimentos)} registros.")
83
  return data_alimentos, lista_condiciones, foodb_index
84
-
85
  except FileNotFoundError as e:
86
  st.error(f"❌ Error: No se encontró el archivo '{e.filename}'. Asegúrate de que está en la carpeta 'DATOS'.")
87
  return None, None, None
88
  except json.JSONDecodeError as e:
89
  st.error(f"❌ Error: Un archivo JSON tiene un formato incorrecto: {e}")
90
  return None, None, None
91
-
92
  alimentos_data, lista_condiciones, foodb_index = load_data()
93
 
94
-
95
- # ... (El resto de diccionarios y funciones de búsqueda no cambian) ...
96
  FOOD_TO_COMPOUND_MAP = {
97
  "pan": ["gluten"], "trigo": ["gluten"], "harina de trigo": ["gluten"], "cebada": ["gluten"],
98
  "centeno": ["gluten"], "pasta": ["gluten"], "galletas": ["gluten"], "avena": ["gluten"], "pizza": ["gluten"], "torta": ["gluten"],
@@ -121,16 +102,25 @@ CONDITION_SYNONYMS = {
121
  "enfermedad celíaca (clásica).": ["dermatitis herpetiforme"]
122
  }
123
 
 
 
124
  def extract_and_infer_with_gemini(query, condiciones):
125
- """Extrae entidades e infiere una condición probable."""
126
  if not model: return None
127
  condiciones_str = "\n".join([f"- {c}" for c in condiciones])
128
  system_prompt = f"""
129
- Eres un asistente de triaje clínico. Analiza la consulta, extrae 'alimentos', 'sintomas' (normalizados) y la 'condicion_probable' más relevante de la lista proporcionada. Devuelve solo un JSON. Si no puedes inferir una condición, déjalo como un string vacío.
 
 
 
 
130
  LISTA DE CONDICIONES POSIBLES:
131
  {condiciones_str}
 
 
 
132
  Consulta: "{query}"
133
  """
 
134
  try:
135
  response = model.generate_content(system_prompt)
136
  json_text_match = re.search(r'```json\s*(\{.*?\})\s*```', response.text, re.DOTALL)
@@ -144,8 +134,8 @@ def extract_and_infer_with_gemini(query, condiciones):
144
  st.error(f"Hubo un problema al interpretar tu consulta con la IA.")
145
  return None
146
 
 
147
  def find_best_matches_hybrid(entities, data):
148
- """Motor de búsqueda híbrido (v4.1) con manejo de sinónimos de condiciones."""
149
  if not entities or not data: return []
150
  user_symptoms = set(s.lower().strip() for s in entities.get("sintomas", []))
151
  user_foods = set(f.lower().strip() for f in entities.get("alimentos", []))
@@ -165,13 +155,13 @@ def find_best_matches_hybrid(entities, data):
165
  score_details['condition'] = 100
166
  entry_compounds_text = entry.get("compuesto_alimento", "").lower()
167
  if any(term in entry_compounds_text for term in candidate_terms):
168
- score_details['food'] = 20
169
  entry_symptoms_keys = set(s.lower().strip() for s in entry.get("sintomas_clave", []))
170
  symptom_score = 0
171
  for user_symptom in user_symptoms:
172
  for key in entry_symptoms_keys:
173
  if key in user_symptom or user_symptom in key:
174
- symptom_score += 5
175
  break
176
  score_details['symptoms'] = symptom_score
177
  total_score = sum(score_details.values())
@@ -182,9 +172,8 @@ def find_best_matches_hybrid(entities, data):
182
  sorted_results = sorted(results, key=lambda x: x['score']['total'], reverse=True)
183
  return sorted_results
184
 
 
185
  def generate_detailed_analysis(query, match):
186
- """Genera la explicación final, manejando la información de dualidad si existe."""
187
- # (Esta función no necesita cambios)
188
  if not model: return "Error: El modelo de IA no está disponible."
189
  prompt_parts = [
190
  "Eres un asistente de IA experto en nutrición personalizada. Tu tono es empático, claro y muy educativo. NO actúas como un médico, sino como un guía informativo.",
@@ -222,47 +211,47 @@ def generate_detailed_analysis(query, match):
222
  except Exception as e:
223
  logger.error(f"Error generando análisis detallado con Gemini: {e}")
224
  return "No se pudo generar el análisis detallado."
225
-
226
  def create_relevance_chart(results):
227
- """Crea un gráfico de barras de Altair legible para visualizar la relevancia."""
228
  top_results = results[:5]
229
- condition_names = []
230
- for res in top_results:
231
- name = re.sub(r'\(.*\)', '', res['entry']['condicion_asociada']).strip()
232
- condition_names.append(name)
233
- chart_data = {
234
- "Condición": condition_names,
235
- "Relevancia": [res['score']['total'] for res in top_results]
236
- }
237
  source = pd.DataFrame(chart_data)
238
  chart = alt.Chart(source).mark_bar(color='#1f77b4').encode(
239
  x=alt.X('Relevancia:Q', title='Puntuación de Relevancia'),
240
- y=alt.Y('Condición:N', sort='-x', title='Posible Condición',
241
- axis=alt.Axis(labelLimit=300)),
242
  tooltip=[alt.Tooltip('Condición:N', title='Condición'), alt.Tooltip('Relevancia:Q', title='Puntuación')]
243
- ).properties(
244
- title='Principales Coincidencias según tu Caso'
245
- ).configure_axis(
246
- labelFontSize=12,
247
- titleFontSize=14
248
- ).configure_title(
249
- fontSize=16,
250
- anchor='start'
251
- )
252
  return chart
253
-
254
- # --- NUEVA FUNCIÓN AUXILIAR ---
255
  def extract_foods_from_string(food_string):
256
- """Extrae una lista de alimentos de la cadena 'compuesto_alimento'."""
257
- # Quita el texto en paréntesis al principio
258
- main_part = re.sub(r'^\w+\s*\(.*?\)\s*', '', food_string)
259
- # Quita el texto en paréntesis al final
260
- foods_part = re.sub(r'\(.*\)', '', main_part).strip()
261
- # Divide por comas y limpia los espacios
262
- return [food.strip().lower() for food in foods_part.split(',') if food.strip()]
 
 
 
 
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
- # --- INTERFAZ DE USUARIO (UI) MODIFICADA ---
266
  col_img, col_text = st.columns([1, 4], gap="medium")
267
  with col_img:
268
  if os.path.exists("imagen.png"):
@@ -272,17 +261,16 @@ with col_text:
272
  st.markdown("##### Describe lo que sientes y lo que comiste para descubrir posibles intolerancias.")
273
  st.markdown("---")
274
 
 
275
  if 'search_results' not in st.session_state: st.session_state.search_results = None
276
  if 'user_query' not in st.session_state: st.session_state.user_query = ""
277
-
278
  def clear_search_state():
279
  st.session_state.search_results = None
280
  st.session_state.user_query = ""
281
-
282
  with st.form(key="search_form"):
283
  query = st.text_area("Describe tu caso aquí:", height=150, placeholder="Ej: Cuando como mucha carne me duele el dedo gordo del pie...")
284
  submitted = st.form_submit_button("Analizar mi caso", type="primary")
285
-
286
  if submitted:
287
  if not query:
288
  st.warning("Por favor, describe lo que sientes y lo que comiste.")
@@ -292,40 +280,43 @@ if submitted:
292
  st.session_state.user_query = query
293
  with st.spinner("🧠 Interpretando tu caso y buscando pistas con IA..."):
294
  entities = extract_and_infer_with_gemini(query, lista_condiciones)
295
-
296
  if entities and (entities.get("alimentos") or entities.get("sintomas")):
297
- # ... (código de info_str sin cambios) ...
298
  info_str = f"IA identificó - Alimentos: {', '.join(entities.get('alimentos',[])) or 'Ninguno'}"
299
  info_str += f", Síntomas: {', '.join(entities.get('sintomas',[])) or 'Ninguno'}"
300
- if entities.get("condicion_probable"):
301
- info_str += f", Condición Probable: {entities.get('condicion_probable')}"
302
  st.info(info_str)
303
  with st.spinner("🔬 Cruzando información y calculando relevancia..."):
304
  results = find_best_matches_hybrid(entities, alimentos_data)
305
  st.session_state.search_results = results
 
 
 
306
  else:
307
  st.error("No se pudieron identificar alimentos o síntomas claros en tu descripción.")
308
  st.session_state.search_results = []
309
-
 
 
 
 
 
 
 
 
310
  if st.session_state.search_results is not None:
311
  st.button("Realizar nueva consulta", on_click=clear_search_state)
312
  st.markdown("---")
313
-
314
  results = st.session_state.search_results
315
-
316
  if not results:
317
- st.warning(f"No se encontraron coincidencias claras para tu caso: '{st.session_state.user_query}'.")
318
  else:
319
  st.success(f"Hemos encontrado {len(results)} posible(s) causa(s) relacionada(s) con tu caso.")
320
-
321
  st.subheader("Análisis de Relevancia de las Coincidencias")
322
  chart = create_relevance_chart(results)
323
  st.altair_chart(chart, use_container_width=True)
324
-
325
  best_match_data = results[0]
326
  best_match = best_match_data['entry']
327
  best_score = best_match_data['score']
328
-
329
  with st.expander(f"**Análisis Detallado de la Principal Coincidencia: {best_match.get('condicion_asociada')}**", expanded=True):
330
  col1, col2 = st.columns([3, 1])
331
  with col1:
@@ -335,39 +326,42 @@ if st.session_state.search_results is not None:
335
  score_col2.metric("Puntos por Alimento", f"{best_score['food']}")
336
  score_col3.metric("Puntos por Síntomas", f"{best_score['symptoms']}")
337
  score_col4.metric("PUNTUACIÓN TOTAL", f"{best_score['total']}", delta="Máxima coincidencia")
338
-
339
- # --- NUEVO POPOVER PARA PROFUNDIZACIÓN CIENTÍFICA ---
340
  with col2:
341
- st.write("") # Espacio para alinear
342
  if foodb_index:
343
- with st.popover("🔬 Evidencia Bioquímica"):
344
- st.markdown("##### Componentes y Efectos Moleculares (Datos de FoodB.ca)")
345
  alimentos_a_buscar = extract_foods_from_string(best_match.get("compuesto_alimento", ""))
346
  if not alimentos_a_buscar:
347
- st.write("No se encontraron alimentos específicos para analizar en la base de datos científica.")
348
  else:
349
  found_data = False
350
- for alimento in alimentos_a_buscar[:3]: # Limitamos a los 3 primeros para no sobrecargar
351
  if alimento in foodb_index:
352
  found_data = True
353
  with st.container(border=True):
354
  st.subheader(f"Análisis de: {alimento.capitalize()}")
355
- for item in foodb_index[alimento][:5]: # Limitamos a 5 compuestos por alimento
356
  st.write(f"**Compuesto:** {item['compound']}")
357
  st.write(f"**Efectos reportados:** {', '.join(item['effects'])}")
358
  st.markdown("---")
359
  if not found_data:
360
- st.write(f"No se encontraron datos moleculares para '{', '.join(alimentos_a_buscar)}' en la base de datos de FoodB.")
361
-
362
  st.markdown("---")
363
-
364
  with st.spinner("✍️ Generando un análisis personalizado con IA..."):
365
  detailed_analysis = generate_detailed_analysis(st.session_state.user_query, best_match)
366
  st.markdown(detailed_analysis)
367
-
 
 
 
 
 
 
 
 
368
  if len(results) > 1:
369
  with st.expander("Otras posibles coincidencias (ordenadas por relevancia)"):
370
- # ... (código para mostrar otras coincidencias no cambia) ...
371
  for result in results[1:]:
372
  entry = result['entry']
373
  score = result['score']
 
1
+ # ==================== El Detective de Alimentos (Versión 6.1 - Precisión Mejorada) =====================================
2
+ # Mejoras: Corrección del motor de búsqueda, reparación de la función de FoodB y refinamiento de prompts.
3
 
4
  import streamlit as st
5
  import google.generativeai as genai
 
10
  import re
11
  import pandas as pd
12
  import altair as alt
13
+ from datetime import datetime
14
 
15
  st.set_page_config(
16
  page_title="El Detective de Alimentos",
 
18
  layout="wide"
19
  )
20
 
21
+ # ... (Configuración de Gemini y carga de datos no cambia) ...
22
  # Configurar logging
23
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
24
  logger = logging.getLogger("food_detective_app")
 
25
  # --- CONFIGURACIÓN DE GEMINI ---
26
  try:
27
  if 'GEMINI_API_KEY' in st.secrets:
28
  GEMINI_API_KEY = st.secrets['GEMINI_API_KEY']
29
  else:
30
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
 
31
  if not GEMINI_API_KEY:
32
  st.error("No se encontró la GEMINI_API_KEY. Por favor, configúrala en los Secrets de Streamlit.")
33
  logger.error("GEMINI_API_KEY no encontrada.")
34
  st.stop()
 
35
  genai.configure(api_key=GEMINI_API_KEY)
36
  logger.info("✅ Configuración de Gemini API realizada.")
37
  except Exception as e:
38
  st.error(f"❌ Error al configurar Gemini API: {e}")
39
  logger.error(f"Error al configurar Gemini API: {e}")
40
  st.stop()
 
41
  @st.cache_resource
42
  def get_gemini_model():
 
43
  logger.info("🔄 Cargando modelo Gemini (gemini-1.5-flash)...")
44
  try:
45
  return genai.GenerativeModel("gemini-1.5-flash")
 
47
  st.error(f"❌ No se pudo cargar el modelo Gemini: {e}")
48
  logger.error(f"Error cargando modelo Gemini: {e}")
49
  return None
 
50
  model = get_gemini_model()
 
 
 
51
  @st.cache_data
52
  def load_data():
 
 
 
53
  try:
54
  path_alimentos = os.path.join('DATOS', 'alimentos_enriquecido.json')
55
  path_foodb_index = os.path.join('DATOS', 'foodb_index.json')
 
56
  with open(path_alimentos, 'r', encoding='utf-8') as f:
57
  data_alimentos = json.load(f)
 
58
  lista_condiciones = sorted(list(set(item['condicion_asociada'] for item in data_alimentos)))
 
 
59
  foodb_index = {}
60
  if os.path.exists(path_foodb_index):
61
  with open(path_foodb_index, 'r', encoding='utf-8') as f:
 
63
  logger.info(f"✅ Índice científico de FoodB cargado con {len(foodb_index)} alimentos.")
64
  else:
65
  logger.warning("Advertencia: No se encontró 'foodb_index.json'. La funcionalidad de profundización científica estará desactivada.")
 
66
  logger.info(f"✅ Base de datos principal cargada con {len(data_alimentos)} registros.")
67
  return data_alimentos, lista_condiciones, foodb_index
 
68
  except FileNotFoundError as e:
69
  st.error(f"❌ Error: No se encontró el archivo '{e.filename}'. Asegúrate de que está en la carpeta 'DATOS'.")
70
  return None, None, None
71
  except json.JSONDecodeError as e:
72
  st.error(f"❌ Error: Un archivo JSON tiene un formato incorrecto: {e}")
73
  return None, None, None
 
74
  alimentos_data, lista_condiciones, foodb_index = load_data()
75
 
76
+ # ... (Diccionarios no cambian) ...
 
77
  FOOD_TO_COMPOUND_MAP = {
78
  "pan": ["gluten"], "trigo": ["gluten"], "harina de trigo": ["gluten"], "cebada": ["gluten"],
79
  "centeno": ["gluten"], "pasta": ["gluten"], "galletas": ["gluten"], "avena": ["gluten"], "pizza": ["gluten"], "torta": ["gluten"],
 
102
  "enfermedad celíaca (clásica).": ["dermatitis herpetiforme"]
103
  }
104
 
105
+
106
+ # --- PROMPT DE IA REFINADO ---
107
  def extract_and_infer_with_gemini(query, condiciones):
 
108
  if not model: return None
109
  condiciones_str = "\n".join([f"- {c}" for c in condiciones])
110
  system_prompt = f"""
111
+ Eres un asistente de triaje clínico experto. Tu tarea es analizar la consulta de un usuario y extraer tres tipos de información:
112
+ 1. `alimentos`: Lista de alimentos consumidos.
113
+ 2. `sintomas`: Lista de síntomas descritos, normalizados a un término común.
114
+ 3. `condicion_probable`: Tu mejor inferencia sobre cuál de las siguientes condiciones podría explicar los síntomas. **Intenta siempre proporcionar una, incluso si la confianza es moderada.**
115
+
116
  LISTA DE CONDICIONES POSIBLES:
117
  {condiciones_str}
118
+
119
+ Devuelve la respuesta ÚNICAMENTE en formato JSON.
120
+
121
  Consulta: "{query}"
122
  """
123
+ # ... (el resto de la función no cambia)
124
  try:
125
  response = model.generate_content(system_prompt)
126
  json_text_match = re.search(r'```json\s*(\{.*?\})\s*```', response.text, re.DOTALL)
 
134
  st.error(f"Hubo un problema al interpretar tu consulta con la IA.")
135
  return None
136
 
137
+ # --- MOTOR DE BÚSQUEDA REFINADO ---
138
  def find_best_matches_hybrid(entities, data):
 
139
  if not entities or not data: return []
140
  user_symptoms = set(s.lower().strip() for s in entities.get("sintomas", []))
141
  user_foods = set(f.lower().strip() for f in entities.get("alimentos", []))
 
155
  score_details['condition'] = 100
156
  entry_compounds_text = entry.get("compuesto_alimento", "").lower()
157
  if any(term in entry_compounds_text for term in candidate_terms):
158
+ score_details['food'] = 15 # Peso ajustado
159
  entry_symptoms_keys = set(s.lower().strip() for s in entry.get("sintomas_clave", []))
160
  symptom_score = 0
161
  for user_symptom in user_symptoms:
162
  for key in entry_symptoms_keys:
163
  if key in user_symptom or user_symptom in key:
164
+ symptom_score += 10 # Peso ajustado
165
  break
166
  score_details['symptoms'] = symptom_score
167
  total_score = sum(score_details.values())
 
172
  sorted_results = sorted(results, key=lambda x: x['score']['total'], reverse=True)
173
  return sorted_results
174
 
175
+ # ... (generate_detailed_analysis y create_relevance_chart no cambian) ...
176
  def generate_detailed_analysis(query, match):
 
 
177
  if not model: return "Error: El modelo de IA no está disponible."
178
  prompt_parts = [
179
  "Eres un asistente de IA experto en nutrición personalizada. Tu tono es empático, claro y muy educativo. NO actúas como un médico, sino como un guía informativo.",
 
211
  except Exception as e:
212
  logger.error(f"Error generando análisis detallado con Gemini: {e}")
213
  return "No se pudo generar el análisis detallado."
 
214
  def create_relevance_chart(results):
 
215
  top_results = results[:5]
216
+ condition_names = [re.sub(r'\(.*\)', '', res['entry']['condicion_asociada']).strip() for res in top_results]
217
+ chart_data = {"Condición": condition_names, "Relevancia": [res['score']['total'] for res in top_results]}
 
 
 
 
 
 
218
  source = pd.DataFrame(chart_data)
219
  chart = alt.Chart(source).mark_bar(color='#1f77b4').encode(
220
  x=alt.X('Relevancia:Q', title='Puntuación de Relevancia'),
221
+ y=alt.Y('Condición:N', sort='-x', title='Posible Condición', axis=alt.Axis(labelLimit=300)),
 
222
  tooltip=[alt.Tooltip('Condición:N', title='Condición'), alt.Tooltip('Relevancia:Q', title='Puntuación')]
223
+ ).properties(title='Principales Coincidencias según tu Caso').configure_axis(labelFontSize=12, titleFontSize=14).configure_title(fontSize=16, anchor='start')
 
 
 
 
 
 
 
 
224
  return chart
225
+
226
+ # --- FUNCIÓN DE EXTRACCIÓN PARA FOODB CORREGIDA ---
227
  def extract_foods_from_string(food_string):
228
+ """Extrae una lista de alimentos de la cadena 'compuesto_alimento' de forma robusta."""
229
+ # Buscar el contenido dentro del último par de paréntesis
230
+ match = re.search(r'\(([^)]+)\)$', food_string)
231
+ if match:
232
+ # Si se encuentra, tomar el contenido y dividirlo por comas
233
+ foods_part = match.group(1)
234
+ return [food.strip().lower() for food in foods_part.split(',') if food.strip()]
235
+ else:
236
+ # Si no hay paréntesis al final, intentar limpiar el string principal
237
+ main_part = re.sub(r'^\w+\s*\(.*?\)\s*', '', food_string).strip()
238
+ return [food.strip().lower() for food in main_part.split(',') if food.strip()]
239
 
240
+ # ... (la función log_feedback no cambia) ...
241
+ def log_feedback(query, result, feedback):
242
+ """Guarda el feedback del usuario en un archivo de texto."""
243
+ log_entry = {
244
+ "timestamp": datetime.now().isoformat(),
245
+ "query": query,
246
+ "best_match_condition": result['entry']['condicion_asociada'],
247
+ "score": result['score']['total'],
248
+ "feedback": feedback # "util" o "no_util"
249
+ }
250
+ os.makedirs("logs", exist_ok=True)
251
+ with open(os.path.join("logs", "feedback_log.txt"), "a", encoding="utf-8") as f:
252
+ f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
253
 
254
+ # --- INTERFAZ DE USUARIO (UI) ---
255
  col_img, col_text = st.columns([1, 4], gap="medium")
256
  with col_img:
257
  if os.path.exists("imagen.png"):
 
261
  st.markdown("##### Describe lo que sientes y lo que comiste para descubrir posibles intolerancias.")
262
  st.markdown("---")
263
 
264
+ # ... (el resto del código de la UI no cambia, PERO INCLUYE LA CORRECCIÓN DEL TÍTULO DEL POPOVER) ...
265
  if 'search_results' not in st.session_state: st.session_state.search_results = None
266
  if 'user_query' not in st.session_state: st.session_state.user_query = ""
267
+ if 'history' not in st.session_state: st.session_state.history = []
268
  def clear_search_state():
269
  st.session_state.search_results = None
270
  st.session_state.user_query = ""
 
271
  with st.form(key="search_form"):
272
  query = st.text_area("Describe tu caso aquí:", height=150, placeholder="Ej: Cuando como mucha carne me duele el dedo gordo del pie...")
273
  submitted = st.form_submit_button("Analizar mi caso", type="primary")
 
274
  if submitted:
275
  if not query:
276
  st.warning("Por favor, describe lo que sientes y lo que comiste.")
 
280
  st.session_state.user_query = query
281
  with st.spinner("🧠 Interpretando tu caso y buscando pistas con IA..."):
282
  entities = extract_and_infer_with_gemini(query, lista_condiciones)
 
283
  if entities and (entities.get("alimentos") or entities.get("sintomas")):
 
284
  info_str = f"IA identificó - Alimentos: {', '.join(entities.get('alimentos',[])) or 'Ninguno'}"
285
  info_str += f", Síntomas: {', '.join(entities.get('sintomas',[])) or 'Ninguno'}"
286
+ if entities.get("condicion_probable"): info_str += f", Condición Probable: {entities.get('condicion_probable')}"
 
287
  st.info(info_str)
288
  with st.spinner("🔬 Cruzando información y calculando relevancia..."):
289
  results = find_best_matches_hybrid(entities, alimentos_data)
290
  st.session_state.search_results = results
291
+ if results:
292
+ st.session_state.history.insert(0, {"query": query, "result": results[0]['entry']['condicion_asociada']})
293
+ st.session_state.history = st.session_state.history[:5]
294
  else:
295
  st.error("No se pudieron identificar alimentos o síntomas claros en tu descripción.")
296
  st.session_state.search_results = []
297
+ with st.sidebar:
298
+ st.header("Historial de Consultas")
299
+ if not st.session_state.history:
300
+ st.write("Aún no has realizado ninguna consulta.")
301
+ else:
302
+ for item in st.session_state.history:
303
+ with st.container(border=True):
304
+ st.write(f"**Consulta:** *{item['query'][:50]}...*")
305
+ st.write(f"**Principal Hallazgo:** {item['result']}")
306
  if st.session_state.search_results is not None:
307
  st.button("Realizar nueva consulta", on_click=clear_search_state)
308
  st.markdown("---")
 
309
  results = st.session_state.search_results
 
310
  if not results:
311
+ st.warning(f"No se encontraron coincidencias claras para tu caso: '{st.session_state.user_query}'.\n\n**Sugerencias:**\n- Intenta ser más específico con los síntomas.\n- Asegúrate de mencionar al menos un alimento o bebida.\n- Reformula tu consulta con otras palabras.")
312
  else:
313
  st.success(f"Hemos encontrado {len(results)} posible(s) causa(s) relacionada(s) con tu caso.")
 
314
  st.subheader("Análisis de Relevancia de las Coincidencias")
315
  chart = create_relevance_chart(results)
316
  st.altair_chart(chart, use_container_width=True)
 
317
  best_match_data = results[0]
318
  best_match = best_match_data['entry']
319
  best_score = best_match_data['score']
 
320
  with st.expander(f"**Análisis Detallado de la Principal Coincidencia: {best_match.get('condicion_asociada')}**", expanded=True):
321
  col1, col2 = st.columns([3, 1])
322
  with col1:
 
326
  score_col2.metric("Puntos por Alimento", f"{best_score['food']}")
327
  score_col3.metric("Puntos por Síntomas", f"{best_score['symptoms']}")
328
  score_col4.metric("PUNTUACIÓN TOTAL", f"{best_score['total']}", delta="Máxima coincidencia")
 
 
329
  with col2:
330
+ st.write("")
331
  if foodb_index:
332
+ # --- TÍTULO DEL POPOVER CORREGIDO ---
333
+ with st.popover("🔬 Principales componentes moleculares del alimento"):
334
  alimentos_a_buscar = extract_foods_from_string(best_match.get("compuesto_alimento", ""))
335
  if not alimentos_a_buscar:
336
+ st.write("No se encontraron alimentos específicos para analizar.")
337
  else:
338
  found_data = False
339
+ for alimento in alimentos_a_buscar[:3]:
340
  if alimento in foodb_index:
341
  found_data = True
342
  with st.container(border=True):
343
  st.subheader(f"Análisis de: {alimento.capitalize()}")
344
+ for item in foodb_index[alimento][:5]:
345
  st.write(f"**Compuesto:** {item['compound']}")
346
  st.write(f"**Efectos reportados:** {', '.join(item['effects'])}")
347
  st.markdown("---")
348
  if not found_data:
349
+ st.write(f"No se encontraron datos moleculares para '{', '.join(alimentos_a_buscar)}' en FoodB.")
 
350
  st.markdown("---")
 
351
  with st.spinner("✍️ Generando un análisis personalizado con IA..."):
352
  detailed_analysis = generate_detailed_analysis(st.session_state.user_query, best_match)
353
  st.markdown(detailed_analysis)
354
+ st.markdown("---")
355
+ st.write("**¿Te fue útil este análisis?**")
356
+ feedback_cols = st.columns(8)
357
+ if feedback_cols[0].button("👍 Útil", key=f"util_{best_match['condicion_asociada']}"):
358
+ log_feedback(st.session_state.user_query, best_match_data, "util")
359
+ st.success("¡Gracias por tu feedback!")
360
+ if feedback_cols[1].button("👎 No útil", key=f"no_util_{best_match['condicion_asociada']}"):
361
+ log_feedback(st.session_state.user_query, best_match_data, "no_util")
362
+ st.warning("Gracias. Usaremos tu feedback para mejorar.")
363
  if len(results) > 1:
364
  with st.expander("Otras posibles coincidencias (ordenadas por relevancia)"):
 
365
  for result in results[1:]:
366
  entry = result['entry']
367
  score = result['score']