Spaces:
Running
Running
| # ==================== El Detective de Alimentos (Versión 3.0 - Refinada) ===================================== | |
| # Mejoras: Base de conocimiento de alimentos expandida, búsqueda de síntomas flexible y generación de respuestas mejorada. | |
| import streamlit as st | |
| import google.generativeai as genai | |
| import google.api_core.exceptions | |
| import os | |
| import json | |
| import logging | |
| import re | |
| st.set_page_config( | |
| page_title="El Detective de Alimentos", | |
| page_icon="🍎", | |
| layout="wide" | |
| ) | |
| # Configurar logging | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
| logger = logging.getLogger("food_detective_app") | |
| # --- CONFIGURACIÓN DE GEMINI --- | |
| 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. Por favor, configúrala en los Secrets de Streamlit.") | |
| logger.error("GEMINI_API_KEY no encontrada.") | |
| st.stop() | |
| genai.configure(api_key=GEMINI_API_KEY) | |
| logger.info("✅ Configuración de Gemini API realizada.") | |
| except Exception as e: | |
| st.error(f"❌ Error al configurar Gemini API: {e}") | |
| logger.error(f"Error al configurar Gemini API: {e}") | |
| st.stop() | |
| def get_gemini_model(): | |
| """Carga el modelo generativo de Gemini.""" | |
| logger.info("🔄 Cargando modelo Gemini (gemini-1.5-flash)...") | |
| try: | |
| return genai.GenerativeModel("gemini-1.5-flash") | |
| except Exception as e: | |
| st.error(f"❌ No se pudo cargar el modelo Gemini: {e}") | |
| logger.error(f"Error cargando modelo Gemini: {e}") | |
| return None | |
| model = get_gemini_model() | |
| def load_data(): | |
| """Carga la base de datos enriquecida y extrae la lista de condiciones.""" | |
| try: | |
| path_alimentos = os.path.join('DATOS', 'alimentos_enriquecido.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))) | |
| logger.info(f"✅ Base de datos enriquecida cargada con {len(data_alimentos)} registros.") | |
| return data_alimentos, lista_condiciones | |
| except FileNotFoundError: | |
| st.error("❌ Error: No se encontró el archivo 'alimentos_enriquecido.json'. Asegúrate de que existe en la carpeta 'DATOS'.") | |
| return None, None | |
| except json.JSONDecodeError: | |
| st.error("❌ Error: El archivo 'alimentos_enriquecido.json' tiene un formato incorrecto.") | |
| return None, None | |
| alimentos_data, lista_condiciones = load_data() | |
| # DICCIONARIO DE TRADUCCIÓN AMPLIADO | |
| FOOD_TO_COMPOUND_MAP = { | |
| # Gluten | |
| "pan": ["gluten"], "trigo": ["gluten"], "harina de trigo": ["gluten"], "cebada": ["gluten"], | |
| "centeno": ["gluten"], "pasta": ["gluten"], "galletas": ["gluten"], "avena": ["gluten"], "pizza": ["gluten"], "torta": ["gluten"], | |
| # Lácteos | |
| "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"], | |
| # Fenoles y Salicilatos | |
| "manzana": ["salicilatos", "fructosa"], "almendras": ["salicilatos"], "uvas": ["salicilatos"], "pasas": ["salicilatos"], | |
| "naranja": ["salicilatos"], "brócoli": ["salicilatos", "goitrógenos"], "cúrcuma": ["salicilatos"], | |
| # Azúcares y Fructosa | |
| "azucar": ["azúcar", "fructosa"], "dulces": ["azúcar"], "refrescos": ["azúcar", "fructosa"], "gaseosas": ["azúcar", "fructosa"], | |
| "miel": ["fructosa"], "jarabe de maiz": ["fructosa"], | |
| # Aminas (Histamina, Tiramina) | |
| "vino tinto": ["histamina", "tiramina", "sulfitos"], "vino rojo": ["histamina", "tiramina", "sulfitos"], | |
| "cerveza": ["histamina", "tiramina", "purinas"], "chocolate": ["cafeína", "tiramina", "níquel"], | |
| "embutidos": ["histamina", "tiramina", "nitritos"], "pescado enlatado": ["histamina"], "tomate": ["histamina", "solaninas"], | |
| # Otros | |
| "carne": ["alfa-gal", "proteínas", "purinas", "hierro"], "carnes rojas": ["purinas", "alfa-gal", "hierro"], | |
| "mariscos": ["purinas", "sulfitos", "alérgenos", "yodo"], "huevo": ["alérgenos"], "soya": ["alérgenos"], | |
| "café": ["cafeína", "ácidos"] | |
| } | |
| # --- LÓGICA DE BÚSQUEDA Y ANÁLISIS (ENFOQUE HÍBRIDO REFINADO) --- | |
| def extract_and_infer_with_gemini(query, condiciones): | |
| """Extrae entidades e infiere una condición probable.""" | |
| 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. Analiza la consulta, extrae 'alimentos', 'sintomas' (normalizados) y la 'condicion_probable' más relevante de la lista proporcionada. Devuelve solo un JSON. Si no puedes inferir una condición, déjalo como un string vacío. | |
| LISTA DE CONDICIONES POSIBLES: | |
| {condiciones_str} | |
| 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): | |
| """Motor de búsqueda híbrido (v3.0) con puntuación ponderada y búsqueda de síntomas flexible.""" | |
| if not entities or not data: return [] | |
| user_symptoms = set(s.lower().strip() for s in entities.get("sintomas", [])) | |
| user_foods = set(f.lower().strip() for f in entities.get("alimentos", [])) | |
| inferred_condition = entities.get("condicion_probable", "").lower().strip() | |
| 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]) | |
| scores = {} | |
| for index, entry in enumerate(data): | |
| score = 0 | |
| # Ponderación 1: Coincidencia de Condición | |
| entry_condition = entry.get("condicion_asociada", "").lower().strip() | |
| if inferred_condition and inferred_condition == entry_condition: | |
| score += 100 | |
| # Ponderación 2: Coincidencia de Alimentos/Compuestos | |
| entry_compounds_text = entry.get("compuesto_alimento", "").lower() | |
| if any(term in entry_compounds_text for term in candidate_terms): | |
| score += 20 | |
| # Ponderación 3: Coincidencia de Síntomas (BÚSQUEDA FLEXIBLE) | |
| entry_symptoms_keys = set(s.lower().strip() for s in entry.get("sintomas_clave", [])) | |
| for user_symptom in user_symptoms: | |
| for key in entry_symptoms_keys: | |
| if key in user_symptom or user_symptom in key: | |
| score += 5 | |
| break # Evita sumar puntos múltiples veces por el mismo síntoma de usuario | |
| if score > 0: | |
| scores[index] = score | |
| if not scores: return [] | |
| sorted_matches_indices = sorted(scores.keys(), key=scores.get, reverse=True) | |
| return [data[i] for i in sorted_matches_indices] | |
| def generate_detailed_analysis(query, match): | |
| """Genera la explicación final para el usuario (PROMPT MEJORADO).""" | |
| if not model: return "Error: El modelo de IA no está disponible." | |
| prompt = f""" | |
| Eres un asistente de IA experto en nutrición y bienestar. Tu tono es empático, claro y muy educativo. NO actúas como un médico, sino como un guía informativo. | |
| El usuario te ha contado esto: "{query}" | |
| Tu sistema ha encontrado la siguiente posible conexión: | |
| - Condición: {match.get("condicion_asociada")} | |
| - Mecanismo: {match.get("mecanismo_fisiologico")} | |
| - Recomendaciones: {match.get("recomendaciones_examenes")} | |
| - Alimentos Implicados: {match.get("compuesto_alimento")} | |
| **Tu Tarea:** Redacta una respuesta excepcional para el usuario usando Markdown. Sigue esta estructura OBLIGATORIAMENTE: | |
| ### Posible Causa: {match.get("condicion_asociada")} | |
| Hola. Entiendo que te preocupa lo que sientes después de comer. Basado en los síntomas y alimentos que mencionaste, como **[menciona aquí los síntomas y alimentos clave del 'query' del usuario]**, existe una posible relación con una condición conocida como **{match.get("condicion_asociada")}**. | |
| ### ¿Qué podría estar pasando en tu cuerpo? | |
| *Explica el campo "mecanismo_fisiologico" en términos muy sencillos. Usa una analogía si es posible. Por ejemplo, si es sobre Gota, explica las purinas y los cristales.* | |
| ### Pasos a Seguir y Recomendaciones | |
| Aquí te dejo algunas recomendaciones generales que se suelen considerar para esta condición. Es fundamental que las converses con un profesional de la salud: | |
| * **[Punto 1 de las recomendaciones, reformulado como un consejo práctico]** | |
| * **[Punto 2 de las recomendaciones, enfocado en los exámenes si los hay]** | |
| * **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 "compuesto_alimento", sin la lista completa]**. | |
| ### **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. | |
| """ | |
| try: | |
| response = model.generate_content(prompt) | |
| return response.text | |
| except Exception as e: | |
| logger.error(f"Error generando análisis detallado con Gemini: {e}") | |
| return "No se pudo generar el análisis detallado." | |
| # --- INTERFAZ DE USUARIO (UI) --- | |
| col_img, col_text = st.columns([1, 4], gap="medium") | |
| with col_img: | |
| 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.") | |
| st.markdown("---") | |
| # --- LÓGICA PRINCIPAL DE LA APLICACIÓN --- | |
| 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 = "" | |
| def clear_search_state(): | |
| st.session_state.search_results = None | |
| st.session_state.user_query = "" | |
| with st.form(key="search_form"): | |
| query = st.text_area("Describe tu caso aquí:", height=150, placeholder="Ej: Cuando como mucha carne me duele el dedo gordo del pie...") | |
| submitted = st.form_submit_button("Analizar mi caso", type="primary") | |
| if submitted: | |
| if not query: | |
| 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. No se puede continuar.") | |
| else: | |
| st.session_state.user_query = query | |
| with st.spinner("🧠 Interpretando tu caso y buscando pistas con IA..."): | |
| entities = extract_and_infer_with_gemini(query, lista_condiciones) | |
| if entities and (entities.get("alimentos") or entities.get("sintomas")): | |
| info_str = f"IA identificó - Alimentos: {', '.join(entities.get('alimentos',[])) or 'Ninguno'}" | |
| info_str += f", 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 en la base de conocimiento..."): | |
| 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 un poco más específico.") | |
| st.session_state.search_results = [] | |
| if st.session_state.search_results is not None: | |
| st.button("Realizar nueva consulta", on_click=clear_search_state) | |
| st.markdown("---") | |
| results = st.session_state.search_results | |
| if not results: | |
| st.warning(f"No se encontraron coincidencias claras en nuestra base de datos para tu caso: '{st.session_state.user_query}'.\n\n" | |
| "**Sugerencias:**\n" | |
| "- Intenta ser más específico con los síntomas.\n" | |
| "- Asegúrate de mencionar al menos un alimento o bebida.\n" | |
| "- Reformula tu consulta con otras palabras.") | |
| else: | |
| st.success(f"Hemos encontrado {len(results)} posible(s) causa(s) relacionada(s) con tu caso. Aquí está el análisis de la más probable:") | |
| best_match = results[0] | |
| with st.expander(f"**Principal Coincidencia: {best_match.get('condicion_asociada')}**", expanded=True): | |
| with st.spinner("✍️ Generando un análisis detallado y personalizado con IA..."): | |
| detailed_analysis = generate_detailed_analysis(st.session_state.user_query, best_match) | |
| st.markdown(detailed_analysis) | |
| if len(results) > 1: | |
| with st.expander("Otras posibles coincidencias (menos probables)"): | |
| for result in results[1:]: | |
| st.subheader(f"{result.get('condicion_asociada')}") | |
| st.write(f"**Compuestos/Alimentos:** {result.get('compuesto_alimento')}") | |
| st.write(f"**Síntomas Clínicos:** {result.get('sintomas_clinicos')}") | |
| st.markdown("---") |