ALIMENTOS / app.py
JairoCesar's picture
Update app.py
625601a verified
raw
history blame
14.2 kB
# ==================== 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()
@st.cache_resource
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()
@st.cache_data
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("---")