Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
-
# ==================== El Detective de Alimentos (Versión
|
| 2 |
-
# Mejoras:
|
| 3 |
|
| 4 |
import streamlit as st
|
| 5 |
import google.generativeai as genai
|
|
@@ -9,7 +9,7 @@ import json
|
|
| 9 |
import logging
|
| 10 |
import re
|
| 11 |
import pandas as pd
|
| 12 |
-
import altair as alt
|
| 13 |
|
| 14 |
st.set_page_config(
|
| 15 |
page_title="El Detective de Alimentos",
|
|
@@ -17,7 +17,7 @@ st.set_page_config(
|
|
| 17 |
layout="wide"
|
| 18 |
)
|
| 19 |
|
| 20 |
-
# ... (El código de configuración de Gemini
|
| 21 |
# Configurar logging
|
| 22 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 23 |
logger = logging.getLogger("food_detective_app")
|
|
@@ -40,7 +40,7 @@ except Exception as e:
|
|
| 40 |
st.error(f"❌ Error al configurar Gemini API: {e}")
|
| 41 |
logger.error(f"Error al configurar Gemini API: {e}")
|
| 42 |
st.stop()
|
| 43 |
-
|
| 44 |
@st.cache_resource
|
| 45 |
def get_gemini_model():
|
| 46 |
"""Carga el modelo generativo de Gemini."""
|
|
@@ -54,79 +54,62 @@ def get_gemini_model():
|
|
| 54 |
|
| 55 |
model = get_gemini_model()
|
| 56 |
|
|
|
|
|
|
|
| 57 |
@st.cache_data
|
| 58 |
def load_data():
|
| 59 |
"""
|
| 60 |
-
Carga
|
| 61 |
"""
|
| 62 |
try:
|
| 63 |
path_alimentos = os.path.join('DATOS', 'alimentos_enriquecido.json')
|
| 64 |
-
|
| 65 |
|
| 66 |
with open(path_alimentos, 'r', encoding='utf-8') as f:
|
| 67 |
data_alimentos = json.load(f)
|
| 68 |
|
| 69 |
-
# Cargar el conocimiento dual (si existe)
|
| 70 |
-
conocimiento_dual = {}
|
| 71 |
-
if os.path.exists(path_dual):
|
| 72 |
-
with open(path_dual, 'r', encoding='utf-8') as f:
|
| 73 |
-
data_dual = json.load(f)
|
| 74 |
-
# Crear un diccionario para un acceso rápido
|
| 75 |
-
for item in data_dual:
|
| 76 |
-
key = item['condicion_asociada']
|
| 77 |
-
conocimiento_dual[key] = {
|
| 78 |
-
'efecto_benefico': item.get('efecto_benefico'),
|
| 79 |
-
'contexto_negativo': item.get('contexto_negativo')
|
| 80 |
-
}
|
| 81 |
-
logger.info(f"✅ Conocimiento dual cargado con {len(conocimiento_dual)} registros.")
|
| 82 |
-
|
| 83 |
-
# Fusionar los datos
|
| 84 |
-
for entry in data_alimentos:
|
| 85 |
-
key = entry['condicion_asociada']
|
| 86 |
-
if key in conocimiento_dual:
|
| 87 |
-
entry['efecto_benefico'] = conocimiento_dual[key]['efecto_benefico']
|
| 88 |
-
entry['contexto_negativo'] = conocimiento_dual[key]['contexto_negativo']
|
| 89 |
-
|
| 90 |
lista_condiciones = sorted(list(set(item['condicion_asociada'] for item in data_alimentos)))
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
except FileNotFoundError as e:
|
| 96 |
st.error(f"❌ Error: No se encontró el archivo '{e.filename}'. Asegúrate de que está en la carpeta 'DATOS'.")
|
| 97 |
-
return None, None
|
| 98 |
except json.JSONDecodeError as e:
|
| 99 |
st.error(f"❌ Error: Un archivo JSON tiene un formato incorrecto: {e}")
|
| 100 |
-
return None, None
|
| 101 |
-
|
|
|
|
|
|
|
| 102 |
|
| 103 |
-
#
|
| 104 |
FOOD_TO_COMPOUND_MAP = {
|
| 105 |
-
# Gluten
|
| 106 |
"pan": ["gluten"], "trigo": ["gluten"], "harina de trigo": ["gluten"], "cebada": ["gluten"],
|
| 107 |
"centeno": ["gluten"], "pasta": ["gluten"], "galletas": ["gluten"], "avena": ["gluten"], "pizza": ["gluten"], "torta": ["gluten"],
|
| 108 |
-
# Lácteos
|
| 109 |
"leche": ["lácteos", "caseína", "lactosa"], "queso": ["lácteos", "caseína", "lactosa", "histamina", "tiramina"],
|
| 110 |
"yogur": ["lácteos", "caseína", "lactosa"], "mantequilla": ["lácteos", "caseína", "lactosa"], "crema": ["lácteos"], "helado": ["lácteos"],
|
| 111 |
-
# Fenoles y Salicilatos
|
| 112 |
"manzana": ["salicilatos", "fructosa"], "almendras": ["salicilatos", "arginina"], "uvas": ["salicilatos"], "pasas": ["salicilatos"],
|
| 113 |
"naranja": ["salicilatos"], "brócoli": ["salicilatos", "goitrógenos", "fodmaps"], "cúrcuma": ["salicilatos"],
|
| 114 |
-
# Azúcares y Fructosa
|
| 115 |
"azucar": ["azúcar", "fructosa"], "dulces": ["azúcar"], "refrescos": ["azúcar", "fructosa"], "gaseosas": ["azúcar", "fructosa"],
|
| 116 |
"miel": ["fructosa", "fodmaps"], "jarabe de maiz": ["fructosa"],
|
| 117 |
-
# Aminas (Histamina, Tiramina)
|
| 118 |
"vino tinto": ["histamina", "tiramina", "sulfitos"], "vino rojo": ["histamina", "tiramina", "sulfitos"],
|
| 119 |
"cerveza": ["histamina", "tiramina", "purinas", "gluten"], "chocolate": ["cafeína", "tiramina", "níquel", "arginina"],
|
| 120 |
"embutidos": ["histamina", "tiramina", "nitritos"], "pescado enlatado": ["histamina"], "tomate": ["histamina", "solaninas", "ácidos"],
|
| 121 |
-
# FODMAPs
|
| 122 |
"aguacate": ["fodmaps", "polioles"], "cebolla": ["fodmaps"], "ajo": ["fodmaps"], "legumbres": ["fodmaps", "gos"],
|
| 123 |
-
# Otros
|
| 124 |
"carne": ["alfa-gal", "proteínas", "purinas", "hierro"], "carnes rojas": ["purinas", "alfa-gal", "hierro"],
|
| 125 |
"mariscos": ["purinas", "sulfitos", "alérgenos", "yodo"], "huevo": ["alérgenos"], "soya": ["alérgenos"],
|
| 126 |
"café": ["cafeína", "ácidos"], "nueces": ["arginina", "salicilatos", "níquel"]
|
| 127 |
}
|
| 128 |
-
|
| 129 |
-
# NUEVO DICCIONARIO DE SINÓNIMOS DE CONDICIONES
|
| 130 |
CONDITION_SYNONYMS = {
|
| 131 |
"síndrome del intestino irritable (sii).": [
|
| 132 |
"intolerancia a los fodmaps",
|
|
@@ -136,23 +119,16 @@ CONDITION_SYNONYMS = {
|
|
| 136 |
"gota / hiperuricemia.": ["acido urico aumentado"],
|
| 137 |
"intolerancia a la lactosa.": ["déficit de lactasa"],
|
| 138 |
"enfermedad celíaca (clásica).": ["dermatitis herpetiforme"]
|
| 139 |
-
# Puedes añadir más sinónimos aquí en el futuro
|
| 140 |
}
|
| 141 |
|
| 142 |
-
|
| 143 |
-
# --- LÓGICA DE BÚSQUEDA Y ANÁLISIS ---
|
| 144 |
-
|
| 145 |
-
# ... (La función extract_and_infer_with_gemini no cambia) ...
|
| 146 |
def extract_and_infer_with_gemini(query, condiciones):
|
| 147 |
"""Extrae entidades e infiere una condición probable."""
|
| 148 |
if not model: return None
|
| 149 |
condiciones_str = "\n".join([f"- {c}" for c in condiciones])
|
| 150 |
system_prompt = f"""
|
| 151 |
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.
|
| 152 |
-
|
| 153 |
LISTA DE CONDICIONES POSIBLES:
|
| 154 |
{condiciones_str}
|
| 155 |
-
|
| 156 |
Consulta: "{query}"
|
| 157 |
"""
|
| 158 |
try:
|
|
@@ -168,43 +144,28 @@ def extract_and_infer_with_gemini(query, condiciones):
|
|
| 168 |
st.error(f"Hubo un problema al interpretar tu consulta con la IA.")
|
| 169 |
return None
|
| 170 |
|
| 171 |
-
# --- FUNCIÓN DE BÚSQUEDA MODIFICADA ---
|
| 172 |
def find_best_matches_hybrid(entities, data):
|
| 173 |
-
"""
|
| 174 |
-
Motor de búsqueda híbrido (v4.1) con manejo de sinónimos de condiciones.
|
| 175 |
-
"""
|
| 176 |
if not entities or not data: return []
|
| 177 |
-
|
| 178 |
user_symptoms = set(s.lower().strip() for s in entities.get("sintomas", []))
|
| 179 |
user_foods = set(f.lower().strip() for f in entities.get("alimentos", []))
|
| 180 |
inferred_condition_raw = entities.get("condicion_probable", "").lower().strip()
|
| 181 |
-
|
| 182 |
candidate_terms = set(user_foods)
|
| 183 |
for food in user_foods:
|
| 184 |
if food in FOOD_TO_COMPOUND_MAP:
|
| 185 |
candidate_terms.update(c.lower() for c in FOOD_TO_COMPOUND_MAP[food])
|
| 186 |
-
|
| 187 |
results = []
|
| 188 |
for index, entry in enumerate(data):
|
| 189 |
score_details = {'condition': 0, 'food': 0, 'symptoms': 0, 'total': 0}
|
| 190 |
-
|
| 191 |
-
# --- PONDERACIÓN 1: COINCIDENCIA DE CONDICIÓN (CON SINÓNIMOS) ---
|
| 192 |
entry_condition = entry.get("condicion_asociada", "").lower().strip()
|
| 193 |
-
|
| 194 |
-
# Comprobación directa
|
| 195 |
if inferred_condition_raw and inferred_condition_raw == entry_condition:
|
| 196 |
score_details['condition'] = 100
|
| 197 |
-
# Comprobación de sinónimos
|
| 198 |
elif inferred_condition_raw and entry_condition in CONDITION_SYNONYMS:
|
| 199 |
if inferred_condition_raw in CONDITION_SYNONYMS[entry_condition]:
|
| 200 |
score_details['condition'] = 100
|
| 201 |
-
|
| 202 |
-
# --- PONDERACIÓN 2: COINCIDENCIA DE ALIMENTOS/COMPUESTOS ---
|
| 203 |
entry_compounds_text = entry.get("compuesto_alimento", "").lower()
|
| 204 |
if any(term in entry_compounds_text for term in candidate_terms):
|
| 205 |
score_details['food'] = 20
|
| 206 |
-
|
| 207 |
-
# --- PONDERACIÓN 3: COINCIDENCIA DE SÍNTOMAS ---
|
| 208 |
entry_symptoms_keys = set(s.lower().strip() for s in entry.get("sintomas_clave", []))
|
| 209 |
symptom_score = 0
|
| 210 |
for user_symptom in user_symptoms:
|
|
@@ -213,23 +174,18 @@ def find_best_matches_hybrid(entities, data):
|
|
| 213 |
symptom_score += 5
|
| 214 |
break
|
| 215 |
score_details['symptoms'] = symptom_score
|
| 216 |
-
|
| 217 |
total_score = sum(score_details.values())
|
| 218 |
if total_score > 0:
|
| 219 |
score_details['total'] = total_score
|
| 220 |
results.append({'entry': entry, 'score': score_details})
|
| 221 |
-
|
| 222 |
if not results: return []
|
| 223 |
-
|
| 224 |
sorted_results = sorted(results, key=lambda x: x['score']['total'], reverse=True)
|
| 225 |
return sorted_results
|
| 226 |
|
| 227 |
-
# ... (La función generate_detailed_analysis no cambia) ...
|
| 228 |
def generate_detailed_analysis(query, match):
|
| 229 |
"""Genera la explicación final, manejando la información de dualidad si existe."""
|
|
|
|
| 230 |
if not model: return "Error: El modelo de IA no está disponible."
|
| 231 |
-
|
| 232 |
-
# Construcción del prompt dinámico
|
| 233 |
prompt_parts = [
|
| 234 |
"Eres un asistente de IA experto en nutrición personalizada. Tu tono es empático, claro y muy educativo. NO actúas como un médico, sino como un guía informativo.",
|
| 235 |
f'El usuario ha descrito el siguiente caso: "{query}"',
|
|
@@ -238,37 +194,28 @@ def generate_detailed_analysis(query, match):
|
|
| 238 |
f'- Mecanismo: {match.get("mecanismo_fisiologico")}',
|
| 239 |
f'- Recomendaciones: {match.get("recomendaciones_examenes")}',
|
| 240 |
f'- Alimentos Implicados: {match.get("compuesto_alimento")}',
|
| 241 |
-
|
| 242 |
"\n**Tu Tarea:** Redacta una respuesta excepcional para el usuario usando Markdown. Sigue esta estructura OBLIGATORIAMENTE:",
|
| 243 |
-
|
| 244 |
f'### Posible Causa: {match.get("condicion_asociada")}',
|
| 245 |
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")}**.',
|
| 246 |
-
|
| 247 |
f'### ¿Qué podría estar pasando en tu cuerpo?',
|
| 248 |
f'Explica el siguiente mecanismo en términos muy sencillos, usando una analogía si es posible: "{match.get("mecanismo_fisiologico")}"'
|
| 249 |
]
|
| 250 |
-
|
| 251 |
-
# --- Lógica para añadir la sección de dualidad ---
|
| 252 |
if match.get("efecto_benefico") and match.get("contexto_negativo"):
|
| 253 |
prompt_parts.append(
|
| 254 |
"\n### El Doble Filo de Estos Alimentos\n"
|
| 255 |
f'**Para la mayoría de las personas:** Resume por qué estos alimentos son generalmente buenos, basándote en: "{match.get("efecto_benefico")}"\n'
|
| 256 |
f'**Sin embargo, en ciertos contextos:** Explica por qué pueden ser problemáticos para el usuario, basándote en: "{match.get("contexto_negativo")}"'
|
| 257 |
)
|
| 258 |
-
|
| 259 |
prompt_parts.extend([
|
| 260 |
"\n### Pasos a Seguir y Recomendaciones",
|
| 261 |
"Aquí te dejo algunas recomendaciones generales. Es fundamental que las converses con un profesional de la salud:",
|
| 262 |
f'* **[Punto 1 de las recomendaciones, reformulado como un consejo práctico basado en "{match.get("recomendaciones_examenes")}"]**',
|
| 263 |
f'* **[Punto 2 de las recomendaciones, enfocado en los exámenes si los hay, basado en "{match.get("recomendaciones_examenes")}"]**',
|
| 264 |
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 "{match.get("compuesto_alimento")}"]**.',
|
| 265 |
-
|
| 266 |
"\n### **IMPORTANTE: Descargo de Responsabilidad**",
|
| 267 |
"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."
|
| 268 |
])
|
| 269 |
-
|
| 270 |
prompt = "\n".join(prompt_parts)
|
| 271 |
-
|
| 272 |
try:
|
| 273 |
response = model.generate_content(prompt)
|
| 274 |
return response.text
|
|
@@ -276,36 +223,23 @@ def generate_detailed_analysis(query, match):
|
|
| 276 |
logger.error(f"Error generando análisis detallado con Gemini: {e}")
|
| 277 |
return "No se pudo generar el análisis detallado."
|
| 278 |
|
| 279 |
-
# --- NUEVA FUNCIÓN PARA CREAR EL GRÁFICO ---
|
| 280 |
def create_relevance_chart(results):
|
| 281 |
"""Crea un gráfico de barras de Altair legible para visualizar la relevancia."""
|
| 282 |
-
# Preparar los datos para el gráfico (tomamos los 5 mejores)
|
| 283 |
top_results = results[:5]
|
| 284 |
-
|
| 285 |
-
# Limpiamos los nombres para que no sean excesivamente largos en el gráfico
|
| 286 |
condition_names = []
|
| 287 |
for res in top_results:
|
| 288 |
-
# Quitamos paréntesis y acortamos si es necesario para el display
|
| 289 |
name = re.sub(r'\(.*\)', '', res['entry']['condicion_asociada']).strip()
|
| 290 |
condition_names.append(name)
|
| 291 |
-
|
| 292 |
chart_data = {
|
| 293 |
"Condición": condition_names,
|
| 294 |
"Relevancia": [res['score']['total'] for res in top_results]
|
| 295 |
}
|
| 296 |
source = pd.DataFrame(chart_data)
|
| 297 |
-
|
| 298 |
-
# Crear el gráfico
|
| 299 |
chart = alt.Chart(source).mark_bar(color='#1f77b4').encode(
|
| 300 |
x=alt.X('Relevancia:Q', title='Puntuación de Relevancia'),
|
| 301 |
y=alt.Y('Condición:N', sort='-x', title='Posible Condición',
|
| 302 |
-
axis=alt.Axis(
|
| 303 |
-
|
| 304 |
-
)),
|
| 305 |
-
tooltip=[
|
| 306 |
-
alt.Tooltip('Condición:N', title='Condición'),
|
| 307 |
-
alt.Tooltip('Relevancia:Q', title='Puntuación')
|
| 308 |
-
]
|
| 309 |
).properties(
|
| 310 |
title='Principales Coincidencias según tu Caso'
|
| 311 |
).configure_axis(
|
|
@@ -313,12 +247,22 @@ def create_relevance_chart(results):
|
|
| 313 |
titleFontSize=14
|
| 314 |
).configure_title(
|
| 315 |
fontSize=16,
|
| 316 |
-
anchor='start'
|
| 317 |
)
|
| 318 |
return chart
|
| 319 |
|
| 320 |
-
|
| 321 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
col_img, col_text = st.columns([1, 4], gap="medium")
|
| 323 |
with col_img:
|
| 324 |
if os.path.exists("imagen.png"):
|
|
@@ -328,7 +272,6 @@ with col_text:
|
|
| 328 |
st.markdown("##### Describe lo que sientes y lo que comiste para descubrir posibles intolerancias.")
|
| 329 |
st.markdown("---")
|
| 330 |
|
| 331 |
-
# --- LÓGICA PRINCIPAL DE LA APLICACIÓN ---
|
| 332 |
if 'search_results' not in st.session_state: st.session_state.search_results = None
|
| 333 |
if 'user_query' not in st.session_state: st.session_state.user_query = ""
|
| 334 |
|
|
@@ -337,7 +280,7 @@ def clear_search_state():
|
|
| 337 |
st.session_state.user_query = ""
|
| 338 |
|
| 339 |
with st.form(key="search_form"):
|
| 340 |
-
query = st.text_area("Describe tu caso aquí:", height=150, placeholder="Ej: Cuando como
|
| 341 |
submitted = st.form_submit_button("Analizar mi caso", type="primary")
|
| 342 |
|
| 343 |
if submitted:
|
|
@@ -351,12 +294,12 @@ if submitted:
|
|
| 351 |
entities = extract_and_infer_with_gemini(query, lista_condiciones)
|
| 352 |
|
| 353 |
if entities and (entities.get("alimentos") or entities.get("sintomas")):
|
|
|
|
| 354 |
info_str = f"IA identificó - Alimentos: {', '.join(entities.get('alimentos',[])) or 'Ninguno'}"
|
| 355 |
info_str += f", Síntomas: {', '.join(entities.get('sintomas',[])) or 'Ninguno'}"
|
| 356 |
if entities.get("condicion_probable"):
|
| 357 |
info_str += f", Condición Probable: {entities.get('condicion_probable')}"
|
| 358 |
st.info(info_str)
|
| 359 |
-
|
| 360 |
with st.spinner("🔬 Cruzando información y calculando relevancia..."):
|
| 361 |
results = find_best_matches_hybrid(entities, alimentos_data)
|
| 362 |
st.session_state.search_results = results
|
|
@@ -375,22 +318,47 @@ if st.session_state.search_results is not None:
|
|
| 375 |
else:
|
| 376 |
st.success(f"Hemos encontrado {len(results)} posible(s) causa(s) relacionada(s) con tu caso.")
|
| 377 |
|
| 378 |
-
# --- SECCIÓN DEL GRÁFICO NUEVA ---
|
| 379 |
st.subheader("Análisis de Relevancia de las Coincidencias")
|
| 380 |
chart = create_relevance_chart(results)
|
| 381 |
st.altair_chart(chart, use_container_width=True)
|
| 382 |
|
| 383 |
-
|
| 384 |
-
|
|
|
|
| 385 |
|
| 386 |
with st.expander(f"**Análisis Detallado de la Principal Coincidencia: {best_match.get('condicion_asociada')}**", expanded=True):
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
st.markdown("---")
|
| 395 |
|
| 396 |
with st.spinner("✍️ Generando un análisis personalizado con IA..."):
|
|
@@ -399,6 +367,7 @@ if st.session_state.search_results is not None:
|
|
| 399 |
|
| 400 |
if len(results) > 1:
|
| 401 |
with st.expander("Otras posibles coincidencias (ordenadas por relevancia)"):
|
|
|
|
| 402 |
for result in results[1:]:
|
| 403 |
entry = result['entry']
|
| 404 |
score = result['score']
|
|
|
|
| 1 |
+
# ==================== El Detective de Alimentos (Versión 5.0 - con Profundización Científica) =====================================
|
| 2 |
+
# Mejoras: Integración de la base de datos FoodB para una capa de evidencia bioquímica.
|
| 3 |
|
| 4 |
import streamlit as st
|
| 5 |
import google.generativeai as genai
|
|
|
|
| 9 |
import logging
|
| 10 |
import re
|
| 11 |
import pandas as pd
|
| 12 |
+
import altair as alt
|
| 13 |
|
| 14 |
st.set_page_config(
|
| 15 |
page_title="El Detective de Alimentos",
|
|
|
|
| 17 |
layout="wide"
|
| 18 |
)
|
| 19 |
|
| 20 |
+
# ... (El código de configuración de Gemini no cambia) ...
|
| 21 |
# Configurar logging
|
| 22 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 23 |
logger = logging.getLogger("food_detective_app")
|
|
|
|
| 40 |
st.error(f"❌ Error al configurar Gemini API: {e}")
|
| 41 |
logger.error(f"Error al configurar Gemini API: {e}")
|
| 42 |
st.stop()
|
| 43 |
+
|
| 44 |
@st.cache_resource
|
| 45 |
def get_gemini_model():
|
| 46 |
"""Carga el modelo generativo de Gemini."""
|
|
|
|
| 54 |
|
| 55 |
model = get_gemini_model()
|
| 56 |
|
| 57 |
+
|
| 58 |
+
# --- FUNCIÓN DE CARGA DE DATOS MODIFICADA ---
|
| 59 |
@st.cache_data
|
| 60 |
def load_data():
|
| 61 |
"""
|
| 62 |
+
Carga todas las bases de conocimiento: datos enriquecidos y el índice de FoodB.
|
| 63 |
"""
|
| 64 |
try:
|
| 65 |
path_alimentos = os.path.join('DATOS', 'alimentos_enriquecido.json')
|
| 66 |
+
path_foodb_index = os.path.join('DATOS', 'foodb_index.json')
|
| 67 |
|
| 68 |
with open(path_alimentos, 'r', encoding='utf-8') as f:
|
| 69 |
data_alimentos = json.load(f)
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
lista_condiciones = sorted(list(set(item['condicion_asociada'] for item in data_alimentos)))
|
| 72 |
|
| 73 |
+
# Cargar el índice de FoodB (si existe)
|
| 74 |
+
foodb_index = {}
|
| 75 |
+
if os.path.exists(path_foodb_index):
|
| 76 |
+
with open(path_foodb_index, 'r', encoding='utf-8') as f:
|
| 77 |
+
foodb_index = json.load(f)
|
| 78 |
+
logger.info(f"✅ Índice científico de FoodB cargado con {len(foodb_index)} alimentos.")
|
| 79 |
+
else:
|
| 80 |
+
logger.warning("Advertencia: No se encontró 'foodb_index.json'. La funcionalidad de profundización científica estará desactivada.")
|
| 81 |
+
|
| 82 |
+
logger.info(f"✅ Base de datos principal cargada con {len(data_alimentos)} registros.")
|
| 83 |
+
return data_alimentos, lista_condiciones, foodb_index
|
| 84 |
|
| 85 |
except FileNotFoundError as e:
|
| 86 |
st.error(f"❌ Error: No se encontró el archivo '{e.filename}'. Asegúrate de que está en la carpeta 'DATOS'.")
|
| 87 |
+
return None, None, None
|
| 88 |
except json.JSONDecodeError as e:
|
| 89 |
st.error(f"❌ Error: Un archivo JSON tiene un formato incorrecto: {e}")
|
| 90 |
+
return None, None, None
|
| 91 |
+
|
| 92 |
+
alimentos_data, lista_condiciones, foodb_index = load_data()
|
| 93 |
+
|
| 94 |
|
| 95 |
+
# ... (El resto de diccionarios y funciones de búsqueda no cambian) ...
|
| 96 |
FOOD_TO_COMPOUND_MAP = {
|
|
|
|
| 97 |
"pan": ["gluten"], "trigo": ["gluten"], "harina de trigo": ["gluten"], "cebada": ["gluten"],
|
| 98 |
"centeno": ["gluten"], "pasta": ["gluten"], "galletas": ["gluten"], "avena": ["gluten"], "pizza": ["gluten"], "torta": ["gluten"],
|
|
|
|
| 99 |
"leche": ["lácteos", "caseína", "lactosa"], "queso": ["lácteos", "caseína", "lactosa", "histamina", "tiramina"],
|
| 100 |
"yogur": ["lácteos", "caseína", "lactosa"], "mantequilla": ["lácteos", "caseína", "lactosa"], "crema": ["lácteos"], "helado": ["lácteos"],
|
|
|
|
| 101 |
"manzana": ["salicilatos", "fructosa"], "almendras": ["salicilatos", "arginina"], "uvas": ["salicilatos"], "pasas": ["salicilatos"],
|
| 102 |
"naranja": ["salicilatos"], "brócoli": ["salicilatos", "goitrógenos", "fodmaps"], "cúrcuma": ["salicilatos"],
|
|
|
|
| 103 |
"azucar": ["azúcar", "fructosa"], "dulces": ["azúcar"], "refrescos": ["azúcar", "fructosa"], "gaseosas": ["azúcar", "fructosa"],
|
| 104 |
"miel": ["fructosa", "fodmaps"], "jarabe de maiz": ["fructosa"],
|
|
|
|
| 105 |
"vino tinto": ["histamina", "tiramina", "sulfitos"], "vino rojo": ["histamina", "tiramina", "sulfitos"],
|
| 106 |
"cerveza": ["histamina", "tiramina", "purinas", "gluten"], "chocolate": ["cafeína", "tiramina", "níquel", "arginina"],
|
| 107 |
"embutidos": ["histamina", "tiramina", "nitritos"], "pescado enlatado": ["histamina"], "tomate": ["histamina", "solaninas", "ácidos"],
|
|
|
|
| 108 |
"aguacate": ["fodmaps", "polioles"], "cebolla": ["fodmaps"], "ajo": ["fodmaps"], "legumbres": ["fodmaps", "gos"],
|
|
|
|
| 109 |
"carne": ["alfa-gal", "proteínas", "purinas", "hierro"], "carnes rojas": ["purinas", "alfa-gal", "hierro"],
|
| 110 |
"mariscos": ["purinas", "sulfitos", "alérgenos", "yodo"], "huevo": ["alérgenos"], "soya": ["alérgenos"],
|
| 111 |
"café": ["cafeína", "ácidos"], "nueces": ["arginina", "salicilatos", "níquel"]
|
| 112 |
}
|
|
|
|
|
|
|
| 113 |
CONDITION_SYNONYMS = {
|
| 114 |
"síndrome del intestino irritable (sii).": [
|
| 115 |
"intolerancia a los fodmaps",
|
|
|
|
| 119 |
"gota / hiperuricemia.": ["acido urico aumentado"],
|
| 120 |
"intolerancia a la lactosa.": ["déficit de lactasa"],
|
| 121 |
"enfermedad celíaca (clásica).": ["dermatitis herpetiforme"]
|
|
|
|
| 122 |
}
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
def extract_and_infer_with_gemini(query, condiciones):
|
| 125 |
"""Extrae entidades e infiere una condición probable."""
|
| 126 |
if not model: return None
|
| 127 |
condiciones_str = "\n".join([f"- {c}" for c in condiciones])
|
| 128 |
system_prompt = f"""
|
| 129 |
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.
|
|
|
|
| 130 |
LISTA DE CONDICIONES POSIBLES:
|
| 131 |
{condiciones_str}
|
|
|
|
| 132 |
Consulta: "{query}"
|
| 133 |
"""
|
| 134 |
try:
|
|
|
|
| 144 |
st.error(f"Hubo un problema al interpretar tu consulta con la IA.")
|
| 145 |
return None
|
| 146 |
|
|
|
|
| 147 |
def find_best_matches_hybrid(entities, data):
|
| 148 |
+
"""Motor de búsqueda híbrido (v4.1) con manejo de sinónimos de condiciones."""
|
|
|
|
|
|
|
| 149 |
if not entities or not data: return []
|
|
|
|
| 150 |
user_symptoms = set(s.lower().strip() for s in entities.get("sintomas", []))
|
| 151 |
user_foods = set(f.lower().strip() for f in entities.get("alimentos", []))
|
| 152 |
inferred_condition_raw = entities.get("condicion_probable", "").lower().strip()
|
|
|
|
| 153 |
candidate_terms = set(user_foods)
|
| 154 |
for food in user_foods:
|
| 155 |
if food in FOOD_TO_COMPOUND_MAP:
|
| 156 |
candidate_terms.update(c.lower() for c in FOOD_TO_COMPOUND_MAP[food])
|
|
|
|
| 157 |
results = []
|
| 158 |
for index, entry in enumerate(data):
|
| 159 |
score_details = {'condition': 0, 'food': 0, 'symptoms': 0, 'total': 0}
|
|
|
|
|
|
|
| 160 |
entry_condition = entry.get("condicion_asociada", "").lower().strip()
|
|
|
|
|
|
|
| 161 |
if inferred_condition_raw and inferred_condition_raw == entry_condition:
|
| 162 |
score_details['condition'] = 100
|
|
|
|
| 163 |
elif inferred_condition_raw and entry_condition in CONDITION_SYNONYMS:
|
| 164 |
if inferred_condition_raw in CONDITION_SYNONYMS[entry_condition]:
|
| 165 |
score_details['condition'] = 100
|
|
|
|
|
|
|
| 166 |
entry_compounds_text = entry.get("compuesto_alimento", "").lower()
|
| 167 |
if any(term in entry_compounds_text for term in candidate_terms):
|
| 168 |
score_details['food'] = 20
|
|
|
|
|
|
|
| 169 |
entry_symptoms_keys = set(s.lower().strip() for s in entry.get("sintomas_clave", []))
|
| 170 |
symptom_score = 0
|
| 171 |
for user_symptom in user_symptoms:
|
|
|
|
| 174 |
symptom_score += 5
|
| 175 |
break
|
| 176 |
score_details['symptoms'] = symptom_score
|
|
|
|
| 177 |
total_score = sum(score_details.values())
|
| 178 |
if total_score > 0:
|
| 179 |
score_details['total'] = total_score
|
| 180 |
results.append({'entry': entry, 'score': score_details})
|
|
|
|
| 181 |
if not results: return []
|
|
|
|
| 182 |
sorted_results = sorted(results, key=lambda x: x['score']['total'], reverse=True)
|
| 183 |
return sorted_results
|
| 184 |
|
|
|
|
| 185 |
def generate_detailed_analysis(query, match):
|
| 186 |
"""Genera la explicación final, manejando la información de dualidad si existe."""
|
| 187 |
+
# (Esta función no necesita cambios)
|
| 188 |
if not model: return "Error: El modelo de IA no está disponible."
|
|
|
|
|
|
|
| 189 |
prompt_parts = [
|
| 190 |
"Eres un asistente de IA experto en nutrición personalizada. Tu tono es empático, claro y muy educativo. NO actúas como un médico, sino como un guía informativo.",
|
| 191 |
f'El usuario ha descrito el siguiente caso: "{query}"',
|
|
|
|
| 194 |
f'- Mecanismo: {match.get("mecanismo_fisiologico")}',
|
| 195 |
f'- Recomendaciones: {match.get("recomendaciones_examenes")}',
|
| 196 |
f'- Alimentos Implicados: {match.get("compuesto_alimento")}',
|
|
|
|
| 197 |
"\n**Tu Tarea:** Redacta una respuesta excepcional para el usuario usando Markdown. Sigue esta estructura OBLIGATORIAMENTE:",
|
|
|
|
| 198 |
f'### Posible Causa: {match.get("condicion_asociada")}',
|
| 199 |
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")}**.',
|
|
|
|
| 200 |
f'### ¿Qué podría estar pasando en tu cuerpo?',
|
| 201 |
f'Explica el siguiente mecanismo en términos muy sencillos, usando una analogía si es posible: "{match.get("mecanismo_fisiologico")}"'
|
| 202 |
]
|
|
|
|
|
|
|
| 203 |
if match.get("efecto_benefico") and match.get("contexto_negativo"):
|
| 204 |
prompt_parts.append(
|
| 205 |
"\n### El Doble Filo de Estos Alimentos\n"
|
| 206 |
f'**Para la mayoría de las personas:** Resume por qué estos alimentos son generalmente buenos, basándote en: "{match.get("efecto_benefico")}"\n'
|
| 207 |
f'**Sin embargo, en ciertos contextos:** Explica por qué pueden ser problemáticos para el usuario, basándote en: "{match.get("contexto_negativo")}"'
|
| 208 |
)
|
|
|
|
| 209 |
prompt_parts.extend([
|
| 210 |
"\n### Pasos a Seguir y Recomendaciones",
|
| 211 |
"Aquí te dejo algunas recomendaciones generales. Es fundamental que las converses con un profesional de la salud:",
|
| 212 |
f'* **[Punto 1 de las recomendaciones, reformulado como un consejo práctico basado en "{match.get("recomendaciones_examenes")}"]**',
|
| 213 |
f'* **[Punto 2 de las recomendaciones, enfocado en los exámenes si los hay, basado en "{match.get("recomendaciones_examenes")}"]**',
|
| 214 |
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 "{match.get("compuesto_alimento")}"]**.',
|
|
|
|
| 215 |
"\n### **IMPORTANTE: Descargo de Responsabilidad**",
|
| 216 |
"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."
|
| 217 |
])
|
|
|
|
| 218 |
prompt = "\n".join(prompt_parts)
|
|
|
|
| 219 |
try:
|
| 220 |
response = model.generate_content(prompt)
|
| 221 |
return response.text
|
|
|
|
| 223 |
logger.error(f"Error generando análisis detallado con Gemini: {e}")
|
| 224 |
return "No se pudo generar el análisis detallado."
|
| 225 |
|
|
|
|
| 226 |
def create_relevance_chart(results):
|
| 227 |
"""Crea un gráfico de barras de Altair legible para visualizar la relevancia."""
|
|
|
|
| 228 |
top_results = results[:5]
|
|
|
|
|
|
|
| 229 |
condition_names = []
|
| 230 |
for res in top_results:
|
|
|
|
| 231 |
name = re.sub(r'\(.*\)', '', res['entry']['condicion_asociada']).strip()
|
| 232 |
condition_names.append(name)
|
|
|
|
| 233 |
chart_data = {
|
| 234 |
"Condición": condition_names,
|
| 235 |
"Relevancia": [res['score']['total'] for res in top_results]
|
| 236 |
}
|
| 237 |
source = pd.DataFrame(chart_data)
|
|
|
|
|
|
|
| 238 |
chart = alt.Chart(source).mark_bar(color='#1f77b4').encode(
|
| 239 |
x=alt.X('Relevancia:Q', title='Puntuación de Relevancia'),
|
| 240 |
y=alt.Y('Condición:N', sort='-x', title='Posible Condición',
|
| 241 |
+
axis=alt.Axis(labelLimit=300)),
|
| 242 |
+
tooltip=[alt.Tooltip('Condición:N', title='Condición'), alt.Tooltip('Relevancia:Q', title='Puntuación')]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
).properties(
|
| 244 |
title='Principales Coincidencias según tu Caso'
|
| 245 |
).configure_axis(
|
|
|
|
| 247 |
titleFontSize=14
|
| 248 |
).configure_title(
|
| 249 |
fontSize=16,
|
| 250 |
+
anchor='start'
|
| 251 |
)
|
| 252 |
return chart
|
| 253 |
|
| 254 |
+
# --- NUEVA FUNCIÓN AUXILIAR ---
|
| 255 |
+
def extract_foods_from_string(food_string):
|
| 256 |
+
"""Extrae una lista de alimentos de la cadena 'compuesto_alimento'."""
|
| 257 |
+
# Quita el texto en paréntesis al principio
|
| 258 |
+
main_part = re.sub(r'^\w+\s*\(.*?\)\s*', '', food_string)
|
| 259 |
+
# Quita el texto en paréntesis al final
|
| 260 |
+
foods_part = re.sub(r'\(.*\)', '', main_part).strip()
|
| 261 |
+
# Divide por comas y limpia los espacios
|
| 262 |
+
return [food.strip().lower() for food in foods_part.split(',') if food.strip()]
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
# --- INTERFAZ DE USUARIO (UI) MODIFICADA ---
|
| 266 |
col_img, col_text = st.columns([1, 4], gap="medium")
|
| 267 |
with col_img:
|
| 268 |
if os.path.exists("imagen.png"):
|
|
|
|
| 272 |
st.markdown("##### Describe lo que sientes y lo que comiste para descubrir posibles intolerancias.")
|
| 273 |
st.markdown("---")
|
| 274 |
|
|
|
|
| 275 |
if 'search_results' not in st.session_state: st.session_state.search_results = None
|
| 276 |
if 'user_query' not in st.session_state: st.session_state.user_query = ""
|
| 277 |
|
|
|
|
| 280 |
st.session_state.user_query = ""
|
| 281 |
|
| 282 |
with st.form(key="search_form"):
|
| 283 |
+
query = st.text_area("Describe tu caso aquí:", height=150, placeholder="Ej: Cuando como mucha carne me duele el dedo gordo del pie...")
|
| 284 |
submitted = st.form_submit_button("Analizar mi caso", type="primary")
|
| 285 |
|
| 286 |
if submitted:
|
|
|
|
| 294 |
entities = extract_and_infer_with_gemini(query, lista_condiciones)
|
| 295 |
|
| 296 |
if entities and (entities.get("alimentos") or entities.get("sintomas")):
|
| 297 |
+
# ... (código de info_str sin cambios) ...
|
| 298 |
info_str = f"IA identificó - Alimentos: {', '.join(entities.get('alimentos',[])) or 'Ninguno'}"
|
| 299 |
info_str += f", Síntomas: {', '.join(entities.get('sintomas',[])) or 'Ninguno'}"
|
| 300 |
if entities.get("condicion_probable"):
|
| 301 |
info_str += f", Condición Probable: {entities.get('condicion_probable')}"
|
| 302 |
st.info(info_str)
|
|
|
|
| 303 |
with st.spinner("🔬 Cruzando información y calculando relevancia..."):
|
| 304 |
results = find_best_matches_hybrid(entities, alimentos_data)
|
| 305 |
st.session_state.search_results = results
|
|
|
|
| 318 |
else:
|
| 319 |
st.success(f"Hemos encontrado {len(results)} posible(s) causa(s) relacionada(s) con tu caso.")
|
| 320 |
|
|
|
|
| 321 |
st.subheader("Análisis de Relevancia de las Coincidencias")
|
| 322 |
chart = create_relevance_chart(results)
|
| 323 |
st.altair_chart(chart, use_container_width=True)
|
| 324 |
|
| 325 |
+
best_match_data = results[0]
|
| 326 |
+
best_match = best_match_data['entry']
|
| 327 |
+
best_score = best_match_data['score']
|
| 328 |
|
| 329 |
with st.expander(f"**Análisis Detallado de la Principal Coincidencia: {best_match.get('condicion_asociada')}**", expanded=True):
|
| 330 |
+
col1, col2 = st.columns([3, 1])
|
| 331 |
+
with col1:
|
| 332 |
+
st.markdown("##### Desglose de la Puntuación de Relevancia:")
|
| 333 |
+
score_col1, score_col2, score_col3, score_col4 = st.columns(4)
|
| 334 |
+
score_col1.metric("Puntos por Condición", f"{best_score['condition']}")
|
| 335 |
+
score_col2.metric("Puntos por Alimento", f"{best_score['food']}")
|
| 336 |
+
score_col3.metric("Puntos por Síntomas", f"{best_score['symptoms']}")
|
| 337 |
+
score_col4.metric("PUNTUACIÓN TOTAL", f"{best_score['total']}", delta="Máxima coincidencia")
|
| 338 |
+
|
| 339 |
+
# --- NUEVO POPOVER PARA PROFUNDIZACIÓN CIENTÍFICA ---
|
| 340 |
+
with col2:
|
| 341 |
+
st.write("") # Espacio para alinear
|
| 342 |
+
if foodb_index:
|
| 343 |
+
with st.popover("🔬 Evidencia Bioquímica"):
|
| 344 |
+
st.markdown("##### Componentes y Efectos Moleculares (Datos de FoodB.ca)")
|
| 345 |
+
alimentos_a_buscar = extract_foods_from_string(best_match.get("compuesto_alimento", ""))
|
| 346 |
+
if not alimentos_a_buscar:
|
| 347 |
+
st.write("No se encontraron alimentos específicos para analizar en la base de datos científica.")
|
| 348 |
+
else:
|
| 349 |
+
found_data = False
|
| 350 |
+
for alimento in alimentos_a_buscar[:3]: # Limitamos a los 3 primeros para no sobrecargar
|
| 351 |
+
if alimento in foodb_index:
|
| 352 |
+
found_data = True
|
| 353 |
+
with st.container(border=True):
|
| 354 |
+
st.subheader(f"Análisis de: {alimento.capitalize()}")
|
| 355 |
+
for item in foodb_index[alimento][:5]: # Limitamos a 5 compuestos por alimento
|
| 356 |
+
st.write(f"**Compuesto:** {item['compound']}")
|
| 357 |
+
st.write(f"**Efectos reportados:** {', '.join(item['effects'])}")
|
| 358 |
+
st.markdown("---")
|
| 359 |
+
if not found_data:
|
| 360 |
+
st.write(f"No se encontraron datos moleculares para '{', '.join(alimentos_a_buscar)}' en la base de datos de FoodB.")
|
| 361 |
+
|
| 362 |
st.markdown("---")
|
| 363 |
|
| 364 |
with st.spinner("✍️ Generando un análisis personalizado con IA..."):
|
|
|
|
| 367 |
|
| 368 |
if len(results) > 1:
|
| 369 |
with st.expander("Otras posibles coincidencias (ordenadas por relevancia)"):
|
| 370 |
+
# ... (código para mostrar otras coincidencias no cambia) ...
|
| 371 |
for result in results[1:]:
|
| 372 |
entry = result['entry']
|
| 373 |
score = result['score']
|