Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -136,171 +136,42 @@ CONDITION_SYNONYMS = {
|
|
| 136 |
|
| 137 |
FOOD_NAME_TO_FOODB_KEY = {
|
| 138 |
# --- CEREALES Y GRANOS ---
|
| 139 |
-
"alforfón": ["buckwheat"],
|
| 140 |
-
"arroz": ["rice"],
|
| 141 |
-
"avena": ["oat", "oats"],
|
| 142 |
-
"cebada": ["barley"],
|
| 143 |
-
"centeno": ["rye"],
|
| 144 |
-
"galleta": ["cookie", "biscuit"],
|
| 145 |
-
"maíz": ["corn", "maize"],
|
| 146 |
-
"pan": ["bread"],
|
| 147 |
-
"pasta": ["pasta"],
|
| 148 |
-
"pizza": ["pizza"],
|
| 149 |
-
"quinoa": ["quinoa"],
|
| 150 |
-
"trigo": ["wheat"],
|
| 151 |
-
"trigo sarraceno": ["buckwheat"],
|
| 152 |
-
|
| 153 |
# --- LÁCTEOS Y DERIVADOS ---
|
| 154 |
-
"crema": ["cream"],
|
| 155 |
-
"helado": ["ice cream"],
|
| 156 |
-
"leche": ["milk"],
|
| 157 |
-
"mantequilla": ["butter"],
|
| 158 |
-
"queso": ["cheese"],
|
| 159 |
-
"yogur": ["yogurt", "yoghurt"],
|
| 160 |
-
|
| 161 |
# --- VEGETALES ---
|
| 162 |
-
"acelga": ["chard", "swiss chard"],
|
| 163 |
-
"ajo": ["garlic"],
|
| 164 |
-
"alcachofa": ["artichoke"],
|
| 165 |
-
"apio": ["celery"],
|
| 166 |
-
"berenjena": ["eggplant", "aubergine"],
|
| 167 |
-
"brócoli": ["broccoli"],
|
| 168 |
-
"calabacín": ["zucchini", "courgette"],
|
| 169 |
-
"calabaza": ["pumpkin", "squash"],
|
| 170 |
-
"cebolla": ["onion"],
|
| 171 |
-
"champiñón": ["mushroom"],
|
| 172 |
-
"col": ["cabbage"],
|
| 173 |
-
"coliflor": ["cauliflower"],
|
| 174 |
-
"edamame": ["edamame"],
|
| 175 |
-
"espárrago": ["asparagus"],
|
| 176 |
-
"espinaca": ["spinach"],
|
| 177 |
-
"garbanzo": ["chickpea"],
|
| 178 |
-
"guisante": ["pea", "peas"],
|
| 179 |
-
"frijol": ["bean", "beans"],
|
| 180 |
-
"lenteja": ["lentil"],
|
| 181 |
-
"patata": ["potato"],
|
| 182 |
-
"pepino": ["cucumber"],
|
| 183 |
-
"pimiento": ["bell pepper", "pepper"],
|
| 184 |
-
"remolacha": ["beet", "beetroot"],
|
| 185 |
-
"repollo": ["cabbage"],
|
| 186 |
-
"seta": ["mushroom"],
|
| 187 |
-
"soja": ["soy", "soybean"],
|
| 188 |
-
"tofu": ["tofu"],
|
| 189 |
-
"tomate": ["tomato"],
|
| 190 |
-
"zanahoria": ["carrot"],
|
| 191 |
-
|
| 192 |
# --- FRUTAS ---
|
| 193 |
-
"aguacate": ["avocado"],
|
| 194 |
-
"albaricoque": ["apricot"],
|
| 195 |
-
"arándano": ["blueberry"],
|
| 196 |
-
"cereza": ["cherry"],
|
| 197 |
-
"ciruela": ["plum"],
|
| 198 |
-
"dátil": ["date"],
|
| 199 |
-
"frambuesa": ["raspberry"],
|
| 200 |
-
"fresa": ["strawberry"],
|
| 201 |
-
"higo": ["fig"],
|
| 202 |
-
"kiwi": ["kiwi", "kiwifruit"],
|
| 203 |
-
"limón": ["lemon"],
|
| 204 |
-
"mandarina": ["tangerine", "mandarin"],
|
| 205 |
-
"mango": ["mango"],
|
| 206 |
-
"manzana": ["apple"],
|
| 207 |
-
"melocotón": ["peach"],
|
| 208 |
-
"melón": ["melon", "cantaloupe"],
|
| 209 |
-
"mora": ["blackberry"],
|
| 210 |
-
"naranja": ["orange"],
|
| 211 |
-
"nectarina": ["nectarine"],
|
| 212 |
-
"papaya": ["papaya"],
|
| 213 |
-
"pera": ["pear"],
|
| 214 |
-
"piña": ["pineapple"],
|
| 215 |
-
"plátano": ["banana"],
|
| 216 |
-
"pomelo": ["grapefruit"],
|
| 217 |
-
"sandía": ["watermelon"],
|
| 218 |
-
"uva": ["grape"],
|
| 219 |
-
|
| 220 |
# --- PROTEÍNAS (CARNES, PESCADOS, HUEVOS) ---
|
| 221 |
-
"anchoa": ["anchovy", "anchovies"],
|
| 222 |
-
"atún": ["tuna"],
|
| 223 |
-
"camarón": ["shrimp", "prawn"],
|
| 224 |
-
"carne": ["meat", "beef", "pork", "lamb"],
|
| 225 |
-
"cerdo": ["pork"],
|
| 226 |
-
"cordero": ["lamb"],
|
| 227 |
-
"gamba": ["shrimp", "prawn"],
|
| 228 |
-
"huevo": ["egg"],
|
| 229 |
-
"marisco": ["shellfish", "seafood"],
|
| 230 |
-
"pavo": ["turkey"],
|
| 231 |
-
"pescado": ["fish"],
|
| 232 |
-
"pollo": ["chicken"],
|
| 233 |
-
"salchicha": ["sausage"],
|
| 234 |
-
"salmón": ["salmon"],
|
| 235 |
-
"sardina": ["sardine"],
|
| 236 |
-
"ternera": ["beef", "veal"],
|
| 237 |
-
|
| 238 |
# --- FRUTOS SECOS Y SEMILLAS ---
|
| 239 |
-
"almendra": ["almond"],
|
| 240 |
-
"anacardo": ["cashew"],
|
| 241 |
-
"avellana": ["hazelnut"],
|
| 242 |
-
"cacahuete": ["peanut"],
|
| 243 |
-
"chía": ["chia", "chia seed"],
|
| 244 |
-
"lino": ["flax", "flaxseed", "linseed"],
|
| 245 |
-
"nuez": ["walnut"],
|
| 246 |
-
"pistacho": ["pistachio"],
|
| 247 |
-
"sésamo": ["sesame", "sesame seed"],
|
| 248 |
-
|
| 249 |
# --- BEBIDAS, DULCES Y CONDIMENTOS ---
|
| 250 |
-
"aceituna": ["olive"],
|
| 251 |
-
"café": ["coffee"],
|
| 252 |
-
"caldo": ["broth", "stock"],
|
| 253 |
-
"cerveza": ["beer"],
|
| 254 |
-
"chocolate": ["chocolate"],
|
| 255 |
-
"jengibre": ["ginger"],
|
| 256 |
-
"cúrcuma": ["turmeric"],
|
| 257 |
-
"miel": ["honey"],
|
| 258 |
-
"mostaza": ["mustard"],
|
| 259 |
-
"té": ["tea"],
|
| 260 |
-
"vinagre": ["vinegar"],
|
| 261 |
-
"vino": ["wine", "red wine", "white wine"]
|
| 262 |
}
|
| 263 |
|
| 264 |
-
# --- AÑADE ESTA FUNCIÓN NUEVA EN TU CÓDIGO ---
|
| 265 |
-
|
| 266 |
def reinforce_entities_with_keywords(entities, query, food_map):
|
| 267 |
-
"""
|
| 268 |
-
Revisa la salida de la IA y añade alimentos clave si fueron omitidos.
|
| 269 |
-
Esta es una red de seguridad contra las fallas de extracción de la IA.
|
| 270 |
-
"""
|
| 271 |
if not entities:
|
| 272 |
entities = {"alimentos": [], "sintomas": []}
|
| 273 |
-
|
| 274 |
query_sanitized = sanitize_text(query)
|
| 275 |
current_foods = entities.get("alimentos", [])
|
| 276 |
-
|
| 277 |
-
# Si la IA ya encontró alimentos, los sanitizamos para la comparación
|
| 278 |
current_foods_sanitized = {sanitize_text(f) for f in current_foods}
|
| 279 |
-
|
| 280 |
-
# Iteramos por nuestra lista de alimentos conocidos
|
| 281 |
for food_keyword in food_map.keys():
|
| 282 |
-
# Si una palabra clave de alimento está en la consulta...
|
| 283 |
if food_keyword in query_sanitized:
|
| 284 |
-
# ...y la IA NO la incluyó en su lista...
|
| 285 |
if food_keyword not in current_foods_sanitized:
|
| 286 |
-
# ...la añadimos nosotros.
|
| 287 |
logger.info(f"Red de seguridad: La IA omitió '{food_keyword}', añadiéndolo manualmente.")
|
| 288 |
current_foods.append(food_keyword)
|
| 289 |
-
|
| 290 |
-
entities["alimentos"] = list(set(current_foods)) # Eliminar duplicados
|
| 291 |
return entities
|
| 292 |
|
| 293 |
-
|
| 294 |
-
# --- LÓGICA DE BÚSQUEDA Y ANÁLISIS ---
|
| 295 |
-
# (Todas las funciones auxiliares se mantienen igual que en la versión anterior)
|
| 296 |
def sanitize_text(text):
|
| 297 |
if not text: return ""
|
| 298 |
return re.sub(r'[.,;()]', '', text).lower().strip()
|
|
|
|
| 299 |
def extract_and_infer_with_gemini(query, condiciones):
|
| 300 |
if not model: return None
|
| 301 |
condiciones_str = "\n".join([f"- {c}" for c in condiciones])
|
| 302 |
-
|
| 303 |
-
# Prompt mejorado para ser más estricto
|
| 304 |
system_prompt = f"""
|
| 305 |
Eres un asistente de triaje clínico experto. Tu tarea es analizar la consulta de un usuario y extraer tres tipos de información:
|
| 306 |
1. `alimentos`: Lista de alimentos consumidos.
|
|
@@ -331,31 +202,23 @@ def extract_and_infer_with_gemini(query, condiciones):
|
|
| 331 |
|
| 332 |
def find_best_matches_hybrid(entities, data):
|
| 333 |
if not entities or not data: return []
|
| 334 |
-
|
| 335 |
user_symptoms = set(sanitize_text(s) for s in entities.get("sintomas", []))
|
| 336 |
user_foods = set(sanitize_text(f) for f in entities.get("alimentos", []))
|
| 337 |
inferred_condition_raw = sanitize_text(entities.get("condicion_probable", ""))
|
| 338 |
-
|
| 339 |
candidate_terms = set(user_foods)
|
| 340 |
for food in user_foods:
|
| 341 |
food_sanitized = sanitize_text(food)
|
| 342 |
if food_sanitized in FOOD_TO_COMPOUND_MAP:
|
| 343 |
candidate_terms.update(c.lower() for c in FOOD_TO_COMPOUND_MAP[food_sanitized])
|
| 344 |
-
# Añadir también los componentes del vino directamente para ser más robusto
|
| 345 |
if food_sanitized in ["vino", "vino tinto", "vino rojo"]:
|
| 346 |
candidate_terms.update(["histamina", "tiramina", "sulfitos"])
|
| 347 |
-
|
| 348 |
results = []
|
| 349 |
for entry in data:
|
| 350 |
entry_compounds_text = sanitize_text(entry.get("compuesto_alimento", ""))
|
| 351 |
food_match = any(term in entry_compounds_text for term in candidate_terms)
|
| 352 |
-
|
| 353 |
if not food_match:
|
| 354 |
continue
|
| 355 |
-
|
| 356 |
-
# Inicializamos el diccionario SIN el total
|
| 357 |
score_details = {'condition': 0, 'food': 15, 'symptoms': 0}
|
| 358 |
-
|
| 359 |
if inferred_condition_raw:
|
| 360 |
entry_condition_sanitized = sanitize_text(entry.get("condicion_asociada", ""))
|
| 361 |
is_match = (entry_condition_sanitized == inferred_condition_raw)
|
|
@@ -364,7 +227,6 @@ def find_best_matches_hybrid(entities, data):
|
|
| 364 |
is_match = True
|
| 365 |
if is_match:
|
| 366 |
score_details['condition'] = 100
|
| 367 |
-
|
| 368 |
entry_symptoms_keys = set(sanitize_text(s) for s in entry.get("sintomas_clave", []))
|
| 369 |
symptom_score = 0
|
| 370 |
matched_symptoms = []
|
|
@@ -375,24 +237,13 @@ def find_best_matches_hybrid(entities, data):
|
|
| 375 |
matched_symptoms.append(key)
|
| 376 |
break
|
| 377 |
score_details['symptoms'] = symptom_score
|
| 378 |
-
|
| 379 |
-
# --- LÍNEA CORREGIDA Y CRÍTICA ---
|
| 380 |
-
# Calculamos la puntuación total y la añadimos al diccionario
|
| 381 |
score_details['total'] = score_details['condition'] + score_details['food'] + score_details['symptoms']
|
| 382 |
-
|
| 383 |
results.append({
|
| 384 |
'entry': entry,
|
| 385 |
'score': score_details,
|
| 386 |
'matched_symptoms': list(set(matched_symptoms))
|
| 387 |
})
|
| 388 |
-
|
| 389 |
if not results: return []
|
| 390 |
-
|
| 391 |
-
# Ahora el ordenamiento funcionará correctamente porque 'total' tiene el valor correcto
|
| 392 |
-
sorted_results = sorted(results, key=lambda x: x['score']['total'], reverse=True)
|
| 393 |
-
return sorted_results
|
| 394 |
-
|
| 395 |
-
# 4. Ordenar y devolver (sin cambios)
|
| 396 |
sorted_results = sorted(results, key=lambda x: x['score']['total'], reverse=True)
|
| 397 |
return sorted_results
|
| 398 |
|
|
@@ -435,7 +286,6 @@ def generate_detailed_analysis(query, match):
|
|
| 435 |
logger.error(f"Error generando análisis detallado con Gemini: {e}")
|
| 436 |
return "No se pudo generar el análisis detallado."
|
| 437 |
def create_relevance_chart(results):
|
| 438 |
-
# ... (Sin cambios)
|
| 439 |
top_results = results[:5]
|
| 440 |
condition_names = [re.sub(r'\(.*\)', '', res['entry']['condicion_asociada']).strip() for res in top_results]
|
| 441 |
chart_data = {"Condición": condition_names, "Relevancia": [res['score']['total'] for res in top_results]}
|
|
@@ -447,7 +297,6 @@ def create_relevance_chart(results):
|
|
| 447 |
).properties(title='Principales Coincidencias según tu Caso').configure_axis(labelFontSize=12, titleFontSize=14).configure_title(fontSize=16, anchor='start')
|
| 448 |
return chart
|
| 449 |
def generate_report_text(query, results):
|
| 450 |
-
# ... (Sin cambios)
|
| 451 |
report_lines = []
|
| 452 |
report_lines.append("="*50)
|
| 453 |
report_lines.append("INFORME DEL DETECTIVE DE ALIMENTOS")
|
|
@@ -472,7 +321,6 @@ def generate_report_text(query, results):
|
|
| 472 |
return "\n".join(report_lines)
|
| 473 |
|
| 474 |
# --- INTERFAZ DE USUARIO Y LÓGICA PRINCIPAL ---
|
| 475 |
-
# --- BLOQUE DE ENCABEZADO MODIFICADO ---
|
| 476 |
col_img1, col_text, col_img2 = st.columns([1, 4, 1], gap="medium")
|
| 477 |
with col_img1:
|
| 478 |
if os.path.exists("imagen.png"):
|
|
@@ -491,6 +339,8 @@ if 'user_query' not in st.session_state: st.session_state.user_query = ""
|
|
| 491 |
if 'entities' not in st.session_state: st.session_state.entities = None
|
| 492 |
if 'analysis_cache' not in st.session_state: st.session_state.analysis_cache = {}
|
| 493 |
if 'query' not in st.session_state: st.session_state.query = ""
|
|
|
|
|
|
|
| 494 |
|
| 495 |
def clear_search_state():
|
| 496 |
st.session_state.search_results = None
|
|
@@ -498,8 +348,10 @@ def clear_search_state():
|
|
| 498 |
st.session_state.entities = None
|
| 499 |
st.session_state.analysis_cache = {}
|
| 500 |
|
| 501 |
-
|
|
|
|
| 502 |
st.session_state.query = example_text
|
|
|
|
| 503 |
|
| 504 |
# SECCIÓN: EJEMPLOS DE CONSULTA
|
| 505 |
st.write("**¿No sabes por dónde empezar? Prueba con un ejemplo:**")
|
|
@@ -509,30 +361,42 @@ example_queries = [
|
|
| 509 |
"Después de tomar leche, tengo muchos gases e hinchazón.",
|
| 510 |
"El vino tinto siempre me da dolor de cabeza."
|
| 511 |
]
|
|
|
|
| 512 |
if example_cols[0].button(example_queries[0]):
|
| 513 |
-
|
| 514 |
if example_cols[1].button(example_queries[1]):
|
| 515 |
-
|
| 516 |
if example_cols[2].button(example_queries[2]):
|
| 517 |
-
|
| 518 |
|
|
|
|
|
|
|
| 519 |
with st.form(key="search_form"):
|
| 520 |
-
|
| 521 |
submitted = st.form_submit_button("Analizar mi caso", type="primary")
|
|
|
|
|
|
|
| 522 |
|
| 523 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
clear_search_state()
|
| 525 |
-
st.session_state.user_query =
|
| 526 |
-
|
|
|
|
| 527 |
st.warning("Por favor, describe lo que sientes y lo que comiste.")
|
| 528 |
elif alimentos_data is None:
|
| 529 |
st.error("La base de datos de alimentos no está disponible.")
|
| 530 |
else:
|
| 531 |
with st.spinner("🧠 Interpretando tu caso y buscando pistas con IA..."):
|
| 532 |
-
entities = extract_and_infer_with_gemini(
|
| 533 |
-
|
| 534 |
-
entities = reinforce_entities_with_keywords(entities, query, FOOD_TO_COMPOUND_MAP)
|
| 535 |
st.session_state.entities = entities
|
|
|
|
| 536 |
if entities and (entities.get("alimentos") or entities.get("sintomas")):
|
| 537 |
info_str = f"IA identificó - Alimentos: {', '.join(entities.get('alimentos',[])) or 'Ninguno'}, Síntomas: {', '.join(entities.get('sintomas',[])) or 'Ninguno'}"
|
| 538 |
if entities.get("condicion_probable"): info_str += f", Condición Probable: {entities.get('condicion_probable')}"
|
|
@@ -544,13 +408,13 @@ if submitted:
|
|
| 544 |
st.error("No se pudieron identificar alimentos o síntomas claros en tu descripción.")
|
| 545 |
st.session_state.search_results = []
|
| 546 |
|
|
|
|
| 547 |
if st.session_state.search_results is not None:
|
| 548 |
results = st.session_state.search_results
|
| 549 |
|
| 550 |
if not results:
|
| 551 |
st.warning(f"No se encontraron coincidencias claras para tu caso: '{st.session_state.user_query}'.")
|
| 552 |
else:
|
| 553 |
-
# --- Cabecera de resultados y botón de descarga (sin cambios) ---
|
| 554 |
col1, col2 = st.columns([3,1])
|
| 555 |
with col1:
|
| 556 |
st.success(f"Hemos encontrado {len(results)} posible(s) causa(s) relacionada(s) con tu caso.")
|
|
@@ -563,12 +427,10 @@ if st.session_state.search_results is not None:
|
|
| 563 |
mime="text/plain"
|
| 564 |
)
|
| 565 |
|
| 566 |
-
|
| 567 |
-
st.subheader("Análisis de Relevancia de las Coinciden cias")
|
| 568 |
chart = create_relevance_chart(results)
|
| 569 |
st.altair_chart(chart, use_container_width=True)
|
| 570 |
|
| 571 |
-
# --- Análisis detallado del MEJOR resultado (sin cambios) ---
|
| 572 |
best_match_data = results[0]
|
| 573 |
best_match = best_match_data['entry']
|
| 574 |
with st.expander(f"**Análisis Detallado de la Principal Coincidencia: {best_match.get('condicion_asociada')}**", expanded=True):
|
|
@@ -605,7 +467,7 @@ if st.session_state.search_results is not None:
|
|
| 605 |
for item in foodb_data[:3]:
|
| 606 |
st.write(f"**Compuesto:** {item['compound']}")
|
| 607 |
st.write(f"**Efectos reportados:** {', '.join(item['effects'])}")
|
| 608 |
-
|
| 609 |
if not found_data:
|
| 610 |
st.warning("Sin datos moleculares para este alimento.")
|
| 611 |
st.markdown("---")
|
|
@@ -614,36 +476,25 @@ if st.session_state.search_results is not None:
|
|
| 614 |
st.session_state.analysis_cache['best_match_analysis'] = generate_detailed_analysis(st.session_state.user_query, best_match)
|
| 615 |
st.markdown(st.session_state.analysis_cache['best_match_analysis'])
|
| 616 |
|
| 617 |
-
# --- NUEVA SECCIÓN MEJORADA: HASTA 4 OTRAS POSIBILIDADES ---
|
| 618 |
if len(results) > 1:
|
| 619 |
with st.expander("🔍 **Explora otras posibilidades relevantes (Diagnóstico Diferencial)**"):
|
| 620 |
-
# Iteramos sobre los resultados del 2 al 5 (hasta 4 alternativas)
|
| 621 |
for i, result in enumerate(results[1:5]):
|
| 622 |
with st.container(border=True):
|
| 623 |
entry = result['entry']
|
| 624 |
score = result['score']
|
| 625 |
-
|
| 626 |
st.subheader(f"{i+2}. {entry.get('condicion_asociada')}")
|
| 627 |
-
|
| 628 |
col_info, col_action = st.columns([3, 1])
|
| 629 |
-
|
| 630 |
with col_info:
|
| 631 |
if result.get('matched_symptoms'):
|
| 632 |
st.markdown(f"**Pistas Clave (Síntomas Coincidentes):** {', '.join(result['matched_symptoms']).capitalize()}")
|
| 633 |
st.markdown(f"**Alimentos Típicos Asociados:** {entry.get('compuesto_alimento')}")
|
| 634 |
-
|
| 635 |
with col_action:
|
| 636 |
st.metric("Relevancia", score['total'])
|
| 637 |
analysis_key = f"analysis_{i+2}"
|
| 638 |
if st.button("Generar análisis", key=analysis_key, help=f"Generar análisis de IA para {entry.get('condicion_asociada')}"):
|
| 639 |
with st.spinner(f"Generando análisis para {entry.get('condicion_asociada')}..."):
|
| 640 |
-
# Guardamos el análisis en el cache para no regenerarlo
|
| 641 |
st.session_state.analysis_cache[analysis_key] = generate_detailed_analysis(st.session_state.user_query, entry)
|
| 642 |
-
|
| 643 |
-
# Muestra el análisis si ya fue generado y guardado en el cache
|
| 644 |
if analysis_key in st.session_state.analysis_cache:
|
| 645 |
st.info(st.session_state.analysis_cache[analysis_key])
|
| 646 |
-
|
| 647 |
-
# Añade un separador visual, excepto para el último elemento
|
| 648 |
if i < len(results[1:5]) - 1:
|
| 649 |
-
st.markdown("---")
|
|
|
|
| 136 |
|
| 137 |
FOOD_NAME_TO_FOODB_KEY = {
|
| 138 |
# --- CEREALES Y GRANOS ---
|
| 139 |
+
"alforfón": ["buckwheat"], "arroz": ["rice"], "avena": ["oat", "oats"], "cebada": ["barley"], "centeno": ["rye"], "galleta": ["cookie", "biscuit"], "maíz": ["corn", "maize"], "pan": ["bread"], "pasta": ["pasta"], "pizza": ["pizza"], "quinoa": ["quinoa"], "trigo": ["wheat"], "trigo sarraceno": ["buckwheat"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
# --- LÁCTEOS Y DERIVADOS ---
|
| 141 |
+
"crema": ["cream"], "helado": ["ice cream"], "leche": ["milk"], "mantequilla": ["butter"], "queso": ["cheese"], "yogur": ["yogurt", "yoghurt"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
# --- VEGETALES ---
|
| 143 |
+
"acelga": ["chard", "swiss chard"], "ajo": ["garlic"], "alcachofa": ["artichoke"], "apio": ["celery"], "berenjena": ["eggplant", "aubergine"], "brócoli": ["broccoli"], "calabacín": ["zucchini", "courgette"], "calabaza": ["pumpkin", "squash"], "cebolla": ["onion"], "champiñón": ["mushroom"], "col": ["cabbage"], "coliflor": ["cauliflower"], "edamame": ["edamame"], "espárrago": ["asparagus"], "espinaca": ["spinach"], "garbanzo": ["chickpea"], "guisante": ["pea", "peas"], "frijol": ["bean", "beans"], "lenteja": ["lentil"], "patata": ["potato"], "pepino": ["cucumber"], "pimiento": ["bell pepper", "pepper"], "remolacha": ["beet", "beetroot"], "repollo": ["cabbage"], "seta": ["mushroom"], "soja": ["soy", "soybean"], "tofu": ["tofu"], "tomate": ["tomato"], "zanahoria": ["carrot"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
# --- FRUTAS ---
|
| 145 |
+
"aguacate": ["avocado"], "albaricoque": ["apricot"], "arándano": ["blueberry"], "cereza": ["cherry"], "ciruela": ["plum"], "dátil": ["date"], "frambuesa": ["raspberry"], "fresa": ["strawberry"], "higo": ["fig"], "kiwi": ["kiwi", "kiwifruit"], "limón": ["lemon"], "mandarina": ["tangerine", "mandarin"], "mango": ["mango"], "manzana": ["apple"], "melocotón": ["peach"], "melón": ["melon", "cantaloupe"], "mora": ["blackberry"], "naranja": ["orange"], "nectarina": ["nectarine"], "papaya": ["papaya"], "pera": ["pear"], "piña": ["pineapple"], "plátano": ["banana"], "pomelo": ["grapefruit"], "sandía": ["watermelon"], "uva": ["grape"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
# --- PROTEÍNAS (CARNES, PESCADOS, HUEVOS) ---
|
| 147 |
+
"anchoa": ["anchovy", "anchovies"], "atún": ["tuna"], "camarón": ["shrimp", "prawn"], "carne": ["meat", "beef", "pork", "lamb"], "cerdo": ["pork"], "cordero": ["lamb"], "gamba": ["shrimp", "prawn"], "huevo": ["egg"], "marisco": ["shellfish", "seafood"], "pavo": ["turkey"], "pescado": ["fish"], "pollo": ["chicken"], "salchicha": ["sausage"], "salmón": ["salmon"], "sardina": ["sardine"], "ternera": ["beef", "veal"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
# --- FRUTOS SECOS Y SEMILLAS ---
|
| 149 |
+
"almendra": ["almond"], "anacardo": ["cashew"], "avellana": ["hazelnut"], "cacahuete": ["peanut"], "chía": ["chia", "chia seed"], "lino": ["flax", "flaxseed", "linseed"], "nuez": ["walnut"], "pistacho": ["pistachio"], "sésamo": ["sesame", "sesame seed"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
# --- BEBIDAS, DULCES Y CONDIMENTOS ---
|
| 151 |
+
"aceituna": ["olive"], "café": ["coffee"], "caldo": ["broth", "stock"], "cerveza": ["beer"], "chocolate": ["chocolate"], "jengibre": ["ginger"], "cúrcuma": ["turmeric"], "miel": ["honey"], "mostaza": ["mustard"], "té": ["tea"], "vinagre": ["vinegar"], "vino": ["wine", "red wine", "white wine"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
}
|
| 153 |
|
|
|
|
|
|
|
| 154 |
def reinforce_entities_with_keywords(entities, query, food_map):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
if not entities:
|
| 156 |
entities = {"alimentos": [], "sintomas": []}
|
|
|
|
| 157 |
query_sanitized = sanitize_text(query)
|
| 158 |
current_foods = entities.get("alimentos", [])
|
|
|
|
|
|
|
| 159 |
current_foods_sanitized = {sanitize_text(f) for f in current_foods}
|
|
|
|
|
|
|
| 160 |
for food_keyword in food_map.keys():
|
|
|
|
| 161 |
if food_keyword in query_sanitized:
|
|
|
|
| 162 |
if food_keyword not in current_foods_sanitized:
|
|
|
|
| 163 |
logger.info(f"Red de seguridad: La IA omitió '{food_keyword}', añadiéndolo manualmente.")
|
| 164 |
current_foods.append(food_keyword)
|
| 165 |
+
entities["alimentos"] = list(set(current_foods))
|
|
|
|
| 166 |
return entities
|
| 167 |
|
|
|
|
|
|
|
|
|
|
| 168 |
def sanitize_text(text):
|
| 169 |
if not text: return ""
|
| 170 |
return re.sub(r'[.,;()]', '', text).lower().strip()
|
| 171 |
+
|
| 172 |
def extract_and_infer_with_gemini(query, condiciones):
|
| 173 |
if not model: return None
|
| 174 |
condiciones_str = "\n".join([f"- {c}" for c in condiciones])
|
|
|
|
|
|
|
| 175 |
system_prompt = f"""
|
| 176 |
Eres un asistente de triaje clínico experto. Tu tarea es analizar la consulta de un usuario y extraer tres tipos de información:
|
| 177 |
1. `alimentos`: Lista de alimentos consumidos.
|
|
|
|
| 202 |
|
| 203 |
def find_best_matches_hybrid(entities, data):
|
| 204 |
if not entities or not data: return []
|
|
|
|
| 205 |
user_symptoms = set(sanitize_text(s) for s in entities.get("sintomas", []))
|
| 206 |
user_foods = set(sanitize_text(f) for f in entities.get("alimentos", []))
|
| 207 |
inferred_condition_raw = sanitize_text(entities.get("condicion_probable", ""))
|
|
|
|
| 208 |
candidate_terms = set(user_foods)
|
| 209 |
for food in user_foods:
|
| 210 |
food_sanitized = sanitize_text(food)
|
| 211 |
if food_sanitized in FOOD_TO_COMPOUND_MAP:
|
| 212 |
candidate_terms.update(c.lower() for c in FOOD_TO_COMPOUND_MAP[food_sanitized])
|
|
|
|
| 213 |
if food_sanitized in ["vino", "vino tinto", "vino rojo"]:
|
| 214 |
candidate_terms.update(["histamina", "tiramina", "sulfitos"])
|
|
|
|
| 215 |
results = []
|
| 216 |
for entry in data:
|
| 217 |
entry_compounds_text = sanitize_text(entry.get("compuesto_alimento", ""))
|
| 218 |
food_match = any(term in entry_compounds_text for term in candidate_terms)
|
|
|
|
| 219 |
if not food_match:
|
| 220 |
continue
|
|
|
|
|
|
|
| 221 |
score_details = {'condition': 0, 'food': 15, 'symptoms': 0}
|
|
|
|
| 222 |
if inferred_condition_raw:
|
| 223 |
entry_condition_sanitized = sanitize_text(entry.get("condicion_asociada", ""))
|
| 224 |
is_match = (entry_condition_sanitized == inferred_condition_raw)
|
|
|
|
| 227 |
is_match = True
|
| 228 |
if is_match:
|
| 229 |
score_details['condition'] = 100
|
|
|
|
| 230 |
entry_symptoms_keys = set(sanitize_text(s) for s in entry.get("sintomas_clave", []))
|
| 231 |
symptom_score = 0
|
| 232 |
matched_symptoms = []
|
|
|
|
| 237 |
matched_symptoms.append(key)
|
| 238 |
break
|
| 239 |
score_details['symptoms'] = symptom_score
|
|
|
|
|
|
|
|
|
|
| 240 |
score_details['total'] = score_details['condition'] + score_details['food'] + score_details['symptoms']
|
|
|
|
| 241 |
results.append({
|
| 242 |
'entry': entry,
|
| 243 |
'score': score_details,
|
| 244 |
'matched_symptoms': list(set(matched_symptoms))
|
| 245 |
})
|
|
|
|
| 246 |
if not results: return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
sorted_results = sorted(results, key=lambda x: x['score']['total'], reverse=True)
|
| 248 |
return sorted_results
|
| 249 |
|
|
|
|
| 286 |
logger.error(f"Error generando análisis detallado con Gemini: {e}")
|
| 287 |
return "No se pudo generar el análisis detallado."
|
| 288 |
def create_relevance_chart(results):
|
|
|
|
| 289 |
top_results = results[:5]
|
| 290 |
condition_names = [re.sub(r'\(.*\)', '', res['entry']['condicion_asociada']).strip() for res in top_results]
|
| 291 |
chart_data = {"Condición": condition_names, "Relevancia": [res['score']['total'] for res in top_results]}
|
|
|
|
| 297 |
).properties(title='Principales Coincidencias según tu Caso').configure_axis(labelFontSize=12, titleFontSize=14).configure_title(fontSize=16, anchor='start')
|
| 298 |
return chart
|
| 299 |
def generate_report_text(query, results):
|
|
|
|
| 300 |
report_lines = []
|
| 301 |
report_lines.append("="*50)
|
| 302 |
report_lines.append("INFORME DEL DETECTIVE DE ALIMENTOS")
|
|
|
|
| 321 |
return "\n".join(report_lines)
|
| 322 |
|
| 323 |
# --- INTERFAZ DE USUARIO Y LÓGICA PRINCIPAL ---
|
|
|
|
| 324 |
col_img1, col_text, col_img2 = st.columns([1, 4, 1], gap="medium")
|
| 325 |
with col_img1:
|
| 326 |
if os.path.exists("imagen.png"):
|
|
|
|
| 339 |
if 'entities' not in st.session_state: st.session_state.entities = None
|
| 340 |
if 'analysis_cache' not in st.session_state: st.session_state.analysis_cache = {}
|
| 341 |
if 'query' not in st.session_state: st.session_state.query = ""
|
| 342 |
+
# --- CAMBIO 1: AÑADIMOS LA BANDERA DE CONTROL ---
|
| 343 |
+
if 'start_analysis' not in st.session_state: st.session_state.start_analysis = False
|
| 344 |
|
| 345 |
def clear_search_state():
|
| 346 |
st.session_state.search_results = None
|
|
|
|
| 348 |
st.session_state.entities = None
|
| 349 |
st.session_state.analysis_cache = {}
|
| 350 |
|
| 351 |
+
# --- CAMBIO 2: NUEVA FUNCIÓN PARA LOS BOTONES DE EJEMPLO ---
|
| 352 |
+
def set_query_and_trigger_analysis(example_text):
|
| 353 |
st.session_state.query = example_text
|
| 354 |
+
st.session_state.start_analysis = True
|
| 355 |
|
| 356 |
# SECCIÓN: EJEMPLOS DE CONSULTA
|
| 357 |
st.write("**¿No sabes por dónde empezar? Prueba con un ejemplo:**")
|
|
|
|
| 361 |
"Después de tomar leche, tengo muchos gases e hinchazón.",
|
| 362 |
"El vino tinto siempre me da dolor de cabeza."
|
| 363 |
]
|
| 364 |
+
# --- CAMBIO 3: LOS BOTONES AHORA LLAMAN A LA NUEVA FUNCIÓN ---
|
| 365 |
if example_cols[0].button(example_queries[0]):
|
| 366 |
+
set_query_and_trigger_analysis(example_queries[0])
|
| 367 |
if example_cols[1].button(example_queries[1]):
|
| 368 |
+
set_query_and_trigger_analysis(example_queries[1])
|
| 369 |
if example_cols[2].button(example_queries[2]):
|
| 370 |
+
set_query_and_trigger_analysis(example_queries[2])
|
| 371 |
|
| 372 |
+
# --- CAMBIO 4: ESTRUCTURA PRINCIPAL MODIFICADA ---
|
| 373 |
+
# El formulario solo define la UI y activa la bandera al ser enviado.
|
| 374 |
with st.form(key="search_form"):
|
| 375 |
+
st.text_area("Describe tu caso aquí:", height=150, key="query")
|
| 376 |
submitted = st.form_submit_button("Analizar mi caso", type="primary")
|
| 377 |
+
if submitted:
|
| 378 |
+
st.session_state.start_analysis = True
|
| 379 |
|
| 380 |
+
# La lógica de análisis se ejecuta si CUALQUIER botón activó la bandera.
|
| 381 |
+
if st.session_state.start_analysis:
|
| 382 |
+
# Inmediatamente bajamos la bandera para evitar ejecuciones repetidas.
|
| 383 |
+
st.session_state.start_analysis = False
|
| 384 |
+
|
| 385 |
+
query_to_analyze = st.session_state.query
|
| 386 |
+
|
| 387 |
clear_search_state()
|
| 388 |
+
st.session_state.user_query = query_to_analyze
|
| 389 |
+
|
| 390 |
+
if not query_to_analyze:
|
| 391 |
st.warning("Por favor, describe lo que sientes y lo que comiste.")
|
| 392 |
elif alimentos_data is None:
|
| 393 |
st.error("La base de datos de alimentos no está disponible.")
|
| 394 |
else:
|
| 395 |
with st.spinner("🧠 Interpretando tu caso y buscando pistas con IA..."):
|
| 396 |
+
entities = extract_and_infer_with_gemini(query_to_analyze, lista_condiciones)
|
| 397 |
+
entities = reinforce_entities_with_keywords(entities, query_to_analyze, FOOD_TO_COMPOUND_MAP)
|
|
|
|
| 398 |
st.session_state.entities = entities
|
| 399 |
+
|
| 400 |
if entities and (entities.get("alimentos") or entities.get("sintomas")):
|
| 401 |
info_str = f"IA identificó - Alimentos: {', '.join(entities.get('alimentos',[])) or 'Ninguno'}, Síntomas: {', '.join(entities.get('sintomas',[])) or 'Ninguno'}"
|
| 402 |
if entities.get("condicion_probable"): info_str += f", Condición Probable: {entities.get('condicion_probable')}"
|
|
|
|
| 408 |
st.error("No se pudieron identificar alimentos o síntomas claros en tu descripción.")
|
| 409 |
st.session_state.search_results = []
|
| 410 |
|
| 411 |
+
# La sección de mostrar resultados permanece igual, se ejecuta si hay resultados en el estado.
|
| 412 |
if st.session_state.search_results is not None:
|
| 413 |
results = st.session_state.search_results
|
| 414 |
|
| 415 |
if not results:
|
| 416 |
st.warning(f"No se encontraron coincidencias claras para tu caso: '{st.session_state.user_query}'.")
|
| 417 |
else:
|
|
|
|
| 418 |
col1, col2 = st.columns([3,1])
|
| 419 |
with col1:
|
| 420 |
st.success(f"Hemos encontrado {len(results)} posible(s) causa(s) relacionada(s) con tu caso.")
|
|
|
|
| 427 |
mime="text/plain"
|
| 428 |
)
|
| 429 |
|
| 430 |
+
st.subheader("Análisis de Relevancia de las Coincidencias")
|
|
|
|
| 431 |
chart = create_relevance_chart(results)
|
| 432 |
st.altair_chart(chart, use_container_width=True)
|
| 433 |
|
|
|
|
| 434 |
best_match_data = results[0]
|
| 435 |
best_match = best_match_data['entry']
|
| 436 |
with st.expander(f"**Análisis Detallado de la Principal Coincidencia: {best_match.get('condicion_asociada')}**", expanded=True):
|
|
|
|
| 467 |
for item in foodb_data[:3]:
|
| 468 |
st.write(f"**Compuesto:** {item['compound']}")
|
| 469 |
st.write(f"**Efectos reportados:** {', '.join(item['effects'])}")
|
| 470 |
+
st.markdown("---")
|
| 471 |
if not found_data:
|
| 472 |
st.warning("Sin datos moleculares para este alimento.")
|
| 473 |
st.markdown("---")
|
|
|
|
| 476 |
st.session_state.analysis_cache['best_match_analysis'] = generate_detailed_analysis(st.session_state.user_query, best_match)
|
| 477 |
st.markdown(st.session_state.analysis_cache['best_match_analysis'])
|
| 478 |
|
|
|
|
| 479 |
if len(results) > 1:
|
| 480 |
with st.expander("🔍 **Explora otras posibilidades relevantes (Diagnóstico Diferencial)**"):
|
|
|
|
| 481 |
for i, result in enumerate(results[1:5]):
|
| 482 |
with st.container(border=True):
|
| 483 |
entry = result['entry']
|
| 484 |
score = result['score']
|
|
|
|
| 485 |
st.subheader(f"{i+2}. {entry.get('condicion_asociada')}")
|
|
|
|
| 486 |
col_info, col_action = st.columns([3, 1])
|
|
|
|
| 487 |
with col_info:
|
| 488 |
if result.get('matched_symptoms'):
|
| 489 |
st.markdown(f"**Pistas Clave (Síntomas Coincidentes):** {', '.join(result['matched_symptoms']).capitalize()}")
|
| 490 |
st.markdown(f"**Alimentos Típicos Asociados:** {entry.get('compuesto_alimento')}")
|
|
|
|
| 491 |
with col_action:
|
| 492 |
st.metric("Relevancia", score['total'])
|
| 493 |
analysis_key = f"analysis_{i+2}"
|
| 494 |
if st.button("Generar análisis", key=analysis_key, help=f"Generar análisis de IA para {entry.get('condicion_asociada')}"):
|
| 495 |
with st.spinner(f"Generando análisis para {entry.get('condicion_asociada')}..."):
|
|
|
|
| 496 |
st.session_state.analysis_cache[analysis_key] = generate_detailed_analysis(st.session_state.user_query, entry)
|
|
|
|
|
|
|
| 497 |
if analysis_key in st.session_state.analysis_cache:
|
| 498 |
st.info(st.session_state.analysis_cache[analysis_key])
|
|
|
|
|
|
|
| 499 |
if i < len(results[1:5]) - 1:
|
| 500 |
+
st.markdown("---")
|