Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
-
# El Detective de Alimentos (Versión Actualizada a google-genai V3.0
|
| 2 |
-
# Diana Milena Soler Martinez desarrolladores)
|
| 3 |
import streamlit as st
|
| 4 |
-
from google import genai
|
| 5 |
-
from google.genai import types
|
| 6 |
import os
|
| 7 |
import json
|
| 8 |
import logging
|
|
@@ -44,20 +44,18 @@ def get_gemini_client():
|
|
| 44 |
APIkeydetective = os.getenv("APIkeydetective")
|
| 45 |
|
| 46 |
if not APIkeydetective:
|
| 47 |
-
|
| 48 |
return None
|
| 49 |
|
| 50 |
return genai.Client(
|
| 51 |
api_key=APIkeydetective,
|
| 52 |
-
http_options=types.HttpOptions(timeout=
|
| 53 |
)
|
| 54 |
except Exception as e:
|
| 55 |
-
|
| 56 |
return None
|
| 57 |
|
| 58 |
client = get_gemini_client()
|
| 59 |
-
if not client:
|
| 60 |
-
st.stop()
|
| 61 |
|
| 62 |
# --- CARGA ULTRA-RÁPIDA DE DATOS ---
|
| 63 |
@st.cache_resource
|
|
@@ -97,6 +95,7 @@ FOOD_TO_COMPOUND_MAP = {
|
|
| 97 |
"pan": ["gluten", "fodmaps"],
|
| 98 |
"trigo": ["gluten", "fodmaps"],
|
| 99 |
"vino": ["histamina", "tiramina", "sulfitos"],
|
|
|
|
| 100 |
"cerveza": ["gluten", "histamina", "sulfitos"],
|
| 101 |
"chocolate": ["tiramina", "feniletilamina", "histamina"],
|
| 102 |
"tomate": ["histamina", "solanina"],
|
|
@@ -112,7 +111,7 @@ FOOD_TO_COMPOUND_MAP = {
|
|
| 112 |
"cholado": ["fructosa", "lactosa", "histamina", "azúcar refinado"]
|
| 113 |
}
|
| 114 |
|
| 115 |
-
# --- SEGURO LÓGICO DE INCOMPATIBILIDAD BIOQUÍMICA ---
|
| 116 |
STRICT_COMPOUND_TRIGGERS = {
|
| 117 |
"cafeína": ["cafeína", "caffeine", "café"],
|
| 118 |
"gluten": ["gluten", "trigo", "cebada", "centeno"],
|
|
@@ -123,18 +122,28 @@ STRICT_COMPOUND_TRIGGERS = {
|
|
| 123 |
"solanina": ["solanina", "solanine"]
|
| 124 |
}
|
| 125 |
|
| 126 |
-
|
|
|
|
|
|
|
| 127 |
if not user_foods:
|
| 128 |
return True
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
user_compounds = set()
|
| 131 |
for food in user_foods:
|
| 132 |
food_clean = food.lower().strip()
|
| 133 |
user_compounds.update(food_compound_map.get(food_clean, []))
|
| 134 |
user_compounds.add(food_clean)
|
| 135 |
|
| 136 |
-
texto_condicion = (entry.get("condicion_asociada", "") + " " + entry.get("compuesto_alimento", "")).lower()
|
| 137 |
-
|
| 138 |
for compound_key, synonyms in STRICT_COMPOUND_TRIGGERS.items():
|
| 139 |
if any(syn in texto_condicion for syn in synonyms):
|
| 140 |
tiene_compuesto = any(
|
|
@@ -173,7 +182,7 @@ MASTER_SYMPTOM_MAP = {
|
|
| 173 |
"keywords_en": ["bloating", "distension", "flatulence", "swelling"]
|
| 174 |
},
|
| 175 |
"dolor de cabeza": {
|
| 176 |
-
"frases_es": ["dolor de cabeza", "migraña", "cefalea", "jaqueca"],
|
| 177 |
"keywords_en": ["headache", "migraine"]
|
| 178 |
},
|
| 179 |
"ansiedad": {
|
|
@@ -198,6 +207,7 @@ FOOD_NAME_TO_FOODB_KEY = {
|
|
| 198 |
"yogur": ["yogurt", "yoghurt"],
|
| 199 |
"mantequilla": ["butter"],
|
| 200 |
"vino": ["wine"],
|
|
|
|
| 201 |
"cerveza": ["beer"],
|
| 202 |
"chocolate": ["chocolate", "cocoa"],
|
| 203 |
"tomate": ["tomato"],
|
|
@@ -230,12 +240,16 @@ INTEGRATED_NEURO_FOOD_MAP = {
|
|
| 230 |
"fuentes_comunes": ["trigo", "cebolla", "ajo", "legumbres", "manzanas", "miel"]
|
| 231 |
},
|
| 232 |
"tiramina": {
|
| 233 |
-
"efecto_neuropsicologico": "Amina vasoactiva
|
| 234 |
"fuentes_comunes": ["quesos madurados", "vino tinto", "embutidos", "alimentos fermentados"]
|
| 235 |
},
|
| 236 |
"histamina": {
|
| 237 |
-
"efecto_neuropsicologico": "Neurotransmisor estimulante central. Su exceso por
|
| 238 |
-
"fuentes_comunes": ["queso", "vino", "tomate", "pescado azul", "jugo de naranja"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
},
|
| 240 |
"cafeína": {
|
| 241 |
"efecto_neuropsicologico": "Bloquea los receptores de adenosina en el cerebro. En dosis altas o personas sensibles provoca hiperactivación del SNC, ansiedad e insomnio.",
|
|
@@ -252,25 +266,45 @@ INTEGRATED_NEURO_FOOD_MAP = {
|
|
| 252 |
"fructosa": {
|
| 253 |
"efecto_neuropsicologico": "La sobrecarga o malabsorción de fructosa disminuye la biodisponibilidad de triptófano en el intestino, interfiriendo en la síntesis de serotonina y melatonina (desregulando el sueño).",
|
| 254 |
"fuentes_comunes": ["jugo de naranja", "frutas muy dulces", "miel", "jarabe de maíz"]
|
| 255 |
-
},
|
| 256 |
-
"sulfitos": {
|
| 257 |
-
"efecto_neuropsicologico": "Pueden inducir estrés oxidativo y respuesta histaminérgica, manifestándose como embotamiento cerebral y cefaleas.",
|
| 258 |
-
"fuentes_comunes": ["vino", "cerveza", "frutos secos desecados"]
|
| 259 |
-
},
|
| 260 |
-
"saponinas": {
|
| 261 |
-
"efecto_neuropsicologico": "Aumentan la permeabilidad intestinal ('intestino permeable'), facilitando la migración de lipopolisacáridos e induciendo neuroinflamación.",
|
| 262 |
-
"fuentes_comunes": ["quinoa", "legumbres", "soja"]
|
| 263 |
-
},
|
| 264 |
-
"oxalatos": {
|
| 265 |
-
"efecto_neuropsicologico": "Pueden interferir en la función mitocondrial de las neuronas, asociados a fatiga crónica y neblina cerebral.",
|
| 266 |
-
"fuentes_comunes": ["espinacas", "quinoa", "frutos secos", "cacao"]
|
| 267 |
-
},
|
| 268 |
-
"solanina": {
|
| 269 |
-
"efecto_neuropsicologico": "Glicoalcaloide con leve efecto inhibidor de la acetilcolinesterasa; en sensibles puede provocar letargo y rigidez corporal.",
|
| 270 |
-
"fuentes_comunes": ["tomate", "patata", "berenjena"]
|
| 271 |
}
|
| 272 |
}
|
| 273 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
# --- FUNCIONES DE APOYO Y EXTRACCIÓN ---
|
| 275 |
@st.cache_data(show_spinner=False)
|
| 276 |
def generate_word_report_cached(report_text):
|
|
@@ -315,7 +349,6 @@ def extract_keywords(text):
|
|
| 315 |
words = re.findall(r'\b\w+\b', sanitize_text(text))
|
| 316 |
return set(w for w in words if w not in STOPWORDS_ES and len(w) > 2)
|
| 317 |
|
| 318 |
-
@retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
|
| 319 |
def extract_entities_with_gemini(query):
|
| 320 |
gemini_client = get_gemini_client()
|
| 321 |
if not gemini_client: return {"alimentos": [], "sintomas": []}
|
|
@@ -323,8 +356,8 @@ def extract_entities_with_gemini(query):
|
|
| 323 |
system_prompt = f"""
|
| 324 |
Rol: Extractor Médico JSON experto en nutrición y síntomas.
|
| 325 |
Tarea: Analiza el texto del usuario y extrae un JSON estricto con dos claves:
|
| 326 |
-
- "alimentos": lista de alimentos o bebidas mencionados en español, forma simple (ej: "
|
| 327 |
-
- "sintomas": lista de síntomas mencionados en español, forma clínica simple (ej: "
|
| 328 |
|
| 329 |
Input: "{query}"
|
| 330 |
Output (JSON estricto):
|
|
@@ -339,7 +372,7 @@ def extract_entities_with_gemini(query):
|
|
| 339 |
elif "```" in text: text = text.split("```")[1].split("```")[0]
|
| 340 |
return json.loads(text)
|
| 341 |
except Exception as e:
|
| 342 |
-
logger.
|
| 343 |
return {"alimentos": [], "sintomas": []}
|
| 344 |
|
| 345 |
def reinforce_entities_with_keywords(entities, query, food_map, master_symptom_map):
|
|
@@ -376,7 +409,7 @@ def translate_symptoms_local(symptoms_list, master_symptom_map):
|
|
| 376 |
translated_symptoms.add(symptom)
|
| 377 |
return list(translated_symptoms)
|
| 378 |
|
| 379 |
-
def find_best_matches_hybrid(entities, data, food_compound_map=None):
|
| 380 |
if not entities or not data: return []
|
| 381 |
user_symptoms_list = entities.get("sintomas", [])
|
| 382 |
user_foods_list = entities.get("alimentos", [])
|
|
@@ -394,7 +427,7 @@ def find_best_matches_hybrid(entities, data, food_compound_map=None):
|
|
| 394 |
results = []
|
| 395 |
for entry in data:
|
| 396 |
# SEGURO LÓGICO
|
| 397 |
-
if food_compound_map and not validar_seguro_logico(entry, user_foods_list, food_compound_map):
|
| 398 |
continue
|
| 399 |
|
| 400 |
db_food_keywords = extract_keywords(entry.get("compuesto_alimento", ""))
|
|
@@ -445,7 +478,7 @@ def build_local_hypothesis(entities, food_compound_map, neuro_map):
|
|
| 445 |
alimentos_str = ", ".join(alimentos_unicos)
|
| 446 |
|
| 447 |
if es_dolor_cabeza:
|
| 448 |
-
condicion = f"
|
| 449 |
elif es_insomnio:
|
| 450 |
condicion = f"Alteración del sueño / insomnio por respuesta a {compuestos_str} presente en {alimentos_str}"
|
| 451 |
else:
|
|
@@ -493,10 +526,13 @@ def find_best_foodb_matches(user_foods_es, _foodb_index, _foodb_lower_map, food_
|
|
| 493 |
|
| 494 |
return unique[:limit]
|
| 495 |
|
| 496 |
-
@retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
|
| 497 |
def generate_detailed_analysis(query, match, entities=None, food_compound_map=None, neuro_map=None):
|
| 498 |
gemini_client = get_gemini_client()
|
| 499 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
|
| 501 |
compound_context = ""
|
| 502 |
if entities and food_compound_map and neuro_map:
|
|
@@ -519,7 +555,7 @@ def generate_detailed_analysis(query, match, entities=None, food_compound_map=No
|
|
| 519 |
Tarea: Escribe un análisis en Markdown estructurado:
|
| 520 |
1. Saludo breve y empático.
|
| 521 |
2. Frase inicial clara con la hipótesis probable.
|
| 522 |
-
3. Explicación de las moléculas responsables (ej: histamina,
|
| 523 |
4. Mecanismo fisiológico en términos sencillos.
|
| 524 |
5. Alimentos a evitar o moderar.
|
| 525 |
6. Alternativas o reemplazos sugeridos.
|
|
@@ -533,8 +569,8 @@ def generate_detailed_analysis(query, match, entities=None, food_compound_map=No
|
|
| 533 |
)
|
| 534 |
return response.text + TEXTO_BANDERAS_ROJAS
|
| 535 |
except Exception as e:
|
| 536 |
-
logger.
|
| 537 |
-
return
|
| 538 |
|
| 539 |
def generate_neuro_report_text(entities, food_map, neuro_map):
|
| 540 |
report_lines = ["\n### 🧠 Efectos Neuropsicológicos Posibles"]
|
|
@@ -624,7 +660,7 @@ with st.form(key="search_form", clear_on_submit=False):
|
|
| 624 |
query_input = st.text_area(
|
| 625 |
"Tu Caso:",
|
| 626 |
height=150,
|
| 627 |
-
placeholder="Ej:
|
| 628 |
key="input_query_text"
|
| 629 |
)
|
| 630 |
submitted = st.form_submit_button("Analizar Caso", type="primary")
|
|
@@ -634,7 +670,7 @@ if submitted:
|
|
| 634 |
current_query = query_input.strip()
|
| 635 |
st.session_state.user_query = current_query
|
| 636 |
st.session_state.has_searched = True
|
| 637 |
-
st.session_state.analysis_cache = {}
|
| 638 |
st.session_state.search_results = None
|
| 639 |
st.session_state.entities = None
|
| 640 |
|
|
@@ -645,7 +681,7 @@ if submitted:
|
|
| 645 |
final_entities = {"alimentos": reinforced.get("alimentos", []), "sintomas": final_symptoms}
|
| 646 |
st.session_state.entities = final_entities
|
| 647 |
|
| 648 |
-
results = find_best_matches_hybrid(final_entities, alimentos_data, FOOD_TO_COMPOUND_MAP)
|
| 649 |
|
| 650 |
if not results:
|
| 651 |
local_hypothesis = build_local_hypothesis(final_entities, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP)
|
|
@@ -656,7 +692,7 @@ if submitted:
|
|
| 656 |
else:
|
| 657 |
st.warning("⚠️ Por favor describe tus síntomas o alimentos consumidos antes de analizar.")
|
| 658 |
|
| 659 |
-
# --- DISPLAY DE RESULTADOS
|
| 660 |
if st.session_state.has_searched:
|
| 661 |
results = st.session_state.search_results
|
| 662 |
|
|
@@ -664,7 +700,6 @@ if st.session_state.has_searched:
|
|
| 664 |
best_match = results[0]['entry']
|
| 665 |
st.success(f"🔎 Coincidencia Principal: **{best_match.get('condicion_asociada')}**")
|
| 666 |
|
| 667 |
-
# Clave de caché única por consulta y condición
|
| 668 |
cache_key = f"analysis_{hash(st.session_state.user_query)}_{best_match.get('condicion_asociada')}"
|
| 669 |
if cache_key not in st.session_state.analysis_cache:
|
| 670 |
with st.spinner("✍️ Redactando informe..."):
|
|
|
|
| 1 |
+
# El Detective de Alimentos (Versión Actualizada a google-genai V3.0 + Modo Local Automático
|
| 2 |
+
# by Jairo Alexander & Diana Milena Soler Martinez desarrolladores)
|
| 3 |
import streamlit as st
|
| 4 |
+
from google import genai
|
| 5 |
+
from google.genai import types
|
| 6 |
import os
|
| 7 |
import json
|
| 8 |
import logging
|
|
|
|
| 44 |
APIkeydetective = os.getenv("APIkeydetective")
|
| 45 |
|
| 46 |
if not APIkeydetective:
|
| 47 |
+
logger.warning("No se encontró la APIkeydetective. Se usará el modo local automático.")
|
| 48 |
return None
|
| 49 |
|
| 50 |
return genai.Client(
|
| 51 |
api_key=APIkeydetective,
|
| 52 |
+
http_options=types.HttpOptions(timeout=10000) # Timeout rápido (10 segundos)
|
| 53 |
)
|
| 54 |
except Exception as e:
|
| 55 |
+
logger.warning(f"Error al configurar Gemini API: {e}. Se usará el modo local.")
|
| 56 |
return None
|
| 57 |
|
| 58 |
client = get_gemini_client()
|
|
|
|
|
|
|
| 59 |
|
| 60 |
# --- CARGA ULTRA-RÁPIDA DE DATOS ---
|
| 61 |
@st.cache_resource
|
|
|
|
| 95 |
"pan": ["gluten", "fodmaps"],
|
| 96 |
"trigo": ["gluten", "fodmaps"],
|
| 97 |
"vino": ["histamina", "tiramina", "sulfitos"],
|
| 98 |
+
"vino tinto": ["histamina", "tiramina", "sulfitos", "taninos"],
|
| 99 |
"cerveza": ["gluten", "histamina", "sulfitos"],
|
| 100 |
"chocolate": ["tiramina", "feniletilamina", "histamina"],
|
| 101 |
"tomate": ["histamina", "solanina"],
|
|
|
|
| 111 |
"cholado": ["fructosa", "lactosa", "histamina", "azúcar refinado"]
|
| 112 |
}
|
| 113 |
|
| 114 |
+
# --- SEGURO LÓGICO DE INCOMPATIBILIDAD BIOQUÍMICA Y FÁRMACOS ---
|
| 115 |
STRICT_COMPOUND_TRIGGERS = {
|
| 116 |
"cafeína": ["cafeína", "caffeine", "café"],
|
| 117 |
"gluten": ["gluten", "trigo", "cebada", "centeno"],
|
|
|
|
| 122 |
"solanina": ["solanina", "solanine"]
|
| 123 |
}
|
| 124 |
|
| 125 |
+
DRUG_KEYWORDS = ["fármaco", "farmaco", "medicamento", "imao", "remedio", "pastilla", "tratamiento", "antidepresiv"]
|
| 126 |
+
|
| 127 |
+
def validar_seguro_logico(entry, user_foods, user_query, food_compound_map):
|
| 128 |
if not user_foods:
|
| 129 |
return True
|
| 130 |
|
| 131 |
+
user_query_clean = user_query.lower() if user_query else ""
|
| 132 |
+
texto_condicion = (entry.get("condicion_asociada", "") + " " + entry.get("compuesto_alimento", "")).lower()
|
| 133 |
+
|
| 134 |
+
# REGLA 1: Descartar interacciones de medicamentos/IMAO si el usuario no los mencionó
|
| 135 |
+
if any(k in texto_condicion for k in ["fármaco", "farmaco", "imao", "interacción fármaco"]):
|
| 136 |
+
if not any(dk in user_query_clean for dk in DRUG_KEYWORDS):
|
| 137 |
+
logger.info(f"🚩 SEGURO LÓGICO ACTIVADO: Se descartó interacción fármaco/IMAO porque el usuario no mencionó medicamentos.")
|
| 138 |
+
return False
|
| 139 |
+
|
| 140 |
+
# REGLA 2: Validar compuestos estrictos
|
| 141 |
user_compounds = set()
|
| 142 |
for food in user_foods:
|
| 143 |
food_clean = food.lower().strip()
|
| 144 |
user_compounds.update(food_compound_map.get(food_clean, []))
|
| 145 |
user_compounds.add(food_clean)
|
| 146 |
|
|
|
|
|
|
|
| 147 |
for compound_key, synonyms in STRICT_COMPOUND_TRIGGERS.items():
|
| 148 |
if any(syn in texto_condicion for syn in synonyms):
|
| 149 |
tiene_compuesto = any(
|
|
|
|
| 182 |
"keywords_en": ["bloating", "distension", "flatulence", "swelling"]
|
| 183 |
},
|
| 184 |
"dolor de cabeza": {
|
| 185 |
+
"frases_es": ["dolor de cabeza", "migraña", "cefalea", "jaqueca", "me duele la cabeza"],
|
| 186 |
"keywords_en": ["headache", "migraine"]
|
| 187 |
},
|
| 188 |
"ansiedad": {
|
|
|
|
| 207 |
"yogur": ["yogurt", "yoghurt"],
|
| 208 |
"mantequilla": ["butter"],
|
| 209 |
"vino": ["wine"],
|
| 210 |
+
"vino tinto": ["wine", "red wine"],
|
| 211 |
"cerveza": ["beer"],
|
| 212 |
"chocolate": ["chocolate", "cocoa"],
|
| 213 |
"tomate": ["tomato"],
|
|
|
|
| 240 |
"fuentes_comunes": ["trigo", "cebolla", "ajo", "legumbres", "manzanas", "miel"]
|
| 241 |
},
|
| 242 |
"tiramina": {
|
| 243 |
+
"efecto_neuropsicologico": "Amina vasoactiva de acción simpaticomimética indirecta. Provoca vasoconstricción inicial seguida de vasodilatación cerebral, siendo uno de los principales desencadenantes de cefaleas y migrañas pulsátiles.",
|
| 244 |
"fuentes_comunes": ["quesos madurados", "vino tinto", "embutidos", "alimentos fermentados"]
|
| 245 |
},
|
| 246 |
"histamina": {
|
| 247 |
+
"efecto_neuropsicologico": "Neurotransmisor estimulante central. Su exceso por bajo DAO o consumo elevado provoca migrañas, insomnio, fragmentación del sueño, ansiedad y vasodilatación cerebral.",
|
| 248 |
+
"fuentes_comunes": ["queso", "vino tinto", "vino", "tomate", "pescado azul", "jugo de naranja"]
|
| 249 |
+
},
|
| 250 |
+
"sulfitos": {
|
| 251 |
+
"efecto_neuropsicologico": "Conservantes orgánicos que inducen estrés oxidativo, vasodilatación periférica y liberación de histamina, manifestándose frecuentemente como dolores de cabeza e pesadez cerebral.",
|
| 252 |
+
"fuentes_comunes": ["vino tinto", "vino", "cerveza", "frutos secos desecados"]
|
| 253 |
},
|
| 254 |
"cafeína": {
|
| 255 |
"efecto_neuropsicologico": "Bloquea los receptores de adenosina en el cerebro. En dosis altas o personas sensibles provoca hiperactivación del SNC, ansiedad e insomnio.",
|
|
|
|
| 266 |
"fructosa": {
|
| 267 |
"efecto_neuropsicologico": "La sobrecarga o malabsorción de fructosa disminuye la biodisponibilidad de triptófano en el intestino, interfiriendo en la síntesis de serotonina y melatonina (desregulando el sueño).",
|
| 268 |
"fuentes_comunes": ["jugo de naranja", "frutas muy dulces", "miel", "jarabe de maíz"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
}
|
| 270 |
}
|
| 271 |
|
| 272 |
+
# --- GENERADOR DE INFORME LOCAL AUTOMÁTICO (SIN IA) ---
|
| 273 |
+
def generate_local_fallback_analysis(query, match, entities, food_compound_map, neuro_map):
|
| 274 |
+
saludo = "### 📋 Informe de Análisis Clínico-Nutricional (Modo Local / Alta Disponibilidad)\n"
|
| 275 |
+
hipotesis = f"**Hipótesis Probable:** Es muy probable que tus síntomas estén relacionados con **{match.get('condicion_asociada', 'una sensibilidad a compuestos alimentarios')}**.\n"
|
| 276 |
+
|
| 277 |
+
user_foods = entities.get("alimentos", [])
|
| 278 |
+
user_symptoms = entities.get("sintomas", [])
|
| 279 |
+
|
| 280 |
+
compuestos_detectados = set()
|
| 281 |
+
for food in user_foods:
|
| 282 |
+
comps = food_compound_map.get(food.lower().strip(), [])
|
| 283 |
+
if comps:
|
| 284 |
+
compuestos_detectados.update(comps)
|
| 285 |
+
|
| 286 |
+
compuestos_str = ", ".join(sorted(list(compuestos_detectados))) if compuestos_detectados else match.get('compuesto_alimento', 'compuestos bioactivos')
|
| 287 |
+
|
| 288 |
+
causa = f"\n### 🧬 Causa e Identificación Molecular\nLos síntomas descritos (**{', '.join(user_symptoms)}**) tras el consumo de **{', '.join(user_foods)}** están asociados a la presencia de sustancias biológicamente activas como **{compuestos_str}**.\n"
|
| 289 |
+
|
| 290 |
+
detalles_compuestos = []
|
| 291 |
+
for comp in compuestos_detectados:
|
| 292 |
+
if comp in neuro_map:
|
| 293 |
+
detalles_compuestos.append(f"- **{comp.capitalize()}**: {neuro_map[comp]['efecto_neuropsicologico']}")
|
| 294 |
+
|
| 295 |
+
if detalles_compuestos:
|
| 296 |
+
causa += "\n**Mecanismo bioquímico e impacto neurológico:**\n" + "\n".join(detalles_compuestos) + "\n"
|
| 297 |
+
elif match.get('mecanismo_fisiologico'):
|
| 298 |
+
causa += f"\n**Mecanismo fisiológico:** {match.get('mecanismo_fisiologico')}\n"
|
| 299 |
+
|
| 300 |
+
evitar = f"\n### 🛑 Alimentos a Moderar o Evitar\n- **Alimentos evaluados:** {', '.join(user_foods).capitalize()}\n- Alimentos ricos en **{compuestos_str}** o fermentados que contengan aminas vasoactivas similares.\n"
|
| 301 |
+
|
| 302 |
+
alternativas = "\n### 💡 Sugerencias y Alternativas\n- Llevar un diario de síntomas registrando alimentos, hora de consumo y aparición de molestias.\n- Reducir temporalmente la ingesta del alimento sospechoso para verificar la remisión del síntoma.\n- Consultar con un nutricionista funcional o médico especialista si las molestias persisten.\n"
|
| 303 |
+
|
| 304 |
+
consejo = "\n### 📌 Consejo Práctico Final\nCada organismo posee una capacidad individual diferente para metabolizar aminas e histamina según la actividad de enzimas como la DAO (Diamino Oxidasa) o la MAO-MTHFR.\n"
|
| 305 |
+
|
| 306 |
+
return saludo + hipotesis + causa + evitar + alternativas + consejo + TEXTO_BANDERAS_ROJAS
|
| 307 |
+
|
| 308 |
# --- FUNCIONES DE APOYO Y EXTRACCIÓN ---
|
| 309 |
@st.cache_data(show_spinner=False)
|
| 310 |
def generate_word_report_cached(report_text):
|
|
|
|
| 349 |
words = re.findall(r'\b\w+\b', sanitize_text(text))
|
| 350 |
return set(w for w in words if w not in STOPWORDS_ES and len(w) > 2)
|
| 351 |
|
|
|
|
| 352 |
def extract_entities_with_gemini(query):
|
| 353 |
gemini_client = get_gemini_client()
|
| 354 |
if not gemini_client: return {"alimentos": [], "sintomas": []}
|
|
|
|
| 356 |
system_prompt = f"""
|
| 357 |
Rol: Extractor Médico JSON experto en nutrición y síntomas.
|
| 358 |
Tarea: Analiza el texto del usuario y extrae un JSON estricto con dos claves:
|
| 359 |
+
- "alimentos": lista de alimentos o bebidas mencionados en español, forma simple (ej: "vino tinto", "queso", "jugo de naranja").
|
| 360 |
+
- "sintomas": lista de síntomas mencionados en español, forma clínica simple (ej: "dolor de cabeza", "insomnio", "hinchazón").
|
| 361 |
|
| 362 |
Input: "{query}"
|
| 363 |
Output (JSON estricto):
|
|
|
|
| 372 |
elif "```" in text: text = text.split("```")[1].split("```")[0]
|
| 373 |
return json.loads(text)
|
| 374 |
except Exception as e:
|
| 375 |
+
logger.warning(f"Extracción por IA no disponible ({e}). Se usa extracción por diccionario.")
|
| 376 |
return {"alimentos": [], "sintomas": []}
|
| 377 |
|
| 378 |
def reinforce_entities_with_keywords(entities, query, food_map, master_symptom_map):
|
|
|
|
| 409 |
translated_symptoms.add(symptom)
|
| 410 |
return list(translated_symptoms)
|
| 411 |
|
| 412 |
+
def find_best_matches_hybrid(entities, data, user_query="", food_compound_map=None):
|
| 413 |
if not entities or not data: return []
|
| 414 |
user_symptoms_list = entities.get("sintomas", [])
|
| 415 |
user_foods_list = entities.get("alimentos", [])
|
|
|
|
| 427 |
results = []
|
| 428 |
for entry in data:
|
| 429 |
# SEGURO LÓGICO
|
| 430 |
+
if food_compound_map and not validar_seguro_logico(entry, user_foods_list, user_query, food_compound_map):
|
| 431 |
continue
|
| 432 |
|
| 433 |
db_food_keywords = extract_keywords(entry.get("compuesto_alimento", ""))
|
|
|
|
| 478 |
alimentos_str = ", ".join(alimentos_unicos)
|
| 479 |
|
| 480 |
if es_dolor_cabeza:
|
| 481 |
+
condicion = f"Cefalea o migraña por aminas vasoactivas / compuestos ({compuestos_str}) en {alimentos_str}"
|
| 482 |
elif es_insomnio:
|
| 483 |
condicion = f"Alteración del sueño / insomnio por respuesta a {compuestos_str} presente en {alimentos_str}"
|
| 484 |
else:
|
|
|
|
| 526 |
|
| 527 |
return unique[:limit]
|
| 528 |
|
|
|
|
| 529 |
def generate_detailed_analysis(query, match, entities=None, food_compound_map=None, neuro_map=None):
|
| 530 |
gemini_client = get_gemini_client()
|
| 531 |
+
|
| 532 |
+
# MODO LOCAL AUTOMÁTICO SI LA API NO ESTÁ DISPONIBLE
|
| 533 |
+
if not gemini_client:
|
| 534 |
+
logger.info("Usando informe local por falta de cliente Gemini API.")
|
| 535 |
+
return generate_local_fallback_analysis(query, match, entities, food_compound_map, neuro_map)
|
| 536 |
|
| 537 |
compound_context = ""
|
| 538 |
if entities and food_compound_map and neuro_map:
|
|
|
|
| 555 |
Tarea: Escribe un análisis en Markdown estructurado:
|
| 556 |
1. Saludo breve y empático.
|
| 557 |
2. Frase inicial clara con la hipótesis probable.
|
| 558 |
+
3. Explicación de las moléculas responsables (ej: histamina, tiramina, sulfitos, fructosa, gluten).
|
| 559 |
4. Mecanismo fisiológico en términos sencillos.
|
| 560 |
5. Alimentos a evitar o moderar.
|
| 561 |
6. Alternativas o reemplazos sugeridos.
|
|
|
|
| 569 |
)
|
| 570 |
return response.text + TEXTO_BANDERAS_ROJAS
|
| 571 |
except Exception as e:
|
| 572 |
+
logger.warning(f"Error o timeout en Gemini API ({e}). Se activa la respuesta automática en modo local.")
|
| 573 |
+
return generate_local_fallback_analysis(query, match, entities, food_compound_map, neuro_map)
|
| 574 |
|
| 575 |
def generate_neuro_report_text(entities, food_map, neuro_map):
|
| 576 |
report_lines = ["\n### 🧠 Efectos Neuropsicológicos Posibles"]
|
|
|
|
| 660 |
query_input = st.text_area(
|
| 661 |
"Tu Caso:",
|
| 662 |
height=150,
|
| 663 |
+
placeholder="Ej: Si tomo mucho vino tinto me duele la cabeza.",
|
| 664 |
key="input_query_text"
|
| 665 |
)
|
| 666 |
submitted = st.form_submit_button("Analizar Caso", type="primary")
|
|
|
|
| 670 |
current_query = query_input.strip()
|
| 671 |
st.session_state.user_query = current_query
|
| 672 |
st.session_state.has_searched = True
|
| 673 |
+
st.session_state.analysis_cache = {}
|
| 674 |
st.session_state.search_results = None
|
| 675 |
st.session_state.entities = None
|
| 676 |
|
|
|
|
| 681 |
final_entities = {"alimentos": reinforced.get("alimentos", []), "sintomas": final_symptoms}
|
| 682 |
st.session_state.entities = final_entities
|
| 683 |
|
| 684 |
+
results = find_best_matches_hybrid(final_entities, alimentos_data, current_query, FOOD_TO_COMPOUND_MAP)
|
| 685 |
|
| 686 |
if not results:
|
| 687 |
local_hypothesis = build_local_hypothesis(final_entities, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP)
|
|
|
|
| 692 |
else:
|
| 693 |
st.warning("⚠️ Por favor describe tus síntomas o alimentos consumidos antes de analizar.")
|
| 694 |
|
| 695 |
+
# --- DISPLAY DE RESULTADOS REUTILIZABLE ---
|
| 696 |
if st.session_state.has_searched:
|
| 697 |
results = st.session_state.search_results
|
| 698 |
|
|
|
|
| 700 |
best_match = results[0]['entry']
|
| 701 |
st.success(f"🔎 Coincidencia Principal: **{best_match.get('condicion_asociada')}**")
|
| 702 |
|
|
|
|
| 703 |
cache_key = f"analysis_{hash(st.session_state.user_query)}_{best_match.get('condicion_asociada')}"
|
| 704 |
if cache_key not in st.session_state.analysis_cache:
|
| 705 |
with st.spinner("✍️ Redactando informe..."):
|