JairoCesar commited on
Commit
ab79b25
·
verified ·
1 Parent(s): 5a2ebee

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +108 -258
app.py CHANGED
@@ -1,9 +1,3 @@
1
- # ==============================================================================
2
- # EL DETECTIVE DE ALIMENTOS - EDICIÓN HÍBRIDA DE ALTA DISPONIBILIDAD
3
- # Desarrollado por: Jairo Alexander & Diana Milena Soler Martinez
4
- # Motor: Gemini 2.5 Flash Lite + FooDB.ca Graph + Fast-Path Local Engine
5
- # ==============================================================================
6
-
7
  import streamlit as st
8
  from google import genai
9
  from google.genai import types
@@ -13,17 +7,14 @@ import logging
13
  import re
14
  import pandas as pd
15
  import altair as alt
16
- from datetime import datetime
17
  from io import BytesIO
18
  import docx
19
  import difflib
20
 
21
- # --- CONFIGURACIÓN DE PÁGINA Y LOGGING ---
22
  st.set_page_config(page_title="El Detective de Alimentos", page_icon="🍎", layout="wide")
23
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
24
  logger = logging.getLogger("food_detective_app")
25
 
26
- # --- TEXTO ESTÁTICO DE SEGURIDAD Y BANDERAS ROJAS ---
27
  TEXTO_BANDERAS_ROJAS = """
28
  \n### **IMPORTANTE: Descargo de Responsabilidad y Banderas Rojas**
29
  Este análisis es una herramienta informativa de IA y **NO es un diagnóstico médico.**
@@ -38,30 +29,33 @@ Este análisis es una herramienta informativa de IA y **NO es un diagnóstico m
38
  **Si tus síntomas son severos, busca ayuda médica urgente.**
39
  """
40
 
41
- # --- CONFIGURACIÓN DE CLIENTE GEMINI SIN CACHÉ PARA EVITAR DEAD SOCKETS ---
42
  def get_gemini_client():
 
43
  try:
44
  if 'APIkeydetective' in st.secrets:
45
- APIkeydetective = st.secrets['APIkeydetective']
46
- else:
47
- APIkeydetective = os.getenv("APIkeydetective")
48
-
49
- if not APIkeydetective:
50
- return None
51
-
 
 
 
 
52
  return genai.Client(
53
- api_key=APIkeydetective,
54
- http_options=types.HttpOptions(timeout=5000)
55
  )
56
  except Exception as e:
57
  logger.warning(f"No se pudo instanciar Gemini Client: {e}")
58
  return None
59
 
60
- # --- CARGA ULTRA-RÁPIDA DE DATOS Y FOODB (CERO COPIA EN MEMORIA) ---
61
  @st.cache_resource
62
  def load_data():
63
  try:
64
- path_alimentos = os.path.join('DATOS', 'alimentos_enriquecido.json')
65
  path_foodb_index = os.path.join('DATOS', 'foodb_index_smart.json')
66
  data_alimentos = []
67
  if os.path.exists(path_alimentos):
@@ -86,19 +80,13 @@ def get_foodb_lower_map(_foodb_index):
86
 
87
  foodb_lower_map = get_foodb_lower_map(foodb_index)
88
 
89
- # --- CACHÉ GLOBAL DE APRENDIZAJE EN RAM ---
90
  @st.cache_resource
91
  def get_global_learned_cache():
92
  return {}
93
 
94
  global_learned_cache = get_global_learned_cache()
95
 
96
- # ==============================================================================
97
- # DICCIONARIOS MASTER BIOMÉDICOS Y FOODB.CA
98
- # ==============================================================================
99
-
100
  FOOD_TO_COMPOUND_MAP = {
101
- # CEREALES Y GRANOS
102
  "trigo": ["gluten", "fodmaps", "lectinas", "fitatos"],
103
  "pan": ["gluten", "fodmaps", "lectinas"],
104
  "pasta": ["gluten", "fodmaps"],
@@ -112,7 +100,6 @@ FOOD_TO_COMPOUND_MAP = {
112
  "alforfón": ["oxalatos", "fagopirina"],
113
  "arroz frito": ["lectinas", "gluten", "soja", "histamina", "fodmaps", "glutamato"],
114
 
115
- # LÁCTEOS
116
  "leche": ["caseína", "lactosa", "fodmaps"],
117
  "queso": ["tiramina", "histamina", "caseína", "lactosa"],
118
  "queso fresco": ["caseína", "lactosa"],
@@ -123,7 +110,6 @@ FOOD_TO_COMPOUND_MAP = {
123
  "crema de leche": ["caseína", "lactosa"],
124
  "helado": ["lactosa", "caseína", "azúcar refinado"],
125
 
126
- # FRUTAS
127
  "chontaduro": ["fenoles", "fructosa", "grasas saturadas", "tiramina", "salicilatos"],
128
  "jugo de chontaduro": ["fenoles", "fructosa", "grasas saturadas", "tiramina", "salicilatos"],
129
  "chotanduro": ["fenoles", "fructosa", "grasas saturadas", "tiramina", "salicilatos"],
@@ -149,7 +135,6 @@ FOOD_TO_COMPOUND_MAP = {
149
  "higo": ["histamina", "fructosa", "salicilatos"],
150
  "cholado": ["fructosa", "lactosa", "histamina", "azúcar refinado", "salicilatos", "fenoles"],
151
 
152
- # VERDURAS Y TUBÉRCULOS
153
  "tomate": ["histamina", "solanina", "salicilatos", "glutamato"],
154
  "salsa de tomate": ["histamina", "solanina", "salicilatos", "glutamato"],
155
  "patata": ["solanina", "oxalatos", "lectinas"],
@@ -169,11 +154,9 @@ FOOD_TO_COMPOUND_MAP = {
169
  "calabaza": ["salicilatos", "fodmaps"],
170
  "zanahoria": ["salicilatos"],
171
 
172
- # FERMENTADOS
173
  "kimchi": ["histamina", "tiramina", "sulfitos", "fodmaps"],
174
  "kombucha": ["histamina", "alcohol", "fructosa"],
175
 
176
- # LEGUMBRES
177
  "frijol": ["lectinas", "fitatos", "fodmaps", "saponinas"],
178
  "frijoles": ["lectinas", "fitatos", "fodmaps", "saponinas"],
179
  "lentejas": ["lectinas", "fitatos", "fodmaps"],
@@ -182,7 +165,6 @@ FOOD_TO_COMPOUND_MAP = {
182
  "tofu": ["soja", "lectinas", "fitatos"],
183
  "arvejas": ["fodmaps", "lectinas"],
184
 
185
- # FRUTOS SECOS Y SEMILLAS
186
  "almendras": ["oxalatos", "salicilatos", "fitatos", "histamina"],
187
  "leche de almendras": ["oxalatos", "salicilatos"],
188
  "nueces": ["histamina", "oxalatos", "salicilatos", "taninos"],
@@ -194,7 +176,6 @@ FOOD_TO_COMPOUND_MAP = {
194
  "semillas de chía": ["fitatos", "oxalatos"],
195
  "ajonjolí": ["fitatos", "oxalatos"],
196
 
197
- # BEBIDAS Y ESTIMULANTES
198
  "café": ["cafeína", "salicilatos", "oxalatos"],
199
  "té": ["cafeína", "taninos", "oxalatos", "salicilatos"],
200
  "té negro": ["cafeína", "taninos", "oxalatos", "salicilatos"],
@@ -218,7 +199,6 @@ FOOD_TO_COMPOUND_MAP = {
218
  "carne roja": ["purinas"],
219
  "pollo": ["purinas", "histamina"],
220
 
221
- # CONDIMENTOS
222
  "pimienta": ["capsaicina", "salicilatos"],
223
  "chile": ["capsaicina", "salicilatos", "solanina"],
224
  "ají": ["capsaicina", "salicilatos", "solanina"],
@@ -439,12 +419,9 @@ MASTER_SYMPTOM_MAP = {
439
  }
440
  }
441
 
442
- # ==============================================================================
443
- # FUNCIONES DE SEGURO LÓGICO, BÚSQUEDA FLEXIBLE Y EXTRACCIÓN
444
- # ==============================================================================
445
-
446
  def sanitize_text(text):
447
- if not text: return ""
 
448
  return re.sub(r'[.,;()¿?¡!:"\'\-]', ' ', text).lower().strip()
449
 
450
  STOPWORDS_ES = {
@@ -464,37 +441,34 @@ def extract_keywords(text):
464
  words = re.findall(r'\b\w+\b', sanitize_text(text))
465
  return set(w for w in words if w not in STOPWORDS_ES and len(w) > 2)
466
 
467
- # --- BUSCADOR UNIVERSAL CON CORRECTOR DIFUSO DE ERRATAS (difflib) ---
468
  def get_compounds_for_food(food_name, food_map):
469
- if not food_name: return []
 
 
470
  food_clean = food_name.lower().strip()
471
-
472
- # 1. Coincidencia exacta
473
  if food_clean in food_map:
474
  return food_map[food_clean]
475
-
476
- # 2. Limpieza de Plurales
477
  for suffix in ["es", "s"]:
478
  if food_clean.endswith(suffix):
479
  norm = food_clean[:-len(suffix)]
480
  if norm in food_map:
481
  return food_map[norm]
482
 
483
- # 3. CORRECTOR DIFUSO DE ERRATAS (Corrige "vino tino" -> "vino tinto", "chotanduro" -> "chontaduro")
484
  fuzzy_matches = difflib.get_close_matches(food_clean, list(food_map.keys()), n=1, cutoff=0.6)
485
  if fuzzy_matches:
486
  logger.info(f"💡 Errata corregida automáticamente: '{food_clean}' -> '{fuzzy_matches[0]}'")
487
  return food_map[fuzzy_matches[0]]
488
 
489
- # 4. Búsqueda por subcadena en el diccionario
490
  matched_compounds = set()
491
  for key, compounds in food_map.items():
492
  if key in food_clean or food_clean in key:
493
  matched_compounds.update(compounds)
 
494
  if matched_compounds:
495
  return list(matched_compounds)
496
 
497
- # 5. Corrección difusa por palabras individuales dentro de frases largas
498
  inferred = set()
499
  words = food_clean.split()
500
  for word in words:
@@ -506,18 +480,23 @@ def get_compounds_for_food(food_name, food_map):
506
  if inferred:
507
  return list(inferred)
508
 
509
- # 6. INFERENCIA UNIVERSAL DE CLASES PARA ALIMENTOS NO REGISTRADOS
510
- if any(w in food_clean for w in ["vino", "cerveza", "licor", "trago", "bebida"]):
511
- return ["histamina", "tiramina", "sulfitos", "alcohol", "taninos"]
512
- elif any(w in food_clean for w in ["carne", "cerdo", "res", "pollo", "pavo", "cordero"]):
513
- return ["histamina", "purinas", "proteínas"]
514
  elif any(w in food_clean for w in ["jugo", "zumo", "batido", "fruta", "licuado"]):
515
- return ["fructosa", "salicilatos", "histamina", "fenoles"]
516
- elif any(w in food_clean for w in ["queso", "leche", "crema", "yogur"]):
517
- return ["caseína", "lactosa", "histamina", "tiramina"]
 
 
 
 
 
 
 
 
518
 
519
- return []
520
-
521
  def validar_seguro_logico(entry, user_foods, user_query, food_compound_map):
522
  if not user_foods:
523
  return True
@@ -525,7 +504,6 @@ def validar_seguro_logico(entry, user_foods, user_query, food_compound_map):
525
  user_query_clean = user_query.lower() if user_query else ""
526
  texto_condicion = (entry.get("condicion_asociada", "") + " " + entry.get("compuesto_alimento", "")).lower()
527
 
528
- # 1. EVALUAR SEGUROS POR CATEGORÍA DE CONDICIONES MÉDICAS
529
  for category, rules in MEDICAL_CONDITION_SAFEGUARDS.items():
530
  if any(trig in texto_condicion for trig in rules["condition_triggers"]):
531
  tiene_contexto = any(req_kw in user_query_clean for req_kw in rules["required_user_keywords"])
@@ -533,7 +511,6 @@ def validar_seguro_logico(entry, user_foods, user_query, food_compound_map):
533
  logger.info(f"🚩 SEGURO LÓGICO ACTIVADO [{category.upper()}]: Se descartó '{entry.get('condicion_asociada')}' por falta de síntomas/contexto clínico.")
534
  return False
535
 
536
- # 2. EVALUAR SEGUROS BIOQUÍMICOS POR COMPUESTOS ESTRICTOS
537
  user_compounds = set()
538
  for food in user_foods:
539
  comps = get_compounds_for_food(food, food_compound_map)
@@ -554,8 +531,9 @@ def validar_seguro_logico(entry, user_foods, user_query, food_compound_map):
554
 
555
  def extract_entities_with_gemini(query):
556
  gemini_client = get_gemini_client()
557
- if not gemini_client: return {"alimentos": [], "sintomas": []}
558
-
 
559
  system_prompt = f"""
560
  Rol: Extractor Médico JSON experto en nutrición y síntomas.
561
  Tarea: Analiza el texto del usuario y extrae un JSON estricto con dos claves:
@@ -571,21 +549,25 @@ def extract_entities_with_gemini(query):
571
  contents=system_prompt
572
  )
573
  text = response.text
574
- if "```json" in text: text = text.split("```json")[1].split("```")[0]
575
- elif "```" in text: text = text.split("```")[1].split("```")[0]
 
 
576
  return json.loads(text)
577
  except Exception as e:
578
  logger.warning(f"Extracción por IA no disponible ({e}). Se usará extracción directa por palabras clave.")
579
  return {"alimentos": [], "sintomas": []}
580
 
581
  def reinforce_entities_with_keywords(entities, query, food_map, master_symptom_map):
582
- if not entities: entities = {"alimentos": [], "sintomas": []}
 
583
  query_sanitized = sanitize_text(query)
584
  current_foods = set(entities.get("alimentos", []))
585
  for food_keyword in food_map.keys():
586
- if food_keyword in query_sanitized: current_foods.add(food_keyword)
 
587
  entities["alimentos"] = list(current_foods)
588
-
589
  current_symptoms = set(entities.get("sintomas", []))
590
  for main_symptom, details in master_symptom_map.items():
591
  for phrase in details.get("frases_es", []):
@@ -596,14 +578,15 @@ def reinforce_entities_with_keywords(entities, query, food_map, master_symptom_m
596
  return entities
597
 
598
  def translate_symptoms_local(symptoms_list, master_symptom_map):
599
- if not symptoms_list: return []
 
600
  translated_symptoms = set()
601
  inverse_index = {}
602
  for clinical_term, details in master_symptom_map.items():
603
- inverse_index[clinical_term.lower()] = clinical_term
604
  for phrase in details.get("frases_es", []):
605
  inverse_index[phrase.lower()] = clinical_term
606
-
607
  for symptom in symptoms_list:
608
  symptom_clean = symptom.lower().strip()
609
  if symptom_clean in inverse_index:
@@ -613,11 +596,13 @@ def translate_symptoms_local(symptoms_list, master_symptom_map):
613
  return list(translated_symptoms)
614
 
615
  def find_best_matches_hybrid(entities, data, user_query="", food_compound_map=None):
616
- if not entities or not data: return []
 
617
  user_symptoms_list = entities.get("sintomas", [])
618
  user_foods_list = entities.get("alimentos", [])
619
 
620
- if not user_foods_list: return []
 
621
 
622
  expanded_foods = list(user_foods_list)
623
  if food_compound_map:
@@ -657,130 +642,7 @@ def build_local_hypothesis(entities, food_compound_map, neuro_map):
657
  return None
658
 
659
  symptom_text = " ".join(user_symptoms).lower()
660
- headache_keywords = ["dolor de cabeza", "migraña", "cefalea", "jaqueca"]
661
- insomnia_keywords = ["insomnio", "no puedo dormir", "sueño", "desvelado"]
662
-
663
- es_dolor_cabeza = any(k in symptom_text for k in headache_keywords)
664
- es_insomnio = any(k in symptom_text for k in insomnia_keywords)
665
-
666
- candidates = []
667
- for food in user_foods:
668
- comps = get_compounds_for_food(food, food_compound_map)
669
- for compound in comps:
670
- info = neuro_map.get(compound)
671
- if not info:
672
- continue
673
- candidates.append({"alimento": food, "compuesto": compound, "efecto": info['efecto_neuropsicologico']})
674
-
675
- if not candidates:
676
- return None
677
-
678
- compuestos_unicos = sorted(set(c['compuesto'] for c in candidates))
679
- alimentos_unicos = sorted(set(c['alimento'] for c in candidates))
680
- compuestos_str = ", ".join(compuestos_unicos)
681
- alimentos_str = ", ".join(alimentos_unicos)
682
-
683
- if es_dolor_cabeza:
684
- condicion = f"Cefalea o migraña por aminas vasoactivas / compuestos ({compuestos_str}) en {alimentos_str}"
685
- elif es_insomnio:
686
- condicion = f"Alteración del sueño / insomnio por respuesta a {compuestos_str} presente en {alimentos_str}"
687
- else:
688
- condicion = f"Sensibilidad o intolerancia a {compuestos_str} presente en {alimentos_str}"
689
-
690
- mecanismo = " | ".join(sorted(set(c['efecto'] for c in candidates)))
691
-
692
- return {
693
- "condicion_asociada": condicion,
694
- "mecanismo_fisiologico": mecanismo,
695
- "compuesto_alimento": f"{alimentos_str} ({compuestos_str})",
696
- "sintomas_clave": user_symptoms,
697
- "es_hipotesis_local": True
698
- }
699
-
700
-
701
- def get_compounds_for_food(food_name, food_map):
702
- """
703
- Buscador e Inferidor Universal de Compuestos (Definitivo):
704
- 1. Coincidencia exacta.
705
- 2. Normalización de plurales.
706
- 3. Corrector difuso de errores de tipeo (difflib).
707
- 4. Búsqueda por subcadena en el diccionario.
708
- 5. Inferencia universal por familia biomédica para alimentos no registrados.
709
- """
710
- if not food_name:
711
- return []
712
-
713
- food_clean = food_name.lower().strip()
714
-
715
- # 1. Coincidencia exacta
716
- if food_clean in food_map:
717
- return food_map[food_clean]
718
-
719
- # 2. Normalización de Plurales
720
- for suffix in ["es", "s"]:
721
- if food_clean.endswith(suffix):
722
- norm = food_clean[:-len(suffix)]
723
- if norm in food_map:
724
- return food_map[norm]
725
-
726
- # 3. Corrección difusa de erratas de tipeo ("vino tino" -> "vino tinto", "chotanduro" -> "chontaduro")
727
- fuzzy_matches = difflib.get_close_matches(food_clean, list(food_map.keys()), n=1, cutoff=0.6)
728
- if fuzzy_matches:
729
- return food_map[fuzzy_matches[0]]
730
-
731
- # 4. Búsqueda por subcadena dentro del diccionario
732
- matched_compounds = set()
733
- for key, compounds in food_map.items():
734
- if key in food_clean or food_clean in key:
735
- matched_compounds.update(compounds)
736
-
737
- if matched_compounds:
738
- return list(matched_compounds)
739
-
740
- # 5. Inferencia difusa por palabras individuales en nombres compuestos
741
- inferred = set()
742
- words = food_clean.split()
743
- for word in words:
744
- if len(word) > 3:
745
- sub_fuzzy = difflib.get_close_matches(word, list(food_map.keys()), n=1, cutoff=0.7)
746
- if sub_fuzzy:
747
- inferred.update(food_map[sub_fuzzy[0]])
748
-
749
- if inferred:
750
- return list(inferred)
751
-
752
- # 6. INFERENCIA UNIVERSAL POR FAMILIA BIOLÓGICA (Alimentos raros o no registrados)
753
- if any(w in food_clean for w in ["carne", "cerdo", "res", "pollo", "pavo", "cordero", "pato", "chicharrón", "tocino"]):
754
- inferred.update(["histamina", "purinas", "proteínas"])
755
- elif any(w in food_clean for w in ["pescado", "atún", "marisco", "camarón", "sardina", "langostino", "pulpo"]):
756
- inferred.update(["histamina", "purinas", "albúmina"])
757
- elif any(w in food_clean for w in ["jugo", "zumo", "batido", "fruta", "licuado"]):
758
- inferred.update(["fructosa", "salicilatos", "histamina", "fenoles"])
759
- elif any(w in food_clean for w in ["queso", "leche", "crema", "yogur", "mantequilla"]):
760
- inferred.update(["caseína", "lactosa", "histamina", "tiramina"])
761
- elif any(w in food_clean for w in ["pan", "galleta", "pasta", "harina", "bizcocho", "tortilla"]):
762
- inferred.update(["gluten", "fodmaps"])
763
- elif any(w in food_clean for w in ["frijol", "lenteja", "garbanzo", "legumbre", "soja"]):
764
- inferred.update(["lectinas", "fitatos", "fodmaps", "saponinas"])
765
- elif any(w in food_clean for w in ["salsa", "sazonador", "aderezo", "caldo", "adobo"]):
766
- inferred.update(["glutamato", "histamina", "sulfitos"])
767
-
768
- return list(inferred)
769
-
770
- def build_local_hypothesis(entities, food_compound_map, neuro_map):
771
- """
772
- Generador Local Definitivo de Hipótesis:
773
- Construye respuestas clínicas estructuradas clasificando los síntomas en 4 grandes
774
- sistemas orgánicos (Neurología, Dermatología, Insomnio, Gastroenterología).
775
- """
776
- user_foods = entities.get("alimentos", [])
777
- user_symptoms = entities.get("sintomas", [])
778
- if not user_foods or not user_symptoms:
779
- return None
780
 
781
- symptom_text = " ".join(user_symptoms).lower()
782
-
783
- # Categorización clínica por sistemas de síntomas
784
  es_dermatologico = any(k in symptom_text for k in ["piel", "rasca", "picazón", "picazon", "manchas", "urticaria", "erupción", "erupcion", "alergia", "ronchas", "granitos"])
785
  es_cefalea = any(k in symptom_text for k in ["dolor de cabeza", "migraña", "cefalea", "jaqueca", "dolor", "cabeza"])
786
  es_insomnio = any(k in symptom_text for k in ["insomnio", "no puedo dormir", "sueño", "desvelado", "desvelo", "despierto"])
@@ -794,11 +656,10 @@ def build_local_hypothesis(entities, food_compound_map, neuro_map):
794
  effect = info.get('efecto_neuropsicologico', f'Sustancia bioactiva ({compound}) con impacto inmunológico o metabolico en {food}')
795
  candidates.append({"alimento": food, "compuesto": compound, "efecto": effect})
796
 
797
- # Si no hay candidatos bioquímicos específicos, genera la hipótesis con la familia inferida
798
  if not candidates:
799
  alimentos_str = ", ".join(user_foods).capitalize()
800
  sintomas_str = ", ".join(user_symptoms)
801
-
802
  if es_cefalea:
803
  condicion_fallback = f"Cefalea o Migraña Alimentaria relacionada con {alimentos_str}"
804
  elif es_insomnio:
@@ -823,7 +684,6 @@ def build_local_hypothesis(entities, food_compound_map, neuro_map):
823
  compuestos_str = ", ".join(compuestos_unicos)
824
  alimentos_str = ", ".join(alimentos_unicos).capitalize()
825
 
826
- # Construcción sintáctica por sistema afectado
827
  if es_cefalea:
828
  condicion = f"Cefalea o Migraña Vasoactiva asociada a {compuestos_str} en {alimentos_str}"
829
  elif es_dermatologico:
@@ -846,19 +706,16 @@ def build_local_hypothesis(entities, food_compound_map, neuro_map):
846
  }
847
 
848
  def generate_ai_hypothesis_on_the_fly(query, entities):
849
- """
850
- Agente Rescatador Universal por IA:
851
- Sintetiza una hipótesis clínica para CUALQUIER caso, alimento o síntoma no registrado localmente.
852
- """
853
  gemini_client = get_gemini_client()
854
- if not gemini_client: return None
855
-
 
856
  foods = entities.get('alimentos', [])
857
  symptoms = entities.get('sintomas', [])
858
-
859
  foods_text = ", ".join(foods) if foods else "alimentos mencionados en la consulta"
860
  symptoms_text = ", ".join(symptoms) if symptoms else "síntomas expresados por el paciente"
861
-
862
  prompt = (
863
  "Rol: Médico Especialista en Nutrición Funcional e Inmunología de Alimentos.\n"
864
  f'Consulta del paciente: "{query}"\n'
@@ -877,17 +734,20 @@ def generate_ai_hypothesis_on_the_fly(query, entities):
877
  contents=prompt
878
  )
879
  raw = response.text.strip()
880
- if "```json" in raw: raw = raw.split("```json")[1].split("```")[0]
881
- elif "```" in raw: raw = raw.split("```")[1].split("```")[0]
 
 
882
  data = json.loads(raw.strip())
883
  data["es_hipotesis_local"] = True
884
  return data
885
  except Exception as e:
886
  logger.error(f"Error en Agente Rescatador Universal de IA: {e}")
887
  return None
888
-
889
  def find_best_foodb_matches(user_foods_es, _foodb_index, _foodb_lower_map, food_name_map, limit=3):
890
- if not user_foods_es or not _foodb_index: return []
 
891
 
892
  search_terms_en = set()
893
  for food_es in user_foods_es:
@@ -896,7 +756,8 @@ def find_best_foodb_matches(user_foods_es, _foodb_index, _foodb_lower_map, food_
896
  if key_es in food_es_clean or food_es_clean in key_es:
897
  search_terms_en.update(value_en_list)
898
 
899
- if not search_terms_en: return []
 
900
 
901
  found_matches = []
902
  for term in search_terms_en:
@@ -907,7 +768,8 @@ def find_best_foodb_matches(user_foods_es, _foodb_index, _foodb_lower_map, food_
907
  for k_lower, orig_key in _foodb_lower_map.items():
908
  if term_lower in k_lower:
909
  found_matches.append(orig_key)
910
- if len(found_matches) >= limit: break
 
911
 
912
  seen = set()
913
  unique = []
@@ -918,41 +780,37 @@ def find_best_foodb_matches(user_foods_es, _foodb_index, _foodb_lower_map, food_
918
 
919
  return unique[:limit]
920
 
921
- # ==============================================================================
922
- # GENERADORES DE INFORMES (LOCAL, IA Y MOLECULAR CON FOODB)
923
- # ==============================================================================
924
-
925
  def generate_local_fallback_analysis(query, match, entities, food_compound_map, neuro_map):
926
  saludo = "### 📋 Informe de Análisis Clínico-Nutricional (Modo Local / Alta Disponibilidad)\n"
927
  hipotesis = f"**Hipótesis Probable:** Es muy probable que tus síntomas estén relacionados con **{match.get('condicion_asociada', 'una sensibilidad a compuestos alimentarios')}**.\n"
928
-
929
  user_foods = entities.get("alimentos", [])
930
  user_symptoms = entities.get("sintomas", [])
931
-
932
  compuestos_detectados = set()
933
  for food in user_foods:
934
  comps = get_compounds_for_food(food, food_compound_map)
935
  if comps:
936
  compuestos_detectados.update(comps)
937
-
938
  compuestos_str = ", ".join(sorted(list(compuestos_detectados))) if compuestos_detectados else match.get('compuesto_alimento', 'compuestos bioactivos')
939
-
940
  causa = f"\n### 🧬 Causa e Identificación Molecular\nLos síntomas descritos (**{', '.join(user_symptoms)}**) tras el consumo de **{', '.join(user_foods)}** están asociados a la presencia de sustancias biológicamente activas como **{compuestos_str}**.\n"
941
-
942
  detalles_compuestos = []
943
  for comp in compuestos_detectados:
944
  if comp in neuro_map:
945
  detalles_compuestos.append(f"- **{comp.capitalize()}**: {neuro_map[comp]['efecto_neuropsicologico']}")
946
-
947
  if detalles_compuestos:
948
  causa += "\n**Mecanismo bioquímico e impacto neurológico:**\n" + "\n".join(detalles_compuestos) + "\n"
949
  elif match.get('mecanismo_fisiologico'):
950
  causa += f"\n**Mecanismo fisiológico:** {match.get('mecanismo_fisiologico')}\n"
951
 
952
  evitar = f"\n### 🛑 Alimentos a Moderar o Evitar\n- **Alimentos evaluados:** {', '.join(user_foods).capitalize()}\n- Alimentos ricos en **{compuestos_str}** o productos derivados que compartan los mismos componentes desencadenantes.\n"
953
-
954
  alternativas = "\n### 💡 Sugerencias y Alternativas\n- Llevar un diario de síntomas registrando alimentos, hora de consumo y aparición de molestias.\n- Reducir temporalmente la ingesta del alimento sospechoso para evaluar la remisión del síntoma.\n- Consultar con un nutricionista funcional o médico especialista si las molestias persisten.\n"
955
-
956
  consejo_texto = None
957
  for comp in compuestos_detectados:
958
  comp_clean = comp.lower().strip()
@@ -967,7 +825,7 @@ def generate_local_fallback_analysis(query, match, entities, food_compound_map,
967
  consejo_texto = CONSEJO_GENERICO_DEFAULT
968
 
969
  consejo = f"\n### 📌 Consejo Práctico Final\n{consejo_texto}\n"
970
-
971
  return saludo + hipotesis + causa + evitar + alternativas + consejo + TEXTO_BANDERAS_ROJAS
972
 
973
  def generate_detailed_analysis(query, match, entities=None, food_compound_map=None, neuro_map=None):
@@ -977,7 +835,7 @@ def generate_detailed_analysis(query, match, entities=None, food_compound_map=No
977
  return global_learned_cache[cache_lookup_key]
978
 
979
  gemini_client = get_gemini_client()
980
-
981
  if not gemini_client:
982
  local_report = generate_local_fallback_analysis(query, match, entities, food_compound_map, neuro_map)
983
  global_learned_cache[cache_lookup_key] = local_report
@@ -1030,25 +888,25 @@ def generate_neuro_report_text(entities, food_map, neuro_map):
1030
  report_lines = ["\n### 🧠 Efectos Neuropsicológicos Posibles"]
1031
  user_foods = entities.get("alimentos", [])
1032
  relevant_compounds = set()
1033
-
1034
  if user_foods:
1035
  for food in user_foods:
1036
  comps = get_compounds_for_food(food, food_map)
1037
  relevant_compounds.update(comps)
1038
-
1039
  found_neuro_effect = False
1040
  if relevant_compounds:
1041
  for compound in sorted(list(relevant_compounds)):
1042
  if compound in neuro_map:
1043
  found_neuro_effect = True
1044
  report_lines.append(f"**{compound.capitalize()}**: _{neuro_map[compound]['efecto_neuropsicologico']}_")
1045
-
1046
- if not found_neuro_effect:
1047
  report_lines.append("No se detectaron efectos neuropsicológicos específicos directos en nuestra base de datos para los alimentos ingresados.")
1048
-
1049
  return "\n".join(report_lines)
1050
 
1051
- def generate_molecular_report_text(best_match, entities, _foodb_index, _foodb_lower_map, food_name_map, synonym_map, food_compound_map, neuro_map):
1052
  report_lines = ["\n### 🔬 Análisis Molecular y Efectos Bioactivos"]
1053
  user_foods = entities.get("alimentos", [])
1054
  if not user_foods:
@@ -1071,21 +929,22 @@ def generate_molecular_report_text(best_match, entities, _foodb_index, _foodb_lo
1071
  best_food_matches = find_best_foodb_matches(user_foods, _foodb_index, _foodb_lower_map, food_name_map)
1072
  for food_key in best_food_matches:
1073
  compounds = _foodb_index.get(food_key, [])
1074
- if not compounds: continue
 
1075
  found_any = True
1076
  report_lines.append(f"\n**{food_key.capitalize()}** (Compuestos y efectos fisiológicos en FooDB):")
1077
-
1078
  for item in compounds[:5]:
1079
  if isinstance(item, dict):
1080
  comp_name = item.get('compound', 'N/A')
1081
  effects = item.get('effects', [])
1082
-
1083
  eff_translated = []
1084
  for eff in effects[:4]:
1085
  eff_clean = str(eff).lower().strip()
1086
  eff_es = FOODB_EFFECTS_TRANSLATION.get(eff_clean, eff_clean)
1087
  eff_translated.append(eff_es)
1088
-
1089
  eff_str = f" — *Efectos registrados: {', '.join(eff_translated)}*" if eff_translated else ""
1090
  report_lines.append(f"- **{comp_name}**{eff_str}")
1091
  else:
@@ -1119,7 +978,7 @@ def generate_word_report(report_text):
1119
 
1120
  doc_io = BytesIO()
1121
  doc.save(doc_io)
1122
- doc_io.seek(0)
1123
  return doc_io.getvalue()
1124
  except Exception as e:
1125
  logger.error(f"Error al generar informe Word: {e}")
@@ -1128,17 +987,13 @@ def generate_word_report(report_text):
1128
  def create_relevance_chart(results):
1129
  top_results = results[:10]
1130
  data = pd.DataFrame({
1131
- "Condición": [re.sub(r'\(.*\)', '', r['entry']['condicion_asociada']).strip() for r in top_results],
1132
  "Relevancia": [r['score']['total'] for r in top_results]
1133
  })
1134
  return alt.Chart(data).mark_bar().encode(
1135
  x='Relevancia', y=alt.Y('Condición', sort='-x'), tooltip=['Condición', 'Relevancia']
1136
  ).properties(title='Top Coincidencias')
1137
 
1138
- # ==============================================================================
1139
- # INICIALIZACIÓN DE ESTADO PERSISTENTE Y UI STREAMLIT
1140
- # ==============================================================================
1141
-
1142
  if 'search_results' not in st.session_state: st.session_state.search_results = None
1143
  if 'user_query' not in st.session_state: st.session_state.user_query = ""
1144
  if 'entities' not in st.session_state: st.session_state.entities = None
@@ -1157,8 +1012,8 @@ with col_img2:
1157
 
1158
  with st.form(key="search_form", clear_on_submit=False):
1159
  query_input = st.text_area(
1160
- "Tu Caso:",
1161
- height=150,
1162
  placeholder="Ej: Si tomo mucho jugo de chontaduro no puedo dormir.",
1163
  key="input_query_text"
1164
  )
@@ -1172,25 +1027,21 @@ if submitted:
1172
  st.session_state.analysis_cache = {}
1173
  st.session_state.search_results = None
1174
  st.session_state.entities = None
1175
-
1176
  with st.spinner("🔍 Analizando pistas con Gemini..."):
1177
- # PASO 1: Extracción con Gemini
1178
  raw_entities = extract_entities_with_gemini(current_query)
1179
  reinforced = reinforce_entities_with_keywords(raw_entities, current_query, FOOD_TO_COMPOUND_MAP, MASTER_SYMPTOM_MAP)
1180
  final_symptoms = translate_symptoms_local(reinforced.get("sintomas", []), MASTER_SYMPTOM_MAP)
1181
  final_entities = {"alimentos": reinforced.get("alimentos", []), "sintomas": final_symptoms}
1182
  st.session_state.entities = final_entities
1183
-
1184
- # PASO 2A: Búsqueda Local en JSON
1185
  results = find_best_matches_hybrid(final_entities, alimentos_data, current_query, FOOD_TO_COMPOUND_MAP)
1186
 
1187
- # PASO 2B: Hipótesis por Diccionarios Locales
1188
  if not results:
1189
  local_hypothesis = build_local_hypothesis(final_entities, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP)
1190
  if local_hypothesis:
1191
  results = [{'entry': local_hypothesis, 'score': {'total': 50}, 'matched_keywords': []}]
1192
 
1193
- # PASO 2C: PUENTE DE IA (Si el alimento no está en la BD, Gemini crea la hipótesis)
1194
  if not results:
1195
  ai_hypothesis = generate_ai_hypothesis_on_the_fly(current_query, final_entities)
1196
  if ai_hypothesis:
@@ -1200,16 +1051,15 @@ if submitted:
1200
  else:
1201
  st.warning("⚠️ Por favor describe tus síntomas o alimentos consumidos antes de analizar.")
1202
 
1203
- # --- DISPLAY DE RESULTADOS OPTIMIZADO ---
1204
  if st.session_state.has_searched:
1205
  results = st.session_state.search_results
1206
-
1207
  if results and len(results) > 0:
1208
  best_match = results[0]['entry']
1209
  st.success(f"🔎 Coincidencia Principal: **{best_match.get('condicion_asociada')}**")
1210
-
1211
  cache_key = f"analysis_{hash(st.session_state.user_query)}_{best_match.get('condicion_asociada')}"
1212
-
1213
  if cache_key not in st.session_state.analysis_cache:
1214
  with st.spinner("✍️ Redactando informe..."):
1215
  analysis = generate_detailed_analysis(
@@ -1217,12 +1067,12 @@ if st.session_state.has_searched:
1217
  st.session_state.entities, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP
1218
  )
1219
  st.session_state.analysis_cache[cache_key] = analysis
1220
-
1221
  full_report = f"REPORTE\n\n{analysis}"
1222
  st.session_state.word_bytes_cache[cache_key] = generate_word_report(full_report)
1223
 
1224
  tab_main, tab_neuro, tab_mol = st.tabs(["💡 Interpretación Clínica", "🧠 Efectos Neuropsicológicos", "🔬 Análisis Molecular"])
1225
-
1226
  with tab_main:
1227
  st.markdown(st.session_state.analysis_cache[cache_key])
1228
  if len(results) > 1:
@@ -1234,7 +1084,7 @@ if st.session_state.has_searched:
1234
  with tab_mol:
1235
  st.markdown(generate_molecular_report_text(
1236
  best_match, st.session_state.entities, foodb_index, foodb_lower_map,
1237
- FOOD_NAME_TO_FOODB_KEY, COMPOUND_SYNONYM_MAP, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP
1238
  ))
1239
 
1240
  word_bytes = st.session_state.word_bytes_cache.get(cache_key)
 
 
 
 
 
 
 
1
  import streamlit as st
2
  from google import genai
3
  from google.genai import types
 
7
  import re
8
  import pandas as pd
9
  import altair as alt
 
10
  from io import BytesIO
11
  import docx
12
  import difflib
13
 
 
14
  st.set_page_config(page_title="El Detective de Alimentos", page_icon="🍎", layout="wide")
15
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
16
  logger = logging.getLogger("food_detective_app")
17
 
 
18
  TEXTO_BANDERAS_ROJAS = """
19
  \n### **IMPORTANTE: Descargo de Responsabilidad y Banderas Rojas**
20
  Este análisis es una herramienta informativa de IA y **NO es un diagnóstico médico.**
 
29
  **Si tus síntomas son severos, busca ayuda médica urgente.**
30
  """
31
 
 
32
  def get_gemini_client():
33
+ api_key = None
34
  try:
35
  if 'APIkeydetective' in st.secrets:
36
+ api_key = st.secrets['APIkeydetective']
37
+ except Exception:
38
+ api_key = None
39
+
40
+ if not api_key:
41
+ api_key = os.getenv("APIkeydetective")
42
+
43
+ if not api_key:
44
+ return None
45
+
46
+ try:
47
  return genai.Client(
48
+ api_key=api_key,
49
+ http_options=types.HttpOptions(timeout=30000)
50
  )
51
  except Exception as e:
52
  logger.warning(f"No se pudo instanciar Gemini Client: {e}")
53
  return None
54
 
 
55
  @st.cache_resource
56
  def load_data():
57
  try:
58
+ path_alimentos = os.path.join('DATOS', 'alimentos_enriquecido.json')
59
  path_foodb_index = os.path.join('DATOS', 'foodb_index_smart.json')
60
  data_alimentos = []
61
  if os.path.exists(path_alimentos):
 
80
 
81
  foodb_lower_map = get_foodb_lower_map(foodb_index)
82
 
 
83
  @st.cache_resource
84
  def get_global_learned_cache():
85
  return {}
86
 
87
  global_learned_cache = get_global_learned_cache()
88
 
 
 
 
 
89
  FOOD_TO_COMPOUND_MAP = {
 
90
  "trigo": ["gluten", "fodmaps", "lectinas", "fitatos"],
91
  "pan": ["gluten", "fodmaps", "lectinas"],
92
  "pasta": ["gluten", "fodmaps"],
 
100
  "alforfón": ["oxalatos", "fagopirina"],
101
  "arroz frito": ["lectinas", "gluten", "soja", "histamina", "fodmaps", "glutamato"],
102
 
 
103
  "leche": ["caseína", "lactosa", "fodmaps"],
104
  "queso": ["tiramina", "histamina", "caseína", "lactosa"],
105
  "queso fresco": ["caseína", "lactosa"],
 
110
  "crema de leche": ["caseína", "lactosa"],
111
  "helado": ["lactosa", "caseína", "azúcar refinado"],
112
 
 
113
  "chontaduro": ["fenoles", "fructosa", "grasas saturadas", "tiramina", "salicilatos"],
114
  "jugo de chontaduro": ["fenoles", "fructosa", "grasas saturadas", "tiramina", "salicilatos"],
115
  "chotanduro": ["fenoles", "fructosa", "grasas saturadas", "tiramina", "salicilatos"],
 
135
  "higo": ["histamina", "fructosa", "salicilatos"],
136
  "cholado": ["fructosa", "lactosa", "histamina", "azúcar refinado", "salicilatos", "fenoles"],
137
 
 
138
  "tomate": ["histamina", "solanina", "salicilatos", "glutamato"],
139
  "salsa de tomate": ["histamina", "solanina", "salicilatos", "glutamato"],
140
  "patata": ["solanina", "oxalatos", "lectinas"],
 
154
  "calabaza": ["salicilatos", "fodmaps"],
155
  "zanahoria": ["salicilatos"],
156
 
 
157
  "kimchi": ["histamina", "tiramina", "sulfitos", "fodmaps"],
158
  "kombucha": ["histamina", "alcohol", "fructosa"],
159
 
 
160
  "frijol": ["lectinas", "fitatos", "fodmaps", "saponinas"],
161
  "frijoles": ["lectinas", "fitatos", "fodmaps", "saponinas"],
162
  "lentejas": ["lectinas", "fitatos", "fodmaps"],
 
165
  "tofu": ["soja", "lectinas", "fitatos"],
166
  "arvejas": ["fodmaps", "lectinas"],
167
 
 
168
  "almendras": ["oxalatos", "salicilatos", "fitatos", "histamina"],
169
  "leche de almendras": ["oxalatos", "salicilatos"],
170
  "nueces": ["histamina", "oxalatos", "salicilatos", "taninos"],
 
176
  "semillas de chía": ["fitatos", "oxalatos"],
177
  "ajonjolí": ["fitatos", "oxalatos"],
178
 
 
179
  "café": ["cafeína", "salicilatos", "oxalatos"],
180
  "té": ["cafeína", "taninos", "oxalatos", "salicilatos"],
181
  "té negro": ["cafeína", "taninos", "oxalatos", "salicilatos"],
 
199
  "carne roja": ["purinas"],
200
  "pollo": ["purinas", "histamina"],
201
 
 
202
  "pimienta": ["capsaicina", "salicilatos"],
203
  "chile": ["capsaicina", "salicilatos", "solanina"],
204
  "ají": ["capsaicina", "salicilatos", "solanina"],
 
419
  }
420
  }
421
 
 
 
 
 
422
  def sanitize_text(text):
423
+ if not text:
424
+ return ""
425
  return re.sub(r'[.,;()¿?¡!:"\'\-]', ' ', text).lower().strip()
426
 
427
  STOPWORDS_ES = {
 
441
  words = re.findall(r'\b\w+\b', sanitize_text(text))
442
  return set(w for w in words if w not in STOPWORDS_ES and len(w) > 2)
443
 
 
444
  def get_compounds_for_food(food_name, food_map):
445
+ if not food_name:
446
+ return []
447
+
448
  food_clean = food_name.lower().strip()
449
+
 
450
  if food_clean in food_map:
451
  return food_map[food_clean]
452
+
 
453
  for suffix in ["es", "s"]:
454
  if food_clean.endswith(suffix):
455
  norm = food_clean[:-len(suffix)]
456
  if norm in food_map:
457
  return food_map[norm]
458
 
 
459
  fuzzy_matches = difflib.get_close_matches(food_clean, list(food_map.keys()), n=1, cutoff=0.6)
460
  if fuzzy_matches:
461
  logger.info(f"💡 Errata corregida automáticamente: '{food_clean}' -> '{fuzzy_matches[0]}'")
462
  return food_map[fuzzy_matches[0]]
463
 
 
464
  matched_compounds = set()
465
  for key, compounds in food_map.items():
466
  if key in food_clean or food_clean in key:
467
  matched_compounds.update(compounds)
468
+
469
  if matched_compounds:
470
  return list(matched_compounds)
471
 
 
472
  inferred = set()
473
  words = food_clean.split()
474
  for word in words:
 
480
  if inferred:
481
  return list(inferred)
482
 
483
+ if any(w in food_clean for w in ["carne", "cerdo", "res", "pollo", "pavo", "cordero", "pato", "chicharrón", "tocino"]):
484
+ inferred.update(["histamina", "purinas", "proteínas"])
485
+ elif any(w in food_clean for w in ["pescado", "atún", "marisco", "camarón", "sardina", "langostino", "pulpo"]):
486
+ inferred.update(["histamina", "purinas", "albúmina"])
 
487
  elif any(w in food_clean for w in ["jugo", "zumo", "batido", "fruta", "licuado"]):
488
+ inferred.update(["fructosa", "salicilatos", "histamina", "fenoles"])
489
+ elif any(w in food_clean for w in ["queso", "leche", "crema", "yogur", "mantequilla"]):
490
+ inferred.update(["caseína", "lactosa", "histamina", "tiramina"])
491
+ elif any(w in food_clean for w in ["pan", "galleta", "pasta", "harina", "bizcocho", "tortilla"]):
492
+ inferred.update(["gluten", "fodmaps"])
493
+ elif any(w in food_clean for w in ["frijol", "lenteja", "garbanzo", "legumbre", "soja"]):
494
+ inferred.update(["lectinas", "fitatos", "fodmaps", "saponinas"])
495
+ elif any(w in food_clean for w in ["salsa", "sazonador", "aderezo", "caldo", "adobo"]):
496
+ inferred.update(["glutamato", "histamina", "sulfitos"])
497
+
498
+ return list(inferred)
499
 
 
 
500
  def validar_seguro_logico(entry, user_foods, user_query, food_compound_map):
501
  if not user_foods:
502
  return True
 
504
  user_query_clean = user_query.lower() if user_query else ""
505
  texto_condicion = (entry.get("condicion_asociada", "") + " " + entry.get("compuesto_alimento", "")).lower()
506
 
 
507
  for category, rules in MEDICAL_CONDITION_SAFEGUARDS.items():
508
  if any(trig in texto_condicion for trig in rules["condition_triggers"]):
509
  tiene_contexto = any(req_kw in user_query_clean for req_kw in rules["required_user_keywords"])
 
511
  logger.info(f"🚩 SEGURO LÓGICO ACTIVADO [{category.upper()}]: Se descartó '{entry.get('condicion_asociada')}' por falta de síntomas/contexto clínico.")
512
  return False
513
 
 
514
  user_compounds = set()
515
  for food in user_foods:
516
  comps = get_compounds_for_food(food, food_compound_map)
 
531
 
532
  def extract_entities_with_gemini(query):
533
  gemini_client = get_gemini_client()
534
+ if not gemini_client:
535
+ return {"alimentos": [], "sintomas": []}
536
+
537
  system_prompt = f"""
538
  Rol: Extractor Médico JSON experto en nutrición y síntomas.
539
  Tarea: Analiza el texto del usuario y extrae un JSON estricto con dos claves:
 
549
  contents=system_prompt
550
  )
551
  text = response.text
552
+ if "```json" in text:
553
+ text = text.split("```json")[1].split("```")[0]
554
+ elif "```" in text:
555
+ text = text.split("```")[1].split("```")[0]
556
  return json.loads(text)
557
  except Exception as e:
558
  logger.warning(f"Extracción por IA no disponible ({e}). Se usará extracción directa por palabras clave.")
559
  return {"alimentos": [], "sintomas": []}
560
 
561
  def reinforce_entities_with_keywords(entities, query, food_map, master_symptom_map):
562
+ if not entities:
563
+ entities = {"alimentos": [], "sintomas": []}
564
  query_sanitized = sanitize_text(query)
565
  current_foods = set(entities.get("alimentos", []))
566
  for food_keyword in food_map.keys():
567
+ if food_keyword in query_sanitized:
568
+ current_foods.add(food_keyword)
569
  entities["alimentos"] = list(current_foods)
570
+
571
  current_symptoms = set(entities.get("sintomas", []))
572
  for main_symptom, details in master_symptom_map.items():
573
  for phrase in details.get("frases_es", []):
 
578
  return entities
579
 
580
  def translate_symptoms_local(symptoms_list, master_symptom_map):
581
+ if not symptoms_list:
582
+ return []
583
  translated_symptoms = set()
584
  inverse_index = {}
585
  for clinical_term, details in master_symptom_map.items():
586
+ inverse_index[clinical_term.lower()] = clinical_term
587
  for phrase in details.get("frases_es", []):
588
  inverse_index[phrase.lower()] = clinical_term
589
+
590
  for symptom in symptoms_list:
591
  symptom_clean = symptom.lower().strip()
592
  if symptom_clean in inverse_index:
 
596
  return list(translated_symptoms)
597
 
598
  def find_best_matches_hybrid(entities, data, user_query="", food_compound_map=None):
599
+ if not entities or not data:
600
+ return []
601
  user_symptoms_list = entities.get("sintomas", [])
602
  user_foods_list = entities.get("alimentos", [])
603
 
604
+ if not user_foods_list:
605
+ return []
606
 
607
  expanded_foods = list(user_foods_list)
608
  if food_compound_map:
 
642
  return None
643
 
644
  symptom_text = " ".join(user_symptoms).lower()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
645
 
 
 
 
646
  es_dermatologico = any(k in symptom_text for k in ["piel", "rasca", "picazón", "picazon", "manchas", "urticaria", "erupción", "erupcion", "alergia", "ronchas", "granitos"])
647
  es_cefalea = any(k in symptom_text for k in ["dolor de cabeza", "migraña", "cefalea", "jaqueca", "dolor", "cabeza"])
648
  es_insomnio = any(k in symptom_text for k in ["insomnio", "no puedo dormir", "sueño", "desvelado", "desvelo", "despierto"])
 
656
  effect = info.get('efecto_neuropsicologico', f'Sustancia bioactiva ({compound}) con impacto inmunológico o metabolico en {food}')
657
  candidates.append({"alimento": food, "compuesto": compound, "efecto": effect})
658
 
 
659
  if not candidates:
660
  alimentos_str = ", ".join(user_foods).capitalize()
661
  sintomas_str = ", ".join(user_symptoms)
662
+
663
  if es_cefalea:
664
  condicion_fallback = f"Cefalea o Migraña Alimentaria relacionada con {alimentos_str}"
665
  elif es_insomnio:
 
684
  compuestos_str = ", ".join(compuestos_unicos)
685
  alimentos_str = ", ".join(alimentos_unicos).capitalize()
686
 
 
687
  if es_cefalea:
688
  condicion = f"Cefalea o Migraña Vasoactiva asociada a {compuestos_str} en {alimentos_str}"
689
  elif es_dermatologico:
 
706
  }
707
 
708
  def generate_ai_hypothesis_on_the_fly(query, entities):
 
 
 
 
709
  gemini_client = get_gemini_client()
710
+ if not gemini_client:
711
+ return None
712
+
713
  foods = entities.get('alimentos', [])
714
  symptoms = entities.get('sintomas', [])
715
+
716
  foods_text = ", ".join(foods) if foods else "alimentos mencionados en la consulta"
717
  symptoms_text = ", ".join(symptoms) if symptoms else "síntomas expresados por el paciente"
718
+
719
  prompt = (
720
  "Rol: Médico Especialista en Nutrición Funcional e Inmunología de Alimentos.\n"
721
  f'Consulta del paciente: "{query}"\n'
 
734
  contents=prompt
735
  )
736
  raw = response.text.strip()
737
+ if "```json" in raw:
738
+ raw = raw.split("```json")[1].split("```")[0]
739
+ elif "```" in raw:
740
+ raw = raw.split("```")[1].split("```")[0]
741
  data = json.loads(raw.strip())
742
  data["es_hipotesis_local"] = True
743
  return data
744
  except Exception as e:
745
  logger.error(f"Error en Agente Rescatador Universal de IA: {e}")
746
  return None
747
+
748
  def find_best_foodb_matches(user_foods_es, _foodb_index, _foodb_lower_map, food_name_map, limit=3):
749
+ if not user_foods_es or not _foodb_index:
750
+ return []
751
 
752
  search_terms_en = set()
753
  for food_es in user_foods_es:
 
756
  if key_es in food_es_clean or food_es_clean in key_es:
757
  search_terms_en.update(value_en_list)
758
 
759
+ if not search_terms_en:
760
+ return []
761
 
762
  found_matches = []
763
  for term in search_terms_en:
 
768
  for k_lower, orig_key in _foodb_lower_map.items():
769
  if term_lower in k_lower:
770
  found_matches.append(orig_key)
771
+ if len(found_matches) >= limit:
772
+ break
773
 
774
  seen = set()
775
  unique = []
 
780
 
781
  return unique[:limit]
782
 
 
 
 
 
783
  def generate_local_fallback_analysis(query, match, entities, food_compound_map, neuro_map):
784
  saludo = "### 📋 Informe de Análisis Clínico-Nutricional (Modo Local / Alta Disponibilidad)\n"
785
  hipotesis = f"**Hipótesis Probable:** Es muy probable que tus síntomas estén relacionados con **{match.get('condicion_asociada', 'una sensibilidad a compuestos alimentarios')}**.\n"
786
+
787
  user_foods = entities.get("alimentos", [])
788
  user_symptoms = entities.get("sintomas", [])
789
+
790
  compuestos_detectados = set()
791
  for food in user_foods:
792
  comps = get_compounds_for_food(food, food_compound_map)
793
  if comps:
794
  compuestos_detectados.update(comps)
795
+
796
  compuestos_str = ", ".join(sorted(list(compuestos_detectados))) if compuestos_detectados else match.get('compuesto_alimento', 'compuestos bioactivos')
797
+
798
  causa = f"\n### 🧬 Causa e Identificación Molecular\nLos síntomas descritos (**{', '.join(user_symptoms)}**) tras el consumo de **{', '.join(user_foods)}** están asociados a la presencia de sustancias biológicamente activas como **{compuestos_str}**.\n"
799
+
800
  detalles_compuestos = []
801
  for comp in compuestos_detectados:
802
  if comp in neuro_map:
803
  detalles_compuestos.append(f"- **{comp.capitalize()}**: {neuro_map[comp]['efecto_neuropsicologico']}")
804
+
805
  if detalles_compuestos:
806
  causa += "\n**Mecanismo bioquímico e impacto neurológico:**\n" + "\n".join(detalles_compuestos) + "\n"
807
  elif match.get('mecanismo_fisiologico'):
808
  causa += f"\n**Mecanismo fisiológico:** {match.get('mecanismo_fisiologico')}\n"
809
 
810
  evitar = f"\n### 🛑 Alimentos a Moderar o Evitar\n- **Alimentos evaluados:** {', '.join(user_foods).capitalize()}\n- Alimentos ricos en **{compuestos_str}** o productos derivados que compartan los mismos componentes desencadenantes.\n"
811
+
812
  alternativas = "\n### 💡 Sugerencias y Alternativas\n- Llevar un diario de síntomas registrando alimentos, hora de consumo y aparición de molestias.\n- Reducir temporalmente la ingesta del alimento sospechoso para evaluar la remisión del síntoma.\n- Consultar con un nutricionista funcional o médico especialista si las molestias persisten.\n"
813
+
814
  consejo_texto = None
815
  for comp in compuestos_detectados:
816
  comp_clean = comp.lower().strip()
 
825
  consejo_texto = CONSEJO_GENERICO_DEFAULT
826
 
827
  consejo = f"\n### 📌 Consejo Práctico Final\n{consejo_texto}\n"
828
+
829
  return saludo + hipotesis + causa + evitar + alternativas + consejo + TEXTO_BANDERAS_ROJAS
830
 
831
  def generate_detailed_analysis(query, match, entities=None, food_compound_map=None, neuro_map=None):
 
835
  return global_learned_cache[cache_lookup_key]
836
 
837
  gemini_client = get_gemini_client()
838
+
839
  if not gemini_client:
840
  local_report = generate_local_fallback_analysis(query, match, entities, food_compound_map, neuro_map)
841
  global_learned_cache[cache_lookup_key] = local_report
 
888
  report_lines = ["\n### 🧠 Efectos Neuropsicológicos Posibles"]
889
  user_foods = entities.get("alimentos", [])
890
  relevant_compounds = set()
891
+
892
  if user_foods:
893
  for food in user_foods:
894
  comps = get_compounds_for_food(food, food_map)
895
  relevant_compounds.update(comps)
896
+
897
  found_neuro_effect = False
898
  if relevant_compounds:
899
  for compound in sorted(list(relevant_compounds)):
900
  if compound in neuro_map:
901
  found_neuro_effect = True
902
  report_lines.append(f"**{compound.capitalize()}**: _{neuro_map[compound]['efecto_neuropsicologico']}_")
903
+
904
+ if not found_neuro_effect:
905
  report_lines.append("No se detectaron efectos neuropsicológicos específicos directos en nuestra base de datos para los alimentos ingresados.")
906
+
907
  return "\n".join(report_lines)
908
 
909
+ def generate_molecular_report_text(best_match, entities, _foodb_index, _foodb_lower_map, food_name_map, food_compound_map, neuro_map):
910
  report_lines = ["\n### 🔬 Análisis Molecular y Efectos Bioactivos"]
911
  user_foods = entities.get("alimentos", [])
912
  if not user_foods:
 
929
  best_food_matches = find_best_foodb_matches(user_foods, _foodb_index, _foodb_lower_map, food_name_map)
930
  for food_key in best_food_matches:
931
  compounds = _foodb_index.get(food_key, [])
932
+ if not compounds:
933
+ continue
934
  found_any = True
935
  report_lines.append(f"\n**{food_key.capitalize()}** (Compuestos y efectos fisiológicos en FooDB):")
936
+
937
  for item in compounds[:5]:
938
  if isinstance(item, dict):
939
  comp_name = item.get('compound', 'N/A')
940
  effects = item.get('effects', [])
941
+
942
  eff_translated = []
943
  for eff in effects[:4]:
944
  eff_clean = str(eff).lower().strip()
945
  eff_es = FOODB_EFFECTS_TRANSLATION.get(eff_clean, eff_clean)
946
  eff_translated.append(eff_es)
947
+
948
  eff_str = f" — *Efectos registrados: {', '.join(eff_translated)}*" if eff_translated else ""
949
  report_lines.append(f"- **{comp_name}**{eff_str}")
950
  else:
 
978
 
979
  doc_io = BytesIO()
980
  doc.save(doc_io)
981
+ doc_io.seek(0)
982
  return doc_io.getvalue()
983
  except Exception as e:
984
  logger.error(f"Error al generar informe Word: {e}")
 
987
  def create_relevance_chart(results):
988
  top_results = results[:10]
989
  data = pd.DataFrame({
990
+ "Condición": [re.sub(r'\(.*\)', '', r['entry'].get('condicion_asociada', '')).strip() for r in top_results],
991
  "Relevancia": [r['score']['total'] for r in top_results]
992
  })
993
  return alt.Chart(data).mark_bar().encode(
994
  x='Relevancia', y=alt.Y('Condición', sort='-x'), tooltip=['Condición', 'Relevancia']
995
  ).properties(title='Top Coincidencias')
996
 
 
 
 
 
997
  if 'search_results' not in st.session_state: st.session_state.search_results = None
998
  if 'user_query' not in st.session_state: st.session_state.user_query = ""
999
  if 'entities' not in st.session_state: st.session_state.entities = None
 
1012
 
1013
  with st.form(key="search_form", clear_on_submit=False):
1014
  query_input = st.text_area(
1015
+ "Tu Caso:",
1016
+ height=150,
1017
  placeholder="Ej: Si tomo mucho jugo de chontaduro no puedo dormir.",
1018
  key="input_query_text"
1019
  )
 
1027
  st.session_state.analysis_cache = {}
1028
  st.session_state.search_results = None
1029
  st.session_state.entities = None
1030
+
1031
  with st.spinner("🔍 Analizando pistas con Gemini..."):
 
1032
  raw_entities = extract_entities_with_gemini(current_query)
1033
  reinforced = reinforce_entities_with_keywords(raw_entities, current_query, FOOD_TO_COMPOUND_MAP, MASTER_SYMPTOM_MAP)
1034
  final_symptoms = translate_symptoms_local(reinforced.get("sintomas", []), MASTER_SYMPTOM_MAP)
1035
  final_entities = {"alimentos": reinforced.get("alimentos", []), "sintomas": final_symptoms}
1036
  st.session_state.entities = final_entities
1037
+
 
1038
  results = find_best_matches_hybrid(final_entities, alimentos_data, current_query, FOOD_TO_COMPOUND_MAP)
1039
 
 
1040
  if not results:
1041
  local_hypothesis = build_local_hypothesis(final_entities, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP)
1042
  if local_hypothesis:
1043
  results = [{'entry': local_hypothesis, 'score': {'total': 50}, 'matched_keywords': []}]
1044
 
 
1045
  if not results:
1046
  ai_hypothesis = generate_ai_hypothesis_on_the_fly(current_query, final_entities)
1047
  if ai_hypothesis:
 
1051
  else:
1052
  st.warning("⚠️ Por favor describe tus síntomas o alimentos consumidos antes de analizar.")
1053
 
 
1054
  if st.session_state.has_searched:
1055
  results = st.session_state.search_results
1056
+
1057
  if results and len(results) > 0:
1058
  best_match = results[0]['entry']
1059
  st.success(f"🔎 Coincidencia Principal: **{best_match.get('condicion_asociada')}**")
1060
+
1061
  cache_key = f"analysis_{hash(st.session_state.user_query)}_{best_match.get('condicion_asociada')}"
1062
+
1063
  if cache_key not in st.session_state.analysis_cache:
1064
  with st.spinner("✍️ Redactando informe..."):
1065
  analysis = generate_detailed_analysis(
 
1067
  st.session_state.entities, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP
1068
  )
1069
  st.session_state.analysis_cache[cache_key] = analysis
1070
+
1071
  full_report = f"REPORTE\n\n{analysis}"
1072
  st.session_state.word_bytes_cache[cache_key] = generate_word_report(full_report)
1073
 
1074
  tab_main, tab_neuro, tab_mol = st.tabs(["💡 Interpretación Clínica", "🧠 Efectos Neuropsicológicos", "🔬 Análisis Molecular"])
1075
+
1076
  with tab_main:
1077
  st.markdown(st.session_state.analysis_cache[cache_key])
1078
  if len(results) > 1:
 
1084
  with tab_mol:
1085
  st.markdown(generate_molecular_report_text(
1086
  best_match, st.session_state.entities, foodb_index, foodb_lower_map,
1087
+ FOOD_NAME_TO_FOODB_KEY, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP
1088
  ))
1089
 
1090
  word_bytes = st.session_state.word_bytes_cache.get(cache_key)