ALIMENTOS / app.py
JairoCesar's picture
Update app.py
451fced verified
raw
history blame
35.4 kB
# ==================== 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()
@st.cache_resource
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()
@st.cache_data
def load_data():
try:
path_alimentos = os.path.join('DATOS', 'alimentos_enriquecido.json')
path_foodb_index = os.path.join('DATOS', 'foodb_index.json')
with open(path_alimentos, 'r', encoding='utf-8') as f:
data_alimentos = json.load(f)
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("---")