Spaces:
Running
Running
| # ==================== El Detective de Alimentos (Versión 12.2 - Sensibilidad Semántica Ampliada) ======================== | |
| # Por: JAIRO CESAR ALEXANDER E. MD DIANA MILENA SOLER MARTINEZ PSI. ESP. U JUAN N CORPAS | |
| import streamlit as st | |
| import google.generativeai as genai | |
| import google.api_core.exceptions | |
| import os | |
| import json | |
| import logging | |
| import re | |
| import pandas as pd | |
| import altair as alt | |
| from datetime import datetime | |
| from tenacity import retry, stop_after_attempt, wait_random_exponential | |
| st.set_page_config(page_title="El Detective de Alimentos", page_icon="🍎", layout="wide") | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
| logger = logging.getLogger("food_detective_app") | |
| try: | |
| if 'GEMINI_API_KEY' in st.secrets: | |
| GEMINI_API_KEY = st.secrets['GEMINI_API_KEY'] | |
| else: | |
| GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") | |
| if not GEMINI_API_KEY: | |
| st.error("No se encontró la GEMINI_API_KEY.") | |
| st.stop() | |
| genai.configure(api_key=GEMINI_API_KEY) | |
| except Exception as e: | |
| st.error(f"❌ Error al configurar Gemini API: {e}") | |
| st.stop() | |
| def get_gemini_model(): | |
| try: | |
| return genai.GenerativeModel("gemini-2.5-flash-lite") | |
| except Exception as e: | |
| st.error(f"❌ No se pudo cargar el modelo Gemini: {e}") | |
| return None | |
| model = get_gemini_model() | |
| def load_data(): | |
| try: | |
| path_alimentos = os.path.join('DATOS', 'alimentos_enriquecido.json') | |
| path_foodb_index = os.path.join('DATOS', 'foodb_index.json') | |
| with open(path_alimentos, 'r', encoding='utf-8') as f: | |
| data_alimentos = json.load(f) | |
| foodb_index = {} | |
| if os.path.exists(path_foodb_index): | |
| with open(path_foodb_index, 'r', encoding='utf-8') as f: | |
| foodb_index = json.load(f) | |
| return data_alimentos, foodb_index | |
| except Exception as e: | |
| st.error(f"Error cargando los archivos de datos: {e}") | |
| return None, None | |
| alimentos_data, foodb_index = load_data() | |
| FOOD_TO_COMPOUND_MAP = { | |
| # --- CEREALES, GRANOS Y DERIVADOS --- | |
| "pan": ["gluten", "fodmaps", "fructanos", "levadura", "aminas", "fitatos"], | |
| "trigo": ["gluten", "fodmaps", "fructanos", "lectinas", "aminas", "fitatos"], | |
| "harina": ["gluten", "fodmaps", "fructanos"], | |
| "pasta": ["gluten", "fodmaps", "fructanos"], | |
| "galletas": ["gluten", "azúcar", "fodmaps", "fructanos", "lácteos"], | |
| "pizza": ["gluten", "lácteos", "histamina", "aminas", "fodmaps", "fructanos", "solaninas"], | |
| "ravioles": ["gluten", "fructanos", "lácteos", "histamina", "oxalatos", "purinas", "solaninas"], | |
| "cebada": ["gluten", "fodmaps", "fructanos"], | |
| "centeno": ["gluten", "fodmaps", "fructanos"], | |
| "avena": ["gluten", "avenina", "níquel", "fitatos"], | |
| "maíz": ["lectinas", "aflatoxinas", "fodmaps", "fructosa"], | |
| "arroz": ["lectinas", "arsénico", "fitatos"], | |
| "arroz frito": ["lectinas", "gluten", "soja", "histamina", "fodmaps", "alérgenos", "glutamato"], | |
| "quinoa": ["saponinas", "oxalatos", "fitatos"], | |
| # --- LÁCTEOS Y DERIVADOS --- | |
| "leche": ["lácteos", "caseína", "lactosa", "fodmaps"], | |
| "queso": ["lácteos", "caseína", "lactosa", "histamina", "tiramina", "aminas"], | |
| "yogur": ["lácteos", "caseína", "lactosa", "histamina"], | |
| "mantequilla": ["lácteos", "caseína", "lactosa"], | |
| "helado": ["lácteos", "azúcar", "lactosa", "fodmaps"], | |
| # --- VEGETALES --- | |
| "tomate": ["histamina", "solaninas", "lectinas", "salicilatos", "aminas"], | |
| "pimiento": ["solaninas", "lectinas", "capsaicina", "salicilatos"], | |
| "berenjena": ["solaninas", "lectinas", "histamina", "salicilatos"], | |
| "patata": ["solaninas", "lectinas", "almidón resistente"], | |
| "papas fritas": ["solaninas", "lectinas", "aminas", "acrilamida", "gluten"], | |
| "cebolla": ["fodmaps", "fructanos", "azufre", "quercetina"], | |
| "ajo": ["fodmaps", "fructanos", "azufre"], | |
| "brócoli": ["fodmaps", "fructanos", "goitrógenos", "azufre", "salicilatos"], | |
| "coliflor": ["fodmaps", "polioles", "goitrógenos", "azufre"], | |
| "repollo": ["fodmaps", "fructanos", "goitrógenos", "azufre", "histamina"], | |
| "espinaca": ["oxalatos", "histamina", "goitrógenos", "níquel", "nitratos"], | |
| "acelga": ["oxalatos"], | |
| "remolacha": ["oxalatos", "fodmaps", "gos", "nitratos"], | |
| "legumbres": ["fodmaps", "gos", "lectinas", "fitatos", "saponinas"], | |
| "lentejas": ["fodmaps", "gos", "lectinas", "fitatos", "níquel"], | |
| "garbanzos": ["fodmaps", "gos", "lectinas", "fitatos"], | |
| "frijoles": ["fodmaps", "gos", "lectinas", "fitatos"], | |
| "soja": ["alérgenos", "fitatos", "goitrógenos", "lectinas", "níquel"], | |
| "tofu": ["soja", "fitatos", "goitrógenos", "lectinas"], | |
| "aguacate": ["fodmaps", "polioles", "histamina", "salicilatos", "aminas"], | |
| "champiñón": ["fodmaps", "polioles"], | |
| "pepino": ["lectinas", "salicilatos"], | |
| "zanahoria": ["salicilatos"], | |
| # --- FRUTAS --- | |
| "manzana": ["fodmaps", "fructosa", "polioles", "salicilatos", "quercetina"], | |
| "pera": ["fodmaps", "fructosa", "polioles"], | |
| "mango": ["fodmaps", "fructosa"], | |
| "cereza": ["fodmaps", "fructosa", "polioles", "salicilatos"], | |
| "sandía": ["fodmaps", "fructosa"], | |
| "miel": ["fodmaps", "fructosa"], | |
| "plátano": ["aminas", "tiramina", "histamina", "almidón resistente"], | |
| "fresa": ["histamina", "salicilatos", "goitrógenos"], | |
| "piña": ["histamina", "salicilatos", "ácidos", "bromelina"], | |
| "cítricos": ["ácidos", "salicilatos", "aminas"], | |
| "naranja": ["ácidos", "salicilatos", "aminas"], | |
| "limón": ["ácidos", "salicilatos", "aminas"], | |
| "papaya": ["histamina", "papaína"], | |
| "pasas": ["histamina", "salicilatos", "fructosa"], | |
| "uvas": ["salicilatos", "fructosa", "quercetina"], | |
| "arándano": ["salicilatos"], | |
| "frambuesa": ["salicilatos"], | |
| # --- PROTEÍNAS (CARNES, PESCADOS, HUEVOS) --- | |
| "carne": ["purinas", "histamina", "alfa-gal", "hierro hemo"], # TÉRMINO GENERAL AÑADIDO | |
| "carne roja": ["purinas", "alfa-gal", "hierro hemo", "histamina"], | |
| "carne procesada": ["nitritos", "histamina", "tiramina", "aminas", "fosfatos"], | |
| "embutidos": ["nitritos", "histamina", "tiramina", "aminas", "fosfatos"], | |
| "hamburguesa": ["gluten", "fructanos", "lácteos", "histamina", "aminas", "purinas", "azúcar", "fodmaps", "lectinas", "solaninas", "semillas de sésamo"], | |
| "huevo": ["alérgenos", "azufre"], | |
| "pescado": ["histamina", "purinas", "mercurio"], | |
| "pescado azul": ["histamina", "purinas", "omega-3"], | |
| "atún": ["histamina", "mercurio"], | |
| "salmón": ["histamina", "purinas"], | |
| "mariscos": ["alérgenos", "purinas", "yodo", "níquel"], | |
| # --- FRUTOS SECOS, SEMILLAS Y DULCES --- | |
| "frutos secos": ["níquel", "fitatos", "oxalatos", "salicilatos", "arginina", "alérgenos"], | |
| "nueces": ["arginina", "salicilatos", "níquel", "oxalatos", "fitatos"], | |
| "almendras": ["salicilatos", "arginina", "oxalatos", "fitatos", "cianuro"], | |
| "maní": ["alérgenos", "arginina", "lectinas", "aflatoxinas"], | |
| "cacahuetes": ["alérgenos", "arginina", "lectinas", "aflatoxinas"], | |
| "chocolate": ["cafeína", "aminas", "tiramina", "níquel", "arginina", "oxalatos", "teobromina"], | |
| # --- BEBIDAS, CONDIMENTOS Y PLATOS COMPLEJOS --- | |
| "café": ["cafeína", "ácidos", "histamina", "salicilatos", "aminas"], | |
| "té": ["cafeína", "taninos", "oxalatos", "salicilatos"], | |
| "vino": ["histamina", "tiramina", "sulfitos", "taninos", "alcohol", "quercetina"], | |
| "cerveza": ["gluten", "histamina", "tiramina", "alcohol"], | |
| "refresco de cola": ["azúcar", "fructosa", "cafeína", "ácidos", "colorantes"], | |
| "bebida gaseosa oscura": ["azúcar", "fructosa", "cafeína", "ácidos", "colorantes"], | |
| "bebida energética": ["cafeína", "azúcar", "edulcorantes", "ácidos", "estimulantes", "saborizantes"], | |
| "vinagre": ["histamina", "sulfitos", "ácido acético"], | |
| "alimentos fermentados": ["histamina", "tiramina"], | |
| "chucrut": ["histamina", "tiramina"], | |
| "sancocho": ["histamina", "purinas", "fodmaps", "fructanos", "lectinas", "solaninas", "almidón"], | |
| "ajiaco": ["solaninas", "lectinas", "purinas", "lácteos", "histamina"], | |
| "bandeja paisa": ["fodmaps", "gos", "lectinas", "histamina", "tiramina", "aminas", "nitritos", "purinas", "alérgenos", "polioles"], | |
| "cholado": ["azúcar", "fructosa", "lácteos", "lactosa", "histamina", "salicilatos", "colorantes"] | |
| } | |
| SYMPTOM_KEYWORD_MAP = { | |
| # --- SISTEMA GASTROINTESTINAL (DIGESTIVO) --- | |
| "dolor abdominal": ["dolor de estómago", "dolor de panza", "dolor abdominal", "retortijones", "cólicos", "calambres abdominales"], | |
| "hinchazón": ["hinchazón abdominal", "vientre hinchado", "distensión abdominal", "hinchazón", "distensión", "estomago se infla", "inflamado", "inflado", "infla", "hincha"], | |
| "gases": ["exceso de gases", "muchos gases", "gases", "flatulencia", "ventosidades"], | |
| "diarrea": ["diarrea crónica", "diarrea persistente", "heces sueltas", "estómago suelto", "diarrea", "deposiciones liquidas"], | |
| "estreñimiento": ["estreñimiento crónico", "dificultad para evacuar", "no poder ir al baño", "estreñimiento"], | |
| "acidez": ["acidez estomacal", "reflujo gastroesofágico", "ardor de estómago", "acidez", "reflujo", "agruras"], | |
| "náuseas": ["náuseas persistentes", "ganas de vomitar", "náuseas", "mareo"], | |
| "vómitos": ["vómitos recurrentes", "vómitos", "vomitar"], | |
| "disfagia": ["dificultad para tragar", "problemas al tragar", "se atora la comida", "disfagia"], | |
| "pérdida de apetito": ["falta de apetito", "no tengo hambre", "pérdida de apetito"], | |
| # --- SISTEMA NEUROLÓGICO --- | |
| "dolor de cabeza": ["dolores de cabeza crónicos", "dolor de cabeza crónico", "dolores de cabeza", "dolor de cabeza", "cefalea", "migraña"], | |
| "niebla mental": ["niebla mental severa", "dificultad para concentrarse", "falta de claridad mental", "niebla mental", "confusión mental", "mente nublada", "deterioro cognitivo"], | |
| "ataxia": [ | |
| "pérdida de equilibrio y coordinación", "problemas de equilibrio y coordinación", "inestabilidad al caminar", | |
| "caminar inestable", "pérdida del equilibrio", "falta de equilibrio", "pérdida de coordinación", | |
| "falta de coordinación", "movimientos torpes", "incoordinación", "inestabilidad", "torpeza", "ataxia" | |
| ], | |
| "neuropatía periférica": [ | |
| "hormigueo en manos y pies", "entumecimiento en manos y pies", "sensación de ardor en los pies", | |
| "calambres en manos y pies", "sensación de agujas", "hormigueo en las extremidades", "parestesias", | |
| "entumecimiento", "hormigueo", "neuropatía" | |
| ], | |
| "mareo/vértigo": ["sensación de que todo da vueltas", "vértigo", "mareos"], | |
| "convulsiones": ["ataques epilépticos", "convulsiones", "ataques", "epilepsia"], | |
| # --- SISTEMA DERMATOLÓGICO (PIEL) --- | |
| "erupción": ["dermatitis herpetiforme", "erupción con ampollas", "erupción cutánea", "erupción", "ronchas", "urticaria", "sarpullido", "granitos", "eczema"], | |
| "picazón": ["picazón intensa", "picor en la piel", "comezón", "picor", "prurito"], | |
| "piel seca": ["piel seca y escamosa", "piel muy seca", "piel reseca", "ictiosis"], | |
| "acné": ["acné quístico", "brote de acné", "espinillas", "granos", "acné"], | |
| # --- SISTEMA MUSCULOESQUELÉTICO --- | |
| "dolor articular": ["dolor en las articulaciones", "articulaciones doloridas", "dolores articulares", "artralgia"], | |
| "dolor muscular": ["dolor muscular generalizado", "dolores musculares", "mialgia"], | |
| "debilidad muscular": ["falta de fuerza", "debilidad en los músculos", "pérdida de fuerza"], | |
| "calambres": ["calambres musculares", "espasmos musculares", "calambres"], | |
| # --- SÍNTOMAS GENERALES Y CONSTITUCIONALES --- | |
| "fatiga": ["fatiga crónica", "cansancio extremo", "agotamiento crónico", "falta de energía", "fatiga", "cansancio", "agotamiento"], | |
| "pérdida de peso": ["pérdida de peso inexplicable", "adelgazamiento involuntario", "pérdida de peso"], | |
| "fiebre": ["fiebre recurrente", "fiebre baja", "temperatura alta", "fiebre"], | |
| "malestar general": ["sentirse mal", "cuerpo cortado", "malestar"], | |
| "inflamación": ["inflamación sistémica", "inflamación general", "inflamación"], | |
| # --- SISTEMA RESPIRATORIO Y ALÉRGICO (ORL) --- | |
| "dificultad para respirar": ["falta de aire", "no poder respirar bien", "disnea"], | |
| "congestión nasal": ["nariz tapada", "congestión nasal", "rinitis"], | |
| "tos": ["tos crónica", "tos seca", "tos persistente", "tos"], | |
| "sibilancias": ["pitido en el pecho", "silbidos al respirar", "sibilancias"], | |
| # --- SISTEMA PSICOLÓGICO / ESTADO DE ÁNIMO --- | |
| "ansiedad": ["ataques de pánico", "nerviosismo", "inquietud", "ansiedad"], | |
| "irritabilidad": ["cambios de humor", "mal humor", "estar irritable", "irritabilidad"], | |
| "depresión": ["tristeza persistente", "desánimo", "depresión"] | |
| } | |
| FOOD_NAME_TO_FOODB_KEY = { | |
| # --- CEREALES Y GRANOS --- | |
| "alforfón": ["buckwheat"], "arroz": ["rice"], "avena": ["oat", "oats"], "cebada": ["barley"], "centeno": ["rye"], "galleta": ["cookie", "biscuit"], "maíz": ["corn", "maize"], "pan": ["bread"], "pasta": ["pasta"], "pizza": ["pizza"], "quinoa": ["quinoa"], "trigo": ["wheat"], "trigo sarraceno": ["buckwheat"], | |
| # --- LÁCTEOS Y DERIVADOS --- | |
| "crema": ["cream"], "helado": ["ice cream"], "leche": ["milk"], "mantequilla": ["butter"], "queso": ["cheese"], "yogur": ["yogurt", "yoghurt"], | |
| # --- VEGETALES --- | |
| "acelga": ["chard", "swiss chard"], "ajo": ["garlic"], "alcachofa": ["artichoke"], "apio": ["celery"], "berenjena": ["eggplant", "aubergine"], "brócoli": ["broccoli"], "calabacín": ["zucchini", "courgette"], "calabaza": ["pumpkin", "squash"], "cebolla": ["onion"], "champiñón": ["mushroom"], "col": ["cabbage"], "coliflor": ["cauliflower"], "edamame": ["edamame"], "espárrago": ["asparagus"], "espinaca": ["spinach"], "garbanzo": ["chickpea"], "guisante": ["pea", "peas"], "frijol": ["bean", "beans"], "lenteja": ["lentil"], "patata": ["potato"], "pepino": ["cucumber"], "pimiento": ["bell pepper", "pepper"], "remolacha": ["beet", "beetroot"], "repollo": ["cabbage"], "seta": ["mushroom"], "soja": ["soy", "soybean"], "tofu": ["tofu"], "tomate": ["tomato"], "zanahoria": ["carrot"], | |
| # --- FRUTAS --- | |
| "aguacate": ["avocado"], "albaricoque": ["apricot"], "arándano": ["blueberry"], "cereza": ["cherry"], "ciruela": ["plum"], "dátil": ["date"], "frambuesa": ["raspberry"], "fresa": ["strawberry"], "higo": ["fig"], "kiwi": ["kiwi", "kiwifruit"], "limón": ["lemon"], "mandarina": ["tangerine", "mandarin"], "mango": ["mango"], "manzana": ["apple"], "melocotón": ["peach"], "melón": ["melon", "cantaloupe"], "mora": ["blackberry"], "naranja": ["orange"], "nectarina": ["nectarine"], "papaya": ["papaya"], "pera": ["pear"], "piña": ["pineapple"], "plátano": ["banana"], "pomelo": ["grapefruit"], "sandía": ["watermelon"], "uva": ["grape"], | |
| # --- PROTEÍNAS (CARNES, PESCADOS, HUEVOS) --- | |
| "anchoa": ["anchovy", "anchovies"], "atún": ["tuna"], "camarón": ["shrimp", "prawn"], "carne": ["meat", "beef", "pork", "lamb"], "cerdo": ["pork"], "cordero": ["lamb"], "gamba": ["shrimp", "prawn"], "huevo": ["egg"], "marisco": ["shellfish", "seafood"], "pavo": ["turkey"], "pescado": ["fish"], "pollo": ["chicken"], "salchicha": ["sausage"], "salmón": ["salmon"], "sardina": ["sardine"], "ternera": ["beef", "veal"], | |
| # --- FRUTOS SECOS Y SEMILLAS --- | |
| "almendra": ["almond"], "anacardo": ["cashew"], "avellana": ["hazelnut"], "cacahuete": ["peanut"], "chía": ["chia", "chia seed"], "lino": ["flax", "flaxseed", "linseed"], "nuez": ["walnut"], "pistacho": ["pistachio"], "sésamo": ["sesame", "sesame seed"], | |
| # --- BEBIDAS, DULCES Y CONDIMENTOS --- | |
| "aceituna": ["olive"], "café": ["coffee"], "caldo": ["broth", "stock"], "cerveza": ["beer"], "chocolate": ["chocolate"], "jengibre": ["ginger"], "cúrcuma": ["turmeric"], "miel": ["honey"], "mostaza": ["mustard"], "té": ["tea"], "vinagre": ["vinegar"], "vino": ["wine", "red wine", "white wine"] | |
| } | |
| INTEGRATED_NEURO_FOOD_MAP = { | |
| "gluten": { | |
| "efecto_neuropsicologico": "En individuos con sensibilidad (celíaca o no celíaca), puede desencadenar una respuesta inmune que resulta en neuroinflamación, manifestándose como niebla mental, depresión, ansiedad y migrañas. La predisposición es fuertemente genética (HLA-DQ2/DQ8).", | |
| "fuentes_comunes": ["pan", "trigo", "pasta", "pizza", "cebada", "centeno"] | |
| }, | |
| "caseína": { | |
| "efecto_neuropsicologico": "En subgrupos sensibles, puede ser inmuno-reactiva. La teoría de las casomorfinas postula que sus péptidos pueden actuar sobre receptores opioides cerebrales, afectando la cognición y el comportamiento.", | |
| "fuentes_comunes": ["leche", "queso", "yogur", "productos lácteos"] | |
| }, | |
| "omega-3 (DHA/EPA)": { | |
| "efecto_neuropsicologico": "Potente agente anti-neuroinflamatorio. El DHA es un componente estructural crítico de las membranas neuronales; el EPA combate la inflamación. La capacidad de convertir ALA (vegetal) a DHA/EPA es dependiente de la genética (FADS1/FADS2).", | |
| "fuentes_comunes": ["pescado graso (salmón, sardinas, arenque)", "nueces (como ALA)"] | |
| }, | |
| "cafeína": { | |
| "efecto_neuropsicologico": "Antagonista de los receptores de adenosina, aumenta la vigilia y la dopamina. La velocidad de su metabolismo (gen CYP1A2) determina su impacto: 'metabolizadores lentos' tienen un riesgo muy superior de sufrir ansiedad, pánico e insomnio.", | |
| "fuentes_comunes": ["café", "té verde", "chocolate negro", "bebidas energéticas"] | |
| }, | |
| "polifenoles": { | |
| "efecto_neuropsicologico": "Reducen el estrés oxidativo y la neuroinflamación. Aumentan el BDNF (Factor Neurotrófico Derivado del Cerebro), promoviendo la neurogénesis y la plasticidad sináptica. Su biodisponibilidad y metabolización dependen del microbioma intestinal.", | |
| "fuentes_comunes": ["arándanos", "uvas", "té verde", "chocolate negro", "aceite de oliva", "nueces"] | |
| }, | |
| "triptófano": { | |
| "efecto_neuropsicologico": "Precursor directo de serotonina (estado de ánimo, calma) y melatonina (sueño). Su transporte al cerebro compite con otros aminoácidos, y su eficiencia de conversión depende de la genética (ej. TPH2).", | |
| "fuentes_comunes": ["pavo", "pollo", "huevo", "plátano", "lentejas", "garbanzos", "aguacate"] | |
| }, | |
| "colina": { | |
| "efecto_neuropsicologico": "Precursor del neurotransmisor acetilcolina, esencial para la memoria, el aprendizaje y la función del hipocampo. La dependencia de la ingesta dietética es mayor en individuos con variantes genéticas específicas (ej. PEMT).", | |
| "fuentes_comunes": ["huevo (yema)", "hígado", "soja", "carne de res"] | |
| }, | |
| "L-teanina": { | |
| "efecto_neuropsicologico": "Ansiolítico no sedante. Aumenta las ondas cerebrales alfa (alerta relajada), modula GABA (inhibidor), dopamina y serotonina, y bloquea receptores de glutamato (excitador).", | |
| "fuentes_comunes": ["té verde", "té negro", "algunos hongos"] | |
| }, | |
| "histamina": { | |
| "efecto_neuropsicologico": "En personas con baja actividad de la enzima DAO (a menudo por causas genéticas), su acumulación sistémica puede provocar ansiedad, ataques de pánico, insomnio y confusión mental.", | |
| "fuentes_comunes": ["quesos curados", "embutidos", "vino tinto", "espinacas", "tomate", "aguacate", "alimentos fermentados"] | |
| }, | |
| "salicilatos": { | |
| "efecto_neuropsicologico": "En individuos con una vía de sulfatación (enzima PST) lenta o sobrecargada, pueden acumularse e interferir con el metabolismo de neurotransmisores, causando hiperactividad, irritabilidad y ansiedad.", | |
| "fuentes_comunes": ["bayas", "manzanas", "uvas", "tomate", "almendras", "cúrcuma", "especias"] | |
| }, | |
| "glutamato monosódico": { | |
| "efecto_neuropsicologico": "El glutamato es el principal neurotransmisor excitatorio. La teoría de la excitotoxicidad sugiere que en exceso y en personas con una barrera hematoencefálica permeable, puede sobreestimular las neuronas, causando migrañas o niebla mental.", | |
| "fuentes_comunes": ["alimentos procesados", "comida rápida", "sopas enlatadas", "salsa de soja"] | |
| }, | |
| "azúcar refinado": { | |
| "efecto_neuropsicologico": "Genera picos glucémicos e inflamación sistémica. Se asocia con una peor plasticidad sináptica, exacerbación de la ansiedad (por hipoglucemia reactiva) y empeoramiento de los síntomas depresivos.", | |
| "fuentes_comunes": ["dulces", "refrescos", "bollería", "cereales azucarados", "salsas procesadas"] | |
| }, | |
| "etanol": { | |
| "efecto_neuropsicologico": "Depresor del sistema nervioso central. Es neurotóxico, interfiere con la arquitectura del sueño REM (crucial para la consolidación de la memoria y la regulación emocional), agrava la depresión y puede anular la eficacia de los antidepresivos.", | |
| "fuentes_comunes": ["vino", "cerveza", "licores"] | |
| }, | |
| "grasas trans": { | |
| "efecto_neuropsicologico": "Promueven la neuroinflamación y la rigidez de las membranas neuronales, afectando negativamente la señalización celular. Se asocian con un mayor riesgo de depresión y deterioro cognitivo.", | |
| "fuentes_comunes": ["margarina", "alimentos fritos industriales", "bollería industrial", "comida rápida"] | |
| }, | |
| "tiramina": { | |
| "efecto_neuropsicologico": "Aminoácido vasopresor. Su consumo es peligroso para personas que toman antidepresivos IMAO, ya que puede provocar una crisis hipertensiva con consecuencias neurológicas graves.", | |
| "fuentes_comunes": ["quesos muy curados", "embutidos", "carnes ahumadas", "soja fermentada", "vino tinto"] | |
| }, | |
| "fodmaps": { | |
| "efecto_neuropsicologico": "Su efecto es indirecto a través del eje intestino-cerebro. La fermentación y el malestar intestinal pueden enviar señales de estrés al cerebro, exacerbando la ansiedad y la sensibilidad visceral.", | |
| "fuentes_comunes": ["trigo", "cebolla", "ajo", "legumbres", "manzanas", "miel", "productos lácteos"] | |
| }, | |
| "probióticos": { | |
| "efecto_neuropsicologico": "Modulan el eje intestino-cerebro. Ciertas cepas pueden producir neurotransmisores (ej. GABA), reducir la inflamación y el cortisol, mejorando la resiliencia al estrés y los síntomas de ansiedad y depresión.", | |
| "fuentes_comunes": ["yogur", "kéfir", "chucrut", "kimchi", "kombucha"] | |
| } | |
| } | |
| def sanitize_text(text): | |
| if not text: return "" | |
| return re.sub(r'[.,;()]', '', text).lower().strip() | |
| def extract_entities_with_gemini(query): | |
| if not model: return None | |
| logger.info("Intentando extracción de entidades con Gemini...") | |
| system_prompt = f""" | |
| Eres un asistente de triaje clínico experto. Tu única tarea es analizar la consulta de un usuario y extraer dos tipos de información: | |
| 1. `alimentos`: Una lista exhaustiva de todos los alimentos, bebidas o ingredientes consumidos mencionados. | |
| 2. `sintomas`: Una lista exhaustiva de todos los síntomas, sensaciones o signos clínicos descritos. | |
| Devuelve la respuesta ÚNICAMENTE en formato JSON estricto. No incluyas explicaciones ni texto adicional. | |
| Consulta: "{query}" | |
| """ | |
| try: | |
| response = model.generate_content(system_prompt) | |
| json_text_match = re.search(r'```json\s*(\{.*?\})\s*```', response.text, re.DOTALL) | |
| if json_text_match: | |
| json_text = json_text_match.group(1) | |
| else: | |
| json_text = re.search(r'\{.*\}', response.text, re.DOTALL).group(0) | |
| logger.info("Extracción con Gemini exitosa.") | |
| return json.loads(json_text) | |
| except (Exception, google.api_core.exceptions.GoogleAPICallError) as e: | |
| logger.error(f"Error en la extracción con Gemini (puede ser reintentado): {e}") | |
| raise e | |
| def reinforce_entities_with_keywords(entities, query, food_map, symptom_map): | |
| if not entities: | |
| entities = {"alimentos": [], "sintomas": []} | |
| query_sanitized = sanitize_text(query) | |
| current_foods = entities.get("alimentos", []) or [] | |
| current_foods_sanitized = {sanitize_text(f) for f in current_foods} | |
| for food_keyword in food_map.keys(): | |
| if food_keyword in query_sanitized and food_keyword not in current_foods_sanitized: | |
| logger.info(f"Red de seguridad (Alimento): Añadiendo '{food_keyword}'.") | |
| current_foods.append(food_keyword) | |
| entities["alimentos"] = list(set(current_foods)) | |
| current_symptoms = entities.get("sintomas", []) or [] | |
| query_to_search_symptoms = " " + query_sanitized + " " | |
| for main_symptom, synonyms in symptom_map.items(): | |
| for synonym in sorted(synonyms, key=len, reverse=True): | |
| if (" " + synonym + " ") in query_to_search_symptoms: | |
| if main_symptom not in current_symptoms: | |
| logger.info(f"Red de seguridad (Síntoma): Normalizando '{synonym}' a '{main_symptom}'.") | |
| current_symptoms.append(main_symptom) | |
| query_to_search_symptoms = query_to_search_symptoms.replace(" " + synonym + " ", " ") | |
| entities["sintomas"] = list(set(current_symptoms)) | |
| return entities | |
| def find_best_matches_hybrid(entities, data): | |
| if not entities or not data: return [] | |
| user_symptoms = set(sanitize_text(s) for s in entities.get("sintomas", [])) | |
| user_foods = set(sanitize_text(f) for f in entities.get("alimentos", [])) | |
| # --- LISTA DE CONDICIONES RARAS (Centralizada para fácil mantenimiento) --- | |
| # Cualquier condición en esta lista recibirá una penalización en su puntuación. | |
| # El resto se considerará de probabilidad normal. | |
| RARE_CONDITIONS = [ | |
| "Porfiria Aguda Intermitente (PAI).", | |
| "Enfermedad de Refsum del Adulto.", | |
| "Ataxia por Gluten.", | |
| "Encefalopatía por Gluten.", | |
| "Enfermedad de Wilson.", | |
| "Aciduria Argininosuccínica.", | |
| "Síndrome de Alagille." | |
| # Añade aquí otras enfermedades raras a medida que las incorpores a tu JSON. | |
| ] | |
| candidate_terms = set(user_foods) | |
| for food in user_foods: | |
| if food in FOOD_TO_COMPOUND_MAP: | |
| candidate_terms.update(c.lower() for c in FOOD_TO_COMPOUND_MAP[food]) | |
| results = [] | |
| for entry in data: | |
| entry_compounds_text = sanitize_text(entry.get("compuesto_alimento", "")) | |
| food_match = any(term in entry_compounds_text for term in candidate_terms) | |
| if not food_match: | |
| continue | |
| score_details = {'food': 20, 'symptoms': 0} | |
| entry_symptoms_keys = set(sanitize_text(s) for s in entry.get("sintomas_clave", [])) | |
| symptom_score = 0 | |
| matched_symptoms = [] | |
| for user_symptom in user_symptoms: | |
| for key in entry_symptoms_keys: | |
| if key in user_symptom or user_symptom in key: | |
| symptom_score += 30 | |
| matched_symptoms.append(key) | |
| break | |
| score_details['symptoms'] = symptom_score | |
| if score_details['symptoms'] > 0: | |
| base_score = score_details['food'] + score_details['symptoms'] | |
| # --- LÓGICA DE PONDERACIÓN SIMPLIFICADA --- | |
| condition_name = entry.get("condicion_asociada", "") | |
| if condition_name in RARE_CONDITIONS: | |
| # Penalización fuerte para condiciones raras | |
| final_score = base_score * 0.4 | |
| else: | |
| # Puntuación completa para el resto (comunes y poco comunes) | |
| final_score = base_score * 1.0 | |
| score_details['total'] = int(final_score) | |
| results.append({ | |
| 'entry': entry, | |
| 'score': score_details, | |
| 'matched_symptoms': list(set(matched_symptoms)) | |
| }) | |
| if not results: return [] | |
| return sorted(results, key=lambda x: x['score']['total'], reverse=True) | |
| def generate_detailed_analysis(query, match): | |
| if not model: return "Error: El modelo de IA no está disponible." | |
| if not match or not isinstance(match, dict): | |
| logger.error("Datos de coincidencia inválidos para análisis detallado.") | |
| return "Error interno al generar análisis." | |
| prompt_parts = [ | |
| "Eres un asistente de IA experto en nutrición y comunicación médica. Explica conceptos complejos de forma sencilla, empática y concisa.", | |
| f'Caso del usuario: "{query}"', | |
| f'Posible conexión identificada: "{match.get("condicion_asociada", "N/A")}".', | |
| f'Mecanismo: "{match.get("mecanismo_fisiologico", "No especificado")}".', | |
| f'Recomendaciones: "{match.get("recomendaciones_examenes", "No especificadas")}".', | |
| f'Alimentos implicados: "{match.get("compuesto_alimento", "No especificados")}".', | |
| "\n**Tu Tarea:** Redacta una respuesta clara y útil para el usuario usando Markdown, siguiendo esta estructura OBLIGATORIA:", | |
| f'### Posible Causa: {match.get("condicion_asociada", "Condición no especificada")}', | |
| f'Hola. Basado en lo que mencionaste, podría existir una relación con una condición conocida como **{match.get("condicion_asociada", "esta condición")}**.', | |
| f'### ¿Qué podría estar pasando en tu cuerpo?', | |
| 'Explica el mecanismo mencionado en términos muy sencillos (usa una analogía si es posible).', | |
| "\n### Pasos a Seguir", | |
| "Estas son recomendaciones generales. Es fundamental que las converses con un profesional de la salud:", | |
| f'* **[Consejo práctico basado en las recomendaciones mencionadas.]**', | |
| f'* **[Sugerencia de exámenes si los hay, basado en las recomendaciones.]**', | |
| f'* **Otros alimentos a observar:** Ten en cuenta otros alimentos como: **[menciona 2-3 ejemplos de los alimentos implicados]**.', | |
| "\n### **IMPORTANTE: Descargo de Responsabilidad**", | |
| "Este análisis es una herramienta informativa de IA, **NO un diagnóstico médico.** Consulta siempre a un profesional cualificado para evaluar tu caso." | |
| ] | |
| prompt = "\n".join(prompt_parts) | |
| try: | |
| logger.info(f"Generando análisis detallado para {match.get('condicion_asociada')}") | |
| response = model.generate_content(prompt) | |
| if response.text and len(response.text) > 1: | |
| logger.info("Análisis detallado generado con éxito.") | |
| return response.text | |
| else: | |
| logger.error("La respuesta de Gemini para el análisis detallado fue vacía.") | |
| raise ValueError("Respuesta vacía de la API") | |
| except (Exception, google.api_core.exceptions.GoogleAPICallError) as e: | |
| logger.error(f"Error generando análisis detallado (puede ser reintentado): {e}") | |
| raise e | |
| def create_relevance_chart(results): | |
| # Modificado para mostrar hasta 10 resultados en el gráfico | |
| top_results = results[:10] | |
| condition_names = [re.sub(r'\(.*\)', '', res['entry']['condicion_asociada']).strip() for res in top_results] | |
| chart_data = {"Condición": condition_names, "Relevancia": [res['score']['total'] for res in top_results]} | |
| source = pd.DataFrame(chart_data) | |
| chart = alt.Chart(source).mark_bar(color='#1f77b4').encode( | |
| x=alt.X('Relevancia:Q', title='Puntuación de Relevancia'), | |
| y=alt.Y('Condición:N', sort='-x', title='', axis=alt.Axis(labelLimit=300)), | |
| tooltip=[alt.Tooltip('Condición:N', title='Condición'), alt.Tooltip('Relevancia:Q', title='Puntuación')] | |
| ).properties( | |
| title='Principales Coincidencias según tu Caso' | |
| ).configure_axis( | |
| labelFontSize=12, | |
| titleFontSize=14 | |
| ).configure_title( | |
| fontSize=16, | |
| anchor='start' | |
| ) | |
| return chart | |
| def generate_report_text(query, results): | |
| report_lines = ["="*50, "INFORME DEL DETECTIVE DE ALIMENTOS", "="*50, f"Fecha: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n", f"CONSULTA ORIGINAL DEL USUARIO:\n'{query}'\n", "-"*50] | |
| if results: | |
| best_match = results[0]['entry'] | |
| report_lines.extend([f"PRINCIPAL COINCIDENCIA ENCONTRADA:\n", f"Condición: {best_match.get('condicion_asociada', 'N/A')}", f"Mecanismo Posible: {best_match.get('mecanismo_fisiologico', 'N/A')}", f"Recomendaciones Generales: {best_match.get('recomendaciones_examenes', 'N/A')}\n"]) | |
| if len(results) > 1: | |
| report_lines.extend(["-"*50, "OTRAS POSIBILIDADES CONSIDERADAS (DIAGNÓSTICO DIFERENCIAL):\n"]) | |
| for i, res in enumerate(results[1:4]): | |
| report_lines.append(f"{i+2}. {res['entry'].get('condicion_asociada', 'N/A')} (Puntuación: {res['score']['total']})") | |
| report_lines.extend(["\n" + "="*50, "IMPORTANTE: Este informe es generado por una herramienta de IA y no constituye un diagnóstico médico..."]) | |
| return "\n".join(report_lines) | |
| col_img1, col_text, col_img2 = st.columns([1, 4, 1], gap="medium") | |
| with col_img1: | |
| if os.path.exists("imagen.png"): st.image("imagen.png", width=150) | |
| with col_text: | |
| st.title("El Detective de Alimentos") | |
| st.markdown("##### Describe lo que sientes y lo que comiste para descubrir posibles intolerancias.") | |
| with col_img2: | |
| if os.path.exists("buho.png"): st.image("buho.png", width=120) | |
| st.markdown("---") | |
| if 'search_results' not in st.session_state: st.session_state.search_results = None | |
| if 'user_query' not in st.session_state: st.session_state.user_query = "" | |
| if 'entities' not in st.session_state: st.session_state.entities = None | |
| if 'analysis_cache' not in st.session_state: st.session_state.analysis_cache = {} | |
| if 'query' not in st.session_state: st.session_state.query = "" | |
| if 'start_analysis' not in st.session_state: st.session_state.start_analysis = False | |
| def clear_search_state(): | |
| st.session_state.search_results = None | |
| st.session_state.user_query = "" | |
| st.session_state.entities = None | |
| st.session_state.analysis_cache = {} | |
| def set_query_and_trigger_analysis(example_text): | |
| st.session_state.query = example_text | |
| st.session_state.start_analysis = True | |
| st.write("**¿No sabes por dónde empezar? Prueba con un ejemplo:**") | |
| example_cols = st.columns(3) | |
| example_queries = ["Cuando como mucha carne me duele, hincha y se pone rojo el primer dedo del pie.", "Después de tomar leche, tengo muchos gases e hinchazón.", "El vino tinto siempre me da dolor de cabeza, a que se debe"] | |
| if example_cols[0].button(example_queries[0]): set_query_and_trigger_analysis(example_queries[0]) | |
| if example_cols[1].button(example_queries[1]): set_query_and_trigger_analysis(example_queries[1]) | |
| if example_cols[2].button(example_queries[2]): set_query_and_trigger_analysis(example_queries[2]) | |
| with st.form(key="search_form"): | |
| st.text_area("Describe tu caso aquí:", height=150, key="query") | |
| if st.form_submit_button("Analizar mi caso", type="primary"): | |
| st.session_state.start_analysis = True | |
| if st.session_state.start_analysis: | |
| st.session_state.start_analysis = False | |
| query_to_analyze = st.session_state.query | |
| clear_search_state() | |
| st.session_state.user_query = query_to_analyze | |
| if not query_to_analyze: | |
| st.warning("Por favor, describe lo que sientes y lo que comiste.") | |
| elif alimentos_data is None: | |
| st.error("La base de datos de alimentos no está disponible.") | |
| else: | |
| entities = None | |
| with st.spinner("🧠 Interpretando tu caso y buscando pistas..."): | |
| try: | |
| entities = extract_entities_with_gemini(query_to_analyze) | |
| except Exception as e: | |
| logger.warning(f"La extracción con Gemini falló; se usará el sistema de respaldo: {e}") | |
| entities = reinforce_entities_with_keywords(entities, query_to_analyze, FOOD_TO_COMPOUND_MAP, SYMPTOM_KEYWORD_MAP) | |
| st.session_state.entities = entities | |
| if entities and (entities.get("alimentos") or entities.get("sintomas")): | |
| info_str = f"Pistas identificadas - Alimentos: {', '.join(entities.get('alimentos',[])) or 'Ninguno'}, Síntomas: {', '.join(entities.get('sintomas',[])) or 'Ninguno'}" | |
| st.info(info_str) | |
| with st.spinner("🔬 Cruzando información y calculando relevancia..."): | |
| results = find_best_matches_hybrid(entities, alimentos_data) | |
| st.session_state.search_results = results | |
| else: | |
| st.error("No se pudieron identificar alimentos o síntomas claros en tu descripción. Intenta ser más específico.") | |
| st.session_state.search_results = [] | |
| if st.session_state.search_results is not None: | |
| results = st.session_state.search_results | |
| if not results: | |
| st.warning(f"No se encontraron coincidencias claras para tu caso: '{st.session_state.user_query}'. Prueba a describir los síntomas de otra manera.") | |
| else: | |
| col1, col2 = st.columns([3,1]) | |
| with col1: | |
| st.success(f"Hemos encontrado {len(results)} posible(s) causa(s) relacionada(s) con tu caso.") | |
| with col2: | |
| report_data = generate_report_text(st.session_state.user_query, results) | |
| st.download_button(label="📄 Descargar Informe", data=report_data, file_name=f"informe_detective_{datetime.now().strftime('%Y%m%d')}.txt", mime="text/plain") | |
| st.subheader("Análisis de Relevancia de las Coincidencias") | |
| st.altair_chart(create_relevance_chart(results), use_container_width=True) | |
| best_match_data = results[0] | |
| best_match = best_match_data['entry'] | |
| with st.expander(f"**Análisis Detallado de la Principal Coincidencia: {best_match.get('condicion_asociada')}**", expanded=True): | |
| # Las columnas se crean DENTRO del expander | |
| col1, col2 = st.columns([3, 1]) | |
| # Contenido de la primera columna | |
| with col1: | |
| st.markdown("##### Desglose de la Puntuación de Relevancia:") | |
| score_col1, score_col2, score_col3 = st.columns(3) | |
| score_col1.metric("Puntos por Alimento(s)", f"{best_match_data['score']['food']}") | |
| score_col2.metric("Puntos por Síntomas", f"{best_match_data['score']['symptoms']}") | |
| score_col3.metric("PUNTUACIÓN TOTAL", f"{best_match_data['score']['total']}", delta="Máxima coincidencia") | |
| # Contenido de la segunda columna (AHORA CORRECTAMENTE INDENTADO) | |
| with col2: | |
| st.write("") # Para alinear verticalmente el popover | |
| if foodb_index: | |
| with st.popover("🔬 Componentes moleculares"): | |
| st.info("Información de la base de datos FoodB (en inglés).") | |
| user_foods_mentioned = st.session_state.entities.get("alimentos", []) | |
| if not user_foods_mentioned: | |
| st.warning("No se identificó un alimento específico para buscar.") | |
| else: | |
| found_data = False | |
| displayed_foodb_keys = set() | |
| for alimento_es in user_foods_mentioned: | |
| search_terms_en = [] | |
| for key_es, value_en_list in FOOD_NAME_TO_FOODB_KEY.items(): | |
| if key_es in alimento_es.lower(): | |
| search_terms_en.extend(value_en_list) | |
| for term in set(search_terms_en): | |
| for foodb_key, foodb_data in foodb_index.items(): | |
| if term in foodb_key and foodb_key not in displayed_foodb_keys: | |
| found_data = True | |
| displayed_foodb_keys.add(foodb_key) | |
| with st.container(border=True): | |
| st.subheader(f"Análisis de: {foodb_key.capitalize()}") | |
| for item in foodb_data[:3]: | |
| st.write(f"**Compuesto:** {item['compound']}") | |
| st.write(f"**Efectos reportados:** {', '.join(item['effects'])}") | |
| st.markdown("---") | |
| if not found_data: | |
| st.warning("No se encontraron datos moleculares para los alimentos mencionados.") | |
| # El separador y el spinner van DESPUÉS de las columnas, pero DENTRO del expander | |
| st.markdown("---") | |
| with st.spinner("✍️ Generando un análisis personalizado con IA..."): | |
| if 'best_match_analysis' not in st.session_state.analysis_cache: | |
| try: | |
| analysis_text = generate_detailed_analysis(st.session_state.user_query, best_match) | |
| st.session_state.analysis_cache['best_match_analysis'] = analysis_text | |
| except Exception as e: | |
| logger.error(f"Falló la generación del análisis detallado principal: {e}") | |
| st.session_state.analysis_cache['best_match_analysis'] = "❌ Lo sentimos, no se pudo generar el análisis detallado en este momento debido a un problema con la IA. Por favor, intenta de nuevo más tarde." | |
| st.markdown(st.session_state.analysis_cache['best_match_analysis']) | |
| # El expander de "Otras posibilidades" va DESPUÉS del expander principal | |
| if len(results) > 1: | |
| with st.expander("🔍 **Explora otras posibilidades relevantes (Diagnóstico Diferencial)**"): | |
| for i, result in enumerate(results[1:5]): | |
| with st.container(border=True): | |
| entry = result['entry'] | |
| score = result['score'] | |
| st.subheader(f"{i+2}. {entry.get('condicion_asociada')}") | |
| col_info, col_action = st.columns([3, 1]) | |
| with col_info: | |
| if result.get('matched_symptoms'): | |
| st.markdown(f"**Pistas Clave (Síntomas Coincidentes):** {', '.join(result['matched_symptoms']).capitalize()}") | |
| st.markdown(f"**Alimentos Típicos Asociados:** {entry.get('compuesto_alimento')}") | |
| with col_action: | |
| st.metric("Relevancia", score['total']) | |
| analysis_key = f"analysis_{i+2}" | |
| if st.button("Generar análisis", key=analysis_key, help=f"Generar análisis de IA para {entry.get('condicion_asociada')}"): | |
| with st.spinner(f"Generando análisis para {entry.get('condicion_asociada')}..."): | |
| try: | |
| analysis_text = generate_detailed_analysis(st.session_state.user_query, entry) | |
| st.session_state.analysis_cache[analysis_key] = analysis_text | |
| except Exception as e: | |
| st.session_state.analysis_cache[analysis_key] = f"❌ Error al generar análisis para {entry.get('condicion_asociada')}." | |
| if analysis_key in st.session_state.analysis_cache: | |
| st.info(st.session_state.analysis_cache[analysis_key]) | |
| if i < len(results[1:5]) - 1: | |
| st.markdown("---") |