JairoCesar commited on
Commit
469d3fc
·
verified ·
1 Parent(s): b6105a0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +68 -144
app.py CHANGED
@@ -1,8 +1,7 @@
1
- # El Detective de Alimentos
2
- # JAIRO CESAR ALEXANDER E. MD DIANA MILENA SOLER MARTINEZ PSI. ESP. U JUAN N CORPAS
3
  import streamlit as st
4
- import google.generativeai as genai
5
- import google.api_core.exceptions
6
  import os
7
  import json
8
  import logging
@@ -10,7 +9,7 @@ import re
10
  import pandas as pd
11
  import altair as alt
12
  from datetime import datetime
13
- from tenacity import retry, stop_after_attempt, wait_random_exponential
14
  from io import BytesIO
15
  import docx
16
  import difflib
@@ -19,22 +18,22 @@ st.set_page_config(page_title="El Detective de Alimentos", page_icon="🍎", lay
19
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
20
  logger = logging.getLogger("food_detective_app")
21
 
22
- # --- 1. TEXTO ESTÁTICO DE SEGURIDAD (AHORRO DE TOKENS) ---
23
  TEXTO_BANDERAS_ROJAS = """
24
  \n### **IMPORTANTE: Descargo de Responsabilidad y Banderas Rojas**
25
- Este análisis es una herramienta informativa de IA y **NO es un diagnóstico médico.** La información proporcionada no debe sustituir la consulta con un profesional cualificado.
26
 
27
- **🚩 BANDERAS ROJAS: ¡Atención!** Ciertas condiciones graves pueden imitar los síntomas de una intolerancia alimentaria. Es crucial que consultes a un médico para descartar problemas serios, especialmente si experimentas alguno de los siguientes escenarios:
28
-
29
- 1. **Cáncer Gástrico o de Colon:** Síntomas como plenitud rápida, pérdida de peso inexplicable, sangre en heces.
30
- 2. **Enfermedad Inflamatoria Intestinal:** Diarrea con sangre/moco, fiebre recurrente, dolor articular.
31
  3. **Embarazo:** Náuseas matutinas, ausencia de menstruación.
32
- 4. **Isquemia Mesentérica:** Dolor abdominal predecible 15-30 min después de comer.
33
- 5. **Trastornos de la Vesícula:** Dolor agudo lado derecho tras comer grasas, ictericia.
34
 
35
- **Si tus síntomas son severos o persistentes, la consulta médica es urgente.**
36
  """
37
 
 
38
  try:
39
  if 'GEMINI_API_KEY' in st.secrets:
40
  GEMINI_API_KEY = st.secrets['GEMINI_API_KEY']
@@ -43,19 +42,17 @@ try:
43
  if not GEMINI_API_KEY:
44
  st.error("No se encontró la GEMINI_API_KEY.")
45
  st.stop()
46
- genai.configure(api_key=GEMINI_API_KEY)
 
 
 
47
  except Exception as e:
48
  st.error(f"❌ Error al configurar Gemini API: {e}")
49
  st.stop()
50
 
51
  @st.cache_resource
52
- def get_gemini_model():
53
- try:
54
- return genai.GenerativeModel("gemini-2.5-flash-lite")
55
- except Exception as e:
56
- st.error(f"❌ No se pudo cargar el modelo Gemini: {e}")
57
- return None
58
- model = get_gemini_model()
59
 
60
  @st.cache_data
61
  def load_data():
@@ -1104,23 +1101,23 @@ def sanitize_text(text):
1104
  if not text: return ""
1105
  return re.sub(r'[.,;()]', '', text).lower().strip()
1106
 
1107
- # --- FUNCIONES DE EXTRACCIÓN Y LÓGICA (OPTIMIZADAS) ---
1108
 
1109
- @retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(3))
1110
  def extract_entities_with_gemini(query):
1111
- if not model: return None
1112
- # PROMPT TELEGRÁFICO (AHORRO)
1113
  system_prompt = f"""
1114
- Rol: Extractor de Entidades Médicas.
1115
- Tarea: Analiza el texto y extrae JSON estricto.
1116
- Campos:
1117
- 1. "alimentos": Lista de comidas/ingredientes.
1118
- 2. "sintomas": Lista de sensaciones/signos físicos.
1119
  Input: "{query}"
1120
- Output (JSON Only):
1121
  """
1122
  try:
1123
- response = model.generate_content(system_prompt)
 
 
 
 
1124
  text = response.text
1125
  if "```json" in text: text = text.split("```json")[1].split("```")[0]
1126
  elif "```" in text: text = text.split("```")[1].split("```")[0]
@@ -1137,7 +1134,7 @@ def reinforce_entities_with_keywords(entities, query, food_map, master_symptom_m
1137
  for food_keyword in food_map.keys():
1138
  if food_keyword in query_sanitized: current_foods.add(food_keyword)
1139
  entities["alimentos"] = list(current_foods)
1140
- # Síntomas simples (sin IA)
1141
  current_symptoms = set(entities.get("sintomas", []))
1142
  for main_symptom, details in master_symptom_map.items():
1143
  for phrase in details.get("frases_es", []):
@@ -1199,104 +1196,61 @@ def find_best_foodb_matches(user_foods_es, foodb_index_keys, food_name_map, limi
1199
  found_matches.extend(matches)
1200
  return list(set(found_matches))[:limit]
1201
 
1202
- # --- GENERADORES DE REPORTES (RESTAURADOS COMPLETOS) ---
1203
-
1204
- @retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(3))
1205
  def generate_detailed_analysis(query, match):
1206
- if not model: return "Error: IA no disponible."
1207
  prompt_parts = [
1208
  "Rol: Nutricionista Funcional.",
1209
  f"Caso: {query}",
1210
  f"Hipótesis: {match.get('condicion_asociada')}",
1211
  f"Mecanismo: {match.get('mecanismo_fisiologico')}",
1212
  f"Alimentos clave: {match.get('compuesto_alimento')}",
1213
- "Tarea: Escribir análisis en Markdown.",
1214
- "Estructura:",
1215
- "1. Saludo y posible causa.",
1216
- "2. Explicación del mecanismo.",
1217
- "3. Alimentos a evitar.",
1218
- "4. Reemplazos sugeridos.",
1219
- "5. Consejo práctico.",
1220
- "IMPORTANTE: NO incluyas descargos de responsabilidad."
1221
  ]
1222
  prompt = "\n".join(prompt_parts)
1223
  try:
1224
- response = model.generate_content(prompt)
1225
- return response.text + TEXTO_BANDERAS_ROJAS # Concatenación local
 
 
 
 
1226
  except Exception as e:
1227
  logger.error(f"Error análisis: {e}")
1228
  raise e
1229
 
1230
  def generate_neuro_report_text(entities, food_map, neuro_map):
1231
- """Genera reporte neuropsicológico basado en mapas locales (Costo 0 tokens)."""
1232
  report_lines = ["\n### 🧠 Efectos Neuropsicológicos Posibles"]
1233
  user_foods = entities.get("alimentos", [])
1234
  relevant_compounds = set()
1235
  if user_foods:
1236
  for food in user_foods:
1237
  if food in food_map: relevant_compounds.update(food_map[food])
1238
-
1239
  found_neuro_effect = False
1240
  if relevant_compounds:
1241
  for compound in sorted(list(relevant_compounds)):
1242
  if compound in neuro_map:
1243
  found_neuro_effect = True
1244
- effect_info = neuro_map[compound]
1245
- report_lines.append(f"**Componente: {compound.capitalize()}**")
1246
- report_lines.append(f"_{effect_info['efecto_neuropsicologico']}_\n")
1247
-
1248
- if not found_neuro_effect:
1249
- report_lines.append("No se detectaron efectos neuropsicológicos específicos en nuestra base de datos para estos alimentos.")
1250
  return "\n".join(report_lines)
1251
 
1252
  def generate_molecular_report_text(best_match, entities, foodb_index, food_name_map, synonym_map, triggers_map):
1253
- """Genera reporte molecular detallado usando FoodB local (Costo 0 tokens)."""
1254
- report_lines = ["\n### 🔬 Análisis Molecular (FoodB)"]
1255
- user_foods_mentioned = entities.get("alimentos", [])
1256
-
1257
- if not user_foods_mentioned:
1258
- return "No se identificaron alimentos específicos para el análisis molecular."
1259
-
1260
- initial_clues = set()
1261
- direct_text = best_match.get("compuesto_alimento", "").lower()
1262
- cleaned_text = re.sub(r'\(.*?\)', '', direct_text)
1263
- initial_clues.update(re.findall(r'\b[a-zA-Z-]+\b', cleaned_text))
1264
-
1265
- main_diagnosis_symptoms = set(s.lower() for s in best_match.get("sintomas_clave", []))
1266
- for compound, triggered_symptoms in triggers_map.items():
1267
- if main_diagnosis_symptoms.intersection(triggered_symptoms):
1268
- initial_clues.add(compound.lower())
1269
-
1270
- final_search_keywords = set()
1271
- for clue in initial_clues:
1272
- final_search_keywords.add(clue)
1273
- if clue in synonym_map: final_search_keywords.update(synonym_map[clue])
1274
 
1275
- best_food_matches = find_best_foodb_matches(user_foods_mentioned, foodb_index.keys(), food_name_map)
 
 
1276
 
1277
- if not best_food_matches:
1278
- return "No se encontraron datos moleculares detallados para los alimentos mencionados."
1279
-
1280
- found_any_data = False
1281
  for food_key in best_food_matches:
1282
- compounds_data = foodb_index.get(food_key, [])
1283
- relevant_compounds = []
1284
- for item in compounds_data:
1285
- if any(target in item['compound'].lower() for target in final_search_keywords):
1286
- relevant_compounds.append(item)
1287
-
1288
- if relevant_compounds:
1289
- found_any_data = True
1290
- report_lines.append(f"\n**Alimento Analizado: {food_key.capitalize()}**")
1291
- unique_compounds = set()
1292
- for item in relevant_compounds:
1293
- if item['compound'] not in unique_compounds:
1294
- report_lines.append(f"- Compuesto: `{item['compound']}` (Vínculo potencial con {best_match.get('condicion_asociada')})")
1295
- unique_compounds.add(item['compound'])
1296
-
1297
- if not found_any_data:
1298
- return f"No se encontraron los compuestos moleculares específicos de esta condición en los alimentos analizados."
1299
-
1300
  return "\n".join(report_lines)
1301
 
1302
  def create_relevance_chart(results):
@@ -1306,14 +1260,11 @@ def create_relevance_chart(results):
1306
  "Relevancia": [r['score']['total'] for r in top_results]
1307
  })
1308
  chart = alt.Chart(data).mark_bar().encode(
1309
- x='Relevancia',
1310
- y=alt.Y('Condición', sort='-x'),
1311
- tooltip=['Condición', 'Relevancia']
1312
  ).properties(title='Top Coincidencias')
1313
  return chart
1314
 
1315
  def generate_word_report(report_text):
1316
- # Simulación simple para no requerir plantilla física en el ejemplo
1317
  doc = docx.Document()
1318
  doc.add_paragraph(report_text)
1319
  doc_io = BytesIO()
@@ -1321,7 +1272,7 @@ def generate_word_report(report_text):
1321
  doc_io.seek(0)
1322
  return doc_io
1323
 
1324
- # --- INTERFAZ DE USUARIO ---
1325
  col_img1, col_text, col_img2 = st.columns([1, 4, 1])
1326
  with col_img1:
1327
  if os.path.exists("imagen.png"): st.image("imagen.png", width=150)
@@ -1337,7 +1288,7 @@ if 'entities' not in st.session_state: st.session_state.entities = None
1337
  if 'analysis_cache' not in st.session_state: st.session_state.analysis_cache = {}
1338
 
1339
  with st.form(key="search_form"):
1340
- query = st.text_area("Tu Caso:", height=150, placeholder="Ej: Me duele la cabeza y me siento hinchado cuando como queso y tomo vino.")
1341
  submitted = st.form_submit_button("Analizar Caso", type="primary")
1342
 
1343
  if submitted and query:
@@ -1345,20 +1296,17 @@ if submitted and query:
1345
  st.session_state.search_results = None
1346
  st.session_state.analysis_cache = {}
1347
 
1348
- with st.spinner("🔍 Analizando pistas con IA y Bases de Datos..."):
1349
- # 1. Extracción (IA Optimizada)
1350
  try:
1351
  raw_entities = extract_entities_with_gemini(query)
1352
  except:
1353
  raw_entities = {"alimentos": [], "sintomas": []}
1354
 
1355
- # 2. Refuerzo + Traducción Local (Local)
1356
- reinforced = reinforce_entities_with_keywords(raw_entities, query, FOOD_TO_COMPOUND_MAP, MASTER_SYMPTOM_MAP)
1357
- final_symptoms = translate_symptoms_local(reinforced.get("sintomas", []), MASTER_SYMPTOM_MAP)
1358
  final_entities = {"alimentos": reinforced.get("alimentos", []), "sintomas": final_symptoms}
1359
  st.session_state.entities = final_entities
1360
 
1361
- # 3. Búsqueda (Local)
1362
  results = find_best_matches_hybrid(final_entities, alimentos_data)
1363
  st.session_state.search_results = results
1364
 
@@ -1368,56 +1316,32 @@ if st.session_state.search_results:
1368
 
1369
  st.success(f"🔎 Coincidencia Principal: **{best_match.get('condicion_asociada')}**")
1370
 
1371
- # Generación de textos (Bajo demanda o caché)
1372
  cache_key = f"analysis_{best_match.get('condicion_asociada')}"
1373
  if cache_key not in st.session_state.analysis_cache:
1374
- with st.spinner("✍️ Redactando informe clínico..."):
1375
  try:
1376
  analysis = generate_detailed_analysis(st.session_state.user_query, best_match)
1377
  st.session_state.analysis_cache[cache_key] = analysis
1378
  except:
1379
- st.session_state.analysis_cache[cache_key] = "No se pudo generar el análisis detallado."
1380
 
1381
- # --- VISUALIZACIÓN EN PESTAÑAS (MÁS LIMPIO) ---
1382
  tab_main, tab_neuro, tab_mol = st.tabs(["💡 Interpretación Clínica", "🧠 Efectos Neuropsicológicos", "🔬 Análisis Molecular"])
1383
 
1384
  with tab_main:
1385
  st.markdown(st.session_state.analysis_cache[cache_key])
1386
- st.markdown("---")
1387
- st.caption("Gráfico de otras posibles causas:")
1388
  st.altair_chart(create_relevance_chart(results), use_container_width=True)
1389
 
1390
  with tab_neuro:
1391
- st.info("Este análisis se basa en la interacción conocida entre nutrientes y neurotransmisores.")
1392
- neuro_text = generate_neuro_report_text(st.session_state.entities, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP)
1393
- st.markdown(neuro_text)
1394
 
1395
  with tab_mol:
1396
- st.info("Desglose químico basado en la base de datos FoodB.")
1397
- mol_text = generate_molecular_report_text(best_match, st.session_state.entities, foodb_index, FOOD_NAME_TO_FOODB_KEY, COMPOUND_SYNONYM_MAP, KNOWN_TRIGGERS_MAP)
1398
- st.markdown(mol_text)
1399
 
1400
- # Botón de descarga (Combinando todo)
1401
- full_report = f"REPORTE CLÍNICO\n\n{st.session_state.analysis_cache[cache_key]}\n\n{neuro_text}\n\n{mol_text}"
1402
  word_data = generate_word_report(full_report)
1403
- st.download_button("📄 Descargar Informe Completo (Word)", data=word_data, file_name="Reporte_Detective.docx", mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document")
1404
 
1405
  elif submitted:
1406
- # Creamos un contenedor de advertencia visualmente más claro
1407
  with st.container(border=True):
1408
- st.warning("⚠️ No pudimos identificar una causa clara con la información proporcionada.")
1409
-
1410
- col_help, col_tips = st.columns([1, 2])
1411
-
1412
- with col_help:
1413
- st.markdown("### ¿Qué pudo pasar?")
1414
- st.markdown("""
1415
- - **Descripción muy breve:** La IA necesita contexto.
1416
- - **Sinónimos desconocidos:** Usaste términos muy coloquiales.
1417
- - **Fallo de conexión:** La IA no respondió a tiempo.
1418
- """)
1419
-
1420
- with col_tips:
1421
- st.info("💡 **Intenta reformular tu consulta así:**")
1422
- st.code("Siento [SÍNTOMA] y [SÍNTOMA] después de comer [ALIMENTO].", language="text")
1423
- st.markdown("**Ejemplo:** _Me duele mucho la cabeza tipo migraña cada vez que como queso curado y tomo vino tinto._")
 
1
+ # El Detective de Alimentos (Versión Actualizada a google-genai V1.0)
 
2
  import streamlit as st
3
+ from google import genai # NUEVA LIBRERÍA
4
+ from google.genai import types # TIPOS DE DATOS NUEVOS
5
  import os
6
  import json
7
  import logging
 
9
  import pandas as pd
10
  import altair as alt
11
  from datetime import datetime
12
+ from tenacity import retry, stop_after_attempt, wait_fixed
13
  from io import BytesIO
14
  import docx
15
  import difflib
 
18
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
19
  logger = logging.getLogger("food_detective_app")
20
 
21
+ # --- TEXTO ESTÁTICO DE SEGURIDAD ---
22
  TEXTO_BANDERAS_ROJAS = """
23
  \n### **IMPORTANTE: Descargo de Responsabilidad y Banderas Rojas**
24
+ Este análisis es una herramienta informativa de IA y **NO es un diagnóstico médico.**
25
 
26
+ **🚩 BANDERAS ROJAS: ¡Atención!** Consulta a un médico si experimentas:
27
+ 1. **Cáncer Gástrico/Colon:** Pérdida de peso, sangre en heces.
28
+ 2. **Enfermedad Inflamatoria Intestinal:** Diarrea con sangre/moco, fiebre.
 
29
  3. **Embarazo:** Náuseas matutinas, ausencia de menstruación.
30
+ 4. **Isquemia Mesentérica:** Dolor fuerte 15-30 min después de comer.
31
+ 5. **Vesícula:** Dolor agudo lado derecho tras grasas, piel amarilla.
32
 
33
+ **Si tus síntomas son severos, busca ayuda médica urgente.**
34
  """
35
 
36
+ # --- CONFIGURACIÓN DE LA NUEVA API (google-genai) ---
37
  try:
38
  if 'GEMINI_API_KEY' in st.secrets:
39
  GEMINI_API_KEY = st.secrets['GEMINI_API_KEY']
 
42
  if not GEMINI_API_KEY:
43
  st.error("No se encontró la GEMINI_API_KEY.")
44
  st.stop()
45
+
46
+ # NUEVA FORMA DE INICIAR EL CLIENTE
47
+ client = genai.Client(api_key=GEMINI_API_KEY)
48
+
49
  except Exception as e:
50
  st.error(f"❌ Error al configurar Gemini API: {e}")
51
  st.stop()
52
 
53
  @st.cache_resource
54
+ def get_gemini_client():
55
+ return client
 
 
 
 
 
56
 
57
  @st.cache_data
58
  def load_data():
 
1101
  if not text: return ""
1102
  return re.sub(r'[.,;()]', '', text).lower().strip()
1103
 
1104
+ # --- FUNCIONES DE EXTRACCIÓN Y LÓGICA ---
1105
 
1106
+ @retry(wait=wait_fixed(2), stop=stop_after_attempt(2)) # Retries rápidos
1107
  def extract_entities_with_gemini(query):
1108
+ if not client: return None
 
1109
  system_prompt = f"""
1110
+ Rol: Extractor Médico JSON.
1111
+ Tarea: Extraer JSON estricto con claves "alimentos" (lista) y "sintomas" (lista).
 
 
 
1112
  Input: "{query}"
1113
+ Output (JSON):
1114
  """
1115
  try:
1116
+ # NUEVA SINTAXIS DE LLAMADA
1117
+ response = client.models.generate_content(
1118
+ model='gemini-2.5-flash-lite',
1119
+ contents=system_prompt
1120
+ )
1121
  text = response.text
1122
  if "```json" in text: text = text.split("```json")[1].split("```")[0]
1123
  elif "```" in text: text = text.split("```")[1].split("```")[0]
 
1134
  for food_keyword in food_map.keys():
1135
  if food_keyword in query_sanitized: current_foods.add(food_keyword)
1136
  entities["alimentos"] = list(current_foods)
1137
+
1138
  current_symptoms = set(entities.get("sintomas", []))
1139
  for main_symptom, details in master_symptom_map.items():
1140
  for phrase in details.get("frases_es", []):
 
1196
  found_matches.extend(matches)
1197
  return list(set(found_matches))[:limit]
1198
 
1199
+ @retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
 
 
1200
  def generate_detailed_analysis(query, match):
1201
+ if not client: return "Error: IA no disponible."
1202
  prompt_parts = [
1203
  "Rol: Nutricionista Funcional.",
1204
  f"Caso: {query}",
1205
  f"Hipótesis: {match.get('condicion_asociada')}",
1206
  f"Mecanismo: {match.get('mecanismo_fisiologico')}",
1207
  f"Alimentos clave: {match.get('compuesto_alimento')}",
1208
+ "Tarea: Escribir análisis Markdown (Saludo, Causa, Mecanismo, Evitar, Reemplazos, Consejo).",
1209
+ "NO incluyas descargos de responsabilidad."
 
 
 
 
 
 
1210
  ]
1211
  prompt = "\n".join(prompt_parts)
1212
  try:
1213
+ # NUEVA SINTAXIS
1214
+ response = client.models.generate_content(
1215
+ model='gemini-2.5-flash-lite',
1216
+ contents=prompt
1217
+ )
1218
+ return response.text + TEXTO_BANDERAS_ROJAS
1219
  except Exception as e:
1220
  logger.error(f"Error análisis: {e}")
1221
  raise e
1222
 
1223
  def generate_neuro_report_text(entities, food_map, neuro_map):
 
1224
  report_lines = ["\n### 🧠 Efectos Neuropsicológicos Posibles"]
1225
  user_foods = entities.get("alimentos", [])
1226
  relevant_compounds = set()
1227
  if user_foods:
1228
  for food in user_foods:
1229
  if food in food_map: relevant_compounds.update(food_map[food])
 
1230
  found_neuro_effect = False
1231
  if relevant_compounds:
1232
  for compound in sorted(list(relevant_compounds)):
1233
  if compound in neuro_map:
1234
  found_neuro_effect = True
1235
+ report_lines.append(f"**{compound.capitalize()}**: _{neuro_map[compound]['efecto_neuropsicologico']}_")
1236
+ if not found_neuro_effect: report_lines.append("No se detectaron efectos específicos.")
 
 
 
 
1237
  return "\n".join(report_lines)
1238
 
1239
  def generate_molecular_report_text(best_match, entities, foodb_index, food_name_map, synonym_map, triggers_map):
1240
+ report_lines = ["\n### 🔬 Análisis Molecular"]
1241
+ user_foods = entities.get("alimentos", [])
1242
+ if not user_foods: return "No hay alimentos para analizar."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1243
 
1244
+ # Lógica de búsqueda simplificada para el ejemplo
1245
+ best_food_matches = find_best_foodb_matches(user_foods, foodb_index.keys(), food_name_map)
1246
+ if not best_food_matches: return "No se encontraron datos moleculares detallados."
1247
 
 
 
 
 
1248
  for food_key in best_food_matches:
1249
+ report_lines.append(f"\n**{food_key.capitalize()}**")
1250
+ compounds = foodb_index.get(food_key, [])[:5] # Limitamos a 5 para el ejemplo
1251
+ for item in compounds:
1252
+ report_lines.append(f"- {item['compound']}")
1253
+
 
 
 
 
 
 
 
 
 
 
 
 
 
1254
  return "\n".join(report_lines)
1255
 
1256
  def create_relevance_chart(results):
 
1260
  "Relevancia": [r['score']['total'] for r in top_results]
1261
  })
1262
  chart = alt.Chart(data).mark_bar().encode(
1263
+ x='Relevancia', y=alt.Y('Condición', sort='-x'), tooltip=['Condición', 'Relevancia']
 
 
1264
  ).properties(title='Top Coincidencias')
1265
  return chart
1266
 
1267
  def generate_word_report(report_text):
 
1268
  doc = docx.Document()
1269
  doc.add_paragraph(report_text)
1270
  doc_io = BytesIO()
 
1272
  doc_io.seek(0)
1273
  return doc_io
1274
 
1275
+ # --- INTERFAZ ---
1276
  col_img1, col_text, col_img2 = st.columns([1, 4, 1])
1277
  with col_img1:
1278
  if os.path.exists("imagen.png"): st.image("imagen.png", width=150)
 
1288
  if 'analysis_cache' not in st.session_state: st.session_state.analysis_cache = {}
1289
 
1290
  with st.form(key="search_form"):
1291
+ query = st.text_area("Tu Caso:", height=150, placeholder="Ej: Me duele la cabeza tipo migraña cada vez que como queso curado.")
1292
  submitted = st.form_submit_button("Analizar Caso", type="primary")
1293
 
1294
  if submitted and query:
 
1296
  st.session_state.search_results = None
1297
  st.session_state.analysis_cache = {}
1298
 
1299
+ with st.spinner("🔍 Analizando pistas..."):
 
1300
  try:
1301
  raw_entities = extract_entities_with_gemini(query)
1302
  except:
1303
  raw_entities = {"alimentos": [], "sintomas": []}
1304
 
1305
+ reinforced = reinforce_entities_with_keywords(raw_entities, query, {}, {}) # Poner mapas completos aquí
1306
+ final_symptoms = translate_symptoms_local(reinforced.get("sintomas", []), {}) # Poner mapas completos aquí
 
1307
  final_entities = {"alimentos": reinforced.get("alimentos", []), "sintomas": final_symptoms}
1308
  st.session_state.entities = final_entities
1309
 
 
1310
  results = find_best_matches_hybrid(final_entities, alimentos_data)
1311
  st.session_state.search_results = results
1312
 
 
1316
 
1317
  st.success(f"🔎 Coincidencia Principal: **{best_match.get('condicion_asociada')}**")
1318
 
 
1319
  cache_key = f"analysis_{best_match.get('condicion_asociada')}"
1320
  if cache_key not in st.session_state.analysis_cache:
1321
+ with st.spinner("✍️ Redactando informe..."):
1322
  try:
1323
  analysis = generate_detailed_analysis(st.session_state.user_query, best_match)
1324
  st.session_state.analysis_cache[cache_key] = analysis
1325
  except:
1326
+ st.session_state.analysis_cache[cache_key] = "Error en análisis detallado."
1327
 
 
1328
  tab_main, tab_neuro, tab_mol = st.tabs(["💡 Interpretación Clínica", "🧠 Efectos Neuropsicológicos", "🔬 Análisis Molecular"])
1329
 
1330
  with tab_main:
1331
  st.markdown(st.session_state.analysis_cache[cache_key])
 
 
1332
  st.altair_chart(create_relevance_chart(results), use_container_width=True)
1333
 
1334
  with tab_neuro:
1335
+ st.markdown(generate_neuro_report_text(st.session_state.entities, {}, {})) # Poner mapas completos
 
 
1336
 
1337
  with tab_mol:
1338
+ st.markdown(generate_molecular_report_text(best_match, st.session_state.entities, foodb_index, {}, {}, {})) # Poner mapas completos
 
 
1339
 
1340
+ full_report = f"REPORTE\n\n{st.session_state.analysis_cache[cache_key]}"
 
1341
  word_data = generate_word_report(full_report)
1342
+ st.download_button("📄 Descargar Informe", data=word_data, file_name="Reporte.docx", mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document")
1343
 
1344
  elif submitted:
 
1345
  with st.container(border=True):
1346
+ st.warning("⚠️ No pudimos identificar una causa clara.")
1347
+ st.info("Intenta reformular: 'Siento [SÍNTOMA] cuando como [ALIMENTO]'.")