Spaces:
Sleeping
Sleeping
Download app.py from JairoCesar/ALIMENTOS: direct link, hf CLI and curl.
- Browser
- Download file 47.1 kB
-
https://huggingface.co/spaces/JairoCesar/ALIMENTOS/resolve/main/app.py
- Command line
-
hf download hf://spaces/JairoCesar/ALIMENTOS/app.py
-
curl -L -o app.py https://huggingface.co/spaces/JairoCesar/ALIMENTOS/resolve/main/app.py
47.1 kB
| # ============================================================================== | |
| # EL DETECTIVE DE ALIMENTOS - EDICIÓN HÍBRIDA CON CORTA-FUEGOS Y ALTA DISPONIBILIDAD | |
| # Desarrollado por: Jairo Alexander & Diana Milena Soler Martinez | |
| # Motor: Gemini Flash + FooDB.ca Graph + Fast-Path Local Engine | |
| # ============================================================================== | |
| import streamlit as st | |
| from google import genai | |
| from google.genai import types | |
| import os | |
| import json | |
| import logging | |
| import re | |
| import pandas as pd | |
| import altair as alt | |
| from io import BytesIO | |
| import docx | |
| import difflib | |
| import concurrent.futures | |
| # --- CONFIGURACIÓN DE PÁGINA Y LOGGING --- | |
| 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") | |
| # --- TEXTO ESTÁTICO DE SEGURIDAD Y BANDERAS ROJAS --- | |
| TEXTO_BANDERAS_ROJAS = """ | |
| \n### **IMPORTANTE: Descargo de Responsabilidad y Banderas Rojas** | |
| Este análisis es una herramienta informativa de IA y **NO es un diagnóstico médico.** | |
| **🚩 BANDERAS ROJAS: ¡Atención!** Consulta a un médico si experimentas: | |
| 1. **Cáncer Gástrico/Colon:** Pérdida de peso involuntaria, sangre en heces. | |
| 2. **Enfermedad Inflamatoria Intestinal:** Diarrea crónica con sangre/moco, fiebre. | |
| 3. **Embarazo:** Náuseas matutinas severas, ausencia de menstruación. | |
| 4. **Isquemia Mesentérica:** Dolor fuerte 15-30 min después de comer. | |
| 5. **Vesícula:** Dolor agudo lado derecho tras grasas, piel/ojos amarillos. | |
| **Si tus síntomas son severos, busca ayuda médica urgente.** | |
| """ | |
| # --- CONFIGURACIÓN DE CLIENTE GEMINI CACHEADO --- | |
| def get_gemini_client(): | |
| api_key = os.getenv("GEMINI_API_KEY") | |
| if not api_key: | |
| try: | |
| if "GEMINI_API_KEY" in st.secrets: | |
| api_key = st.secrets["GEMINI_API_KEY"] | |
| elif "APIkeydetective" in st.secrets: | |
| api_key = st.secrets["APIkeydetective"] | |
| except Exception: | |
| pass | |
| if not api_key: | |
| api_key = os.getenv("APIkeydetective") | |
| if not api_key: | |
| logger.warning("❌ GEMINI_API_KEY no encontrada en variables de entorno ni en secretos.") | |
| return None | |
| try: | |
| # Timeout HTTP a nivel de socket ajustado a 4.0 segundos | |
| return genai.Client( | |
| api_key=api_key, | |
| http_options=types.HttpOptions(timeout=4.0) | |
| ) | |
| except Exception as e: | |
| logger.warning(f"❌ Error instanciando cliente Gemini: {e}") | |
| return None | |
| # --- CORTA-FUEGOS CON TIEMPO LÍMITE ESTRICTO DE 3.5 SEGUNDOS --- | |
| def generate_content_with_fallback(gemini_client, prompt): | |
| models_to_try = [ | |
| 'gemini-3.5-flash-lite', | |
| 'gemini-2.5-flash', | |
| 'gemini-1.5-flash' | |
| ] | |
| for model_name in models_to_try: | |
| try: | |
| response = gemini_client.models.generate_content( | |
| model=model_name, | |
| contents=prompt | |
| ) | |
| if response and response.text: | |
| logger.info(f"✅ Respuesta exitosa generada por el modelo: {model_name}") | |
| return response.text, model_name | |
| except Exception as e: | |
| logger.warning(f"⚠️ Modelo {model_name} no disponible ({e}), probando siguiente...") | |
| continue | |
| raise RuntimeError("Ningún modelo de Gemini respondió adecuadamente.") | |
| def generate_content_with_strict_timeout(gemini_client, prompt, timeout_seconds=3.5): | |
| """ | |
| Ejecuta la llamada a Gemini dentro de un hilo aislado. | |
| Si Gemini tarda más de 3.5s, corta la ejecución inmediatamente | |
| y activa el motor local sin bloquear la interfaz. | |
| """ | |
| executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) | |
| future = executor.submit(generate_content_with_fallback, gemini_client, prompt) | |
| try: | |
| result = future.result(timeout=timeout_seconds) | |
| executor.shutdown(wait=False) | |
| return result | |
| except concurrent.futures.TimeoutError: | |
| logger.warning(f"⏳ Gemini tardó más de {timeout_seconds}s. Se corta la espera y se activa el Motor Local.") | |
| executor.shutdown(wait=False, cancel_futures=True) | |
| raise TimeoutError("Tiempo límite de IA superado.") | |
| except Exception as e: | |
| executor.shutdown(wait=False) | |
| raise e | |
| # --- CARGA DE DATOS EN MEMORIA --- | |
| def load_data(): | |
| try: | |
| path_alimentos = os.path.join('DATOS', 'alimentos_enriquecido.json') | |
| path_foodb_index = os.path.join('DATOS', 'foodb_index_smart.json') | |
| data_alimentos = [] | |
| if os.path.exists(path_alimentos): | |
| 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: | |
| logger.error(f"Error cargando archivos de datos: {e}") | |
| return [], {} | |
| alimentos_data, foodb_index = load_data() | |
| def get_foodb_lower_map(_foodb_index): | |
| if not _foodb_index: | |
| return {} | |
| return {str(k).lower(): k for k in _foodb_index.keys()} | |
| foodb_lower_map = get_foodb_lower_map(foodb_index) | |
| def get_global_learned_cache(): | |
| return {} | |
| global_learned_cache = get_global_learned_cache() | |
| # ============================================================================== | |
| # DICCIONARIOS MASTER BIOMÉDICOS | |
| # ============================================================================== | |
| FOOD_TO_COMPOUND_MAP = { | |
| # CEREALES Y GRANOS | |
| "trigo": ["gluten", "fodmaps", "lectinas", "fitatos"], | |
| "pan": ["gluten", "fodmaps", "lectinas"], | |
| "pasta": ["gluten", "fodmaps"], | |
| "galletas": ["gluten", "fodmaps", "azúcar refinado"], | |
| "cebada": ["gluten", "fodmaps", "fitatos"], | |
| "centeno": ["gluten", "fodmaps", "fitatos"], | |
| "avena": ["fodmaps", "avenina", "fitatos"], | |
| "arroz": ["fitatos", "lectinas"], | |
| "maíz": ["zeína", "lectinas", "fitatos", "fodmaps"], | |
| "quinoa": ["saponinas", "oxalatos", "fitatos"], | |
| "alforfón": ["oxalatos", "fagopirina"], | |
| # LÁCTEOS | |
| "leche": ["caseína", "lactosa", "fodmaps"], | |
| "queso": ["tiramina", "histamina", "caseína", "lactosa"], | |
| "queso fresco": ["caseína", "lactosa"], | |
| "queso maduro": ["tiramina", "histamina", "caseína"], | |
| "yogur": ["histamina", "caseína", "lactosa"], | |
| "mantequilla": ["caseína", "lactosa"], | |
| "crema de leche": ["caseína", "lactosa"], | |
| "helado": ["lactosa", "caseína", "azúcar refinado"], | |
| # PLATOS Y TÍPICOS DE COLOMBIA | |
| "bandeja paisa": ["fodmaps", "lectinas", "fitatos", "saponinas", "inhibidores_proteasas", "histamina", "tiramina", "nitritos", "purinas", "zeína", "albúmina", "grasas saturadas"], | |
| "ajiaco": ["fodmaps", "solanina", "lactosa", "caseína", "zeína", "purinas", "salicilatos", "histamina"], | |
| "sancocho": ["fodmaps", "solanina", "histamina", "tiramina", "purinas", "zeína", "fitatos"], | |
| "tamal": ["fodmaps", "lectinas", "zeína", "histamina", "purinas", "salicilatos", "albúmina", "nitritos", "fitatos", "inhibidores_proteasas"], | |
| "changua": ["lactosa", "caseína", "albúmina", "gluten", "fodmaps", "compuestos azufrados"], | |
| "empanada": ["gluten", "zeína", "solanina", "lectinas"], | |
| "arepa": ["zeína", "lectinas", "fitatos"], | |
| "lechona": ["histamina", "nitritos", "grasas saturadas", "purinas"], | |
| # FRUTAS Y TROPICALES | |
| "noni": ["escopoletina", "fenoles", "salicilatos", "fructosa", "histamina"], | |
| "chontaduro": ["fenoles", "fructosa", "grasas saturadas", "tiramina", "salicilatos"], | |
| "naranja": ["fructosa", "histamina", "salicilatos", "fenoles"], | |
| "jugo de naranja": ["fructosa", "histamina", "salicilatos", "fenoles"], | |
| "limón": ["salicilatos", "histamina", "fenoles", "ácido cítrico"], | |
| "manzana": ["fructosa", "sorbitol", "fodmaps", "salicilatos"], | |
| "pera": ["fructosa", "sorbitol", "fodmaps", "salicilatos"], | |
| "plátano": ["tiramina", "histamina", "salicilatos", "fenoles"], | |
| "banano": ["tiramina", "histamina", "salicilatos", "fenoles"], | |
| "aguacate": ["histamina", "tiramina", "salicilatos", "fodmaps"], | |
| "fresa": ["histamina", "salicilatos", "oxalatos"], | |
| "mora": ["salicilatos", "oxalatos", "histamina", "taninos"], | |
| "uva": ["fructosa", "salicilatos", "histamina", "taninos"], | |
| "piña": ["salicilatos", "histamina", "fructosa"], | |
| "mango": ["fructosa", "salicilatos", "histamina"], | |
| "lulo": ["salicilatos", "fructosa", "ácido cítrico"], | |
| "maracuyá": ["fructosa", "histamina", "salicilatos"], | |
| "maracuya": ["fructosa", "histamina", "salicilatos"], | |
| "feijoa": ["fructosa", "salicilatos", "taninos"], | |
| "tomate de árbol": ["solanina", "histamina", "salicilatos"], | |
| "uchuva": ["solanina", "histamina", "salicilatos", "oxalatos"], | |
| # VERDURAS Y SOLANÁCEAS | |
| "tomate": ["histamina", "solanina", "salicilatos", "glutamato"], | |
| "papa": ["solanina", "oxalatos", "lectinas"], | |
| "patata": ["solanina", "oxalatos", "lectinas"], | |
| "berenjena": ["solanina", "histamina", "salicilatos"], | |
| "pimiento": ["capsaicina", "solanina", "salicilatos"], | |
| "espinaca": ["oxalatos", "nitratos", "histamina"], | |
| "cebolla": ["fodmaps", "compuestos azufrados", "salicilatos"], | |
| "ajo": ["fodmaps", "compuestos azufrados"], | |
| "brócoli": ["fodmaps", "compuestos azufrados", "salicilatos", "goitrógenos"], | |
| "yuca": ["fodmaps", "fitatos"], | |
| "ñame": ["oxalatos", "fitatos", "fodmaps"], | |
| # LEGUMBRES (PERFIL COMPLETO) | |
| "frijol": ["fodmaps", "lectinas", "fitatos", "saponinas", "inhibidores_proteasas", "purinas", "oxalatos", "taninos"], | |
| "frijoles": ["fodmaps", "lectinas", "fitatos", "saponinas", "inhibidores_proteasas", "purinas", "oxalatos", "taninos"], | |
| "frijol paisa": ["fodmaps", "lectinas", "fitatos", "saponinas", "inhibidores_proteasas", "purinas", "oxalatos", "taninos"], | |
| "lentejas": ["fodmaps", "lectinas", "fitatos", "saponinas", "inhibidores_proteasas", "purinas", "oxalatos", "taninos"], | |
| "garbanzos": ["fodmaps", "lectinas", "fitatos", "saponinas", "inhibidores_proteasas", "purinas", "oxalatos"], | |
| "arvejas": ["fodmaps", "lectinas", "fitatos", "inhibidores_proteasas"], | |
| "soja": ["soja", "isoflavonas", "lectinas", "fitatos", "fodmaps", "saponinas", "goitrógenos", "inhibidores_proteasas"], | |
| "tofu": ["soja", "isoflavonas", "lectinas", "fitatos"], | |
| "haba": ["vicina_convicina", "fodmaps", "lectinas", "fitatos", "purinas"], | |
| "habas": ["vicina_convicina", "fodmaps", "lectinas", "fitatos", "purinas"], | |
| # CARNES, EMBUTIDOS Y BEBIDAS | |
| "carne de cerdo": ["histamina", "alfa-gal", "grasas saturadas", "purinas"], | |
| "cerdo": ["histamina", "alfa-gal", "grasas saturadas", "purinas"], | |
| "chicharrón": ["histamina", "grasas saturadas"], | |
| "chicharron": ["histamina", "grasas saturadas"], | |
| "carne de res": ["histamina", "alfa-gal", "purinas"], | |
| "res": ["histamina", "alfa-gal", "purinas"], | |
| "café": ["cafeína", "salicilatos", "oxalatos"], | |
| "cafe": ["cafeína", "salicilatos", "oxalatos"], | |
| "té": ["cafeína", "taninos", "oxalatos", "salicilatos"], | |
| "chocolate": ["teobromina", "feniletilamina", "tiramina", "histamina", "oxalatos", "cafeína"], | |
| "vino": ["histamina", "tiramina", "sulfitos", "taninos", "alcohol"], | |
| "cerveza": ["gluten", "histamina", "sulfitos", "alcohol", "purinas"], | |
| "embutidos": ["histamina", "tiramina", "nitritos", "glutamato"], | |
| "pescado": ["histamina", "purinas", "albúmina"], | |
| "huevo": ["albúmina"] | |
| } | |
| FOOD_NAME_TO_FOODB_KEY = { | |
| "alforfón": ["buckwheat"], "arroz": ["rice"], "avena": ["oat", "oats"], "cebada": ["barley"], | |
| "queso": ["cheese"], "leche": ["milk"], "yogur": ["yogurt"], "mantequilla": ["butter"], | |
| "vino": ["wine"], "cerveza": ["beer"], "chocolate": ["chocolate", "cocoa"], | |
| "tomate": ["tomato"], "aguacate": ["avocado"], "plátano": ["banana", "plantain"], "banano": ["banana"], | |
| "naranja": ["orange"], "pan": ["bread", "wheat"], "trigo": ["wheat"], "café": ["coffee"], "cafe": ["coffee"], | |
| "manzana": ["apple"], "fresa": ["strawberry"], "uva": ["grape"], "piña": ["pineapple"], | |
| "papa": ["potato"], "patata": ["potato"], "frijol": ["bean", "beans", "kidney bean", "black bean"], | |
| "frijoles": ["bean", "beans"], "frijol paisa": ["bean", "pork", "rice"], | |
| "lentejas": ["lentil"], "garbanzos": ["chickpea", "garbanzo"], "arveja": ["pea"], | |
| "haba": ["broad bean", "fava bean"], "habas": ["broad bean", "fava bean"], | |
| "bandeja paisa": ["bean", "pork", "rice", "banana", "egg", "avocado"], | |
| "ajiaco": ["potato", "chicken", "corn", "milk", "cream"], | |
| "sancocho": ["cassava", "plantain", "potato", "pork", "beef", "chicken"], | |
| "tamal": ["corn", "pork", "chicken", "pea"], | |
| "changua": ["milk", "egg", "bread", "onion", "coriander"] | |
| } | |
| STRICT_COMPOUND_TRIGGERS = { | |
| "cafeína": ["cafeína", "caffeine", "café", "cafe", "coffee"], | |
| "teobromina": ["teobromina", "theobromine", "cacao", "chocolate"], | |
| "solanina": ["solanina", "solanine", "solanáceas", "tomate", "patata", "papa"], | |
| "capsaicina": ["capsaicina", "capsaicin", "chile", "ají", "picante"], | |
| "gluten": ["gluten", "trigo", "wheat", "cebada", "centeno"], | |
| "caseína": ["caseína", "casein", "queso", "lácteos"], | |
| "soja": ["soja", "soya", "soy", "tofu", "tempeh"], | |
| "albúmina": ["albúmina", "albumin", "huevo"], | |
| "lactosa": ["lactosa", "lactose"], | |
| "fructosa": ["fructosa", "fructose"], | |
| "fodmaps": ["fodmap", "fodmaps", "fructanos", "galactanos", "sorbitol", "rafinosa", "estaquiosa"], | |
| "histamina": ["histamina", "histamine", "dao"], | |
| "tiramina": ["tiramina", "tyramine"], | |
| "oxalatos": ["oxalato", "oxalatos", "oxalate"], | |
| "saponinas": ["saponina", "saponinas"], | |
| "lectinas": ["lectina", "lectinas", "frijoles", "legumbres"], | |
| "fitatos": ["fitato", "fitatos", "ácido fítico"], | |
| "salicilatos": ["salicilato", "salicilatos"], | |
| "sulfitos": ["sulfito", "sulfitos"], | |
| "glutamato": ["glutamato", "glutamate", "msg"], | |
| "nitritos": ["nitrito", "nitritos", "embutidos"], | |
| "alcohol": ["alcohol", "etanol", "cerveza", "vino"], | |
| "purinas": ["purina", "purinas", "ácido úrico", "gota"], | |
| "inhibidores_proteasas": ["inhibidor de tripsina", "inhibidores de proteasas", "tripsina"], | |
| "vicina_convicina": ["vicina", "convicina", "favismo", "g6pd", "haba", "habas"] | |
| } | |
| MEDICAL_CONDITION_SAFEGUARDS = { | |
| "farmacos_interacciones": { | |
| "condition_triggers": ["fármaco", "farmaco", "medicamento", "imao", "anticoagulante"], | |
| "required_user_keywords": ["fármaco", "farmaco", "medicamento", "imao", "pastilla", "remedio", "receta"] | |
| }, | |
| "respiratorio_asma": { | |
| "condition_triggers": ["asma", "sibilancia", "broncoespasmo"], | |
| "required_user_keywords": ["asma", "tos", "respirar", "pecho", "sibilancia", "ahogo"] | |
| }, | |
| "dermatologico_alergia": { | |
| "condition_triggers": ["urticaria", "angioedema", "anafilaxia", "dermatitis"], | |
| "required_user_keywords": ["piel", "pica", "picazón", "manchas", "rojo", "alergia", "ronchas"] | |
| }, | |
| "articular_gota": { | |
| "condition_triggers": ["gota", "artritis", "hiperuricemia"], | |
| "required_user_keywords": ["articulación", "coyuntura", "dedo gordo", "rodilla", "gota"] | |
| }, | |
| "renal_litiasis": { | |
| "condition_triggers": ["litiasis", "cálculo renal", "piedras en el riñón"], | |
| "required_user_keywords": ["riñón", "riñon", "orina", "orinar", "cálculo", "piedra"] | |
| }, | |
| "embarazo_lactancia": { | |
| "condition_triggers": ["embarazo", "gestación", "lactancia"], | |
| "required_user_keywords": ["embarazada", "embarazo", "bebé", "lactando"] | |
| }, | |
| "hepato_vesicular": { | |
| "condition_triggers": ["vesícula", "vesicula", "colelitiasis", "hígado"], | |
| "required_user_keywords": ["vesícula", "vesicula", "hígado", "higado", "piel amarilla"] | |
| } | |
| } | |
| INTEGRATED_NEURO_FOOD_MAP = { | |
| "gluten": { | |
| "efecto_neuropsicologico": "En individuos sensibles, puede desencadenar neuroinflamación manifestada como niebla mental, depresión, ansiedad, migrañas y ataxia.", | |
| "fuentes_comunes": ["pan", "trigo", "pasta", "cebada", "centeno"] | |
| }, | |
| "caseína": { | |
| "efecto_neuropsicologico": "Sus péptidos (casomorfinas) pueden actuar sobre receptores opioides en el SNC, afectando la cognición y el estado de ánimo.", | |
| "fuentes_comunes": ["leche", "queso", "yogur"] | |
| }, | |
| "fodmaps": { | |
| "efecto_neuropsicologico": "Mediante el eje intestino-cerebro, la fermentación intestinal excesiva por oligosacáridos (GOS/rafinosa) genera distensión visceral, enviando señales de estrés por el nervio vago que aumentan la ansiedad e hipersensibilidad.", | |
| "fuentes_comunes": ["frijoles", "lentejas", "cebolla", "ajo", "trigo"] | |
| }, | |
| "inhibidores_proteasas": { | |
| "efecto_neuropsicologico": "Impiden la descomposición enzimática adecuada de proteínas, produciendo indigestión severa, hipoperfusión gastrointestinal y letargo posprandial.", | |
| "fuentes_comunes": ["frijoles", "legumbres mal cocidas", "soja"] | |
| }, | |
| "vicina_convicina": { | |
| "efecto_neuropsicologico": "En personas con deficiencia de la enzima G6PD (favismo), desencadena estrés oxidativo eritrocitario que produce cefalea vascular, astenia y mareos.", | |
| "fuentes_comunes": ["habas"] | |
| }, | |
| "tiramina": { | |
| "efecto_neuropsicologico": "Amina vasoactiva. Provoca vasoconstricción inicial seguida de vasodilatación cerebral, desencadenando migrañas.", | |
| "fuentes_comunes": ["quesos madurados", "vino tinto", "embutidos"] | |
| }, | |
| "histamina": { | |
| "efecto_neuropsicologico": "Neurotransmisor estimulante central. Su exceso por bajo DAO provoca migrañas, insomnio, fragmentación del sueño y ansiedad.", | |
| "fuentes_comunes": ["queso", "vino tinto", "tomate", "pescado azul", "conservas"] | |
| }, | |
| "salicilatos": { | |
| "efecto_neuropsicologico": "En personas sensibles provocan estimulación excesiva del SNC, hiperactividad e insomnio.", | |
| "fuentes_comunes": ["cítricos", "manzanas", "tomate", "especias", "té"] | |
| }, | |
| "fenoles": { | |
| "efecto_neuropsicologico": "La sobrecarga de fenoles satura las vías de sulfatación hepática, acumulando catecolaminas que producen agitación nocturna e insomnio.", | |
| "fuentes_comunes": ["jugo de naranja", "chontaduro", "noni", "té"] | |
| }, | |
| "cafeína": { | |
| "efecto_neuropsicologico": "Bloquea los receptores de adenosina en el cerebro. Provoca hiperactivación del SNC, ansiedad e insomnio.", | |
| "fuentes_comunes": ["café", "té negro", "energizantes", "chocolate"] | |
| }, | |
| "lactosa": { | |
| "efecto_neuropsicologico": "La malabsorción provoca disbiosis e inflamación sistémica leve que se traduce en fatiga y letargo posprandial.", | |
| "fuentes_comunes": ["leche", "queso fresco", "crema de leche"] | |
| }, | |
| "fructosa": { | |
| "efecto_neuropsicologico": "Disminuye la biodisponibilidad de triptófano en el intestino, interfiriendo en la síntesis de serotonina y melatonina.", | |
| "fuentes_comunes": ["jugos concentrados", "miel", "frutas muy dulces"] | |
| }, | |
| "solanina": { | |
| "efecto_neuropsicologico": "Glicoalcaloide con leve efecto inhibidor de la acetilcolinesterasa; puede provocar letargo y rigidez corporal.", | |
| "fuentes_comunes": ["tomate", "patata", "berenjena"] | |
| } | |
| } | |
| CONSEJOS_POR_COMPUESTO = { | |
| "fodmaps": "Para reducir los GOS (rafinosa y estaquiosa) de los frijoles y legumbres, realiza un remojo de 12 a 24 horas desechando el agua, y cocina a alta presión adicionando comino o epazote.", | |
| "inhibidores_proteasas": "La cocción prolongada y el calor de la olla a presión inactivan casi por completo los inhibidores de tripsina en legumbres.", | |
| "vicina_convicina": "Si padeces favismo (deficiencia de G6PD), evita de forma estricta el consumo de habas.", | |
| "gluten": "La respuesta al gluten debe diferenciar Enfermedad Celíaca, SGNC y alergia al trigo. Realiza pruebas serológicas antes de retirarlo.", | |
| "lactosa": "La intolerancia se debe a baja lactasa. Se puede manejar con lactasa exógena o productos deslactosados.", | |
| "histamina": "La acumulación de histamina se asocia a bajo DAO. Una dieta baja en histamina y rica en vitamina B6 y C ayuda a restaurar el equilibrio.", | |
| "tiramina": "Controlar el consumo de alimentos añejados previene picos de presión arterial y cefaleas.", | |
| "salicilatos": "Moderar el consumo concentrado de jugos cítricos y especias ayuda a estabilizar el sueño.", | |
| "cafeína": "Moderar su consumo antes de mitad de tarde protege la arquitectura del sueño." | |
| } | |
| CONSEJO_GENERICO_DEFAULT = "Cada organismo posee una individualidad bioquímica única según su perfil enzimático y digestivo. Monitorear tus respuestas personales te permitirá adaptar la dieta a tu umbral de tolerancia." | |
| FOODB_EFFECTS_TRANSLATION = { | |
| "flatugenic": "genera gases/flatulencia", "antioxidant": "antioxidante", | |
| "hypercholesterolemic": "puede elevar colesterol", "triglycerigenic": "puede elevar triglicéridos", | |
| "uricogenic": "puede elevar ácido úrico", "vasodilator": "vasodilatador (posible migraña)", | |
| "laxative": "efecto laxante", "diuretic": "efecto diurético", | |
| "stimulant": "estimulante del SNC", "antiinflammatory": "antiinflamatorio" | |
| } | |
| COMPOUND_SYNONYM_MAP = { | |
| "gluten": ["gluten", "gliadin"], "caseína": ["casein"], "lactosa": ["lactose"], "fructosa": ["fructose"] | |
| } | |
| MASTER_SYMPTOM_MAP = { | |
| "dolor abdominal": { | |
| "frases_es": ["dolor de estómago", "dolor de panza", "dolor abdominal", "retortijones", "cólicos", "gastritis", "acidez", "reflujo", "pesadez"], | |
| "keywords_en": ["abdominal", "pain", "stomach", "cramp"] | |
| }, | |
| "hinchazón": { | |
| "frases_es": ["hinchazón abdominal", "vientre hinchado", "distensión abdominal", "hinchazón", "distensión", "inflamado", "gases", "flatulencia"], | |
| "keywords_en": ["bloating", "distension", "flatulence"] | |
| }, | |
| "diarrea": { | |
| "frases_es": ["diarrea", "diarreas", "soltura", "estómago suelto", "deposiciones líquidas"], | |
| "keywords_en": ["diarrhea", "loose stools"] | |
| }, | |
| "náuseas": { | |
| "frases_es": ["náuseas", "nauseas", "vómito", "vomito", "ganas de vomitar"], | |
| "keywords_en": ["nausea", "vomiting"] | |
| }, | |
| "dolor de cabeza": { | |
| "frases_es": ["dolor de cabeza", "migraña", "cefalea", "jaqueca", "me duele la cabeza"], | |
| "keywords_en": ["headache", "migraine"] | |
| }, | |
| "ansiedad": { | |
| "frases_es": ["ansiedad", "nerviosismo", "angustia", "irritabilidad"], | |
| "keywords_en": ["anxiety"] | |
| }, | |
| "insomnio": { | |
| "frases_es": ["insomnio", "no puedo dormir", "desvelado", "no duermo", "desvelo"], | |
| "keywords_en": ["insomnia"] | |
| }, | |
| "alergia cutánea": { | |
| "frases_es": ["piel", "rasca", "picazón", "urticaria", "erupción", "alergia", "ronchas"], | |
| "keywords_en": ["rash", "itching"] | |
| } | |
| } | |
| # ============================================================================== | |
| # FUNCIONES AUXILIARES Y DE BÚSQUEDA | |
| # ============================================================================== | |
| def sanitize_text(text): | |
| if not text: | |
| return "" | |
| text_clean = re.sub(r'[.,;()¿?¡!:"\'\-]', ' ', text).lower() | |
| text_clean = re.sub(r'\s+', ' ', text_clean).strip() | |
| return text_clean | |
| STOPWORDS_ES = { | |
| "de", "la", "el", "los", "las", "un", "una", "unos", "unas", "y", "o", "u", | |
| "en", "con", "por", "para", "que", "se", "su", "sus", "al", "del", "lo", | |
| "como", "es", "son", "muy", "mas", "más", "no", "si", "sí", "tu", "mi", | |
| "me", "te", "le", "les", "nos", "cuando", "donde", "porque", "pero", | |
| "tomo", "como", "da" | |
| } | |
| def extract_keywords(text): | |
| words = re.findall(r'\b\w+\b', sanitize_text(text)) | |
| return set(w for w in words if w not in STOPWORDS_ES and len(w) > 2) | |
| def get_compounds_for_food(food_name, food_map): | |
| if not food_name: | |
| return [] | |
| food_clean = food_name.lower().strip() | |
| if food_clean in food_map: | |
| return food_map[food_clean] | |
| for suffix in ["es", "s"]: | |
| if food_clean.endswith(suffix): | |
| norm = food_clean[:-len(suffix)] | |
| if norm in food_map: | |
| return food_map[norm] | |
| fuzzy_matches = difflib.get_close_matches(food_clean, list(food_map.keys()), n=1, cutoff=0.6) | |
| if fuzzy_matches: | |
| return food_map[fuzzy_matches[0]] | |
| matched_compounds = set() | |
| for key, compounds in food_map.items(): | |
| if re.search(r'\b' + re.escape(key) + r'\b', food_clean) or food_clean in key: | |
| matched_compounds.update(compounds) | |
| if matched_compounds: | |
| return list(matched_compounds) | |
| inferred = set() | |
| if any(w in food_clean for w in ["frijol", "lenteja", "garbanzo", "legumbre", "soja", "haba", "arveja"]): | |
| inferred.update(["fodmaps", "lectinas", "fitatos", "saponinas", "inhibidores_proteasas", "purinas"]) | |
| elif any(w in food_clean for w in ["carne", "cerdo", "res", "pollo", "pavo", "chicharrón"]): | |
| inferred.update(["histamina", "purinas"]) | |
| elif any(w in food_clean for w in ["jugo", "zumo", "batido", "fruta"]): | |
| inferred.update(["fructosa", "salicilatos", "histamina", "fenoles"]) | |
| elif any(w in food_clean for w in ["queso", "leche", "crema", "yogur"]): | |
| inferred.update(["caseína", "lactosa", "histamina", "tiramina"]) | |
| return list(inferred) | |
| def validar_seguro_logico(entry, user_foods, user_query, food_compound_map): | |
| if not user_foods: | |
| return True | |
| user_query_clean = user_query.lower() if user_query else "" | |
| texto_condicion = (entry.get("condicion_asociada", "") + " " + entry.get("compuesto_alimento", "")).lower() | |
| for category, rules in MEDICAL_CONDITION_SAFEGUARDS.items(): | |
| if any(trig in texto_condicion for trig in rules["condition_triggers"]): | |
| tiene_contexto = any(req_kw in user_query_clean for req_kw in rules["required_user_keywords"]) | |
| if not tiene_contexto: | |
| return False | |
| user_compounds = set() | |
| for food in user_foods: | |
| comps = get_compounds_for_food(food, food_compound_map) | |
| user_compounds.update(comps) | |
| user_compounds.add(food.lower().strip()) | |
| for compound_key, synonyms in STRICT_COMPOUND_TRIGGERS.items(): | |
| if any(syn in texto_condicion for syn in synonyms): | |
| tiene_compuesto = any( | |
| syn in user_compounds or any(syn in c.lower() for c in user_compounds) | |
| for syn in synonyms | |
| ) | |
| if not tiene_compuesto: | |
| return False | |
| return True | |
| def extract_entities_with_gemini(query): | |
| gemini_client = get_gemini_client() | |
| if not gemini_client: | |
| return {"alimentos": [], "sintomas": []} | |
| system_prompt = f""" | |
| Rol: Extractor Médico JSON de alimentos y síntomas. | |
| Extrae un JSON estricto con dos claves: "alimentos" (lista) y "sintomas" (lista). | |
| Input: "{query}" | |
| """ | |
| try: | |
| raw_text, _ = generate_content_with_strict_timeout(gemini_client, system_prompt, timeout_seconds=3.5) | |
| text = raw_text | |
| if "```json" in text: | |
| text = text.split("```json")[1].split("```")[0] | |
| elif "```" in text: | |
| text = text.split("```")[1].split("```")[0] | |
| return json.loads(text) | |
| except Exception as e: | |
| logger.warning(f"Extracción por IA no disponible o cancelada ({e}). Se usa extracción directa local.") | |
| return {"alimentos": [], "sintomas": []} | |
| def reinforce_entities_with_keywords(entities, query, food_map, master_symptom_map): | |
| if not entities: | |
| entities = {"alimentos": [], "sintomas": []} | |
| query_sanitized = sanitize_text(query) | |
| current_foods = set(entities.get("alimentos", [])) | |
| for food_keyword in food_map.keys(): | |
| if re.search(r'\b' + re.escape(food_keyword) + r'\b', query_sanitized): | |
| current_foods.add(food_keyword) | |
| entities["alimentos"] = list(current_foods) | |
| current_symptoms = set(entities.get("sintomas", [])) | |
| for main_symptom, details in master_symptom_map.items(): | |
| for phrase in details.get("frases_es", []): | |
| if phrase in query_sanitized: | |
| current_symptoms.add(main_symptom) | |
| break | |
| entities["sintomas"] = list(current_symptoms) | |
| return entities | |
| def translate_symptoms_local(symptoms_list, master_symptom_map): | |
| if not symptoms_list: | |
| return [] | |
| translated_symptoms = set() | |
| inverse_index = {} | |
| for clinical_term, details in master_symptom_map.items(): | |
| inverse_index[clinical_term.lower()] = clinical_term | |
| for phrase in details.get("frases_es", []): | |
| inverse_index[phrase.lower()] = clinical_term | |
| for symptom in symptoms_list: | |
| symptom_clean = symptom.lower().strip() | |
| if symptom_clean in inverse_index: | |
| translated_symptoms.add(inverse_index[symptom_clean]) | |
| else: | |
| translated_symptoms.add(symptom) | |
| return list(translated_symptoms) | |
| def find_best_matches_hybrid(entities, data, user_query="", food_compound_map=None): | |
| if not entities or not data: | |
| return [] | |
| user_symptoms_list = entities.get("sintomas", []) | |
| user_foods_list = entities.get("alimentos", []) | |
| if not user_foods_list: | |
| return [] | |
| expanded_foods = list(user_foods_list) | |
| if food_compound_map: | |
| for food in user_foods_list: | |
| expanded_foods.extend(get_compounds_for_food(food, food_compound_map)) | |
| food_keywords = extract_keywords(" ".join(expanded_foods)) | |
| symptom_keywords = extract_keywords(" ".join(user_symptoms_list)) | |
| results = [] | |
| for entry in data: | |
| if food_compound_map and not validar_seguro_logico(entry, user_foods_list, user_query, food_compound_map): | |
| continue | |
| db_food_keywords = extract_keywords(entry.get("compuesto_alimento", "")) | |
| db_symptom_keywords = extract_keywords(" ".join(entry.get("sintomas_clave", []))) | |
| food_intersection = food_keywords.intersection(db_food_keywords) | |
| symptom_intersection = symptom_keywords.intersection(db_symptom_keywords) | |
| if not food_intersection or not symptom_intersection: | |
| continue | |
| score = (len(food_intersection) ** 2) * 10 + (len(symptom_intersection) ** 2) * 5 | |
| if score > 10: | |
| results.append({ | |
| 'entry': entry, | |
| 'score': {'total': score}, | |
| 'matched_keywords': list(food_intersection | symptom_intersection) | |
| }) | |
| return sorted(results, key=lambda x: x['score']['total'], reverse=True) | |
| def build_local_hypothesis(entities, food_compound_map, neuro_map): | |
| user_foods = entities.get("alimentos", []) | |
| user_symptoms = entities.get("sintomas", []) | |
| if not user_foods or not user_symptoms: | |
| return None | |
| candidates = [] | |
| for food in user_foods: | |
| comps = get_compounds_for_food(food, food_compound_map) | |
| for compound in comps: | |
| info = neuro_map.get(compound, {}) | |
| effect = info.get('efecto_neuropsicologico', f'Sustancia bioactiva ({compound}) en {food}') | |
| candidates.append({"alimento": food, "compuesto": compound, "efecto": effect}) | |
| compuestos_unicos = sorted(set(c['compuesto'] for c in candidates)) if candidates else ["Compuestos bioactivos"] | |
| alimentos_unicos = sorted(set(user_foods)) | |
| compuestos_str = ", ".join(compuestos_unicos) | |
| alimentos_str = ", ".join(alimentos_unicos).capitalize() | |
| condicion = f"Hipersensibilidad o Intolerancia a {compuestos_str} en {alimentos_str}" | |
| mecanismo = " | ".join(sorted(set(c['efecto'] for c in candidates))) if candidates else "Proceso de malabsorción o reactividad digestiva/metabólica." | |
| return { | |
| "condicion_asociada": condicion, | |
| "mecanismo_fisiologico": mecanismo, | |
| "compuesto_alimento": f"{alimentos_str} ({compuestos_str})", | |
| "sintomas_clave": user_symptoms, | |
| "es_hipotesis_local": True | |
| } | |
| def find_best_foodb_matches(user_foods_es, _foodb_index, _foodb_lower_map, food_name_map, limit=3): | |
| if not user_foods_es or not _foodb_index: | |
| return [] | |
| search_terms_en = set() | |
| for food_es in user_foods_es: | |
| food_es_clean = food_es.lower().strip() | |
| for key_es, value_en_list in food_name_map.items(): | |
| if key_es in food_es_clean or food_es_clean in key_es: | |
| search_terms_en.update(value_en_list) | |
| if not search_terms_en: | |
| return [] | |
| found_matches = [] | |
| for term in search_terms_en: | |
| term_lower = term.lower().strip() | |
| if term_lower in _foodb_lower_map: | |
| found_matches.append(_foodb_lower_map[term_lower]) | |
| continue | |
| for k_lower, orig_key in _foodb_lower_map.items(): | |
| if term_lower in k_lower: | |
| found_matches.append(orig_key) | |
| if len(found_matches) >= limit: | |
| break | |
| seen = set() | |
| unique = [] | |
| for item in found_matches: | |
| if item not in seen: | |
| seen.add(item) | |
| unique.append(item) | |
| return unique[:limit] | |
| # ============================================================================== | |
| # GENERADORES DE INFORMES | |
| # ============================================================================== | |
| def generate_local_fallback_analysis(query, match, entities, food_compound_map, neuro_map): | |
| saludo = "### 📋 Informe de Análisis Clínico-Nutricional (Modo Local / Alta Disponibilidad)\n" | |
| 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" | |
| user_foods = entities.get("alimentos", []) | |
| user_symptoms = entities.get("sintomas", []) | |
| compuestos_detectados = set() | |
| for food in user_foods: | |
| comps = get_compounds_for_food(food, food_compound_map) | |
| if comps: | |
| compuestos_detectados.update(comps) | |
| compuestos_str = ", ".join(sorted(list(compuestos_detectados))) if compuestos_detectados else match.get('compuesto_alimento', 'compuestos bioactivos') | |
| 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 **{compuestos_str}**.\n" | |
| detalles_compuestos = [] | |
| for comp in compuestos_detectados: | |
| if comp in neuro_map: | |
| detalles_compuestos.append(f"- **{comp.capitalize()}**: {neuro_map[comp]['efecto_neuropsicologico']}") | |
| if detalles_compuestos: | |
| causa += "\n**Mecanismo bioquímico:**\n" + "\n".join(detalles_compuestos) + "\n" | |
| evitar = f"\n### 🛑 Alimentos a Moderar o Evitar\n- **Alimentos evaluados:** {', '.join(user_foods).capitalize()}\n- Productos ricos en **{compuestos_str}**.\n" | |
| alternativas = "\n### 💡 Sugerencias\n- Llevar un diario de síntomas registrando alimentos y horas de consumo.\n- Reducir la ingesta del alimento para evaluar remisión de síntomas.\n" | |
| consejo_texto = None | |
| for comp in compuestos_detectados: | |
| comp_clean = comp.lower().strip() | |
| if comp_clean in CONSEJOS_POR_COMPUESTO: | |
| consejo_texto = CONSEJOS_POR_COMPUESTO[comp_clean] | |
| break | |
| if not consejo_texto: | |
| consejo_texto = CONSEJO_GENERICO_DEFAULT | |
| consejo = f"\n### 📌 Consejo Práctico Final\n{consejo_texto}\n" | |
| return saludo + hipotesis + causa + evitar + alternativas + consejo + TEXTO_BANDERAS_ROJAS | |
| def generate_detailed_analysis(query, match, entities=None, food_compound_map=None, neuro_map=None): | |
| gemini_client = get_gemini_client() | |
| if not gemini_client: | |
| return generate_local_fallback_analysis(query, match, entities, food_compound_map, neuro_map) | |
| compound_context = "" | |
| if entities and food_compound_map and neuro_map: | |
| lines = [] | |
| for food in entities.get("alimentos", []): | |
| comps = get_compounds_for_food(food, food_compound_map) | |
| for compound in comps: | |
| if compound in neuro_map: | |
| lines.append(f"- {food.capitalize()} contiene {compound}: {neuro_map[compound]['efecto_neuropsicologico']}") | |
| if lines: | |
| compound_context = "Compuestos detectados:\n" + "\n".join(lines) | |
| prompt = f""" | |
| Rol: Nutricionista Funcional. | |
| Caso: {query} | |
| Hipótesis clínica: {match.get('condicion_asociada')} | |
| Mecanismo: {match.get('mecanismo_fisiologico')} | |
| {compound_context} | |
| Escribe un análisis estructurado en Markdown (saludo, hipótesis, moléculas responsables, alimentos a evitar, alternativas y consejo). | |
| NO incluyas descargos de responsabilidad. | |
| """ | |
| try: | |
| raw_text, used_model = generate_content_with_strict_timeout(gemini_client, prompt, timeout_seconds=3.5) | |
| return f"🤖 **Análisis Redactado por IA Gemini ({used_model}):**\n\n" + raw_text + TEXTO_BANDERAS_ROJAS | |
| except Exception as e: | |
| logger.warning(f"Gemini no respondió en < 3.5s ({e}). Se activa la respuesta en modo local.") | |
| return generate_local_fallback_analysis(query, match, entities, food_compound_map, neuro_map) | |
| def generate_neuro_report_text(entities, food_map, neuro_map): | |
| report_lines = ["\n### 🧠 Efectos Neuropsicológicos Posibles"] | |
| user_foods = entities.get("alimentos", []) | |
| relevant_compounds = set() | |
| if user_foods: | |
| for food in user_foods: | |
| comps = get_compounds_for_food(food, food_map) | |
| relevant_compounds.update(comps) | |
| found = False | |
| if relevant_compounds: | |
| for compound in sorted(list(relevant_compounds)): | |
| if compound in neuro_map: | |
| found = True | |
| report_lines.append(f"**{compound.capitalize()}**: _{neuro_map[compound]['efecto_neuropsicologico']}_") | |
| if not found: | |
| report_lines.append("No se detectaron efectos neuropsicológicos específicos directos.") | |
| return "\n".join(report_lines) | |
| def generate_molecular_report_text(best_match, entities, _foodb_index, _foodb_lower_map, food_name_map, synonym_map, food_compound_map, neuro_map): | |
| report_lines = ["\n### 🔬 Análisis Molecular y Efectos Bioactivos"] | |
| user_foods = entities.get("alimentos", []) | |
| if not user_foods: | |
| return "No hay alimentos seleccionados." | |
| found_any = False | |
| for food in user_foods: | |
| compounds = get_compounds_for_food(food, food_compound_map) | |
| if compounds: | |
| found_any = True | |
| report_lines.append(f"\n**{food.capitalize()}** — compuestos clave identificados:") | |
| for compound in compounds: | |
| if compound in neuro_map: | |
| report_lines.append(f"- **{compound.capitalize()}**: {neuro_map[compound]['efecto_neuropsicologico']}") | |
| else: | |
| report_lines.append(f"- **{compound.capitalize()}**") | |
| if _foodb_index: | |
| best_food_matches = find_best_foodb_matches(user_foods, _foodb_index, _foodb_lower_map, food_name_map) | |
| for food_key in best_food_matches: | |
| compounds = _foodb_index.get(food_key, []) | |
| if not compounds: | |
| continue | |
| found_any = True | |
| report_lines.append(f"\n**{food_key.capitalize()}** (Efectos en FooDB):") | |
| for item in compounds[:5]: | |
| if isinstance(item, dict): | |
| comp_name = item.get('compound', 'N/A') | |
| effects = item.get('effects', []) | |
| eff_translated = [FOODB_EFFECTS_TRANSLATION.get(str(e).lower().strip(), str(e)) for e in effects[:4]] | |
| eff_str = f" — *Efectos: {', '.join(eff_translated)}*" if eff_translated else "" | |
| report_lines.append(f"- **{comp_name}**{eff_str}") | |
| if not found_any: | |
| report_lines.append("No se encontraron registros moleculares adicionales.") | |
| return "\n".join(report_lines) | |
| def generate_word_report(report_text): | |
| if not report_text: | |
| return None | |
| try: | |
| doc = docx.Document() | |
| doc.add_paragraph(report_text) | |
| doc_io = BytesIO() | |
| doc.save(doc_io) | |
| doc_io.seek(0) | |
| return doc_io.getvalue() | |
| except Exception as e: | |
| logger.error(f"Error generando informe Word: {e}") | |
| return None | |
| def create_relevance_chart(results): | |
| top_results = results[:10] | |
| data = pd.DataFrame({ | |
| "Condición": [re.sub(r'\(.*\)', '', r['entry'].get('condicion_asociada', '')).strip() for r in top_results], | |
| "Relevancia": [r.get('score', {}).get('total', 50) for r in top_results] | |
| }) | |
| return alt.Chart(data).mark_bar().encode( | |
| x='Relevancia', y=alt.Y('Condición', sort='-x'), tooltip=['Condición', 'Relevancia'] | |
| ).properties(title='Top Coincidencias') | |
| # ============================================================================== | |
| # INTERFAZ STREAMLIT | |
| # ============================================================================== | |
| 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 'main_analysis' not in st.session_state: st.session_state.main_analysis = "" | |
| if 'neuro_text_cache' not in st.session_state: st.session_state.neuro_text_cache = "" | |
| if 'mol_text_cache' not in st.session_state: st.session_state.mol_text_cache = "" | |
| if 'word_bytes_cache' not in st.session_state: st.session_state.word_bytes_cache = None | |
| if 'has_searched' not in st.session_state: st.session_state.has_searched = False | |
| col_img1, col_text, col_img2 = st.columns([1, 4, 1]) | |
| 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 tus SÍNTOMAS y los ALIMENTOS que sospechas.") | |
| with col_img2: | |
| if os.path.exists("buho.png"): st.image("buho.png", width=120) | |
| gemini_test = get_gemini_client() | |
| if gemini_test: | |
| st.caption("🟢 **Motor de IA Gemini Conectado:** Extracción y Redacción Activadas (Timeout: 3.5s)") | |
| else: | |
| st.caption("🟡 **Modo Local / Alta Disponibilidad:** Activo") | |
| with st.form(key="search_form", clear_on_submit=False): | |
| query_input = st.text_area( | |
| "Tu Caso:", | |
| height=150, | |
| placeholder="Ej: Me da diarrea e hinchazón si como frijoles o una bandeja paisa.", | |
| key="input_query_text" | |
| ) | |
| submitted = st.form_submit_button("Analizar Caso", type="primary") | |
| if submitted: | |
| if query_input and query_input.strip(): | |
| # Limpieza explícita del estado para búsquedas consecutivas sin congelamiento | |
| st.session_state.search_results = None | |
| st.session_state.main_analysis = "" | |
| st.session_state.neuro_text_cache = "" | |
| st.session_state.mol_text_cache = "" | |
| st.session_state.word_bytes_cache = None | |
| current_query = query_input.strip() | |
| st.session_state.user_query = current_query | |
| st.session_state.has_searched = True | |
| with st.spinner("🔍 Analizando caso con corta-fuegos activado..."): | |
| raw_entities = extract_entities_with_gemini(current_query) | |
| reinforced = reinforce_entities_with_keywords(raw_entities, current_query, FOOD_TO_COMPOUND_MAP, MASTER_SYMPTOM_MAP) | |
| final_symptoms = translate_symptoms_local(reinforced.get("sintomas", []), MASTER_SYMPTOM_MAP) | |
| final_entities = {"alimentos": reinforced.get("alimentos", []), "sintomas": final_symptoms} | |
| st.session_state.entities = final_entities | |
| results = find_best_matches_hybrid(final_entities, alimentos_data, current_query, FOOD_TO_COMPOUND_MAP) | |
| if not results: | |
| local_hypothesis = build_local_hypothesis(final_entities, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP) | |
| if local_hypothesis: | |
| results = [{'entry': local_hypothesis, 'score': {'total': 50}, 'matched_keywords': []}] | |
| st.session_state.search_results = results if results else [] | |
| if st.session_state.search_results: | |
| best_match = st.session_state.search_results[0]['entry'] | |
| st.session_state.main_analysis = generate_detailed_analysis( | |
| st.session_state.user_query, best_match, | |
| st.session_state.entities, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP | |
| ) | |
| st.session_state.neuro_text_cache = generate_neuro_report_text( | |
| st.session_state.entities, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP | |
| ) | |
| st.session_state.mol_text_cache = generate_molecular_report_text( | |
| best_match, st.session_state.entities, foodb_index, foodb_lower_map, | |
| FOOD_NAME_TO_FOODB_KEY, COMPOUND_SYNONYM_MAP, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP | |
| ) | |
| st.session_state.word_bytes_cache = generate_word_report(f"REPORTE DE SALUD\n\n{st.session_state.main_analysis}") | |
| else: | |
| st.warning("⚠️ Por favor describe tus síntomas o alimentos consumidos antes de analizar.") | |
| # --- MOSTRAR RESULTADOS --- | |
| if st.session_state.has_searched: | |
| results = st.session_state.search_results | |
| if results and len(results) > 0: | |
| best_match = results[0]['entry'] | |
| st.success(f"🔎 Coincidencia Principal: **{best_match.get('condicion_asociada')}**") | |
| tab_main, tab_neuro, tab_mol = st.tabs(["💡 Interpretación Clínica", "🧠 Efectos Neuropsicológicos", "🔬 Análisis Molecular"]) | |
| with tab_main: | |
| st.markdown(st.session_state.get('main_analysis', 'Análisis no disponible.')) | |
| if len(results) > 1: | |
| st.altair_chart(create_relevance_chart(results), use_container_width=True) | |
| with tab_neuro: | |
| st.markdown(st.session_state.get('neuro_text_cache', 'No hay datos neuropsicológicos.')) | |
| with tab_mol: | |
| st.markdown(st.session_state.get('mol_text_cache', 'No hay datos moleculares.')) | |
| word_bytes = st.session_state.get('word_bytes_cache') | |
| if word_bytes: | |
| st.download_button( | |
| label="📄 Descargar Informe Word", | |
| data=word_bytes, | |
| file_name="Reporte_Detective_Alimentos.docx", | |
| mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", | |
| key="dl_btn_report" | |
| ) | |
| else: | |
| st.warning("⚠️ No pudimos identificar una causa clara para este caso específico.") | |
| st.info("Intenta especificar el alimento y el síntoma. Ej: 'Siento [SÍNTOMA] cuando como [ALIMENTO]'.") |