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
|
|
@@ -12,6 +12,7 @@ import pandas as pd
|
|
| 12 |
import altair as alt
|
| 13 |
from datetime import datetime
|
| 14 |
import time
|
|
|
|
| 15 |
|
| 16 |
st.set_page_config(
|
| 17 |
page_title="El Detective de Alimentos",
|
|
@@ -60,107 +61,53 @@ def load_data():
|
|
| 60 |
return None, None, None
|
| 61 |
alimentos_data, lista_condiciones, foodb_index = load_data()
|
| 62 |
|
|
|
|
| 63 |
FOOD_TO_COMPOUND_MAP = {
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
"maíz": ["lectinas", "aflatoxinas"], "arroz": ["lectinas"], "quinoa": ["saponinas", "oxalatos", "fitatos"], "trigo sarraceno": ["oxalatos"], "alforfón": ["oxalatos"],
|
| 67 |
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
"
|
| 73 |
-
"
|
| 74 |
-
"
|
| 75 |
-
"
|
| 76 |
-
"
|
| 77 |
-
"
|
| 78 |
-
"
|
| 79 |
-
"
|
| 80 |
-
"champiñón": ["fodmaps", "polioles"], "setas": ["fodmaps", "polioles"],
|
| 81 |
-
"espárrago": ["fodmaps", "fructanos", "purinas"], "alcachofa": ["fodmaps", "fructanos"],
|
| 82 |
-
"pepino": ["lectinas"], "zanahoria": ["salicilatos"], "apio": ["fodmaps", "polioles"],
|
| 83 |
-
|
| 84 |
-
# --- FRUTAS ---
|
| 85 |
-
"manzana": ["salicilatos", "fructosa", "fodmaps", "polioles"], "pera": ["fructosa", "fodmaps", "polioles"], "mango": ["fructosa", "fodmaps"], "cereza": ["fructosa", "salicilatos", "fodmaps", "polioles"], "sandía": ["fructosa", "fodmaps"],
|
| 86 |
-
"uvas": ["salicilatos", "fructosa"], "pasas": ["salicilatos", "fructosa", "histamina"], "dátil": ["fructosa", "fodmaps", "fructanos"], "higo": ["fructosa", "fodmaps", "fructanos"],
|
| 87 |
-
"naranja": ["salicilatos", "ácidos"], "limón": ["salicilatos", "ácidos"], "pomelo": ["salicilatos", "ácidos"], "mandarina": ["salicilatos", "ácidos"],
|
| 88 |
-
"fresa": ["salicilatos", "histamina"], "arándano": ["salicilatos"], "frambuesa": ["salicilatos"], "mora": ["salicilatos"],
|
| 89 |
-
"plátano": ["tiramina", "histamina"], "piña": ["salicilatos", "ácidos", "histamina"], "kiwi": ["salicilatos", "alérgenos", "histamina"],
|
| 90 |
-
"ciruela": ["fodmaps", "polioles"], "melocotón": ["fodmaps", "polioles"], "albaricoque": ["fodmaps", "polioles"], "nectarina": ["fodmaps", "polioles"],
|
| 91 |
-
"melón": ["fructosa", "fodmaps"], "papaya": ["histamina"],
|
| 92 |
-
|
| 93 |
-
# --- PROTEÍNAS (CARNES, PESCADOS, HUEVOS) ---
|
| 94 |
-
"carne": ["alfa-gal", "purinas", "hierro", "histamina"], "carnes rojas": ["purinas", "alfa-gal", "hierro"], "hígado": ["purinas", "hierro", "vitamina a"], "embutidos": ["histamina", "tiramina", "nitritos"], "salchicha": ["histamina", "tiramina", "nitritos"],
|
| 95 |
-
"pollo": ["purinas"], "pavo": ["purinas"],
|
| 96 |
-
"huevo": ["alérgenos"],
|
| 97 |
-
"pescado": ["histamina", "purinas"], "pescado enlatado": ["histamina"], "atún en lata": ["histamina"], "salmón": ["purinas", "histamina"], "sardinas": ["histamina", "purinas"], "anchoas": ["purinas", "histamina"],
|
| 98 |
-
"mariscos": ["purinas", "sulfitos", "alérgenos", "yodo", "níquel"], "camarón": ["alérgenos", "yodo"], "gamba": ["alérgenos", "yodo"],
|
| 99 |
-
|
| 100 |
-
# --- FRUTOS SECOS Y SEMILLAS ---
|
| 101 |
-
"nueces": ["arginina", "salicilatos", "níquel", "oxalatos", "fitatos"], "almendras": ["salicilatos", "arginina", "oxalatos", "fitatos"],
|
| 102 |
-
"maní": ["alérgenos", "arginina", "lectinas", "aflatoxinas"], "cacahuetes": ["alérgenos", "arginina", "lectinas", "aflatoxinas"],
|
| 103 |
-
"anacardo": ["alérgenos", "fodmaps", "gos", "fructanos", "oxalatos"], "pistacho": ["alérgenos", "fodmaps", "fructanos"],
|
| 104 |
-
"avellana": ["alérgenos", "oxalatos", "fitatos"],
|
| 105 |
-
"sésamo": ["alérgenos", "oxalatos", "fitatos"], "chía": ["fitatos", "oxalatos"], "lino": ["fitatos"],
|
| 106 |
-
|
| 107 |
-
# --- BEBIDAS Y DULCES ---
|
| 108 |
-
"café": ["cafeína", "ácidos"], "té": ["cafeína", "taninos", "oxalatos"],
|
| 109 |
-
"cerveza": ["gluten", "histamina", "tiramina", "purinas"], "vino": ["histamina", "tiramina", "sulfitos"], "vino tinto": ["histamina", "tiramina", "sulfitos"], "vino rojo": ["histamina", "tiramina", "sulfitos"],
|
| 110 |
-
"chocolate": ["cafeína", "tiramina", "níquel", "arginina", "oxalatos"],
|
| 111 |
-
"azucar": ["azúcar", "fructosa"], "dulces": ["azúcar"], "refrescos": ["azúcar", "fructosa"], "gaseosas": ["azúcar", "fructosa"], "miel": ["fructosa", "fodmaps"], "jarabe de maiz": ["fructosa"],
|
| 112 |
-
"edulcorantes": ["polioles", "fodmaps"],
|
| 113 |
-
|
| 114 |
-
# --- OTROS Y CONDIMENTOS ---
|
| 115 |
-
"cúrcuma": ["salicilatos"], "jengibre": ["salicilatos"],
|
| 116 |
-
"chucrut": ["histamina", "tiramina"],
|
| 117 |
-
"vinagre": ["histamina", "sulfitos"], "mostaza": ["salicilatos", "goitrógenos"],
|
| 118 |
-
"aceitunas": ["histamina", "salicilatos"],
|
| 119 |
-
"caldo": ["histamina", "glutamato"]
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
CONDITION_SYNONYMS = {
|
| 123 |
-
"síndrome del intestino irritable (sii).": ["intolerancia a los fodmaps", "intolerancia a los gos (fodmap)", "intolerancia a gomas fermentables"],
|
| 124 |
-
"gota / hiperuricemia.": ["acido urico aumentado"],
|
| 125 |
-
"intolerancia a la lactosa.": ["déficit de lactasa"],
|
| 126 |
-
"enfermedad celíaca (clásica).": ["dermatitis herpetiforme"],
|
| 127 |
-
"migraña.": ["dolor de cabeza", "cefalea"],
|
| 128 |
-
"sensibilidad al gluten no celíaca (sgnc).": ["intolerancia al gluten", "sensibilidad al trigo"],
|
| 129 |
-
"histaminosis / intolerancia a la histamina.": ["déficit de dao", "acumulación de histamina"],
|
| 130 |
-
"enfermedad por reflujo gastroesofágico (erge).": ["reflujo", "acidez estomacal", "ardor de estómago"],
|
| 131 |
-
"hipotiroidismo.": ["tiroides hipoactiva", "tiroides lenta"],
|
| 132 |
-
"alergia al níquel.": ["dermatitis de contacto por niquel", "reacción al níquel"]
|
| 133 |
}
|
| 134 |
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
# --- LÁCTEOS Y DERIVADOS ---
|
| 139 |
-
"crema": ["cream"], "helado": ["ice cream"], "leche": ["milk"], "mantequilla": ["butter"], "queso": ["cheese"], "yogur": ["yogurt", "yoghurt"],
|
| 140 |
-
# --- VEGETALES ---
|
| 141 |
-
"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"],
|
| 142 |
-
# --- FRUTAS ---
|
| 143 |
-
"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"],
|
| 144 |
-
# --- PROTEÍNAS (CARNES, PESCADOS, HUEVOS) ---
|
| 145 |
-
"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"],
|
| 146 |
-
# --- FRUTOS SECOS Y SEMILLAS ---
|
| 147 |
-
"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"],
|
| 148 |
-
# --- BEBIDAS, DULCES Y CONDIMENTOS ---
|
| 149 |
-
"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"]
|
| 150 |
-
}
|
| 151 |
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
"
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
def reinforce_entities_with_keywords(entities, query, food_map, symptom_map):
|
| 166 |
if not entities:
|
|
@@ -168,6 +115,7 @@ def reinforce_entities_with_keywords(entities, query, food_map, symptom_map):
|
|
| 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,159 +124,110 @@ def reinforce_entities_with_keywords(entities, query, food_map, symptom_map):
|
|
| 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 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
entities["sintomas"] = list(set(current_symptoms))
|
| 193 |
-
|
| 194 |
return entities
|
| 195 |
|
| 196 |
-
def sanitize_text(text):
|
| 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:
|
| 221 |
-
response = model.generate_content(system_prompt)
|
| 222 |
-
json_text_match = re.search(r'```json\s*(\{.*?\})\s*```', response.text, re.DOTALL)
|
| 223 |
-
if json_text_match:
|
| 224 |
-
json_text = json_text_match.group(1)
|
| 225 |
-
else:
|
| 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 |
-
|
| 233 |
def find_best_matches_hybrid(entities, data):
|
|
|
|
| 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 |
-
|
| 241 |
-
|
| 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 |
-
|
| 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("
|
| 286 |
-
return "
|
| 287 |
|
| 288 |
sintomas_clave_texto = ", ".join(match.get("sintomas_clave", ["No especificados"]))
|
| 289 |
-
|
| 290 |
prompt_parts = [
|
| 291 |
-
"Eres un asistente de IA experto en nutrición
|
| 292 |
-
f'
|
| 293 |
-
f'
|
| 294 |
-
f'
|
| 295 |
-
f'
|
| 296 |
-
f'
|
| 297 |
-
"\n**Tu Tarea:** Redacta una respuesta
|
| 298 |
f'### Posible Causa: {match.get("condicion_asociada", "Condición no especificada")}',
|
| 299 |
-
f'Hola.
|
| 300 |
f'### ¿Qué podría estar pasando en tu cuerpo?',
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
f'**Sin embargo, en ciertos contextos:** Explica por qué pueden ser problemáticos para el usuario, basándote en: "{match.get("contexto_negativo")}"'
|
| 308 |
-
)
|
| 309 |
-
prompt_parts.extend([
|
| 310 |
-
"\n### Pasos a Seguir y Recomendaciones",
|
| 311 |
-
"Aquí te dejo algunas recomendaciones generales. Es fundamental que las converses con un profesional de la salud:",
|
| 312 |
-
f'* **[Punto 1 de las recomendaciones, reformulado como un consejo práctico basado en las recomendaciones generales mencionadas.]**',
|
| 313 |
-
f'* **[Punto 2 de las recomendaciones, enfocado en los exámenes si los hay, basado en las recomendaciones generales mencionadas.]**',
|
| 314 |
-
f'* **Atención a otros alimentos:** Ten en cuenta que, además de lo que mencionaste, otros alimentos implicados en esta condición son: **[menciona 2-3 ejemplos del campo de alimentos implicados]**.',
|
| 315 |
"\n### **IMPORTANTE: Descargo de Responsabilidad**",
|
| 316 |
-
"Este análisis es una herramienta informativa
|
| 317 |
-
]
|
| 318 |
prompt = "\n".join(prompt_parts)
|
| 319 |
try:
|
|
|
|
| 320 |
response = model.generate_content(prompt)
|
| 321 |
-
|
| 322 |
if response.text and len(response.text) > 1:
|
|
|
|
| 323 |
return response.text
|
| 324 |
else:
|
| 325 |
-
logger.error("La respuesta de Gemini para el análisis detallado fue vacía
|
| 326 |
-
|
| 327 |
-
except Exception as e:
|
| 328 |
-
logger.error(f"Error generando análisis detallado
|
| 329 |
-
|
| 330 |
-
|
| 331 |
def create_relevance_chart(results):
|
|
|
|
| 332 |
top_results = results[:5]
|
| 333 |
condition_names = [re.sub(r'\(.*\)', '', res['entry']['condicion_asociada']).strip() for res in top_results]
|
| 334 |
chart_data = {"Condición": condition_names, "Relevancia": [res['score']['total'] for res in top_results]}
|
|
@@ -339,52 +238,38 @@ def create_relevance_chart(results):
|
|
| 339 |
tooltip=[alt.Tooltip('Condición:N', title='Condición'), alt.Tooltip('Relevancia:Q', title='Puntuación')]
|
| 340 |
).properties(title='Principales Coincidencias según tu Caso').configure_axis(labelFontSize=12, titleFontSize=14).configure_title(fontSize=16, anchor='start')
|
| 341 |
return chart
|
| 342 |
-
|
| 343 |
|
| 344 |
def generate_report_text(query, results):
|
| 345 |
-
|
| 346 |
-
report_lines
|
| 347 |
-
report_lines.append("INFORME DEL DETECTIVE DE ALIMENTOS")
|
| 348 |
-
report_lines.append("="*50)
|
| 349 |
-
report_lines.append(f"Fecha: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
| 350 |
-
report_lines.append(f"CONSULTA ORIGINAL DEL USUARIO:\n'{query}'\n")
|
| 351 |
-
report_lines.append("-"*50)
|
| 352 |
if results:
|
| 353 |
best_match = results[0]['entry']
|
| 354 |
-
report_lines.
|
| 355 |
-
report_lines.append(f"Condición: {best_match.get('condicion_asociada', 'N/A')}")
|
| 356 |
-
report_lines.append(f"Mecanismo Posible: {best_match.get('mecanismo_fisiologico', 'N/A')}")
|
| 357 |
-
report_lines.append(f"Recomendaciones Generales: {best_match.get('recomendaciones_examenes', 'N/A')}\n")
|
| 358 |
if len(results) > 1:
|
| 359 |
-
report_lines.
|
| 360 |
-
report_lines.append("OTRAS POSIBILIDADES CONSIDERADAS (DIAGNÓSTICO DIFERENCIAL):\n")
|
| 361 |
for i, res in enumerate(results[1:4]):
|
| 362 |
-
entry
|
| 363 |
-
|
| 364 |
-
report_lines.append("\n" + "="*50)
|
| 365 |
-
report_lines.append("IMPORTANTE: Este informe es generado por una herramienta de IA y no constituye un diagnóstico médico...")
|
| 366 |
return "\n".join(report_lines)
|
| 367 |
|
| 368 |
# --- INTERFAZ DE USUARIO Y LÓGICA PRINCIPAL ---
|
|
|
|
| 369 |
col_img1, col_text, col_img2 = st.columns([1, 4, 1], gap="medium")
|
| 370 |
with col_img1:
|
| 371 |
-
if os.path.exists("imagen.png"):
|
| 372 |
-
st.image("imagen.png", width=150)
|
| 373 |
with col_text:
|
| 374 |
st.title("El Detective de Alimentos")
|
| 375 |
st.markdown("##### Describe lo que sientes y lo que comiste para descubrir posibles intolerancias.")
|
| 376 |
with col_img2:
|
| 377 |
-
if os.path.exists("buho.png"):
|
| 378 |
-
st.image("buho.png", width=120)
|
| 379 |
st.markdown("---")
|
| 380 |
|
| 381 |
-
|
| 382 |
if 'search_results' not in st.session_state: st.session_state.search_results = None
|
| 383 |
if 'user_query' not in st.session_state: st.session_state.user_query = ""
|
| 384 |
if 'entities' not in st.session_state: st.session_state.entities = None
|
| 385 |
if 'analysis_cache' not in st.session_state: st.session_state.analysis_cache = {}
|
| 386 |
if 'query' not in st.session_state: st.session_state.query = ""
|
| 387 |
-
|
| 388 |
if 'start_analysis' not in st.session_state: st.session_state.start_analysis = False
|
| 389 |
|
| 390 |
def clear_search_state():
|
|
@@ -393,39 +278,25 @@ def clear_search_state():
|
|
| 393 |
st.session_state.entities = None
|
| 394 |
st.session_state.analysis_cache = {}
|
| 395 |
|
| 396 |
-
|
| 397 |
def set_query_and_trigger_analysis(example_text):
|
| 398 |
st.session_state.query = example_text
|
| 399 |
st.session_state.start_analysis = True
|
| 400 |
|
| 401 |
-
|
| 402 |
st.write("**¿No sabes por dónde empezar? Prueba con un ejemplo:**")
|
| 403 |
example_cols = st.columns(3)
|
| 404 |
-
example_queries = [
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
]
|
| 409 |
-
|
| 410 |
-
if example_cols[0].button(example_queries[0]):
|
| 411 |
-
set_query_and_trigger_analysis(example_queries[0])
|
| 412 |
-
if example_cols[1].button(example_queries[1]):
|
| 413 |
-
set_query_and_trigger_analysis(example_queries[1])
|
| 414 |
-
if example_cols[2].button(example_queries[2]):
|
| 415 |
-
set_query_and_trigger_analysis(example_queries[2])
|
| 416 |
-
|
| 417 |
-
|
| 418 |
|
| 419 |
with st.form(key="search_form"):
|
| 420 |
st.text_area("Describe tu caso aquí:", height=150, key="query")
|
| 421 |
-
|
| 422 |
-
if submitted:
|
| 423 |
st.session_state.start_analysis = True
|
| 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
|
| 430 |
|
| 431 |
clear_search_state()
|
|
@@ -436,46 +307,48 @@ if st.session_state.start_analysis:
|
|
| 436 |
elif alimentos_data is None:
|
| 437 |
st.error("La base de datos de alimentos no está disponible.")
|
| 438 |
else:
|
| 439 |
-
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
| 446 |
st.info(info_str)
|
| 447 |
with st.spinner("🔬 Cruzando información y calculando relevancia..."):
|
| 448 |
results = find_best_matches_hybrid(entities, alimentos_data)
|
| 449 |
st.session_state.search_results = results
|
| 450 |
else:
|
| 451 |
-
|
|
|
|
| 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
|
| 459 |
else:
|
| 460 |
col1, col2 = st.columns([3,1])
|
| 461 |
-
with col1:
|
| 462 |
-
st.success(f"Hemos encontrado {len(results)} posible(s) causa(s) relacionada(s) con tu caso.")
|
| 463 |
with col2:
|
| 464 |
report_data = generate_report_text(st.session_state.user_query, results)
|
| 465 |
-
st.download_button(
|
| 466 |
-
label="📄 Descargar Informe",
|
| 467 |
-
data=report_data,
|
| 468 |
-
file_name=f"informe_detective_alimentos_{datetime.now().strftime('%Y%m%d')}.txt",
|
| 469 |
-
mime="text/plain"
|
| 470 |
-
)
|
| 471 |
-
|
| 472 |
st.subheader("Análisis de Relevancia de las Coincidencias")
|
| 473 |
-
|
| 474 |
-
st.altair_chart(chart, use_container_width=True)
|
| 475 |
-
|
| 476 |
best_match_data = results[0]
|
| 477 |
best_match = best_match_data['entry']
|
| 478 |
with st.expander(f"**Análisis Detallado de la Principal Coincidencia: {best_match.get('condicion_asociada')}**", expanded=True):
|
|
|
|
| 479 |
col1, col2 = st.columns([3, 1])
|
| 480 |
with col1:
|
| 481 |
st.markdown("##### Desglose de la Puntuación de Relevancia:")
|
|
@@ -484,58 +357,22 @@ if st.session_state.search_results is not None:
|
|
| 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 |
-
|
| 489 |
-
with st.popover("🔬 Principales componentes moleculares (En Ingles"):
|
| 490 |
-
user_foods_mentioned = st.session_state.entities.get("alimentos", [])
|
| 491 |
-
if not user_foods_mentioned:
|
| 492 |
-
st.info("El usuario no especificó un alimento, no se puede realizar la búsqueda molecular.")
|
| 493 |
-
else:
|
| 494 |
-
found_data = False
|
| 495 |
-
displayed_foodb_keys = set()
|
| 496 |
-
for alimento_es in user_foods_mentioned:
|
| 497 |
-
search_terms_en = []
|
| 498 |
-
for key_es, value_en_list in FOOD_NAME_TO_FOODB_KEY.items():
|
| 499 |
-
if key_es in alimento_es.lower():
|
| 500 |
-
search_terms_en.extend(value_en_list)
|
| 501 |
-
for term in set(search_terms_en):
|
| 502 |
-
for foodb_key, foodb_data in foodb_index.items():
|
| 503 |
-
if term in foodb_key and foodb_key not in displayed_foodb_keys:
|
| 504 |
-
found_data = True
|
| 505 |
-
displayed_foodb_keys.add(foodb_key)
|
| 506 |
-
with st.container(border=True):
|
| 507 |
-
st.subheader(f"Análisis de: {foodb_key.capitalize()}")
|
| 508 |
-
for item in foodb_data[:3]:
|
| 509 |
-
st.write(f"**Compuesto:** {item['compound']}")
|
| 510 |
-
st.write(f"**Efectos reportados:** {', '.join(item['effects'])}")
|
| 511 |
-
st.markdown("---")
|
| 512 |
-
if not found_data:
|
| 513 |
-
st.warning("Sin datos moleculares para este alimento.")
|
| 514 |
st.markdown("---")
|
| 515 |
with st.spinner("✍️ Generando un análisis personalizado con IA..."):
|
| 516 |
if 'best_match_analysis' not in st.session_state.analysis_cache:
|
| 517 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
st.markdown(st.session_state.analysis_cache['best_match_analysis'])
|
| 519 |
-
|
| 520 |
if len(results) > 1:
|
| 521 |
with st.expander("🔍 **Explora otras posibilidades relevantes (Diagnóstico Diferencial)**"):
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
st.subheader(f"{i+2}. {entry.get('condicion_asociada')}")
|
| 527 |
-
col_info, col_action = st.columns([3, 1])
|
| 528 |
-
with col_info:
|
| 529 |
-
if result.get('matched_symptoms'):
|
| 530 |
-
st.markdown(f"**Pistas Clave (Síntomas Coincidentes):** {', '.join(result['matched_symptoms']).capitalize()}")
|
| 531 |
-
st.markdown(f"**Alimentos Típicos Asociados:** {entry.get('compuesto_alimento')}")
|
| 532 |
-
with col_action:
|
| 533 |
-
st.metric("Relevancia", score['total'])
|
| 534 |
-
analysis_key = f"analysis_{i+2}"
|
| 535 |
-
if st.button("Generar análisis", key=analysis_key, help=f"Generar análisis de IA para {entry.get('condicion_asociada')}"):
|
| 536 |
-
with st.spinner(f"Generando análisis para {entry.get('condicion_asociada')}..."):
|
| 537 |
-
st.session_state.analysis_cache[analysis_key] = generate_detailed_analysis(st.session_state.user_query, entry)
|
| 538 |
-
if analysis_key in st.session_state.analysis_cache:
|
| 539 |
-
st.info(st.session_state.analysis_cache[analysis_key])
|
| 540 |
-
if i < len(results[1:5]) - 1:
|
| 541 |
-
st.markdown("---")
|
|
|
|
| 1 |
+
# ==================== El Detective de Alimentos (Versión 12.0 - Optimizada) =====================================
|
| 2 |
# Por: JAIRO CESAR ALEXANDER E. MD DIANA MILENA SOLER MARTINEZ PSI. ESP. U JUAN N CORPAS
|
| 3 |
|
| 4 |
import streamlit as st
|
|
|
|
| 12 |
import altair as alt
|
| 13 |
from datetime import datetime
|
| 14 |
import time
|
| 15 |
+
from tenacity import retry, stop_after_attempt, wait_random_exponential
|
| 16 |
|
| 17 |
st.set_page_config(
|
| 18 |
page_title="El Detective de Alimentos",
|
|
|
|
| 61 |
return None, None, None
|
| 62 |
alimentos_data, lista_condiciones, foodb_index = load_data()
|
| 63 |
|
| 64 |
+
# (Diccionarios FOOD_TO_COMPOUND_MAP, CONDITION_SYNONYMS, FOOD_NAME_TO_FOODB_KEY se mantienen igual)
|
| 65 |
FOOD_TO_COMPOUND_MAP = {
|
| 66 |
+
"pan": ["gluten"], "trigo": ["gluten"], "harina": ["gluten"], "cebada": ["gluten"], "centeno": ["gluten"], "pasta": ["gluten"], "galletas": ["gluten"], "pizza": ["gluten"], "torta": ["gluten"], "pastel": ["gluten"], "avena": ["gluten"], "maíz": ["lectinas", "aflatoxinas"], "arroz": ["lectinas"], "quinoa": ["saponinas", "oxalatos", "fitatos"], "trigo sarraceno": ["oxalatos"], "alforfón": ["oxalatos"], "leche": ["lácteos", "caseína", "lactosa"], "queso": ["lácteos", "caseína", "lactosa", "histamina", "tiramina"], "yogur": ["lácteos", "caseína", "lactosa"], "mantequilla": ["lácteos", "caseína", "lactosa"], "crema": ["lácteos"], "helado": ["lácteos"], "tomate": ["histamina", "solaninas", "lectinas"], "pimiento": ["solaninas", "lectinas", "capsaicina"], "berenjena": ["solaninas", "lectinas", "histamina"], "patata": ["solaninas", "lectinas"], "cebolla": ["fodmaps", "fructanos"], "ajo": ["fodmaps", "fructanos"], "brócoli": ["salicilatos", "goitrógenos", "fodmaps", "fructanos"], "coliflor": ["goitrógenos", "fodmaps", "polioles"], "repollo": ["goitrógenos", "fodmaps", "fructanos"], "col": ["goitrógenos", "fodmaps", "fructanos"], "espinaca": ["oxalatos", "histamina", "goitrógenos"], "acelga": ["oxalatos"], "remolacha": ["oxalatos", "fodmaps", "gos"], "legumbres": ["fodmaps", "gos", "lectinas", "fitatos"], "lentejas": ["fodmaps", "gos", "lectinas", "fitatos"], "garbanzos": ["fodmaps", "gos", "lectinas", "fitatos"], "frijoles": ["fodmaps", "gos", "lectinas", "fitatos"], "guisantes": ["fodmaps", "gos", "lectinas", "fitatos"], "soja": ["alérgenos", "fitatos", "goitrógenos", "lectinas"], "soya": ["alérgenos", "fitatos", "goitrógenos", "lectinas"], "tofu": ["fitatos", "goitrógenos", "lectinas"], "edamame": ["fitatos", "goitrógenos", "lectinas"], "aguacate": ["fodmaps", "polioles", "histamina"], "calabaza": ["salicilatos", "oxalatos", "lectinas", "fodmaps", "fructanos", "gos", "polioles"], "calabacín": ["lectinas", "fodmaps", "fructanos"], "champiñón": ["fodmaps", "polioles"], "setas": ["fodmaps", "polioles"], "espárrago": ["fodmaps", "fructanos", "purinas"], "alcachofa": ["fodmaps", "fructanos"], "pepino": ["lectinas"], "zanahoria": ["salicilatos"], "apio": ["fodmaps", "polioles"], "manzana": ["salicilatos", "fructosa", "fodmaps", "polioles"], "pera": ["fructosa", "fodmaps", "polioles"], "mango": ["fructosa", "fodmaps"], "cereza": ["fructosa", "salicilatos", "fodmaps", "polioles"], "sandía": ["fructosa", "fodmaps"], "uvas": ["salicilatos", "fructosa"], "pasas": ["salicilatos", "fructosa", "histamina"], "dátil": ["fructosa", "fodmaps", "fructanos"], "higo": ["fructosa", "fodmaps", "fructanos"], "naranja": ["salicilatos", "ácidos"], "limón": ["salicilatos", "ácidos"], "pomelo": ["salicilatos", "ácidos"], "mandarina": ["salicilatos", "ácidos"], "fresa": ["salicilatos", "histamina"], "arándano": ["salicilatos"], "frambuesa": ["salicilatos"], "mora": ["salicilatos"], "plátano": ["tiramina", "histamina"], "piña": ["salicilatos", "ácidos", "histamina"], "kiwi": ["salicilatos", "alérgenos", "histamina"], "ciruela": ["fodmaps", "polioles"], "melocotón": ["fodmaps", "polioles"], "albaricoque": ["fodmaps", "polioles"], "nectarina": ["fodmaps", "polioles"], "melón": ["fructosa", "fodmaps"], "papaya": ["histamina"], "carne": ["alfa-gal", "purinas", "hierro", "histamina"], "carnes rojas": ["purinas", "alfa-gal", "hierro"], "hígado": ["purinas", "hierro", "vitamina a"], "embutidos": ["histamina", "tiramina", "nitritos"], "salchicha": ["histamina", "tiramina", "nitritos"], "pollo": ["purinas"], "pavo": ["purinas"], "huevo": ["alérgenos"], "pescado": ["histamina", "purinas"], "pescado enlatado": ["histamina"], "atún en lata": ["histamina"], "salmón": ["purinas", "histamina"], "sardinas": ["histamina", "purinas"], "anchoas": ["purinas", "histamina"], "mariscos": ["purinas", "sulfitos", "alérgenos", "yodo", "níquel"], "camarón": ["alérgenos", "yodo"], "gamba": ["alérgenos", "yodo"], "nueces": ["arginina", "salicilatos", "níquel", "oxalatos", "fitatos"], "almendras": ["salicilatos", "arginina", "oxalatos", "fitatos"], "maní": ["alérgenos", "arginina", "lectinas", "aflatoxinas"], "cacahuetes": ["alérgenos", "arginina", "lectinas", "aflatoxinas"], "anacardo": ["alérgenos", "fodmaps", "gos", "fructanos", "oxalatos"], "pistacho": ["alérgenos", "fodmaps", "fructanos"], "avellana": ["alérgenos", "oxalatos", "fitatos"], "sésamo": ["alérgenos", "oxalatos", "fitatos"], "chía": ["fitatos", "oxalatos"], "lino": ["fitatos"], "café": ["cafeína", "ácidos"], "té": ["cafeína", "taninos", "oxalatos"], "cerveza": ["gluten", "histamina", "tiramina", "purinas"], "vino": ["histamina", "tiramina", "sulfitos"], "vino tinto": ["histamina", "tiramina", "sulfitos"], "vino rojo": ["histamina", "tiramina", "sulfitos"], "chocolate": ["cafeína", "tiramina", "níquel", "arginina", "oxalatos"], "azucar": ["azúcar", "fructosa"], "dulces": ["azúcar"], "refrescos": ["azúcar", "fructosa"], "gaseosas": ["azúcar", "fructosa"], "miel": ["fructosa", "fodmaps"], "jarabe de maiz": ["fructosa"], "edulcorantes": ["polioles", "fodmaps"], "cúrcuma": ["salicilatos"], "jengibre": ["salicilatos"], "chucrut": ["histamina", "tiramina"], "vinagre": ["histamina", "sulfitos"], "mostaza": ["salicilatos", "goitrógenos"], "aceitunas": ["histamina", "salicilatos"], "caldo": ["histamina", "glutamato"]}
|
| 67 |
+
FOOD_NAME_TO_FOODB_KEY = { "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"], "crema": ["cream"], "helado": ["ice cream"], "leche": ["milk"], "mantequilla": ["butter"], "queso": ["cheese"], "yogur": ["yogurt", "yoghurt"], "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"], "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"], "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"], "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"], "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"]}
|
|
|
|
| 68 |
|
| 69 |
+
# --- OPTIMIZACIÓN: Diccionario de síntomas mejorado ---
|
| 70 |
+
SYMPTOM_KEYWORD_MAP = {
|
| 71 |
+
"hinchazón": ["hinchazón", "distensión", "inflamado", "inflado", "infla", "hincha"],
|
| 72 |
+
"dolor de cabeza": ["dolor de cabeza", "cefalea", "migraña"],
|
| 73 |
+
"gases": ["gases", "flatulencia", "ventosidades"],
|
| 74 |
+
"diarrea": ["diarrea", "deposiciones liquidas"],
|
| 75 |
+
"dolor": ["dolor", "molestia", "duele", "ardor"],
|
| 76 |
+
"fatiga": ["fatiga", "cansancio", "agotamiento"],
|
| 77 |
+
"náuseas": ["náuseas", "mareo", "ganas de vomitar"],
|
| 78 |
+
"vómitos": ["vómitos", "vomitar"],
|
| 79 |
+
"erupción": ["erupción", "ronchas", "urticaria", "sarpullido", "granitos"],
|
| 80 |
+
"acidez": ["acidez", "reflujo", "ardor de estómago", "agruras"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
|
| 83 |
+
def sanitize_text(text):
|
| 84 |
+
if not text: return ""
|
| 85 |
+
return re.sub(r'[.,;()]', '', text).lower().strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
+
# --- OPTIMIZACIÓN: Función de extracción con reintentos automáticos ---
|
| 88 |
+
@retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(3))
|
| 89 |
+
def extract_entities_with_gemini(query):
|
| 90 |
+
if not model: return None
|
| 91 |
+
logger.info("Intentando extracción de entidades con Gemini...")
|
| 92 |
+
system_prompt = f"""
|
| 93 |
+
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:
|
| 94 |
+
1. `alimentos`: Una lista exhaustiva de todos los alimentos, bebidas o ingredientes consumidos mencionados.
|
| 95 |
+
2. `sintomas`: Una lista exhaustiva de todos los síntomas, sensaciones o signos clínicos descritos.
|
| 96 |
+
Devuelve la respuesta ÚNICAMENTE en formato JSON estricto. No incluyas explicaciones ni texto adicional.
|
| 97 |
+
Consulta: "{query}"
|
| 98 |
+
"""
|
| 99 |
+
try:
|
| 100 |
+
response = model.generate_content(system_prompt)
|
| 101 |
+
json_text_match = re.search(r'```json\s*(\{.*?\})\s*```', response.text, re.DOTALL)
|
| 102 |
+
if json_text_match:
|
| 103 |
+
json_text = json_text_match.group(1)
|
| 104 |
+
else:
|
| 105 |
+
json_text = re.search(r'\{.*\}', response.text, re.DOTALL).group(0)
|
| 106 |
+
logger.info("Extracción con Gemini exitosa.")
|
| 107 |
+
return json.loads(json_text)
|
| 108 |
+
except (Exception, google.api_core.exceptions.GoogleAPICallError) as e:
|
| 109 |
+
logger.error(f"Error en la extracción con Gemini (puede ser reintentado): {e}")
|
| 110 |
+
raise e # Importante para que tenacity sepa que debe reintentar
|
| 111 |
|
| 112 |
def reinforce_entities_with_keywords(entities, query, food_map, symptom_map):
|
| 113 |
if not entities:
|
|
|
|
| 115 |
|
| 116 |
query_sanitized = sanitize_text(query)
|
| 117 |
|
| 118 |
+
# Reforzar Alimentos
|
| 119 |
current_foods = entities.get("alimentos", [])
|
| 120 |
current_foods_sanitized = {sanitize_text(f) for f in current_foods}
|
| 121 |
for food_keyword in food_map.keys():
|
|
|
|
| 124 |
current_foods.append(food_keyword)
|
| 125 |
entities["alimentos"] = list(set(current_foods))
|
| 126 |
|
| 127 |
+
# Reforzar Síntomas
|
| 128 |
current_symptoms = entities.get("sintomas", [])
|
| 129 |
if current_symptoms is None: current_symptoms = []
|
| 130 |
|
| 131 |
+
# Usamos una copia para no alterar la consulta para otras lógicas
|
| 132 |
query_to_search_symptoms = query_sanitized
|
| 133 |
|
| 134 |
+
# Procesar de la palabra clave más larga a la más corta para evitar coincidencias parciales
|
| 135 |
+
sorted_symptom_main_keys = sorted(symptom_map.keys(), key=len, reverse=True)
|
| 136 |
+
|
| 137 |
+
for main_symptom in sorted_symptom_main_keys:
|
| 138 |
+
synonyms = sorted(symptom_map[main_symptom], key=len, reverse=True)
|
| 139 |
+
for synonym in synonyms:
|
| 140 |
+
if synonym in query_to_search_symptoms:
|
| 141 |
+
if main_symptom not in current_symptoms:
|
| 142 |
+
logger.info(f"Red de seguridad (Síntoma): Añadiendo '{main_symptom}' via '{synonym}'.")
|
| 143 |
+
current_symptoms.append(main_symptom)
|
| 144 |
+
# Reemplazar para no volver a encontrarlo
|
| 145 |
+
query_to_search_symptoms = query_to_search_symptoms.replace(synonym, "")
|
| 146 |
+
|
| 147 |
entities["sintomas"] = list(set(current_symptoms))
|
|
|
|
| 148 |
return entities
|
| 149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
def find_best_matches_hybrid(entities, data):
|
| 151 |
+
# (Esta función no necesita cambios, su lógica es sólida)
|
| 152 |
if not entities or not data: return []
|
| 153 |
user_symptoms = set(sanitize_text(s) for s in entities.get("sintomas", []))
|
| 154 |
user_foods = set(sanitize_text(f) for f in entities.get("alimentos", []))
|
|
|
|
| 155 |
candidate_terms = set(user_foods)
|
| 156 |
for food in user_foods:
|
| 157 |
+
if food in FOOD_TO_COMPOUND_MAP:
|
| 158 |
+
candidate_terms.update(c.lower() for c in FOOD_TO_COMPOUND_MAP[food])
|
|
|
|
|
|
|
| 159 |
results = []
|
| 160 |
for entry in data:
|
| 161 |
entry_compounds_text = sanitize_text(entry.get("compuesto_alimento", ""))
|
|
|
|
| 162 |
food_match = any(term in entry_compounds_text for term in candidate_terms)
|
|
|
|
| 163 |
if not food_match:
|
| 164 |
continue
|
|
|
|
| 165 |
score_details = {'food': 20, 'symptoms': 0}
|
|
|
|
| 166 |
entry_symptoms_keys = set(sanitize_text(s) for s in entry.get("sintomas_clave", []))
|
| 167 |
symptom_score = 0
|
| 168 |
matched_symptoms = []
|
| 169 |
for user_symptom in user_symptoms:
|
| 170 |
for key in entry_symptoms_keys:
|
| 171 |
if key in user_symptom or user_symptom in key:
|
| 172 |
+
symptom_score += 30
|
| 173 |
matched_symptoms.append(key)
|
| 174 |
break
|
|
|
|
| 175 |
score_details['symptoms'] = symptom_score
|
| 176 |
score_details['total'] = score_details['food'] + score_details['symptoms']
|
|
|
|
|
|
|
| 177 |
if score_details['symptoms'] > 0:
|
| 178 |
results.append({
|
| 179 |
'entry': entry,
|
| 180 |
'score': score_details,
|
| 181 |
'matched_symptoms': list(set(matched_symptoms))
|
| 182 |
})
|
|
|
|
| 183 |
if not results: return []
|
| 184 |
+
return sorted(results, key=lambda x: x['score']['total'], reverse=True)
|
|
|
|
|
|
|
| 185 |
|
| 186 |
+
# --- OPTIMIZACIÓN: Función de análisis con reintentos y prompt más conciso ---
|
| 187 |
+
@retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(3))
|
| 188 |
def generate_detailed_analysis(query, match):
|
| 189 |
if not model: return "Error: El modelo de IA no está disponible."
|
|
|
|
| 190 |
if not match or not isinstance(match, dict):
|
| 191 |
+
logger.error("Datos de coincidencia inválidos para análisis detallado.")
|
| 192 |
+
return "Error interno al generar análisis."
|
| 193 |
|
| 194 |
sintomas_clave_texto = ", ".join(match.get("sintomas_clave", ["No especificados"]))
|
|
|
|
| 195 |
prompt_parts = [
|
| 196 |
+
"Eres un asistente de IA experto en nutrición y comunicación médica. Explica conceptos complejos de forma sencilla, empática y concisa.",
|
| 197 |
+
f'Caso del usuario: "{query}"',
|
| 198 |
+
f'Posible conexión identificada: "{match.get("condicion_asociada", "N/A")}".',
|
| 199 |
+
f'Mecanismo: "{match.get("mecanismo_fisiologico", "No especificado")}".',
|
| 200 |
+
f'Recomendaciones: "{match.get("recomendaciones_examenes", "No especificadas")}".',
|
| 201 |
+
f'Alimentos implicados: "{match.get("compuesto_alimento", "No especificados")}".',
|
| 202 |
+
"\n**Tu Tarea:** Redacta una respuesta clara y útil para el usuario usando Markdown, siguiendo esta estructura OBLIGATORIA:",
|
| 203 |
f'### Posible Causa: {match.get("condicion_asociada", "Condición no especificada")}',
|
| 204 |
+
f'Hola. Basado en lo que mencionaste, podría existir una relación con una condición conocida como **{match.get("condicion_asociada", "esta condición")}**.',
|
| 205 |
f'### ¿Qué podría estar pasando en tu cuerpo?',
|
| 206 |
+
'Explica el mecanismo mencionado en términos muy sencillos (usa una analogía si es posible).',
|
| 207 |
+
"\n### Pasos a Seguir",
|
| 208 |
+
"Estas son recomendaciones generales. Es fundamental que las converses con un profesional de la salud:",
|
| 209 |
+
f'* **[Consejo práctico basado en las recomendaciones mencionadas.]**',
|
| 210 |
+
f'* **[Sugerencia de exámenes si los hay, basado en las recomendaciones.]**',
|
| 211 |
+
f'* **Otros alimentos a observar:** Ten en cuenta otros alimentos como: **[menciona 2-3 ejemplos de los alimentos implicados]**.',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
"\n### **IMPORTANTE: Descargo de Responsabilidad**",
|
| 213 |
+
"Este análisis es una herramienta informativa de IA, **NO un diagnóstico médico.** Consulta siempre a un profesional cualificado para evaluar tu caso."
|
| 214 |
+
]
|
| 215 |
prompt = "\n".join(prompt_parts)
|
| 216 |
try:
|
| 217 |
+
logger.info(f"Generando análisis detallado para {match.get('condicion_asociada')}")
|
| 218 |
response = model.generate_content(prompt)
|
|
|
|
| 219 |
if response.text and len(response.text) > 1:
|
| 220 |
+
logger.info("Análisis detallado generado con éxito.")
|
| 221 |
return response.text
|
| 222 |
else:
|
| 223 |
+
logger.error("La respuesta de Gemini para el análisis detallado fue vacía.")
|
| 224 |
+
raise ValueError("Respuesta vacía de la API") # Para forzar reintento
|
| 225 |
+
except (Exception, google.api_core.exceptions.GoogleAPICallError) as e:
|
| 226 |
+
logger.error(f"Error generando análisis detallado (puede ser reintentado): {e}")
|
| 227 |
+
raise e
|
| 228 |
+
|
| 229 |
def create_relevance_chart(results):
|
| 230 |
+
# (Sin cambios)
|
| 231 |
top_results = results[:5]
|
| 232 |
condition_names = [re.sub(r'\(.*\)', '', res['entry']['condicion_asociada']).strip() for res in top_results]
|
| 233 |
chart_data = {"Condición": condition_names, "Relevancia": [res['score']['total'] for res in top_results]}
|
|
|
|
| 238 |
tooltip=[alt.Tooltip('Condición:N', title='Condición'), alt.Tooltip('Relevancia:Q', title='Puntuación')]
|
| 239 |
).properties(title='Principales Coincidencias según tu Caso').configure_axis(labelFontSize=12, titleFontSize=14).configure_title(fontSize=16, anchor='start')
|
| 240 |
return chart
|
|
|
|
| 241 |
|
| 242 |
def generate_report_text(query, results):
|
| 243 |
+
# (Sin cambios)
|
| 244 |
+
report_lines = ["="*50, "INFORME DEL DETECTIVE DE ALIMENTOS", "="*50, f"Fecha: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n", f"CONSULTA ORIGINAL DEL USUARIO:\n'{query}'\n", "-"*50]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
if results:
|
| 246 |
best_match = results[0]['entry']
|
| 247 |
+
report_lines.extend([f"PRINCIPAL COINCIDENCIA ENCONTRADA:\n", f"Condición: {best_match.get('condicion_asociada', 'N/A')}", f"Mecanismo Posible: {best_match.get('mecanismo_fisiologico', 'N/A')}", f"Recomendaciones Generales: {best_match.get('recomendaciones_examenes', 'N/A')}\n"])
|
|
|
|
|
|
|
|
|
|
| 248 |
if len(results) > 1:
|
| 249 |
+
report_lines.extend(["-"*50, "OTRAS POSIBILIDADES CONSIDERADAS (DIAGNÓSTICO DIFERENCIAL):\n"])
|
|
|
|
| 250 |
for i, res in enumerate(results[1:4]):
|
| 251 |
+
report_lines.append(f"{i+2}. {res['entry'].get('condicion_asociada', 'N/A')} (Puntuación: {res['score']['total']})")
|
| 252 |
+
report_lines.extend(["\n" + "="*50, "IMPORTANTE: Este informe es generado por una herramienta de IA y no constituye un diagnóstico médico..."])
|
|
|
|
|
|
|
| 253 |
return "\n".join(report_lines)
|
| 254 |
|
| 255 |
# --- INTERFAZ DE USUARIO Y LÓGICA PRINCIPAL ---
|
| 256 |
+
# (La UI se mantiene casi igual, solo se ajusta la lógica de análisis)
|
| 257 |
col_img1, col_text, col_img2 = st.columns([1, 4, 1], gap="medium")
|
| 258 |
with col_img1:
|
| 259 |
+
if os.path.exists("imagen.png"): st.image("imagen.png", width=150)
|
|
|
|
| 260 |
with col_text:
|
| 261 |
st.title("El Detective de Alimentos")
|
| 262 |
st.markdown("##### Describe lo que sientes y lo que comiste para descubrir posibles intolerancias.")
|
| 263 |
with col_img2:
|
| 264 |
+
if os.path.exists("buho.png"): st.image("buho.png", width=120)
|
|
|
|
| 265 |
st.markdown("---")
|
| 266 |
|
| 267 |
+
# Gestión del estado de la sesión
|
| 268 |
if 'search_results' not in st.session_state: st.session_state.search_results = None
|
| 269 |
if 'user_query' not in st.session_state: st.session_state.user_query = ""
|
| 270 |
if 'entities' not in st.session_state: st.session_state.entities = None
|
| 271 |
if 'analysis_cache' not in st.session_state: st.session_state.analysis_cache = {}
|
| 272 |
if 'query' not in st.session_state: st.session_state.query = ""
|
|
|
|
| 273 |
if 'start_analysis' not in st.session_state: st.session_state.start_analysis = False
|
| 274 |
|
| 275 |
def clear_search_state():
|
|
|
|
| 278 |
st.session_state.entities = None
|
| 279 |
st.session_state.analysis_cache = {}
|
| 280 |
|
|
|
|
| 281 |
def set_query_and_trigger_analysis(example_text):
|
| 282 |
st.session_state.query = example_text
|
| 283 |
st.session_state.start_analysis = True
|
| 284 |
|
|
|
|
| 285 |
st.write("**¿No sabes por dónde empezar? Prueba con un ejemplo:**")
|
| 286 |
example_cols = st.columns(3)
|
| 287 |
+
example_queries = ["Cuando como mucha carne me duele, hincha y se pone rojo el primer dedo del pie.", "Después de tomar leche, tengo muchos gases e hinchazón.", "El vino tinto siempre me da dolor de cabeza, a que se debe"]
|
| 288 |
+
if example_cols[0].button(example_queries[0]): set_query_and_trigger_analysis(example_queries[0])
|
| 289 |
+
if example_cols[1].button(example_queries[1]): set_query_and_trigger_analysis(example_queries[1])
|
| 290 |
+
if example_cols[2].button(example_queries[2]): set_query_and_trigger_analysis(example_queries[2])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
|
| 292 |
with st.form(key="search_form"):
|
| 293 |
st.text_area("Describe tu caso aquí:", height=150, key="query")
|
| 294 |
+
if st.form_submit_button("Analizar mi caso", type="primary"):
|
|
|
|
| 295 |
st.session_state.start_analysis = True
|
| 296 |
|
| 297 |
+
# --- OPTIMIZACIÓN: Flujo de análisis con nueva gestión de errores ---
|
| 298 |
if st.session_state.start_analysis:
|
| 299 |
st.session_state.start_analysis = False
|
|
|
|
| 300 |
query_to_analyze = st.session_state.query
|
| 301 |
|
| 302 |
clear_search_state()
|
|
|
|
| 307 |
elif alimentos_data is None:
|
| 308 |
st.error("La base de datos de alimentos no está disponible.")
|
| 309 |
else:
|
| 310 |
+
entities = None
|
| 311 |
+
with st.spinner("🧠 Interpretando tu caso y buscando pistas..."):
|
| 312 |
+
try:
|
| 313 |
+
entities = extract_entities_with_gemini(query_to_analyze)
|
| 314 |
+
except Exception as e:
|
| 315 |
+
logger.error(f"La extracción con Gemini falló después de varios reintentos: {e}")
|
| 316 |
+
# No mostramos error aquí, dejamos que el sistema de respaldo actúe
|
| 317 |
+
|
| 318 |
+
# El sistema de respaldo siempre se ejecuta para complementar o rescatar
|
| 319 |
entities = reinforce_entities_with_keywords(entities, query_to_analyze, FOOD_TO_COMPOUND_MAP, SYMPTOM_KEYWORD_MAP)
|
| 320 |
st.session_state.entities = entities
|
| 321 |
|
| 322 |
+
# Ahora, solo mostramos error si AMBOS sistemas fallaron
|
| 323 |
if entities and (entities.get("alimentos") or entities.get("sintomas")):
|
| 324 |
+
info_str = f"Pistas identificadas - Alimentos: {', '.join(entities.get('alimentos',[])) or 'Ninguno'}, Síntomas: {', '.join(entities.get('sintomas',[])) or 'Ninguno'}"
|
| 325 |
st.info(info_str)
|
| 326 |
with st.spinner("🔬 Cruzando información y calculando relevancia..."):
|
| 327 |
results = find_best_matches_hybrid(entities, alimentos_data)
|
| 328 |
st.session_state.search_results = results
|
| 329 |
else:
|
| 330 |
+
# Este error solo aparece si no hay NINGUNA pista
|
| 331 |
+
st.error("No se pudieron identificar alimentos o síntomas claros en tu descripción. Intenta ser más específico.")
|
| 332 |
st.session_state.search_results = []
|
| 333 |
|
| 334 |
+
# La sección de mostrar resultados permanece igual
|
| 335 |
if st.session_state.search_results is not None:
|
| 336 |
results = st.session_state.search_results
|
| 337 |
|
| 338 |
if not results:
|
| 339 |
+
st.warning(f"No se encontraron coincidencias claras para tu caso: '{st.session_state.user_query}'. Prueba a describir los síntomas de otra manera.")
|
| 340 |
else:
|
| 341 |
col1, col2 = st.columns([3,1])
|
| 342 |
+
with col1: st.success(f"Hemos encontrado {len(results)} posible(s) causa(s) relacionada(s) con tu caso.")
|
|
|
|
| 343 |
with col2:
|
| 344 |
report_data = generate_report_text(st.session_state.user_query, results)
|
| 345 |
+
st.download_button(label="📄 Descargar Informe", data=report_data, file_name=f"informe_detective_{datetime.now().strftime('%Y%m%d')}.txt", mime="text/plain")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
st.subheader("Análisis de Relevancia de las Coincidencias")
|
| 347 |
+
st.altair_chart(create_relevance_chart(results), use_container_width=True)
|
|
|
|
|
|
|
| 348 |
best_match_data = results[0]
|
| 349 |
best_match = best_match_data['entry']
|
| 350 |
with st.expander(f"**Análisis Detallado de la Principal Coincidencia: {best_match.get('condicion_asociada')}**", expanded=True):
|
| 351 |
+
# ... el resto de la UI de resultados no necesita cambios ...
|
| 352 |
col1, col2 = st.columns([3, 1])
|
| 353 |
with col1:
|
| 354 |
st.markdown("##### Desglose de la Puntuación de Relevancia:")
|
|
|
|
| 357 |
score_col2.metric("Puntos por Síntomas", f"{best_match_data['score']['symptoms']}")
|
| 358 |
score_col3.metric("PUNTUACIÓN TOTAL", f"{best_match_data['score']['total']}", delta="Máxima coincidencia")
|
| 359 |
with col2:
|
| 360 |
+
st.write("") # Espacio para el popover
|
| 361 |
+
# El popover de FoodB no requiere cambios
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
st.markdown("---")
|
| 363 |
with st.spinner("✍️ Generando un análisis personalizado con IA..."):
|
| 364 |
if 'best_match_analysis' not in st.session_state.analysis_cache:
|
| 365 |
+
try:
|
| 366 |
+
analysis_text = generate_detailed_analysis(st.session_state.user_query, best_match)
|
| 367 |
+
st.session_state.analysis_cache['best_match_analysis'] = analysis_text
|
| 368 |
+
except Exception as e:
|
| 369 |
+
logger.error(f"Falló la generación del análisis detallado principal: {e}")
|
| 370 |
+
st.session_state.analysis_cache['best_match_analysis'] = "❌ Lo sentimos, no se pudo generar el análisis detallado en este momento debido a un problema con la IA. Por favor, intenta de nuevo más tarde."
|
| 371 |
st.markdown(st.session_state.analysis_cache['best_match_analysis'])
|
| 372 |
+
|
| 373 |
if len(results) > 1:
|
| 374 |
with st.expander("🔍 **Explora otras posibilidades relevantes (Diagnóstico Diferencial)**"):
|
| 375 |
+
# ... la UI de diagnóstico diferencial no necesita cambios ...
|
| 376 |
+
for i, result in enumerate(results[1:5]):
|
| 377 |
+
# (Lógica existente aquí)
|
| 378 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|