JairoCesar commited on
Commit
bb2fac2
·
verified ·
1 Parent(s): 71dcc09

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +43 -60
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # ==================== El Detective de Alimentos (Versión 10.1) =====================================
2
  # Por: JAIRO CESAR ALEXANDER E. MD DIANA MILENA SOLER MARTINEZ PSI. ESP. U JUAN N CORPAS
3
 
4
  import streamlit as st
@@ -20,7 +20,6 @@ st.set_page_config(
20
  )
21
 
22
  # --- CONFIGURACIÓN Y CARGA DE DATOS ---
23
- # (Esta sección no tiene cambios)
24
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
25
  logger = logging.getLogger("food_detective_app")
26
  try:
@@ -164,16 +163,11 @@ SYMPTOM_KEYWORD_MAP = {
164
  }
165
 
166
  def reinforce_entities_with_keywords(entities, query, food_map, symptom_map):
167
- """
168
- Revisa la salida de la IA y añade alimentos Y síntomas clave si fueron omitidos.
169
- Esta versión es más inteligente para evitar la doble contabilización de síntomas.
170
- """
171
  if not entities:
172
  entities = {"alimentos": [], "sintomas": []}
173
 
174
  query_sanitized = sanitize_text(query)
175
 
176
- # --- Reforzar Alimentos (Lógica existente, sin cambios) ---
177
  current_foods = entities.get("alimentos", [])
178
  current_foods_sanitized = {sanitize_text(f) for f in current_foods}
179
  for food_keyword in food_map.keys():
@@ -182,23 +176,17 @@ def reinforce_entities_with_keywords(entities, query, food_map, symptom_map):
182
  current_foods.append(food_keyword)
183
  entities["alimentos"] = list(set(current_foods))
184
 
185
- # --- LÓGICA MEJORADA: Reforzar Síntomas (Evita redundancia) ---
186
  current_symptoms = entities.get("sintomas", [])
187
  if current_symptoms is None: current_symptoms = []
188
 
189
- # Usamos una copia de la consulta para poder modificarla
190
  query_to_search_symptoms = query_sanitized
191
 
192
- # Ordenamos las palabras clave de síntomas por longitud, de la más larga a la más corta
193
- # Esto asegura que "dolor de cabeza" se procese antes que "dolor"
194
  sorted_symptom_keywords = sorted(symptom_map.keys(), key=len, reverse=True)
195
 
196
  for symptom_keyword in sorted_symptom_keywords:
197
- # Si encontramos el síntoma más específico...
198
  if symptom_keyword in query_to_search_symptoms:
199
  logger.info(f"Red de seguridad (Síntoma): Añadiendo '{symptom_keyword}'.")
200
  current_symptoms.append(symptom_keyword)
201
- # ...lo eliminamos de la cadena de búsqueda para que sus sub-partes no se vuelvan a encontrar.
202
  query_to_search_symptoms = query_to_search_symptoms.replace(symptom_keyword, "")
203
 
204
  entities["sintomas"] = list(set(current_symptoms))
@@ -209,22 +197,24 @@ def sanitize_text(text):
209
  if not text: return ""
210
  return re.sub(r'[.,;()]', '', text).lower().strip()
211
 
212
- def extract_and_infer_with_gemini(query, condiciones):
213
  if not model: return None
214
- condiciones_str = "\n".join([f"- {c}" for c in condiciones])
215
  system_prompt = f"""
216
- Eres un asistente de triaje clínico experto. Tu tarea es analizar la consulta de un usuario y extraer tres tipos de información:
217
- 1. `alimentos`: Lista de alimentos consumidos.
218
- 2. `sintomas`: Lista de síntomas descritos.
219
- 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.
220
-
221
- LISTA DE CONDICIONES POSIBLES:
222
- {condiciones_str}
223
-
224
- Devuelve la respuesta ÚNICAMENTE en formato JSON estricto.
225
 
226
- 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.".
227
-
 
 
 
 
 
 
 
 
 
228
  Consulta: "{query}"
229
  """
230
  try:
@@ -236,7 +226,7 @@ def extract_and_infer_with_gemini(query, condiciones):
236
  json_text = re.search(r'\{.*\}', response.text, re.DOTALL).group(0)
237
  return json.loads(json_text)
238
  except Exception as e:
239
- logger.error(f"Error en la extracción/inferencia con Gemini: {e}")
240
  st.error(f"Hubo un problema al interpretar tu consulta con la IA.")
241
  return None
242
 
@@ -244,60 +234,57 @@ def find_best_matches_hybrid(entities, data):
244
  if not entities or not data: return []
245
  user_symptoms = set(sanitize_text(s) for s in entities.get("sintomas", []))
246
  user_foods = set(sanitize_text(f) for f in entities.get("alimentos", []))
247
- inferred_condition_raw = sanitize_text(entities.get("condicion_probable", ""))
248
  candidate_terms = set(user_foods)
249
  for food in user_foods:
250
  food_sanitized = sanitize_text(food)
251
  if food_sanitized in FOOD_TO_COMPOUND_MAP:
252
  candidate_terms.update(c.lower() for c in FOOD_TO_COMPOUND_MAP[food_sanitized])
253
- if food_sanitized in ["vino", "vino tinto", "vino rojo"]:
254
- candidate_terms.update(["histamina", "tiramina", "sulfitos"])
255
  results = []
256
  for entry in data:
257
  entry_compounds_text = sanitize_text(entry.get("compuesto_alimento", ""))
 
258
  food_match = any(term in entry_compounds_text for term in candidate_terms)
 
259
  if not food_match:
260
  continue
261
- score_details = {'condition': 0, 'food': 15, 'symptoms': 0}
262
- if inferred_condition_raw:
263
- entry_condition_sanitized = sanitize_text(entry.get("condicion_asociada", ""))
264
- is_match = (entry_condition_sanitized == inferred_condition_raw)
265
- if not is_match and entry_condition_sanitized in CONDITION_SYNONYMS:
266
- if inferred_condition_raw in [sanitize_text(s) for s in CONDITION_SYNONYMS.get(entry_condition_sanitized, [])]:
267
- is_match = True
268
- if is_match:
269
- score_details['condition'] = 100
270
  entry_symptoms_keys = set(sanitize_text(s) for s in entry.get("sintomas_clave", []))
271
  symptom_score = 0
272
  matched_symptoms = []
273
  for user_symptom in user_symptoms:
274
  for key in entry_symptoms_keys:
275
  if key in user_symptom or user_symptom in key:
276
- symptom_score += 10
277
  matched_symptoms.append(key)
278
- break
 
279
  score_details['symptoms'] = symptom_score
280
- score_details['total'] = score_details['condition'] + score_details['food'] + score_details['symptoms']
281
- results.append({
282
- 'entry': entry,
283
- 'score': score_details,
284
- 'matched_symptoms': list(set(matched_symptoms))
285
- })
 
 
 
 
286
  if not results: return []
287
  sorted_results = sorted(results, key=lambda x: x['score']['total'], reverse=True)
288
  return sorted_results
289
 
290
- # --- REEMPLAZA ESTA FUNCIÓN COMPLETA ---
291
 
292
  def generate_detailed_analysis(query, match):
293
  if not model: return "Error: El modelo de IA no está disponible."
294
 
295
- # Comprobación de seguridad: si 'match' es inválido, no podemos continuar.
296
  if not match or not isinstance(match, dict):
297
  logger.error("Se intentó generar un análisis detallado con datos de coincidencia inválidos.")
298
  return "No se pudo generar el análisis detallado debido a un error interno (datos de coincidencia inválidos)."
299
 
300
- # Preparamos una lista de síntomas clave desde nuestra base de datos para enriquecer el prompt
301
  sintomas_clave_texto = ", ".join(match.get("sintomas_clave", ["No especificados"]))
302
 
303
  prompt_parts = [
@@ -437,7 +424,6 @@ with st.form(key="search_form"):
437
 
438
 
439
  if st.session_state.start_analysis:
440
- # Inmediatamente bajamos la bandera para evitar ejecuciones repetidas.
441
  st.session_state.start_analysis = False
442
 
443
  query_to_analyze = st.session_state.query
@@ -451,13 +437,12 @@ if st.session_state.start_analysis:
451
  st.error("La base de datos de alimentos no está disponible.")
452
  else:
453
  with st.spinner("🧠 Interpretando tu caso y buscando pistas con IA..."):
454
- entities = extract_and_infer_with_gemini(query_to_analyze, lista_condiciones)
455
  entities = reinforce_entities_with_keywords(entities, query_to_analyze, FOOD_TO_COMPOUND_MAP, SYMPTOM_KEYWORD_MAP)
456
  st.session_state.entities = entities
457
 
458
  if entities and (entities.get("alimentos") or entities.get("sintomas")):
459
  info_str = f"IA identificó - Alimentos: {', '.join(entities.get('alimentos',[])) or 'Ninguno'}, Síntomas: {', '.join(entities.get('sintomas',[])) or 'Ninguno'}"
460
- if entities.get("condicion_probable"): info_str += f", Condición Probable: {entities.get('condicion_probable')}"
461
  st.info(info_str)
462
  with st.spinner("🔬 Cruzando información y calculando relevancia..."):
463
  results = find_best_matches_hybrid(entities, alimentos_data)
@@ -466,12 +451,11 @@ if st.session_state.start_analysis:
466
  st.error("No se pudieron identificar alimentos o síntomas claros en tu descripción.")
467
  st.session_state.search_results = []
468
 
469
- # La sección de mostrar resultados permanece igual, se ejecuta si hay resultados en el estado.
470
  if st.session_state.search_results is not None:
471
  results = st.session_state.search_results
472
 
473
  if not results:
474
- st.warning(f"No se encontraron coincidencias claras para tu caso: '{st.session_state.user_query}'.")
475
  else:
476
  col1, col2 = st.columns([3,1])
477
  with col1:
@@ -495,11 +479,10 @@ if st.session_state.search_results is not None:
495
  col1, col2 = st.columns([3, 1])
496
  with col1:
497
  st.markdown("##### Desglose de la Puntuación de Relevancia:")
498
- score_col1, score_col2, score_col3, score_col4 = st.columns(4)
499
- score_col1.metric("Puntos por Condición", f"{best_match_data['score']['condition']}")
500
- score_col2.metric("Puntos por Alimento", f"{best_match_data['score']['food']}")
501
- score_col3.metric("Puntos por Síntomas", f"{best_match_data['score']['symptoms']}")
502
- score_col4.metric("PUNTUACIÓN TOTAL", f"{best_match_data['score']['total']}", delta="Máxima coincidencia")
503
  with col2:
504
  st.write("")
505
  if foodb_index:
 
1
+ # ==================== El Detective de Alimentos (Versión 11.0) =====================================
2
  # Por: JAIRO CESAR ALEXANDER E. MD DIANA MILENA SOLER MARTINEZ PSI. ESP. U JUAN N CORPAS
3
 
4
  import streamlit as st
 
20
  )
21
 
22
  # --- CONFIGURACIÓN Y CARGA DE DATOS ---
 
23
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
24
  logger = logging.getLogger("food_detective_app")
25
  try:
 
163
  }
164
 
165
  def reinforce_entities_with_keywords(entities, query, food_map, symptom_map):
 
 
 
 
166
  if not entities:
167
  entities = {"alimentos": [], "sintomas": []}
168
 
169
  query_sanitized = sanitize_text(query)
170
 
 
171
  current_foods = entities.get("alimentos", [])
172
  current_foods_sanitized = {sanitize_text(f) for f in current_foods}
173
  for food_keyword in food_map.keys():
 
176
  current_foods.append(food_keyword)
177
  entities["alimentos"] = list(set(current_foods))
178
 
 
179
  current_symptoms = entities.get("sintomas", [])
180
  if current_symptoms is None: current_symptoms = []
181
 
 
182
  query_to_search_symptoms = query_sanitized
183
 
 
 
184
  sorted_symptom_keywords = sorted(symptom_map.keys(), key=len, reverse=True)
185
 
186
  for symptom_keyword in sorted_symptom_keywords:
 
187
  if symptom_keyword in query_to_search_symptoms:
188
  logger.info(f"Red de seguridad (Síntoma): Añadiendo '{symptom_keyword}'.")
189
  current_symptoms.append(symptom_keyword)
 
190
  query_to_search_symptoms = query_to_search_symptoms.replace(symptom_keyword, "")
191
 
192
  entities["sintomas"] = list(set(current_symptoms))
 
197
  if not text: return ""
198
  return re.sub(r'[.,;()]', '', text).lower().strip()
199
 
200
+ def extract_and_infer_with_gemini(query):
201
  if not model: return None
 
202
  system_prompt = f"""
203
+ Eres un asistente de triaje clínico experto. Tu única tarea es analizar la consulta de un usuario y extraer dos tipos de información:
204
+ 1. `alimentos`: Una lista exhaustiva de todos los alimentos, bebidas o ingredientes consumidos mencionados. Incluye términos generales y específicos.
205
+ 2. `sintomas`: Una lista exhaustiva de todos los síntomas, sensaciones o signos clínicos descritos. Incluye términos médicos si se usan.
 
 
 
 
 
 
206
 
207
+ Tu objetivo es la extracción precisa. No infieras, no adivines, no añadas información que no esté en el texto.
208
+ Devuelve la respuesta ÚNICamente en formato JSON estricto.
209
+
210
+ Ejemplo de Consulta: "Después de comer pizza con queso y tomar vino tinto, tuve un fuerte dolor de cabeza y algo de hinchazón."
211
+ Ejemplo de Respuesta JSON:
212
+ {{
213
+ "alimentos": ["pizza", "queso", "vino tinto"],
214
+ "sintomas": ["dolor de cabeza", "hinchazón"]
215
+ }}
216
+
217
+ Ahora, procesa la siguiente consulta:
218
  Consulta: "{query}"
219
  """
220
  try:
 
226
  json_text = re.search(r'\{.*\}', response.text, re.DOTALL).group(0)
227
  return json.loads(json_text)
228
  except Exception as e:
229
+ logger.error(f"Error en la extracción con Gemini: {e}")
230
  st.error(f"Hubo un problema al interpretar tu consulta con la IA.")
231
  return None
232
 
 
234
  if not entities or not data: return []
235
  user_symptoms = set(sanitize_text(s) for s in entities.get("sintomas", []))
236
  user_foods = set(sanitize_text(f) for f in entities.get("alimentos", []))
237
+
238
  candidate_terms = set(user_foods)
239
  for food in user_foods:
240
  food_sanitized = sanitize_text(food)
241
  if food_sanitized in FOOD_TO_COMPOUND_MAP:
242
  candidate_terms.update(c.lower() for c in FOOD_TO_COMPOUND_MAP[food_sanitized])
243
+
 
244
  results = []
245
  for entry in data:
246
  entry_compounds_text = sanitize_text(entry.get("compuesto_alimento", ""))
247
+
248
  food_match = any(term in entry_compounds_text for term in candidate_terms)
249
+
250
  if not food_match:
251
  continue
252
+
253
+ score_details = {'food': 20, 'symptoms': 0}
254
+
 
 
 
 
 
 
255
  entry_symptoms_keys = set(sanitize_text(s) for s in entry.get("sintomas_clave", []))
256
  symptom_score = 0
257
  matched_symptoms = []
258
  for user_symptom in user_symptoms:
259
  for key in entry_symptoms_keys:
260
  if key in user_symptom or user_symptom in key:
261
+ symptom_score += 30
262
  matched_symptoms.append(key)
263
+ break
264
+
265
  score_details['symptoms'] = symptom_score
266
+ score_details['total'] = score_details['food'] + score_details['symptoms']
267
+
268
+ # Solo añadir si hay alguna coincidencia de síntomas
269
+ if score_details['symptoms'] > 0:
270
+ results.append({
271
+ 'entry': entry,
272
+ 'score': score_details,
273
+ 'matched_symptoms': list(set(matched_symptoms))
274
+ })
275
+
276
  if not results: return []
277
  sorted_results = sorted(results, key=lambda x: x['score']['total'], reverse=True)
278
  return sorted_results
279
 
 
280
 
281
  def generate_detailed_analysis(query, match):
282
  if not model: return "Error: El modelo de IA no está disponible."
283
 
 
284
  if not match or not isinstance(match, dict):
285
  logger.error("Se intentó generar un análisis detallado con datos de coincidencia inválidos.")
286
  return "No se pudo generar el análisis detallado debido a un error interno (datos de coincidencia inválidos)."
287
 
 
288
  sintomas_clave_texto = ", ".join(match.get("sintomas_clave", ["No especificados"]))
289
 
290
  prompt_parts = [
 
424
 
425
 
426
  if st.session_state.start_analysis:
 
427
  st.session_state.start_analysis = False
428
 
429
  query_to_analyze = st.session_state.query
 
437
  st.error("La base de datos de alimentos no está disponible.")
438
  else:
439
  with st.spinner("🧠 Interpretando tu caso y buscando pistas con IA..."):
440
+ entities = extract_and_infer_with_gemini(query_to_analyze)
441
  entities = reinforce_entities_with_keywords(entities, query_to_analyze, FOOD_TO_COMPOUND_MAP, SYMPTOM_KEYWORD_MAP)
442
  st.session_state.entities = entities
443
 
444
  if entities and (entities.get("alimentos") or entities.get("sintomas")):
445
  info_str = f"IA identificó - Alimentos: {', '.join(entities.get('alimentos',[])) or 'Ninguno'}, Síntomas: {', '.join(entities.get('sintomas',[])) or 'Ninguno'}"
 
446
  st.info(info_str)
447
  with st.spinner("🔬 Cruzando información y calculando relevancia..."):
448
  results = find_best_matches_hybrid(entities, alimentos_data)
 
451
  st.error("No se pudieron identificar alimentos o síntomas claros en tu descripción.")
452
  st.session_state.search_results = []
453
 
 
454
  if st.session_state.search_results is not None:
455
  results = st.session_state.search_results
456
 
457
  if not results:
458
+ st.warning(f"No se encontraron coincidencias claras para tu caso: '{st.session_state.user_query}'. Prueba a ser más específico con los síntomas.")
459
  else:
460
  col1, col2 = st.columns([3,1])
461
  with col1:
 
479
  col1, col2 = st.columns([3, 1])
480
  with col1:
481
  st.markdown("##### Desglose de la Puntuación de Relevancia:")
482
+ score_col1, score_col2, score_col3 = st.columns(3)
483
+ score_col1.metric("Puntos por Alimento(s)", f"{best_match_data['score']['food']}")
484
+ score_col2.metric("Puntos por Síntomas", f"{best_match_data['score']['symptoms']}")
485
+ score_col3.metric("PUNTUACIÓN TOTAL", f"{best_match_data['score']['total']}", delta="Máxima coincidencia")
 
486
  with col2:
487
  st.write("")
488
  if foodb_index: