Spaces:
Sleeping
Sleeping
| # ==================== El Detective de Alimentos (Versión 10.1) ===================================== | |
| # 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 | |
| import time | |
| st.set_page_config( | |
| page_title="El Detective de Alimentos", | |
| page_icon="🍎", | |
| layout="wide" | |
| ) | |
| # --- CONFIGURACIÓN Y CARGA DE DATOS --- | |
| # (Esta sección no tiene cambios) | |
| 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-1.5-flash") | |
| 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) | |
| lista_condiciones = sorted(list(set(item['condicion_asociada'] for item in data_alimentos))) | |
| 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, lista_condiciones, foodb_index | |
| except Exception as e: | |
| st.error(f"Error cargando los archivos de datos: {e}") | |
| return None, None, None | |
| alimentos_data, lista_condiciones, foodb_index = load_data() | |
| FOOD_TO_COMPOUND_MAP = { | |
| # --- CEREALES Y GRANOS --- | |
| "pan": ["gluten"], "trigo": ["gluten"], "harina": ["gluten"], "cebada": ["gluten"], "centeno": ["gluten"], "pasta": ["gluten"], "galletas": ["gluten"], "pizza": ["gluten"], "torta": ["gluten"], "pastel": ["gluten"], "avena": ["gluten"], # Avena es sensible para celíacos por contaminación cruzada y avenina | |
| "maíz": ["lectinas", "aflatoxinas"], "arroz": ["lectinas"], "quinoa": ["saponinas", "oxalatos", "fitatos"], "trigo sarraceno": ["oxalatos"], "alforfón": ["oxalatos"], | |
| # --- LÁCTEOS Y DERIVADOS --- | |
| "leche": ["lácteos", "caseína", "lactosa"], "queso": ["lácteos", "caseína", "lactosa", "histamina", "tiramina"], "yogur": ["lácteos", "caseína", "lactosa"], "mantequilla": ["lácteos", "caseína", "lactosa"], "crema": ["lácteos"], "helado": ["lácteos"], | |
| # --- VEGETALES --- | |
| "tomate": ["histamina", "solaninas", "lectinas"], "pimiento": ["solaninas", "lectinas", "capsaicina"], "berenjena": ["solaninas", "lectinas", "histamina"], "patata": ["solaninas", "lectinas"], | |
| "cebolla": ["fodmaps", "fructanos"], "ajo": ["fodmaps", "fructanos"], | |
| "brócoli": ["salicilatos", "goitrógenos", "fodmaps", "fructanos"], "coliflor": ["goitrógenos", "fodmaps", "polioles"], "repollo": ["goitrógenos", "fodmaps", "fructanos"], "col": ["goitrógenos", "fodmaps", "fructanos"], | |
| "espinaca": ["oxalatos", "histamina", "goitrógenos"], "acelga": ["oxalatos"], "remolacha": ["oxalatos", "fodmaps", "gos"], | |
| "legumbres": ["fodmaps", "gos", "lectinas", "fitatos"], "lentejas": ["fodmaps", "gos", "lectinas", "fitatos"], "garbanzos": ["fodmaps", "gos", "lectinas", "fitatos"], "frijoles": ["fodmaps", "gos", "lectinas", "fitatos"], "guisantes": ["fodmaps", "gos", "lectinas", "fitatos"], | |
| "soja": ["alérgenos", "fitatos", "goitrógenos", "lectinas"], "soya": ["alérgenos", "fitatos", "goitrógenos", "lectinas"], "tofu": ["fitatos", "goitrógenos", "lectinas"], "edamame": ["fitatos", "goitrógenos", "lectinas"], | |
| "aguacate": ["fodmaps", "polioles", "histamina"], | |
| "calabaza": ["salicilatos", "oxalatos", "lectinas", "fodmaps", "fructanos", "gos", "polioles"], "calabacín": ["lectinas", "fodmaps", "fructanos"], | |
| "champiñón": ["fodmaps", "polioles"], "setas": ["fodmaps", "polioles"], | |
| "espárrago": ["fodmaps", "fructanos", "purinas"], "alcachofa": ["fodmaps", "fructanos"], | |
| "pepino": ["lectinas"], "zanahoria": ["salicilatos"], "apio": ["fodmaps", "polioles"], | |
| # --- FRUTAS --- | |
| "manzana": ["salicilatos", "fructosa", "fodmaps", "polioles"], "pera": ["fructosa", "fodmaps", "polioles"], "mango": ["fructosa", "fodmaps"], "cereza": ["fructosa", "salicilatos", "fodmaps", "polioles"], "sandía": ["fructosa", "fodmaps"], | |
| "uvas": ["salicilatos", "fructosa"], "pasas": ["salicilatos", "fructosa", "histamina"], "dátil": ["fructosa", "fodmaps", "fructanos"], "higo": ["fructosa", "fodmaps", "fructanos"], | |
| "naranja": ["salicilatos", "ácidos"], "limón": ["salicilatos", "ácidos"], "pomelo": ["salicilatos", "ácidos"], "mandarina": ["salicilatos", "ácidos"], | |
| "fresa": ["salicilatos", "histamina"], "arándano": ["salicilatos"], "frambuesa": ["salicilatos"], "mora": ["salicilatos"], | |
| "plátano": ["tiramina", "histamina"], "piña": ["salicilatos", "ácidos", "histamina"], "kiwi": ["salicilatos", "alérgenos", "histamina"], | |
| "ciruela": ["fodmaps", "polioles"], "melocotón": ["fodmaps", "polioles"], "albaricoque": ["fodmaps", "polioles"], "nectarina": ["fodmaps", "polioles"], | |
| "melón": ["fructosa", "fodmaps"], "papaya": ["histamina"], | |
| # --- PROTEÍNAS (CARNES, PESCADOS, HUEVOS) --- | |
| "carne": ["alfa-gal", "purinas", "hierro", "histamina"], "carnes rojas": ["purinas", "alfa-gal", "hierro"], "hígado": ["purinas", "hierro", "vitamina a"], "embutidos": ["histamina", "tiramina", "nitritos"], "salchicha": ["histamina", "tiramina", "nitritos"], | |
| "pollo": ["purinas"], "pavo": ["purinas"], | |
| "huevo": ["alérgenos"], | |
| "pescado": ["histamina", "purinas"], "pescado enlatado": ["histamina"], "atún en lata": ["histamina"], "salmón": ["purinas", "histamina"], "sardinas": ["histamina", "purinas"], "anchoas": ["purinas", "histamina"], | |
| "mariscos": ["purinas", "sulfitos", "alérgenos", "yodo", "níquel"], "camarón": ["alérgenos", "yodo"], "gamba": ["alérgenos", "yodo"], | |
| # --- FRUTOS SECOS Y SEMILLAS --- | |
| "nueces": ["arginina", "salicilatos", "níquel", "oxalatos", "fitatos"], "almendras": ["salicilatos", "arginina", "oxalatos", "fitatos"], | |
| "maní": ["alérgenos", "arginina", "lectinas", "aflatoxinas"], "cacahuetes": ["alérgenos", "arginina", "lectinas", "aflatoxinas"], | |
| "anacardo": ["alérgenos", "fodmaps", "gos", "fructanos", "oxalatos"], "pistacho": ["alérgenos", "fodmaps", "fructanos"], | |
| "avellana": ["alérgenos", "oxalatos", "fitatos"], | |
| "sésamo": ["alérgenos", "oxalatos", "fitatos"], "chía": ["fitatos", "oxalatos"], "lino": ["fitatos"], | |
| # --- BEBIDAS Y DULCES --- | |
| "café": ["cafeína", "ácidos"], "té": ["cafeína", "taninos", "oxalatos"], | |
| "cerveza": ["gluten", "histamina", "tiramina", "purinas"], "vino": ["histamina", "tiramina", "sulfitos"], "vino tinto": ["histamina", "tiramina", "sulfitos"], "vino rojo": ["histamina", "tiramina", "sulfitos"], | |
| "chocolate": ["cafeína", "tiramina", "níquel", "arginina", "oxalatos"], | |
| "azucar": ["azúcar", "fructosa"], "dulces": ["azúcar"], "refrescos": ["azúcar", "fructosa"], "gaseosas": ["azúcar", "fructosa"], "miel": ["fructosa", "fodmaps"], "jarabe de maiz": ["fructosa"], | |
| "edulcorantes": ["polioles", "fodmaps"], | |
| # --- OTROS Y CONDIMENTOS --- | |
| "cúrcuma": ["salicilatos"], "jengibre": ["salicilatos"], | |
| "chucrut": ["histamina", "tiramina"], | |
| "vinagre": ["histamina", "sulfitos"], "mostaza": ["salicilatos", "goitrógenos"], | |
| "aceitunas": ["histamina", "salicilatos"], | |
| "caldo": ["histamina", "glutamato"] | |
| } | |
| CONDITION_SYNONYMS = { | |
| "síndrome del intestino irritable (sii).": ["intolerancia a los fodmaps", "intolerancia a los gos (fodmap)", "intolerancia a gomas fermentables"], | |
| "gota / hiperuricemia.": ["acido urico aumentado"], | |
| "intolerancia a la lactosa.": ["déficit de lactasa"], | |
| "enfermedad celíaca (clásica).": ["dermatitis herpetiforme"], | |
| "migraña.": ["dolor de cabeza", "cefalea"], | |
| "sensibilidad al gluten no celíaca (sgnc).": ["intolerancia al gluten", "sensibilidad al trigo"], | |
| "histaminosis / intolerancia a la histamina.": ["déficit de dao", "acumulación de histamina"], | |
| "enfermedad por reflujo gastroesofágico (erge).": ["reflujo", "acidez estomacal", "ardor de estómago"], | |
| "hipotiroidismo.": ["tiroides hipoactiva", "tiroides lenta"], | |
| "alergia al níquel.": ["dermatitis de contacto por niquel", "reacción al níquel"] | |
| } | |
| 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"] | |
| } | |
| SYMPTOM_KEYWORD_MAP = { | |
| "dolor de cabeza": ["dolor de cabeza", "cefalea"], | |
| "hinchazón": ["hinchazón", "distensión", "inflamado"], | |
| "gases": ["gases", "flatulencia"], | |
| "diarrea": ["diarrea"], | |
| "dolor": ["dolor"], | |
| "fatiga": ["fatiga", "cansancio"], | |
| "náuseas": ["náuseas", "mareo"], | |
| "vómitos": ["vómitos"], | |
| "erupción": ["erupción", "ronchas", "urticaria"], | |
| "acidez": ["acidez", "reflujo", "ardor de estómago"] | |
| } | |
| def reinforce_entities_with_keywords(entities, query, food_map, symptom_map): | |
| """ | |
| Revisa la salida de la IA y añade alimentos Y síntomas clave si fueron omitidos. | |
| Esta versión es más inteligente para evitar la doble contabilización de síntomas. | |
| """ | |
| if not entities: | |
| entities = {"alimentos": [], "sintomas": []} | |
| query_sanitized = sanitize_text(query) | |
| # --- Reforzar Alimentos (Lógica existente, sin cambios) --- | |
| current_foods = entities.get("alimentos", []) | |
| 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)) | |
| # --- LÓGICA MEJORADA: Reforzar Síntomas (Evita redundancia) --- | |
| current_symptoms = entities.get("sintomas", []) | |
| if current_symptoms is None: current_symptoms = [] | |
| # Usamos una copia de la consulta para poder modificarla | |
| query_to_search_symptoms = query_sanitized | |
| # Ordenamos las palabras clave de síntomas por longitud, de la más larga a la más corta | |
| # Esto asegura que "dolor de cabeza" se procese antes que "dolor" | |
| sorted_symptom_keywords = sorted(symptom_map.keys(), key=len, reverse=True) | |
| for symptom_keyword in sorted_symptom_keywords: | |
| # Si encontramos el síntoma más específico... | |
| if symptom_keyword in query_to_search_symptoms: | |
| logger.info(f"Red de seguridad (Síntoma): Añadiendo '{symptom_keyword}'.") | |
| current_symptoms.append(symptom_keyword) | |
| # ...lo eliminamos de la cadena de búsqueda para que sus sub-partes no se vuelvan a encontrar. | |
| query_to_search_symptoms = query_to_search_symptoms.replace(symptom_keyword, "") | |
| entities["sintomas"] = list(set(current_symptoms)) | |
| return entities | |
| def sanitize_text(text): | |
| if not text: return "" | |
| return re.sub(r'[.,;()]', '', text).lower().strip() | |
| def extract_and_infer_with_gemini(query, condiciones): | |
| if not model: return None | |
| condiciones_str = "\n".join([f"- {c}" for c in condiciones]) | |
| system_prompt = f""" | |
| Eres un asistente de triaje clínico experto. Tu tarea es analizar la consulta de un usuario y extraer tres tipos de información: | |
| 1. `alimentos`: Lista de alimentos consumidos. | |
| 2. `sintomas`: Lista de síntomas descritos. | |
| 3. `condicion_probable`: Tu mejor inferencia sobre cuál de las siguientes condiciones podría explicar la **conexión directa entre los alimentos y los síntomas mencionados**. Debes elegir la opción más lógica de la lista. Si ninguna encaja bien, puedes dejarlo en blanco. | |
| LISTA DE CONDICIONES POSIBLES: | |
| {condiciones_str} | |
| Devuelve la respuesta ÚNICAMENTE en formato JSON estricto. | |
| Ejemplo: si el usuario dice "vino tinto y dolor de cabeza", la condición probable es "Intolerancia a la Quercetina." o "Migraña.", no "Intolerancia a la Lactosa.". | |
| 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) | |
| return json.loads(json_text) | |
| except Exception as e: | |
| logger.error(f"Error en la extracción/inferencia con Gemini: {e}") | |
| st.error(f"Hubo un problema al interpretar tu consulta con la IA.") | |
| return None | |
| 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", [])) | |
| inferred_condition_raw = sanitize_text(entities.get("condicion_probable", "")) | |
| candidate_terms = set(user_foods) | |
| for food in user_foods: | |
| food_sanitized = sanitize_text(food) | |
| if food_sanitized in FOOD_TO_COMPOUND_MAP: | |
| candidate_terms.update(c.lower() for c in FOOD_TO_COMPOUND_MAP[food_sanitized]) | |
| if food_sanitized in ["vino", "vino tinto", "vino rojo"]: | |
| candidate_terms.update(["histamina", "tiramina", "sulfitos"]) | |
| 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 = {'condition': 0, 'food': 15, 'symptoms': 0} | |
| if inferred_condition_raw: | |
| entry_condition_sanitized = sanitize_text(entry.get("condicion_asociada", "")) | |
| is_match = (entry_condition_sanitized == inferred_condition_raw) | |
| if not is_match and entry_condition_sanitized in CONDITION_SYNONYMS: | |
| if inferred_condition_raw in [sanitize_text(s) for s in CONDITION_SYNONYMS.get(entry_condition_sanitized, [])]: | |
| is_match = True | |
| if is_match: | |
| score_details['condition'] = 100 | |
| 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 += 10 | |
| matched_symptoms.append(key) | |
| break | |
| score_details['symptoms'] = symptom_score | |
| score_details['total'] = score_details['condition'] + score_details['food'] + score_details['symptoms'] | |
| results.append({ | |
| 'entry': entry, | |
| 'score': score_details, | |
| 'matched_symptoms': list(set(matched_symptoms)) | |
| }) | |
| if not results: return [] | |
| sorted_results = sorted(results, key=lambda x: x['score']['total'], reverse=True) | |
| return sorted_results | |
| # --- REEMPLAZA ESTA FUNCIÓN COMPLETA --- | |
| def generate_detailed_analysis(query, match): | |
| if not model: return "Error: El modelo de IA no está disponible." | |
| # Comprobación de seguridad: si 'match' es inválido, no podemos continuar. | |
| if not match or not isinstance(match, dict): | |
| logger.error("Se intentó generar un análisis detallado con datos de coincidencia inválidos.") | |
| return "No se pudo generar el análisis detallado debido a un error interno (datos de coincidencia inválidos)." | |
| # Preparamos una lista de síntomas clave desde nuestra base de datos para enriquecer el prompt | |
| sintomas_clave_texto = ", ".join(match.get("sintomas_clave", ["No especificados"])) | |
| prompt_parts = [ | |
| "Eres un asistente de IA experto en nutrición personalizada y comunicación médica. Tu objetivo es explicar conceptos complejos de forma sencilla y empática.", | |
| f'El usuario ha descrito el siguiente caso: "{query}"', | |
| f'Tu sistema ha identificado una posible conexión con "{match.get("condicion_asociada", "N/A")}". Los síntomas clave asociados en la base de datos para esta condición son: {sintomas_clave_texto}.', | |
| f'El mecanismo fisiológico es: "{match.get("mecanismo_fisiologico", "No especificado")}".', | |
| f'Las recomendaciones generales son: "{match.get("recomendaciones_examenes", "No especificadas")}".', | |
| f'Otros alimentos implicados son: "{match.get("compuesto_alimento", "No especificados")}".', | |
| "\n**Tu Tarea:** Redacta una respuesta excepcional para el usuario usando Markdown. Sigue esta estructura OBLIGATORIAMENTE:", | |
| f'### Posible Causa: {match.get("condicion_asociada", "Condición no especificada")}', | |
| f'Hola. Entiendo que te preocupa lo que sientes. Basado en los síntomas y alimentos que mencionaste, existe una posible relación con una condición conocida como **{match.get("condicion_asociada", "esta condición")}**.', | |
| f'### ¿Qué podría estar pasando en tu cuerpo?', | |
| f'Explica el mecanismo fisiológico mencionado anteriormente en términos muy sencillos, usando una analogía si es posible.', | |
| ] | |
| if match.get("efecto_benefico") and match.get("contexto_negativo"): | |
| prompt_parts.append( | |
| "\n### El Doble Filo de Estos Alimentos\n" | |
| f'**Para la mayoría de las personas:** Resume por qué estos alimentos son generalmente buenos, basándote en: "{match.get("efecto_benefico")}"\n' | |
| f'**Sin embargo, en ciertos contextos:** Explica por qué pueden ser problemáticos para el usuario, basándote en: "{match.get("contexto_negativo")}"' | |
| ) | |
| prompt_parts.extend([ | |
| "\n### Pasos a Seguir y Recomendaciones", | |
| "Aquí te dejo algunas recomendaciones generales. Es fundamental que las converses con un profesional de la salud:", | |
| f'* **[Punto 1 de las recomendaciones, reformulado como un consejo práctico basado en las recomendaciones generales mencionadas.]**', | |
| f'* **[Punto 2 de las recomendaciones, enfocado en los exámenes si los hay, basado en las recomendaciones generales mencionadas.]**', | |
| f'* **Atención a otros alimentos:** Ten en cuenta que, además de lo que mencionaste, otros alimentos implicados en esta condición son: **[menciona 2-3 ejemplos del campo de alimentos implicados]**.', | |
| "\n### **IMPORTANTE: Descargo de Responsabilidad**", | |
| "Este análisis es una herramienta informativa y de orientación basada en inteligencia artificial. **NO es un diagnóstico médico.** La información proporcionada no debe sustituir la consulta, el diagnóstico o el tratamiento de un médico o profesional de la salud cualificado. Consulta siempre a un experto para evaluar tu caso particular." | |
| ]) | |
| prompt = "\n".join(prompt_parts) | |
| try: | |
| response = model.generate_content(prompt) | |
| if response.text and len(response.text) > 1: | |
| return response.text | |
| else: | |
| logger.error("La respuesta de Gemini para el análisis detallado fue vacía o inválida.") | |
| return "No se pudo generar el análisis detallado. La respuesta de la IA fue inválida." | |
| except Exception as e: | |
| logger.error(f"Error generando análisis detallado con Gemini: {e}") | |
| return "No se pudo generar el análisis detallado debido a un problema con la API." | |
| def create_relevance_chart(results): | |
| top_results = results[:5] | |
| 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='Posible Condición', 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 = [] | |
| report_lines.append("="*50) | |
| report_lines.append("INFORME DEL DETECTIVE DE ALIMENTOS") | |
| report_lines.append("="*50) | |
| report_lines.append(f"Fecha: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") | |
| report_lines.append(f"CONSULTA ORIGINAL DEL USUARIO:\n'{query}'\n") | |
| report_lines.append("-"*50) | |
| if results: | |
| best_match = results[0]['entry'] | |
| report_lines.append("PRINCIPAL COINCIDENCIA ENCONTRADA:\n") | |
| report_lines.append(f"Condición: {best_match.get('condicion_asociada', 'N/A')}") | |
| report_lines.append(f"Mecanismo Posible: {best_match.get('mecanismo_fisiologico', 'N/A')}") | |
| report_lines.append(f"Recomendaciones Generales: {best_match.get('recomendaciones_examenes', 'N/A')}\n") | |
| if len(results) > 1: | |
| report_lines.append("-"*50) | |
| report_lines.append("OTRAS POSIBILIDADES CONSIDERADAS (DIAGNÓSTICO DIFERENCIAL):\n") | |
| for i, res in enumerate(results[1:4]): | |
| entry = res['entry'] | |
| report_lines.append(f"{i+2}. {entry.get('condicion_asociada', 'N/A')} (Puntuación: {res['score']['total']})") | |
| report_lines.append("\n" + "="*50) | |
| report_lines.append("IMPORTANTE: Este informe es generado por una herramienta de IA y no constituye un diagnóstico médico...") | |
| return "\n".join(report_lines) | |
| # --- INTERFAZ DE USUARIO Y LÓGICA PRINCIPAL --- | |
| 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") | |
| submitted = st.form_submit_button("Analizar mi caso", type="primary") | |
| if submitted: | |
| st.session_state.start_analysis = True | |
| if st.session_state.start_analysis: | |
| # Inmediatamente bajamos la bandera para evitar ejecuciones repetidas. | |
| 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: | |
| with st.spinner("🧠 Interpretando tu caso y buscando pistas con IA..."): | |
| entities = extract_and_infer_with_gemini(query_to_analyze, lista_condiciones) | |
| 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"IA identificó - Alimentos: {', '.join(entities.get('alimentos',[])) or 'Ninguno'}, Síntomas: {', '.join(entities.get('sintomas',[])) or 'Ninguno'}" | |
| if entities.get("condicion_probable"): info_str += f", Condición Probable: {entities.get('condicion_probable')}" | |
| 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.") | |
| st.session_state.search_results = [] | |
| # La sección de mostrar resultados permanece igual, se ejecuta si hay resultados en el estado. | |
| 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}'.") | |
| 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_alimentos_{datetime.now().strftime('%Y%m%d')}.txt", | |
| mime="text/plain" | |
| ) | |
| st.subheader("Análisis de Relevancia de las Coincidencias") | |
| chart = create_relevance_chart(results) | |
| st.altair_chart(chart, 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): | |
| col1, col2 = st.columns([3, 1]) | |
| with col1: | |
| st.markdown("##### Desglose de la Puntuación de Relevancia:") | |
| score_col1, score_col2, score_col3, score_col4 = st.columns(4) | |
| score_col1.metric("Puntos por Condición", f"{best_match_data['score']['condition']}") | |
| score_col2.metric("Puntos por Alimento", f"{best_match_data['score']['food']}") | |
| score_col3.metric("Puntos por Síntomas", f"{best_match_data['score']['symptoms']}") | |
| score_col4.metric("PUNTUACIÓN TOTAL", f"{best_match_data['score']['total']}", delta="Máxima coincidencia") | |
| with col2: | |
| st.write("") | |
| if foodb_index: | |
| with st.popover("🔬 Principales componentes moleculares (En Ingles"): | |
| user_foods_mentioned = st.session_state.entities.get("alimentos", []) | |
| if not user_foods_mentioned: | |
| st.info("El usuario no especificó un alimento, no se puede realizar la búsqueda molecular.") | |
| 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("Sin datos moleculares para este alimento.") | |
| st.markdown("---") | |
| with st.spinner("✍️ Generando un análisis personalizado con IA..."): | |
| if 'best_match_analysis' not in st.session_state.analysis_cache: | |
| st.session_state.analysis_cache['best_match_analysis'] = generate_detailed_analysis(st.session_state.user_query, best_match) | |
| st.markdown(st.session_state.analysis_cache['best_match_analysis']) | |
| 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')}..."): | |
| st.session_state.analysis_cache[analysis_key] = generate_detailed_analysis(st.session_state.user_query, entry) | |
| 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("---") |