Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# ==================== El Detective de Alimentos (Versión
|
| 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
|
| 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
|
| 217 |
-
1. `alimentos`:
|
| 218 |
-
2. `sintomas`:
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 262 |
-
|
| 263 |
-
|
| 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 +=
|
| 277 |
matched_symptoms.append(key)
|
| 278 |
-
break
|
|
|
|
| 279 |
score_details['symptoms'] = symptom_score
|
| 280 |
-
score_details['total'] = score_details['
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 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
|
| 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
|
| 499 |
-
score_col1.metric("Puntos por
|
| 500 |
-
score_col2.metric("Puntos por
|
| 501 |
-
score_col3.metric("
|
| 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:
|