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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +81 -112
app.py CHANGED
@@ -1,5 +1,5 @@
1
- # ==================== El Detective de Alimentos (Versión 4.0 - Transparencia con Gráficos) =====================================
2
- # Mejoras: Gráfico de relevancia de resultados, desglose de puntuación y motor de búsqueda modificado.
3
 
4
  import streamlit as st
5
  import google.generativeai as genai
@@ -9,7 +9,7 @@ import json
9
  import logging
10
  import re
11
  import pandas as pd
12
- import altair as alt # <-- LIBRERÍA NUEVA
13
 
14
  st.set_page_config(
15
  page_title="El Detective de Alimentos",
@@ -17,7 +17,7 @@ st.set_page_config(
17
  layout="wide"
18
  )
19
 
20
- # ... (El código de configuración de Gemini y carga de datos 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")
@@ -40,7 +40,7 @@ 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."""
@@ -54,79 +54,62 @@ def get_gemini_model():
54
 
55
  model = get_gemini_model()
56
 
 
 
57
  @st.cache_data
58
  def load_data():
59
  """
60
- Carga la base de datos principal y la enriquece con el conocimiento dual.
61
  """
62
  try:
63
  path_alimentos = os.path.join('DATOS', 'alimentos_enriquecido.json')
64
- path_dual = os.path.join('DATOS', 'conocimiento_dual.json')
65
 
66
  with open(path_alimentos, 'r', encoding='utf-8') as f:
67
  data_alimentos = json.load(f)
68
 
69
- # Cargar el conocimiento dual (si existe)
70
- conocimiento_dual = {}
71
- if os.path.exists(path_dual):
72
- with open(path_dual, 'r', encoding='utf-8') as f:
73
- data_dual = json.load(f)
74
- # Crear un diccionario para un acceso rápido
75
- for item in data_dual:
76
- key = item['condicion_asociada']
77
- conocimiento_dual[key] = {
78
- 'efecto_benefico': item.get('efecto_benefico'),
79
- 'contexto_negativo': item.get('contexto_negativo')
80
- }
81
- logger.info(f"✅ Conocimiento dual cargado con {len(conocimiento_dual)} registros.")
82
-
83
- # Fusionar los datos
84
- for entry in data_alimentos:
85
- key = entry['condicion_asociada']
86
- if key in conocimiento_dual:
87
- entry['efecto_benefico'] = conocimiento_dual[key]['efecto_benefico']
88
- entry['contexto_negativo'] = conocimiento_dual[key]['contexto_negativo']
89
-
90
  lista_condiciones = sorted(list(set(item['condicion_asociada'] for item in data_alimentos)))
91
 
92
- logger.info(f"✅ Base de datos principal enriquecida y fusionada.")
93
- return data_alimentos, lista_condiciones
 
 
 
 
 
 
 
 
 
94
 
95
  except FileNotFoundError as e:
96
  st.error(f"❌ Error: No se encontró el archivo '{e.filename}'. Asegúrate de que está en la carpeta 'DATOS'.")
97
- return None, None
98
  except json.JSONDecodeError as e:
99
  st.error(f"❌ Error: Un archivo JSON tiene un formato incorrecto: {e}")
100
- return None, None
101
- alimentos_data, lista_condiciones = load_data()
 
 
102
 
103
- # DICCIONARIO DE TRADUCCIÓN AMPLIADO (v3.1)
104
  FOOD_TO_COMPOUND_MAP = {
105
- # Gluten
106
  "pan": ["gluten"], "trigo": ["gluten"], "harina de trigo": ["gluten"], "cebada": ["gluten"],
107
  "centeno": ["gluten"], "pasta": ["gluten"], "galletas": ["gluten"], "avena": ["gluten"], "pizza": ["gluten"], "torta": ["gluten"],
108
- # Lácteos
109
  "leche": ["lácteos", "caseína", "lactosa"], "queso": ["lácteos", "caseína", "lactosa", "histamina", "tiramina"],
110
  "yogur": ["lácteos", "caseína", "lactosa"], "mantequilla": ["lácteos", "caseína", "lactosa"], "crema": ["lácteos"], "helado": ["lácteos"],
111
- # Fenoles y Salicilatos
112
  "manzana": ["salicilatos", "fructosa"], "almendras": ["salicilatos", "arginina"], "uvas": ["salicilatos"], "pasas": ["salicilatos"],
113
  "naranja": ["salicilatos"], "brócoli": ["salicilatos", "goitrógenos", "fodmaps"], "cúrcuma": ["salicilatos"],
114
- # Azúcares y Fructosa
115
  "azucar": ["azúcar", "fructosa"], "dulces": ["azúcar"], "refrescos": ["azúcar", "fructosa"], "gaseosas": ["azúcar", "fructosa"],
116
  "miel": ["fructosa", "fodmaps"], "jarabe de maiz": ["fructosa"],
117
- # Aminas (Histamina, Tiramina)
118
  "vino tinto": ["histamina", "tiramina", "sulfitos"], "vino rojo": ["histamina", "tiramina", "sulfitos"],
119
  "cerveza": ["histamina", "tiramina", "purinas", "gluten"], "chocolate": ["cafeína", "tiramina", "níquel", "arginina"],
120
  "embutidos": ["histamina", "tiramina", "nitritos"], "pescado enlatado": ["histamina"], "tomate": ["histamina", "solaninas", "ácidos"],
121
- # FODMAPs
122
  "aguacate": ["fodmaps", "polioles"], "cebolla": ["fodmaps"], "ajo": ["fodmaps"], "legumbres": ["fodmaps", "gos"],
123
- # Otros
124
  "carne": ["alfa-gal", "proteínas", "purinas", "hierro"], "carnes rojas": ["purinas", "alfa-gal", "hierro"],
125
  "mariscos": ["purinas", "sulfitos", "alérgenos", "yodo"], "huevo": ["alérgenos"], "soya": ["alérgenos"],
126
  "café": ["cafeína", "ácidos"], "nueces": ["arginina", "salicilatos", "níquel"]
127
  }
128
-
129
- # NUEVO DICCIONARIO DE SINÓNIMOS DE CONDICIONES
130
  CONDITION_SYNONYMS = {
131
  "síndrome del intestino irritable (sii).": [
132
  "intolerancia a los fodmaps",
@@ -136,23 +119,16 @@ CONDITION_SYNONYMS = {
136
  "gota / hiperuricemia.": ["acido urico aumentado"],
137
  "intolerancia a la lactosa.": ["déficit de lactasa"],
138
  "enfermedad celíaca (clásica).": ["dermatitis herpetiforme"]
139
- # Puedes añadir más sinónimos aquí en el futuro
140
  }
141
 
142
-
143
- # --- LÓGICA DE BÚSQUEDA Y ANÁLISIS ---
144
-
145
- # ... (La función extract_and_infer_with_gemini no cambia) ...
146
  def extract_and_infer_with_gemini(query, condiciones):
147
  """Extrae entidades e infiere una condición probable."""
148
  if not model: return None
149
  condiciones_str = "\n".join([f"- {c}" for c in condiciones])
150
  system_prompt = f"""
151
  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.
152
-
153
  LISTA DE CONDICIONES POSIBLES:
154
  {condiciones_str}
155
-
156
  Consulta: "{query}"
157
  """
158
  try:
@@ -168,43 +144,28 @@ def extract_and_infer_with_gemini(query, condiciones):
168
  st.error(f"Hubo un problema al interpretar tu consulta con la IA.")
169
  return None
170
 
171
- # --- FUNCIÓN DE BÚSQUEDA MODIFICADA ---
172
  def find_best_matches_hybrid(entities, data):
173
- """
174
- Motor de búsqueda híbrido (v4.1) con manejo de sinónimos de condiciones.
175
- """
176
  if not entities or not data: return []
177
-
178
  user_symptoms = set(s.lower().strip() for s in entities.get("sintomas", []))
179
  user_foods = set(f.lower().strip() for f in entities.get("alimentos", []))
180
  inferred_condition_raw = entities.get("condicion_probable", "").lower().strip()
181
-
182
  candidate_terms = set(user_foods)
183
  for food in user_foods:
184
  if food in FOOD_TO_COMPOUND_MAP:
185
  candidate_terms.update(c.lower() for c in FOOD_TO_COMPOUND_MAP[food])
186
-
187
  results = []
188
  for index, entry in enumerate(data):
189
  score_details = {'condition': 0, 'food': 0, 'symptoms': 0, 'total': 0}
190
-
191
- # --- PONDERACIÓN 1: COINCIDENCIA DE CONDICIÓN (CON SINÓNIMOS) ---
192
  entry_condition = entry.get("condicion_asociada", "").lower().strip()
193
-
194
- # Comprobación directa
195
  if inferred_condition_raw and inferred_condition_raw == entry_condition:
196
  score_details['condition'] = 100
197
- # Comprobación de sinónimos
198
  elif inferred_condition_raw and entry_condition in CONDITION_SYNONYMS:
199
  if inferred_condition_raw in CONDITION_SYNONYMS[entry_condition]:
200
  score_details['condition'] = 100
201
-
202
- # --- PONDERACIÓN 2: COINCIDENCIA DE ALIMENTOS/COMPUESTOS ---
203
  entry_compounds_text = entry.get("compuesto_alimento", "").lower()
204
  if any(term in entry_compounds_text for term in candidate_terms):
205
  score_details['food'] = 20
206
-
207
- # --- PONDERACIÓN 3: COINCIDENCIA DE SÍNTOMAS ---
208
  entry_symptoms_keys = set(s.lower().strip() for s in entry.get("sintomas_clave", []))
209
  symptom_score = 0
210
  for user_symptom in user_symptoms:
@@ -213,23 +174,18 @@ def find_best_matches_hybrid(entities, data):
213
  symptom_score += 5
214
  break
215
  score_details['symptoms'] = symptom_score
216
-
217
  total_score = sum(score_details.values())
218
  if total_score > 0:
219
  score_details['total'] = total_score
220
  results.append({'entry': entry, 'score': score_details})
221
-
222
  if not results: return []
223
-
224
  sorted_results = sorted(results, key=lambda x: x['score']['total'], reverse=True)
225
  return sorted_results
226
 
227
- # ... (La función generate_detailed_analysis no cambia) ...
228
  def generate_detailed_analysis(query, match):
229
  """Genera la explicación final, manejando la información de dualidad si existe."""
 
230
  if not model: return "Error: El modelo de IA no está disponible."
231
-
232
- # Construcción del prompt dinámico
233
  prompt_parts = [
234
  "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.",
235
  f'El usuario ha descrito el siguiente caso: "{query}"',
@@ -238,37 +194,28 @@ def generate_detailed_analysis(query, match):
238
  f'- Mecanismo: {match.get("mecanismo_fisiologico")}',
239
  f'- Recomendaciones: {match.get("recomendaciones_examenes")}',
240
  f'- Alimentos Implicados: {match.get("compuesto_alimento")}',
241
-
242
  "\n**Tu Tarea:** Redacta una respuesta excepcional para el usuario usando Markdown. Sigue esta estructura OBLIGATORIAMENTE:",
243
-
244
  f'### Posible Causa: {match.get("condicion_asociada")}',
245
  f'Hola. Entiendo que te preocupa lo que sientes. Basado en los síntomas y alimentos que mencionaste, existe una posible relación con una condición conocida como **{match.get("condicion_asociada")}**.',
246
-
247
  f'### ¿Qué podría estar pasando en tu cuerpo?',
248
  f'Explica el siguiente mecanismo en términos muy sencillos, usando una analogía si es posible: "{match.get("mecanismo_fisiologico")}"'
249
  ]
250
-
251
- # --- Lógica para añadir la sección de dualidad ---
252
  if match.get("efecto_benefico") and match.get("contexto_negativo"):
253
  prompt_parts.append(
254
  "\n### El Doble Filo de Estos Alimentos\n"
255
  f'**Para la mayoría de las personas:** Resume por qué estos alimentos son generalmente buenos, basándote en: "{match.get("efecto_benefico")}"\n'
256
  f'**Sin embargo, en ciertos contextos:** Explica por qué pueden ser problemáticos para el usuario, basándote en: "{match.get("contexto_negativo")}"'
257
  )
258
-
259
  prompt_parts.extend([
260
  "\n### Pasos a Seguir y Recomendaciones",
261
  "Aquí te dejo algunas recomendaciones generales. Es fundamental que las converses con un profesional de la salud:",
262
  f'* **[Punto 1 de las recomendaciones, reformulado como un consejo práctico basado en "{match.get("recomendaciones_examenes")}"]**',
263
  f'* **[Punto 2 de las recomendaciones, enfocado en los exámenes si los hay, basado en "{match.get("recomendaciones_examenes")}"]**',
264
  f'* **Atención a otros alimentos:** Ten en cuenta que, además de lo que mencionaste, otros alimentos implicados en esta condición son: **[menciona 2-3 ejemplos del campo "{match.get("compuesto_alimento")}"]**.',
265
-
266
  "\n### **IMPORTANTE: Descargo de Responsabilidad**",
267
  "Este análisis es una herramienta informativa y de orientación basada en inteligencia artificial. **NO es un diagnóstico médico.** La información proporcionada no debe sustituir la consulta, el diagnóstico o el tratamiento de un médico o profesional de la salud cualificado. Consulta siempre a un experto para evaluar tu caso particular."
268
  ])
269
-
270
  prompt = "\n".join(prompt_parts)
271
-
272
  try:
273
  response = model.generate_content(prompt)
274
  return response.text
@@ -276,36 +223,23 @@ def generate_detailed_analysis(query, match):
276
  logger.error(f"Error generando análisis detallado con Gemini: {e}")
277
  return "No se pudo generar el análisis detallado."
278
 
279
- # --- NUEVA FUNCIÓN PARA CREAR EL GRÁFICO ---
280
  def create_relevance_chart(results):
281
  """Crea un gráfico de barras de Altair legible para visualizar la relevancia."""
282
- # Preparar los datos para el gráfico (tomamos los 5 mejores)
283
  top_results = results[:5]
284
-
285
- # Limpiamos los nombres para que no sean excesivamente largos en el gráfico
286
  condition_names = []
287
  for res in top_results:
288
- # Quitamos paréntesis y acortamos si es necesario para el display
289
  name = re.sub(r'\(.*\)', '', res['entry']['condicion_asociada']).strip()
290
  condition_names.append(name)
291
-
292
  chart_data = {
293
  "Condición": condition_names,
294
  "Relevancia": [res['score']['total'] for res in top_results]
295
  }
296
  source = pd.DataFrame(chart_data)
297
-
298
- # Crear el gráfico
299
  chart = alt.Chart(source).mark_bar(color='#1f77b4').encode(
300
  x=alt.X('Relevancia:Q', title='Puntuación de Relevancia'),
301
  y=alt.Y('Condición:N', sort='-x', title='Posible Condición',
302
- axis=alt.Axis(
303
- labelLimit=300 # <-- ¡ESTA ES LA LÍNEA CLAVE! Aumenta el espacio para las etiquetas.
304
- )),
305
- tooltip=[
306
- alt.Tooltip('Condición:N', title='Condición'),
307
- alt.Tooltip('Relevancia:Q', title='Puntuación')
308
- ]
309
  ).properties(
310
  title='Principales Coincidencias según tu Caso'
311
  ).configure_axis(
@@ -313,12 +247,22 @@ def create_relevance_chart(results):
313
  titleFontSize=14
314
  ).configure_title(
315
  fontSize=16,
316
- anchor='start' # Alinea el título a la izquierda, se ve más profesional
317
  )
318
  return chart
319
 
320
-
321
- # --- INTERFAZ DE USUARIO (UI) ---
 
 
 
 
 
 
 
 
 
 
322
  col_img, col_text = st.columns([1, 4], gap="medium")
323
  with col_img:
324
  if os.path.exists("imagen.png"):
@@ -328,7 +272,6 @@ with col_text:
328
  st.markdown("##### Describe lo que sientes y lo que comiste para descubrir posibles intolerancias.")
329
  st.markdown("---")
330
 
331
- # --- LÓGICA PRINCIPAL DE LA APLICACIÓN ---
332
  if 'search_results' not in st.session_state: st.session_state.search_results = None
333
  if 'user_query' not in st.session_state: st.session_state.user_query = ""
334
 
@@ -337,7 +280,7 @@ def clear_search_state():
337
  st.session_state.user_query = ""
338
 
339
  with st.form(key="search_form"):
340
- query = st.text_area("Describe tu caso aquí:", height=150, placeholder="Ej: Cuando como mucho pan se me hincha el estomago y me salen ampollas en los codos")
341
  submitted = st.form_submit_button("Analizar mi caso", type="primary")
342
 
343
  if submitted:
@@ -351,12 +294,12 @@ if submitted:
351
  entities = extract_and_infer_with_gemini(query, lista_condiciones)
352
 
353
  if entities and (entities.get("alimentos") or entities.get("sintomas")):
 
354
  info_str = f"IA identificó - Alimentos: {', '.join(entities.get('alimentos',[])) or 'Ninguno'}"
355
  info_str += f", Síntomas: {', '.join(entities.get('sintomas',[])) or 'Ninguno'}"
356
  if entities.get("condicion_probable"):
357
  info_str += f", Condición Probable: {entities.get('condicion_probable')}"
358
  st.info(info_str)
359
-
360
  with st.spinner("🔬 Cruzando información y calculando relevancia..."):
361
  results = find_best_matches_hybrid(entities, alimentos_data)
362
  st.session_state.search_results = results
@@ -375,22 +318,47 @@ if st.session_state.search_results is not None:
375
  else:
376
  st.success(f"Hemos encontrado {len(results)} posible(s) causa(s) relacionada(s) con tu caso.")
377
 
378
- # --- SECCIÓN DEL GRÁFICO NUEVA ---
379
  st.subheader("Análisis de Relevancia de las Coincidencias")
380
  chart = create_relevance_chart(results)
381
  st.altair_chart(chart, use_container_width=True)
382
 
383
- best_match = results[0]['entry']
384
- best_score = results[0]['score']
 
385
 
386
  with st.expander(f"**Análisis Detallado de la Principal Coincidencia: {best_match.get('condicion_asociada')}**", expanded=True):
387
- # Mostramos el desglose de la puntuación
388
- st.markdown("##### Desglose de la Puntuación de Relevancia:")
389
- col1, col2, col3, col4 = st.columns(4)
390
- col1.metric("Puntos por Condición", f"{best_score['condition']}")
391
- col2.metric("Puntos por Alimento", f"{best_score['food']}")
392
- col3.metric("Puntos por Síntomas", f"{best_score['symptoms']}")
393
- col4.metric("PUNTUACIÓN TOTAL", f"{best_score['total']}", delta="Máxima coincidencia")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  st.markdown("---")
395
 
396
  with st.spinner("✍️ Generando un análisis personalizado con IA..."):
@@ -399,6 +367,7 @@ if st.session_state.search_results is not None:
399
 
400
  if len(results) > 1:
401
  with st.expander("Otras posibles coincidencias (ordenadas por relevancia)"):
 
402
  for result in results[1:]:
403
  entry = result['entry']
404
  score = result['score']
 
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
 
9
  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
  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")
 
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."""
 
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:
77
+ foodb_index = json.load(f)
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"],
 
99
  "leche": ["lácteos", "caseína", "lactosa"], "queso": ["lácteos", "caseína", "lactosa", "histamina", "tiramina"],
100
  "yogur": ["lácteos", "caseína", "lactosa"], "mantequilla": ["lácteos", "caseína", "lactosa"], "crema": ["lácteos"], "helado": ["lácteos"],
 
101
  "manzana": ["salicilatos", "fructosa"], "almendras": ["salicilatos", "arginina"], "uvas": ["salicilatos"], "pasas": ["salicilatos"],
102
  "naranja": ["salicilatos"], "brócoli": ["salicilatos", "goitrógenos", "fodmaps"], "cúrcuma": ["salicilatos"],
 
103
  "azucar": ["azúcar", "fructosa"], "dulces": ["azúcar"], "refrescos": ["azúcar", "fructosa"], "gaseosas": ["azúcar", "fructosa"],
104
  "miel": ["fructosa", "fodmaps"], "jarabe de maiz": ["fructosa"],
 
105
  "vino tinto": ["histamina", "tiramina", "sulfitos"], "vino rojo": ["histamina", "tiramina", "sulfitos"],
106
  "cerveza": ["histamina", "tiramina", "purinas", "gluten"], "chocolate": ["cafeína", "tiramina", "níquel", "arginina"],
107
  "embutidos": ["histamina", "tiramina", "nitritos"], "pescado enlatado": ["histamina"], "tomate": ["histamina", "solaninas", "ácidos"],
 
108
  "aguacate": ["fodmaps", "polioles"], "cebolla": ["fodmaps"], "ajo": ["fodmaps"], "legumbres": ["fodmaps", "gos"],
 
109
  "carne": ["alfa-gal", "proteínas", "purinas", "hierro"], "carnes rojas": ["purinas", "alfa-gal", "hierro"],
110
  "mariscos": ["purinas", "sulfitos", "alérgenos", "yodo"], "huevo": ["alérgenos"], "soya": ["alérgenos"],
111
  "café": ["cafeína", "ácidos"], "nueces": ["arginina", "salicilatos", "níquel"]
112
  }
 
 
113
  CONDITION_SYNONYMS = {
114
  "síndrome del intestino irritable (sii).": [
115
  "intolerancia a los fodmaps",
 
119
  "gota / hiperuricemia.": ["acido urico aumentado"],
120
  "intolerancia a la lactosa.": ["déficit de lactasa"],
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:
 
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", []))
152
  inferred_condition_raw = entities.get("condicion_probable", "").lower().strip()
 
153
  candidate_terms = set(user_foods)
154
  for food in user_foods:
155
  if food in FOOD_TO_COMPOUND_MAP:
156
  candidate_terms.update(c.lower() for c in FOOD_TO_COMPOUND_MAP[food])
 
157
  results = []
158
  for index, entry in enumerate(data):
159
  score_details = {'condition': 0, 'food': 0, 'symptoms': 0, 'total': 0}
 
 
160
  entry_condition = entry.get("condicion_asociada", "").lower().strip()
 
 
161
  if inferred_condition_raw and inferred_condition_raw == entry_condition:
162
  score_details['condition'] = 100
 
163
  elif inferred_condition_raw and entry_condition in CONDITION_SYNONYMS:
164
  if inferred_condition_raw in CONDITION_SYNONYMS[entry_condition]:
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:
 
174
  symptom_score += 5
175
  break
176
  score_details['symptoms'] = symptom_score
 
177
  total_score = sum(score_details.values())
178
  if total_score > 0:
179
  score_details['total'] = total_score
180
  results.append({'entry': entry, 'score': score_details})
 
181
  if not results: return []
 
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.",
191
  f'El usuario ha descrito el siguiente caso: "{query}"',
 
194
  f'- Mecanismo: {match.get("mecanismo_fisiologico")}',
195
  f'- Recomendaciones: {match.get("recomendaciones_examenes")}',
196
  f'- Alimentos Implicados: {match.get("compuesto_alimento")}',
 
197
  "\n**Tu Tarea:** Redacta una respuesta excepcional para el usuario usando Markdown. Sigue esta estructura OBLIGATORIAMENTE:",
 
198
  f'### Posible Causa: {match.get("condicion_asociada")}',
199
  f'Hola. Entiendo que te preocupa lo que sientes. Basado en los síntomas y alimentos que mencionaste, existe una posible relación con una condición conocida como **{match.get("condicion_asociada")}**.',
 
200
  f'### ¿Qué podría estar pasando en tu cuerpo?',
201
  f'Explica el siguiente mecanismo en términos muy sencillos, usando una analogía si es posible: "{match.get("mecanismo_fisiologico")}"'
202
  ]
 
 
203
  if match.get("efecto_benefico") and match.get("contexto_negativo"):
204
  prompt_parts.append(
205
  "\n### El Doble Filo de Estos Alimentos\n"
206
  f'**Para la mayoría de las personas:** Resume por qué estos alimentos son generalmente buenos, basándote en: "{match.get("efecto_benefico")}"\n'
207
  f'**Sin embargo, en ciertos contextos:** Explica por qué pueden ser problemáticos para el usuario, basándote en: "{match.get("contexto_negativo")}"'
208
  )
 
209
  prompt_parts.extend([
210
  "\n### Pasos a Seguir y Recomendaciones",
211
  "Aquí te dejo algunas recomendaciones generales. Es fundamental que las converses con un profesional de la salud:",
212
  f'* **[Punto 1 de las recomendaciones, reformulado como un consejo práctico basado en "{match.get("recomendaciones_examenes")}"]**',
213
  f'* **[Punto 2 de las recomendaciones, enfocado en los exámenes si los hay, basado en "{match.get("recomendaciones_examenes")}"]**',
214
  f'* **Atención a otros alimentos:** Ten en cuenta que, además de lo que mencionaste, otros alimentos implicados en esta condición son: **[menciona 2-3 ejemplos del campo "{match.get("compuesto_alimento")}"]**.',
 
215
  "\n### **IMPORTANTE: Descargo de Responsabilidad**",
216
  "Este análisis es una herramienta informativa y de orientación basada en inteligencia artificial. **NO es un diagnóstico médico.** La información proporcionada no debe sustituir la consulta, el diagnóstico o el tratamiento de un médico o profesional de la salud cualificado. Consulta siempre a un experto para evaluar tu caso particular."
217
  ])
 
218
  prompt = "\n".join(prompt_parts)
 
219
  try:
220
  response = model.generate_content(prompt)
221
  return response.text
 
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(
 
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
  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
 
 
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:
 
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
 
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:
332
+ st.markdown("##### Desglose de la Puntuación de Relevancia:")
333
+ score_col1, score_col2, score_col3, score_col4 = st.columns(4)
334
+ score_col1.metric("Puntos por Condición", f"{best_score['condition']}")
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..."):
 
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']