JairoCesar commited on
Commit
b95781f
·
verified ·
1 Parent(s): dd98e3c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +188 -238
app.py CHANGED
@@ -1,7 +1,6 @@
1
- # ==================== Buscador de Eventos para Notificación SIVIGILA - Colombia 2025 =====================================
2
-
3
- # JAIRO ALEXANDER ERASO MD U Nacional de Colombia.
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 datetime
13
-
14
 
15
  st.set_page_config(
16
- page_title="Buscador Inteligente SIVIGILA Colombia",
17
- page_icon="buho.png",
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("sivigila_app")
24
 
25
  # --- CONFIGURACIÓN DE GEMINI ---
26
  try:
27
- GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
 
 
 
 
 
 
28
  if not GEMINI_API_KEY:
29
- st.error("No se encontró la variable de entorno GEMINI_API_KEY. Por favor, configúrala en tu archivo .env o en los Secrets.")
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
- logger.info("🔄 Cargando modelo Gemini (gemini-2.0-flash-lite)...")
 
42
  try:
43
- return genai.GenerativeModel("gemini-2.0-flash-lite")
 
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 y procesa los archivos JSON, uniéndolos para un acceso rápido."""
54
  try:
55
- path_codigos = os.path.join('PLANTILLAS', 'DIC_NOMBRES_CODIGOS_FICHAS.json')
56
- path_notificacion = os.path.join('PLANTILLAS', 'DATOS_NOTIFICACION.json')
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
- data_codigos, data_notificacion, notificacion_map, nombres_eventos_limpios = load_data()
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 search_with_gemini(query, event_list):
111
- if not model: return "Error: Modelo Gemini no disponible."
 
112
  system_prompt = f"""
113
- Eres un asesor experto en SIVIGILA, el sistema de vigilancia en salud pública de Colombia.
114
- Tu tarea es interpretar la consulta de un profesional de la salud, que puede ser un término coloquial, un sinónimo o una descripción, y asociarla al nombre del evento oficial más relevante de una lista.
115
- LISTA DE EVENTOS OFICIALES:
116
- {'; '.join(event_list)}
117
- Analiza la siguiente consulta y devuelve ÚNICAMENTE el nombre exacto del evento oficial de la lista que mejor corresponda.
118
- Por ejemplo, si la consulta es 'mordedura de culebra', la respuesta debe ser 'Accidente Ofídico'.
119
- Si encuentras varios, sepáralos con un punto y coma (;).
120
- Si la consulta no corresponde a ningún evento de la lista, responde con "NO_ENCONTRADO".
121
- No añadas explicaciones ni texto adicional.
122
- Consulta del usuario: "{query}"
 
 
 
 
 
 
 
 
 
 
 
 
123
  """
124
  try:
125
  response = model.generate_content(system_prompt)
126
- if response.parts:
127
- result_text = response.text.strip()
128
- if result_text == "NO_ENCONTRADO": return []
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
- return f"Error en la comunicación con la API de Gemini: {e}"
136
-
 
137
 
138
- def analyze_query_with_gemini(query, definition, evento_name, ficha_number):
139
- if not model: return "Error: Modelo Gemini no disponible."
140
- analysis_prompt = f"""
141
- Eres un auditor experto del sistema SIVIGILA de Colombia. Tu única misión es determinar si un caso descrito por un profesional de la salud cumple con los criterios de la definición de caso oficial para ser de notificación obligatoria.
142
- **Reglas Estrictas:**
143
- 1. Tu decisión final solo puede ser "NOTIFICAR" o "NO CUMPLE CRITERIOS".
144
- 2. Debes basar tu justificación únicamente en la comparación entre la consulta y la definición de caso proporcionada.
145
- 3. No debes inventar información ni hacer suposiciones clínicas.
146
- **CONTEXTO DEL EVENTO YA IDENTIFICADO:**
147
- - Nombre del Evento: "{evento_name}"
148
- - Ficha de Notificación: "{ficha_number}"
149
- **CONSULTA DEL USUARIO:**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  "{query}"
151
- **DEFINICIÓN DE CASO OFICIAL:**
152
- "{definition}"
153
- **TU ANÁLISIS ESTRUCTURADO:**
154
- Basado en la comparación, responde OBLIGATORIAMENTE en el siguiente formato, sin añadir texto adicional:
155
- **Decisión:** [Escribe aquí "NOTIFICAR" o "NO CUMPLE CRITERIOS"]
156
- **Justificación:**
157
- [Si la decisión es NOTIFICAR, explica brevemente qué criterios de la definición de caso se cumplen en la consulta del usuario. Si la decisión es NO CUMPLE CRITERIOS, explica qué criterios clave faltan o se contradicen explícitamente.]
158
- **Recomendación:**
159
- [Si la decisión es NOTIFICAR, redacta una recomendación para proceder con la notificación, mencionando explícitamente el número de Ficha y el nombre del Evento proporcionados en el contexto. Si la decisión es NO CUMPLE CRITERIOS, escribe: "Se recomienda revisar las guías y protocolos del Instituto Nacional de Salud de Colombia para determinar el manejo adecuado del caso."]
 
 
 
 
 
 
 
 
 
160
  """
161
  try:
162
- response = model.generate_content(analysis_prompt)
163
- if response.parts: return response.text
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
- return f"Ocurrió un error durante el análisis de la IA: {e}"
169
-
170
 
171
  # --- INTERFAZ DE USUARIO (UI) ---
172
- col_img, col_text = st.columns([1, 5], gap="medium")
173
- with col_img: st.image("buho.png", width=100)
174
  with col_text:
175
- st.title("Buscador Inteligente SIVIGILA")
176
- st.markdown("Herramienta de apoyo para la notificación de eventos de salud pública en Colombia por Diana Soler")
177
 
 
178
  if 'search_results' not in st.session_state: st.session_state.search_results = None
179
- if 'last_query' not in st.session_state: st.session_state.last_query = ""
180
 
181
  def clear_search_state():
182
  st.session_state.search_results = None
183
- st.session_state.last_query = ""
184
 
185
  with st.form(key="search_form"):
186
- query = st.text_input("Ingrese su búsqueda:", placeholder="Ej: Lepra, T630, mordedura de culebra, inhalación de canela, niño de 4 años y 3 kilos..", help="Puede buscar por nombre de la enfermedad, palabras clave, o códigos CIE.")
187
- submitted = st.form_submit_button("Buscar", type="primary")
188
 
189
  if submitted:
190
- if not query: st.warning("Por favor, ingrese un término de búsqueda.")
191
- elif data_codigos is None: st.error("Los datos no se pudieron cargar.")
 
 
192
  else:
193
- with st.spinner("Buscando con IA (Gemini)..."):
194
- results = []
195
- direct_results = search_direct(query, data_codigos, notificacion_map)
196
-
197
- if not direct_results:
198
- logger.info(f"Búsqueda directa para '{query}' no arrojó resultados. Usando IA (Gemini)...")
199
- event_names = search_with_gemini(query, nombres_eventos_limpios)
200
-
201
- if isinstance(event_names, str):
202
- st.error(event_names)
203
- event_names = []
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.info(f"No se encontraron eventos de notificación obligatoria para su búsqueda: '{st.session_state.last_query}'.")
247
  else:
248
- st.success(f"Se encontraron {len(results)} evento(s) relacionado(s) con su búsqueda: '{st.session_state.last_query}'")
249
- for result in results:
250
- info = result["info_notificacion"]
251
- codigos = result["detalles_codigos"]
252
- with st.expander(f"**Evento: {info.get('Evento', 'Sin nombre')} (Ficha: {info.get('FICHA', 'N/A')})**", expanded=True):
253
- tab_info, tab_def, tab_analysis = st.tabs(["Información General", "📖 Definición de Caso", "🤖 Análisis por IA"])
254
- with tab_info:
255
- st.subheader("Información de Notificación")
256
- notif_super = info.get("Notificación superinmedita", "NO") == "SI"
257
- if notif_super: st.error(f"**¡ATENCIÓN! Requiere Notificación SUPERINMEDIATA:** {info.get('Descripción superinmedita', '')}")
258
- elif info.get("Notificación Inmediata", "NO") == "SI": st.warning("**Requiere Notificación Inmediata (dentro de las 24 horas).**")
259
- else: st.info("**Notificación Semanal Individual.**")
260
- st.markdown(f"**Requisito:** {info.get('REQUISITO', 'No especificado')}")
261
- fichas_texto = info.get('Fichas a Utilizar', 'No especificado').replace('+', '\n+ ')
262
- st.markdown(f"**Fichas a utilizar:** {fichas_texto}")
263
- st.markdown("---")
264
- st.subheader("Clasificación de Caso Permitida")
265
- classification_fields=[("Sospechoso","Clasificación Permitida Sospechoso"),("Probable","Clasificación Permitida Probable"),("Conf. Clínica","Clasificación Permitida Conf. Clínica"),("Conf. Laboratorio","Clasificación Permitida Conf. Laboratorio"),("Conf. Nexo Epi.","Clasificación Permitida Conf. Nexo Epi."),("Descartado","Clasificación Permitida Descartado")]
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("---")