Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
-
# ==================== Buscador de
|
| 2 |
-
|
| 3 |
-
# JAIRO ALEXANDER ERASO MD
|
| 4 |
-
# DIANA MILENA SOLER MARTINEZ Psc.U Juan N. Corpas, U. Bosque.
|
| 5 |
|
| 6 |
import streamlit as st
|
| 7 |
import google.generativeai as genai
|
|
@@ -9,26 +8,32 @@ import google.api_core.exceptions
|
|
| 9 |
import os
|
| 10 |
import json
|
| 11 |
import logging
|
| 12 |
-
import
|
| 13 |
-
|
| 14 |
|
| 15 |
st.set_page_config(
|
| 16 |
-
page_title="
|
| 17 |
-
page_icon="
|
| 18 |
layout="wide"
|
| 19 |
)
|
| 20 |
|
| 21 |
# Configurar logging
|
| 22 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 23 |
-
logger = logging.getLogger("
|
| 24 |
|
| 25 |
# --- CONFIGURACIÓN DE GEMINI ---
|
| 26 |
try:
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
if not GEMINI_API_KEY:
|
| 29 |
-
st.error("No se encontró la
|
| 30 |
logger.error("GEMINI_API_KEY no encontrada.")
|
| 31 |
st.stop()
|
|
|
|
| 32 |
genai.configure(api_key=GEMINI_API_KEY)
|
| 33 |
logger.info("✅ Configuración de Gemini API realizada.")
|
| 34 |
except Exception as e:
|
|
@@ -38,9 +43,11 @@ except Exception as e:
|
|
| 38 |
|
| 39 |
@st.cache_resource
|
| 40 |
def get_gemini_model():
|
| 41 |
-
|
|
|
|
| 42 |
try:
|
| 43 |
-
|
|
|
|
| 44 |
except Exception as e:
|
| 45 |
st.error(f"❌ No se pudo cargar el modelo Gemini: {e}")
|
| 46 |
logger.error(f"Error cargando modelo Gemini: {e}")
|
|
@@ -50,264 +57,207 @@ model = get_gemini_model()
|
|
| 50 |
|
| 51 |
@st.cache_data
|
| 52 |
def load_data():
|
| 53 |
-
"""Carga
|
| 54 |
try:
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
with open(path_codigos, 'r', encoding='utf-8') as f:
|
| 59 |
-
data_codigos = json.load(f)
|
| 60 |
-
|
| 61 |
-
with open(path_notificacion, 'r', encoding='utf-8') as f:
|
| 62 |
-
data_notificacion = json.load(f)
|
| 63 |
-
|
| 64 |
-
notificacion_map = {item['FICHA']: item for item in data_notificacion}
|
| 65 |
-
nombres_eventos_limpios = sorted(list(set(item['Evento'] for item in data_notificacion)))
|
| 66 |
-
|
| 67 |
-
logger.info(f"✅ Datos cargados.")
|
| 68 |
-
return data_codigos, data_notificacion, notificacion_map, nombres_eventos_limpios
|
| 69 |
-
|
| 70 |
-
except FileNotFoundError as e:
|
| 71 |
-
st.error(f"❌ Error: No se encontró el archivo {e.filename}. Asegúrate de que los archivos JSON estén en la carpeta 'PLANTILLAS'.")
|
| 72 |
-
return None, None, None, None
|
| 73 |
-
except json.JSONDecodeError as e:
|
| 74 |
-
st.error(f"❌ Error: El archivo JSON tiene un formato incorrecto: {e}")
|
| 75 |
-
return None, None, None, None
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
# --- LÓGICA DE BÚSQUEDA ---
|
| 81 |
-
|
| 82 |
-
def search_direct(query, data, notif_map):
|
| 83 |
-
query_lower = query.lower().strip()
|
| 84 |
-
matching_items = []
|
| 85 |
-
for item in data:
|
| 86 |
-
if (query_lower == item.get("CIE 10", "").lower().strip() or
|
| 87 |
-
query_lower == item.get("CIE 11", "").lower().strip() or
|
| 88 |
-
query_lower == item.get("FICHA", "").lower().strip() or
|
| 89 |
-
query_lower in item.get("NOMBRES DE PROTOCOLOS", "").lower()):
|
| 90 |
-
matching_items.append(item)
|
| 91 |
-
if not matching_items: return []
|
| 92 |
-
grouped_by_ficha = {}
|
| 93 |
-
for item in matching_items:
|
| 94 |
-
ficha = item.get("FICHA")
|
| 95 |
-
if ficha:
|
| 96 |
-
if ficha not in grouped_by_ficha: grouped_by_ficha[ficha] = []
|
| 97 |
-
grouped_by_ficha[ficha].append(item)
|
| 98 |
-
final_results = []
|
| 99 |
-
for ficha, matched_details in grouped_by_ficha.items():
|
| 100 |
-
info_notificacion = notif_map.get(ficha)
|
| 101 |
-
if info_notificacion:
|
| 102 |
-
final_results.append({"info_notificacion": info_notificacion, "detalles_codigos": matched_details})
|
| 103 |
-
return final_results
|
| 104 |
|
|
|
|
|
|
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
|
|
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
|
|
|
| 109 |
|
| 110 |
-
def
|
| 111 |
-
|
|
|
|
| 112 |
system_prompt = f"""
|
| 113 |
-
Eres un
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
"""
|
| 124 |
try:
|
| 125 |
response = model.generate_content(system_prompt)
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
return [name.strip() for name in result_text.split(';') if name.strip()]
|
| 130 |
-
else:
|
| 131 |
-
return "Respuesta bloqueada por filtros de seguridad."
|
| 132 |
-
except google.api_core.exceptions.ResourceExhausted as e:
|
| 133 |
-
return "Se ha alcanzado el límite de consultas. Por favor, espere un minuto y vuelva a intentarlo."
|
| 134 |
except Exception as e:
|
| 135 |
-
|
| 136 |
-
|
|
|
|
| 137 |
|
| 138 |
-
def
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
"{query}"
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
**
|
| 154 |
-
|
| 155 |
-
**
|
| 156 |
-
**
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
"""
|
| 161 |
try:
|
| 162 |
-
response = model.generate_content(
|
| 163 |
-
|
| 164 |
-
else: return "El análisis no pudo ser completado. La respuesta de la IA fue bloqueada por filtros de seguridad."
|
| 165 |
-
except google.api_core.exceptions.ResourceExhausted:
|
| 166 |
-
return "Se ha alcanzado el límite de consultas para el análisis. Por favor, espere un minuto y vuelva a intentarlo."
|
| 167 |
except Exception as e:
|
| 168 |
-
|
| 169 |
-
|
| 170 |
|
| 171 |
# --- INTERFAZ DE USUARIO (UI) ---
|
| 172 |
-
col_img, col_text = st.columns([1,
|
| 173 |
-
with col_img: st.image("
|
| 174 |
with col_text:
|
| 175 |
-
st.title("
|
| 176 |
-
st.markdown("
|
| 177 |
|
|
|
|
| 178 |
if 'search_results' not in st.session_state: st.session_state.search_results = None
|
| 179 |
-
if '
|
| 180 |
|
| 181 |
def clear_search_state():
|
| 182 |
st.session_state.search_results = None
|
| 183 |
-
st.session_state.
|
| 184 |
|
| 185 |
with st.form(key="search_form"):
|
| 186 |
-
query = st.
|
| 187 |
-
submitted = st.form_submit_button("
|
| 188 |
|
| 189 |
if submitted:
|
| 190 |
-
if not query:
|
| 191 |
-
|
|
|
|
|
|
|
| 192 |
else:
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
if event_names:
|
| 206 |
-
fichas_encontradas = set()
|
| 207 |
-
for name in event_names:
|
| 208 |
-
for item in data_notificacion:
|
| 209 |
-
if item['Evento'] == name: fichas_encontradas.add(item['FICHA'])
|
| 210 |
-
|
| 211 |
-
query_keywords = [kw for kw in query.lower().split() if len(kw) > 2] # Keywords de la búsqueda original
|
| 212 |
-
|
| 213 |
-
for ficha in fichas_encontradas:
|
| 214 |
-
info_notif = notificacion_map.get(ficha)
|
| 215 |
-
all_details_for_ficha = [d for d in data_codigos if d.get("FICHA") == ficha]
|
| 216 |
-
|
| 217 |
-
filtered_details = []
|
| 218 |
-
if query_keywords:
|
| 219 |
-
for detail in all_details_for_ficha:
|
| 220 |
-
protocol_name_lower = detail.get("NOMBRES DE PROTOCOLOS", "").lower()
|
| 221 |
-
if any(keyword in protocol_name_lower for keyword in query_keywords):
|
| 222 |
-
filtered_details.append(detail)
|
| 223 |
-
|
| 224 |
-
final_details = filtered_details if filtered_details else all_details_for_ficha
|
| 225 |
-
|
| 226 |
-
if info_notif:
|
| 227 |
-
results.append({
|
| 228 |
-
"info_notificacion": info_notif,
|
| 229 |
-
"detalles_codigos": final_details
|
| 230 |
-
})
|
| 231 |
-
else:
|
| 232 |
-
results = direct_results
|
| 233 |
-
|
| 234 |
-
st.session_state.search_results = results
|
| 235 |
-
st.session_state.last_query = query
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
if st.session_state.search_results is not None: st.button("Limpiar Búsqueda", on_click=clear_search_state)
|
| 241 |
-
st.markdown("---")
|
| 242 |
|
| 243 |
if st.session_state.search_results is not None:
|
|
|
|
|
|
|
|
|
|
| 244 |
results = st.session_state.search_results
|
| 245 |
if not results:
|
| 246 |
-
st.
|
| 247 |
else:
|
| 248 |
-
st.success(f"
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
st.
|
| 263 |
-
st.
|
| 264 |
-
st.
|
| 265 |
-
|
| 266 |
-
col1,col2=st.columns(2)
|
| 267 |
-
for i,(display_name,field_key) in enumerate(classification_fields):
|
| 268 |
-
value=info.get(field_key,"NO")
|
| 269 |
-
icon="✅" if value=="SI" else "❌"
|
| 270 |
-
md_string=f"{icon} **{display_name}**"
|
| 271 |
-
if i<len(classification_fields)/2: col1.markdown(md_string)
|
| 272 |
-
else: col2.markdown(md_string)
|
| 273 |
-
st.markdown("---")
|
| 274 |
-
st.subheader("Rango de Edad Permitido para Notificación")
|
| 275 |
-
age_fields=[("Menor de 1 año","Menor de 1 año"),("De 1 a 4 años","De 1 a 4 años"),("De 5 a 14 años","De 5 a 14 años"),("De 15 a 44 años","De 15 a 44 años"),("De 45 a 59 años","De 45 a 59 años"),("De 60 y más","De 60 y más")]
|
| 276 |
-
age_col1,age_col2=st.columns(2)
|
| 277 |
-
for i,(display_name,field_key) in enumerate(age_fields):
|
| 278 |
-
value=info.get(field_key,"NO")
|
| 279 |
-
icon="✅" if value=="SI" else "❌"
|
| 280 |
-
md_string=f"{icon} **{display_name}**"
|
| 281 |
-
if i<len(age_fields)/2: age_col1.markdown(md_string)
|
| 282 |
-
else: age_col2.markdown(md_string)
|
| 283 |
-
st.markdown("---")
|
| 284 |
-
st.subheader("Condición Final Permitida")
|
| 285 |
-
final_condition_fields=[("Vivo","Condición final Vivo"),("Muerto","Condición final Muerto")]
|
| 286 |
-
cond_col1,cond_col2=st.columns(2)
|
| 287 |
-
for i,(display_name,field_key) in enumerate(final_condition_fields):
|
| 288 |
-
value=info.get(field_key,"NO")
|
| 289 |
-
icon="✅" if value=="SI" else "❌"
|
| 290 |
-
md_string=f"{icon} **{display_name}**"
|
| 291 |
-
if i==0: cond_col1.markdown(md_string)
|
| 292 |
-
else: cond_col2.markdown(md_string)
|
| 293 |
-
st.subheader("Códigos Relacionados (CIE-10 y CIE-11)")
|
| 294 |
-
for codigo in codigos:
|
| 295 |
-
st.markdown(f"""- **Nombre Protocolo:** `{codigo.get('NOMBRES DE PROTOCOLOS', 'N/A')}`\n - **CIE-10:** `{codigo.get('CIE 10', 'N/A')}`\n - **CIE-11:** `{codigo.get('CIE 11', 'N/A')}` | **Nombre CIE-11:** *{codigo.get('NOMBRES DE CIE 11', 'N/A')}*""")
|
| 296 |
-
|
| 297 |
-
with tab_def:
|
| 298 |
-
definicion_de_caso = info.get("Definición de caso", "").strip()
|
| 299 |
-
if definicion_de_caso: st.markdown(definicion_de_caso.replace('\n', ' \n'))
|
| 300 |
-
else: st.info("No se encontró una definición de caso oficial para este evento.")
|
| 301 |
-
|
| 302 |
-
with tab_analysis:
|
| 303 |
-
st.info("Este análisis compara su búsqueda original con la definición oficial del evento encontrado.")
|
| 304 |
-
definicion_de_caso_para_analisis = info.get("Definición de caso", "").strip()
|
| 305 |
-
if not definicion_de_caso_para_analisis:
|
| 306 |
-
st.warning("No se puede realizar el análisis porque no hay una definición de caso disponible para este evento.")
|
| 307 |
-
else:
|
| 308 |
-
|
| 309 |
-
with st.spinner("Realizando análisis con IA..."):
|
| 310 |
-
evento_name = info.get("Evento", "N/A")
|
| 311 |
-
ficha_number = info.get("FICHA", "N/A")
|
| 312 |
-
analysis_result = analyze_query_with_gemini(st.session_state.last_query, definicion_de_caso_para_analisis, evento_name, ficha_number)
|
| 313 |
-
st.markdown(analysis_result)
|
|
|
|
| 1 |
+
# ==================== Buscador de Intolerancias Alimentarias Basado en Síntomas =====================================
|
| 2 |
+
# Adaptado por la IA para un nuevo objetivo de análisis de casos clínicos.
|
| 3 |
+
# Idea original y estructura de JAIRO ALEXANDER ERASO MD y DIANA MILENA SOLER MARTINEZ Psc.
|
|
|
|
| 4 |
|
| 5 |
import streamlit as st
|
| 6 |
import google.generativeai as genai
|
|
|
|
| 8 |
import os
|
| 9 |
import json
|
| 10 |
import logging
|
| 11 |
+
import re
|
|
|
|
| 12 |
|
| 13 |
st.set_page_config(
|
| 14 |
+
page_title="Asistente de Bienestar Digestivo",
|
| 15 |
+
page_icon="🍎", # Ícono cambiado para reflejar el tema de alimentos
|
| 16 |
layout="wide"
|
| 17 |
)
|
| 18 |
|
| 19 |
# Configurar logging
|
| 20 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 21 |
+
logger = logging.getLogger("food_intolerance_app")
|
| 22 |
|
| 23 |
# --- CONFIGURACIÓN DE GEMINI ---
|
| 24 |
try:
|
| 25 |
+
# Para Streamlit Community Cloud, usa st.secrets
|
| 26 |
+
if 'GEMINI_API_KEY' in st.secrets:
|
| 27 |
+
GEMINI_API_KEY = st.secrets['GEMINI_API_KEY']
|
| 28 |
+
else:
|
| 29 |
+
# Para desarrollo local, usa variables de entorno
|
| 30 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 31 |
+
|
| 32 |
if not GEMINI_API_KEY:
|
| 33 |
+
st.error("No se encontró la GEMINI_API_KEY. Por favor, configúrala en los Secrets de Streamlit.")
|
| 34 |
logger.error("GEMINI_API_KEY no encontrada.")
|
| 35 |
st.stop()
|
| 36 |
+
|
| 37 |
genai.configure(api_key=GEMINI_API_KEY)
|
| 38 |
logger.info("✅ Configuración de Gemini API realizada.")
|
| 39 |
except Exception as e:
|
|
|
|
| 43 |
|
| 44 |
@st.cache_resource
|
| 45 |
def get_gemini_model():
|
| 46 |
+
"""Carga el modelo generativo de Gemini."""
|
| 47 |
+
logger.info("🔄 Cargando modelo Gemini (gemini-1.5-flash)...")
|
| 48 |
try:
|
| 49 |
+
# Usamos un modelo más reciente y potente para tareas de extracción y análisis
|
| 50 |
+
return genai.GenerativeModel("gemini-1.5-flash")
|
| 51 |
except Exception as e:
|
| 52 |
st.error(f"❌ No se pudo cargar el modelo Gemini: {e}")
|
| 53 |
logger.error(f"Error cargando modelo Gemini: {e}")
|
|
|
|
| 57 |
|
| 58 |
@st.cache_data
|
| 59 |
def load_data():
|
| 60 |
+
"""Carga la base de datos de alimentos e intolerancias."""
|
| 61 |
try:
|
| 62 |
+
# Asegúrate de que el archivo se llame 'alimentos.json' y esté en una carpeta 'DATOS'
|
| 63 |
+
path_alimentos = os.path.join('DATOS', 'alimentos.json')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
+
with open(path_alimentos, 'r', encoding='utf-8') as f:
|
| 66 |
+
data_alimentos = json.load(f)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
+
logger.info(f"✅ Base de datos de alimentos cargada con {len(data_alimentos)} registros.")
|
| 69 |
+
return data_alimentos
|
| 70 |
|
| 71 |
+
except FileNotFoundError:
|
| 72 |
+
st.error("❌ Error: No se encontró el archivo 'alimentos.json' en la carpeta 'DATOS'.")
|
| 73 |
+
return None
|
| 74 |
+
except json.JSONDecodeError:
|
| 75 |
+
st.error("❌ Error: El archivo 'alimentos.json' tiene un formato incorrecto.")
|
| 76 |
+
return None
|
| 77 |
|
| 78 |
+
alimentos_data = load_data()
|
| 79 |
|
| 80 |
+
# Mapeo de alimentos comunes a los compuestos/intolerancias del JSON.
|
| 81 |
+
# Este diccionario es crucial y debe expandirse.
|
| 82 |
+
FOOD_TO_COMPOUND_MAP = {
|
| 83 |
+
"pan": ["Gluten"], "trigo": ["Gluten"], "cebada": ["Gluten"], "centeno": ["Gluten"], "pasta": ["Gluten"], "galletas": ["Gluten"],
|
| 84 |
+
"leche": ["Caseína", "Lactosa"], "queso": ["Caseína", "Lactosa"], "yogur": ["Caseína", "Lactosa"], "mantequilla": ["Caseína", "Lactosa"],
|
| 85 |
+
"manzana": ["Salicilatos"], "almendras": ["Salicilatos"], "uvas": ["Salicilatos"],
|
| 86 |
+
"azucar": ["Azúcar"], "dulces": ["Azúcar"], "refrescos": ["Azúcar"], "gaseosas": ["Azúcar"]
|
| 87 |
+
}
|
| 88 |
|
| 89 |
+
# --- LÓGICA DE BÚSQUEDA Y ANÁLISIS ---
|
| 90 |
|
| 91 |
+
def extract_entities_with_gemini(query):
|
| 92 |
+
"""Usa Gemini para extraer alimentos y síntomas de la consulta del usuario."""
|
| 93 |
+
if not model: return None
|
| 94 |
system_prompt = f"""
|
| 95 |
+
Eres un experto en procesar textos clínicos. Tu tarea es analizar la consulta de un usuario y extraer dos tipos de entidades: alimentos consumidos y síntomas experimentados.
|
| 96 |
+
Debes devolver la respuesta ÚNICAMENTE en formato JSON, con las claves "alimentos" y "sintomas".
|
| 97 |
+
Normaliza los síntomas a términos médicos o comunes. Por ejemplo, "se me hincha el estómago" debería ser "hinchazón abdominal". "Mente nublada" debería ser "niebla mental".
|
| 98 |
+
|
| 99 |
+
Ejemplo 1:
|
| 100 |
+
Consulta: "Cuando como mucho pan se me hincha el estomago, me da niebla mental y siento fatiga."
|
| 101 |
+
Respuesta:
|
| 102 |
+
{{
|
| 103 |
+
"alimentos": ["pan"],
|
| 104 |
+
"sintomas": ["hinchazón abdominal", "niebla mental", "fatiga"]
|
| 105 |
+
}}
|
| 106 |
+
|
| 107 |
+
Ejemplo 2:
|
| 108 |
+
Consulta: "Después de tomar leche, mi hijo tiene diarrea y se queja de dolor de barriga."
|
| 109 |
+
Respuesta:
|
| 110 |
+
{{
|
| 111 |
+
"alimentos": ["leche"],
|
| 112 |
+
"sintomas": ["diarrea", "dolor abdominal"]
|
| 113 |
+
}}
|
| 114 |
+
|
| 115 |
+
Ahora, analiza la siguiente consulta:
|
| 116 |
+
Consulta: "{query}"
|
| 117 |
"""
|
| 118 |
try:
|
| 119 |
response = model.generate_content(system_prompt)
|
| 120 |
+
# Limpiar y parsear la respuesta JSON
|
| 121 |
+
json_text = re.search(r'\{.*\}', response.text, re.DOTALL).group(0)
|
| 122 |
+
return json.loads(json_text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
except Exception as e:
|
| 124 |
+
logger.error(f"Error extrayendo entidades con Gemini: {e}")
|
| 125 |
+
st.error(f"Hubo un problema al interpretar tu consulta con la IA. Error: {e}")
|
| 126 |
+
return None
|
| 127 |
|
| 128 |
+
def find_best_matches(entities, data):
|
| 129 |
+
"""Encuentra las mejores coincidencias en la base de datos basadas en las entidades extraídas."""
|
| 130 |
+
if not entities or not data: return []
|
| 131 |
+
|
| 132 |
+
user_symptoms = set([s.lower() for s in entities.get("sintomas", [])])
|
| 133 |
+
user_foods = [f.lower() for f in entities.get("alimentos", [])]
|
| 134 |
+
|
| 135 |
+
# Traducir alimentos a compuestos
|
| 136 |
+
candidate_compounds = set()
|
| 137 |
+
for food in user_foods:
|
| 138 |
+
if food in FOOD_TO_COMPOUND_MAP:
|
| 139 |
+
candidate_compounds.update(FOOD_TO_COMPOUND_MAP[food])
|
| 140 |
+
|
| 141 |
+
scores = {}
|
| 142 |
+
for index, entry in enumerate(data):
|
| 143 |
+
score = 0
|
| 144 |
+
entry_compound = entry.get("Compuesto / Alimento Intolerante", "").split('(')[0].strip()
|
| 145 |
+
entry_symptoms = entry.get("Posibles Síntomas (Presentación Clínica Representativa)", "").lower()
|
| 146 |
+
|
| 147 |
+
# Puntuar por coincidencia de compuesto
|
| 148 |
+
if entry_compound in candidate_compounds:
|
| 149 |
+
score += 10 # Puntuación alta por coincidencia directa
|
| 150 |
+
|
| 151 |
+
# Puntuar por coincidencia de síntomas
|
| 152 |
+
for symptom in user_symptoms:
|
| 153 |
+
if symptom in entry_symptoms:
|
| 154 |
+
score += 5
|
| 155 |
+
|
| 156 |
+
if score > 0:
|
| 157 |
+
scores[index] = score
|
| 158 |
+
|
| 159 |
+
# Ordenar por puntuación descendente
|
| 160 |
+
sorted_matches_indices = sorted(scores, key=scores.get, reverse=True)
|
| 161 |
+
|
| 162 |
+
# Devolver los objetos completos de las mejores coincidencias
|
| 163 |
+
return [data[i] for i in sorted_matches_indices]
|
| 164 |
+
|
| 165 |
+
def generate_detailed_analysis(query, match):
|
| 166 |
+
"""Usa Gemini para generar una explicación detallada y amigable para el usuario."""
|
| 167 |
+
if not model: return "Error: El modelo de IA no está disponible."
|
| 168 |
+
|
| 169 |
+
prompt = f"""
|
| 170 |
+
Eres un asistente de IA experto en nutrición y bienestar, diseñado para ayudar a las personas a entender posibles intolerancias alimentarias. Tu tono debe ser claro, empático y educativo.
|
| 171 |
+
|
| 172 |
+
Un usuario ha descrito el siguiente caso:
|
| 173 |
"{query}"
|
| 174 |
+
|
| 175 |
+
Basado en este caso, nuestro sistema ha encontrado una posible coincidencia con la siguiente intolerancia:
|
| 176 |
+
- **Compuesto Intolerante:** {match.get("Compuesto / Alimento Intolerante")}
|
| 177 |
+
- **Síntomas Comunes:** {match.get("Posibles Síntomas (Presentación Clínica Representativa)")}
|
| 178 |
+
- **Enfermedades Asociadas:** {match.get("Enfermedades Asociadas")}
|
| 179 |
+
- **Mecanismo Fisiológico:** {match.get("Mecanismo de Acción / Fisiología Alterada")}
|
| 180 |
+
- **Recomendaciones Generales:** {match.get("Recomendaciones Médicas / Exámenes Confirmatorios")}
|
| 181 |
+
|
| 182 |
+
**Tu Tarea:**
|
| 183 |
+
Redacta una respuesta completa para el usuario. La respuesta debe estar estructurada en las siguientes secciones (usa los títulos en negrita):
|
| 184 |
+
|
| 185 |
+
1. **Posible Causa:** Comienza resumiendo por qué sus síntomas podrían estar relacionados con este compuesto, vinculando directamente su caso con la información proporcionada.
|
| 186 |
+
2. **¿Qué podría estar pasando en tu cuerpo?:** Explica el "Mecanismo Fisiológico" en términos sencillos que cualquiera pueda entender.
|
| 187 |
+
3. **Condiciones Relacionadas:** Menciona las "Enfermedades Asociadas", aclarando que son asociaciones y no un diagnóstico.
|
| 188 |
+
4. **Pasos a Seguir y Recomendaciones:** Presenta las "Recomendaciones" como sugerencias generales, enfatizando la importancia de la supervisión profesional.
|
| 189 |
+
5. **Descargo de Responsabilidad Médico:** Incluye OBLIGATORIAMENTE un párrafo final claro y visible que indique que esta es una herramienta informativa y NO un diagnóstico médico, y que debe consultar a un profesional de la salud.
|
| 190 |
+
|
| 191 |
+
Mantén un lenguaje accesible y evita el alarmismo. El objetivo es informar y guiar, no diagnosticar.
|
| 192 |
"""
|
| 193 |
try:
|
| 194 |
+
response = model.generate_content(prompt)
|
| 195 |
+
return response.text
|
|
|
|
|
|
|
|
|
|
| 196 |
except Exception as e:
|
| 197 |
+
logger.error(f"Error generando análisis detallado con Gemini: {e}")
|
| 198 |
+
return "No se pudo generar el análisis detallado. Por favor, inténtalo de nuevo."
|
| 199 |
|
| 200 |
# --- INTERFAZ DE USUARIO (UI) ---
|
| 201 |
+
col_img, col_text = st.columns([1, 6])
|
| 202 |
+
with col_img: st.image("🍎", width=100) # Ícono actualizado
|
| 203 |
with col_text:
|
| 204 |
+
st.title("Asistente de Bienestar Digestivo")
|
| 205 |
+
st.markdown("Explora la posible relación entre lo que comes y cómo te sientes.")
|
| 206 |
|
| 207 |
+
# Manejo de estado
|
| 208 |
if 'search_results' not in st.session_state: st.session_state.search_results = None
|
| 209 |
+
if 'user_query' not in st.session_state: st.session_state.user_query = ""
|
| 210 |
|
| 211 |
def clear_search_state():
|
| 212 |
st.session_state.search_results = None
|
| 213 |
+
st.session_state.user_query = ""
|
| 214 |
|
| 215 |
with st.form(key="search_form"):
|
| 216 |
+
query = st.text_area("Describe tu caso aquí:", height=150, placeholder="Ej: Cuando como mucho pan se me hincha el estómago, me da niebla mental y siento fatiga, ¿qué puede ser?")
|
| 217 |
+
submitted = st.form_submit_button("Analizar mi caso", type="primary")
|
| 218 |
|
| 219 |
if submitted:
|
| 220 |
+
if not query:
|
| 221 |
+
st.warning("Por favor, describe lo que sientes y lo que comiste.")
|
| 222 |
+
elif alimentos_data is None:
|
| 223 |
+
st.error("La base de datos de alimentos no está disponible. No se puede continuar.")
|
| 224 |
else:
|
| 225 |
+
st.session_state.user_query = query
|
| 226 |
+
with st.spinner("Interpretando tu caso con IA..."):
|
| 227 |
+
entities = extract_entities_with_gemini(query)
|
| 228 |
+
|
| 229 |
+
if entities:
|
| 230 |
+
st.info(f"IA identificó - Alimentos: {', '.join(entities.get('alimentos',[])) or 'Ninguno'}, Síntomas: {', '.join(entities.get('sintomas',[])) or 'Ninguno'}")
|
| 231 |
+
with st.spinner("Buscando posibles causas en la base de conocimiento..."):
|
| 232 |
+
results = find_best_matches(entities, alimentos_data)
|
| 233 |
+
st.session_state.search_results = results
|
| 234 |
+
else:
|
| 235 |
+
st.error("No se pudieron identificar alimentos o síntomas claros en tu descripción. Intenta ser un poco más específico.")
|
| 236 |
+
st.session_state.search_results = [] # Resetear a lista vacía
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
|
| 238 |
if st.session_state.search_results is not None:
|
| 239 |
+
st.button("Realizar nueva consulta", on_click=clear_search_state)
|
| 240 |
+
st.markdown("---")
|
| 241 |
+
|
| 242 |
results = st.session_state.search_results
|
| 243 |
if not results:
|
| 244 |
+
st.warning(f"No se encontraron coincidencias claras en nuestra base de datos para tu caso: '{st.session_state.user_query}'.")
|
| 245 |
else:
|
| 246 |
+
st.success(f"Hemos encontrado {len(results)} posible(s) causa(s) relacionada(s) con tu caso.")
|
| 247 |
+
|
| 248 |
+
# Tomar solo el mejor resultado para el análisis detallado
|
| 249 |
+
best_match = results[0]
|
| 250 |
+
|
| 251 |
+
with st.expander(f"**Principal Coincidencia: {best_match.get('Compuesto / Alimento Intolerante')}**", expanded=True):
|
| 252 |
+
with st.spinner("Generando un análisis detallado con IA..."):
|
| 253 |
+
detailed_analysis = generate_detailed_analysis(st.session_state.user_query, best_match)
|
| 254 |
+
st.markdown(detailed_analysis)
|
| 255 |
+
|
| 256 |
+
# Opcional: Mostrar otros resultados menos probables
|
| 257 |
+
if len(results) > 1:
|
| 258 |
+
with st.expander("Otras posibles coincidencias (menos probables)"):
|
| 259 |
+
for result in results[1:]:
|
| 260 |
+
st.subheader(f"{result.get('Compuesto / Alimento Intolerante')}")
|
| 261 |
+
st.write(f"**Síntomas asociados:** {result.get('Posibles Síntomas (Presentación Clínica Representativa)')}")
|
| 262 |
+
st.write(f"**Mecanismo:** {result.get('Mecanismo de Acción / Fisiología Alterada')}")
|
| 263 |
+
st.markdown("---")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|