ALIMENTOS / app.py
JairoCesar's picture
Update app.py
f31fcb2 verified
raw
history blame
44.3 kB
# ==================== 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()
@st.cache_resource
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()
@st.cache_data
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()
@retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(3))
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)
@retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(3))
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("---")