Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -34,30 +34,32 @@ Este análisis es una herramienta informativa de IA y **NO es un diagnóstico m
|
|
| 34 |
**Si tus síntomas son severos, busca ayuda médica urgente.**
|
| 35 |
"""
|
| 36 |
|
| 37 |
-
# --- CONFIGURACIÓN DE LA API GEMINI ---
|
| 38 |
-
try:
|
| 39 |
-
if 'APIkeydetective' in st.secrets:
|
| 40 |
-
APIkeydetective = st.secrets['APIkeydetective']
|
| 41 |
-
else:
|
| 42 |
-
APIkeydetective = os.getenv("APIkeydetective")
|
| 43 |
-
if not APIkeydetective:
|
| 44 |
-
st.error("No se encontró la APIkeydetective.")
|
| 45 |
-
st.stop()
|
| 46 |
-
|
| 47 |
-
client = genai.Client(
|
| 48 |
-
api_key=APIkeydetective,
|
| 49 |
-
http_options=types.HttpOptions(timeout=30000)
|
| 50 |
-
)
|
| 51 |
-
|
| 52 |
-
except Exception as e:
|
| 53 |
-
st.error(f"❌ Error al configurar Gemini API: {e}")
|
| 54 |
-
st.stop()
|
| 55 |
-
|
| 56 |
@st.cache_resource
|
| 57 |
def get_gemini_client():
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
@st.cache_resource
|
| 62 |
def load_data():
|
| 63 |
try:
|
|
@@ -110,7 +112,7 @@ FOOD_TO_COMPOUND_MAP = {
|
|
| 110 |
"cholado": ["fructosa", "lactosa", "histamina", "azúcar refinado"]
|
| 111 |
}
|
| 112 |
|
| 113 |
-
# --- SEGURO LÓGICO
|
| 114 |
STRICT_COMPOUND_TRIGGERS = {
|
| 115 |
"cafeína": ["cafeína", "caffeine", "café"],
|
| 116 |
"gluten": ["gluten", "trigo", "cebada", "centeno"],
|
|
@@ -122,12 +124,6 @@ STRICT_COMPOUND_TRIGGERS = {
|
|
| 122 |
}
|
| 123 |
|
| 124 |
def validar_seguro_logico(entry, user_foods, food_compound_map):
|
| 125 |
-
"""
|
| 126 |
-
SEGURO LÓGICO: Si una condición atribuida en la BD o hipótesis requiere un
|
| 127 |
-
compuesto exclusivo (ej: cafeína, gluten, lactosa, tiramina), verifica que AL MENOS
|
| 128 |
-
UNO de los alimentos ingresados por el usuario contenga efectivamente dicho compuesto.
|
| 129 |
-
Evita falsos positivos como asociar cafeína al jugo de naranja.
|
| 130 |
-
"""
|
| 131 |
if not user_foods:
|
| 132 |
return True
|
| 133 |
|
|
@@ -275,7 +271,7 @@ INTEGRATED_NEURO_FOOD_MAP = {
|
|
| 275 |
}
|
| 276 |
}
|
| 277 |
|
| 278 |
-
# --- FUNCIONES DE
|
| 279 |
@st.cache_data(show_spinner=False)
|
| 280 |
def generate_word_report_cached(report_text):
|
| 281 |
try:
|
|
@@ -321,7 +317,9 @@ def extract_keywords(text):
|
|
| 321 |
|
| 322 |
@retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
|
| 323 |
def extract_entities_with_gemini(query):
|
| 324 |
-
|
|
|
|
|
|
|
| 325 |
system_prompt = f"""
|
| 326 |
Rol: Extractor Médico JSON experto en nutrición y síntomas.
|
| 327 |
Tarea: Analiza el texto del usuario y extrae un JSON estricto con dos claves:
|
|
@@ -332,7 +330,7 @@ def extract_entities_with_gemini(query):
|
|
| 332 |
Output (JSON estricto):
|
| 333 |
"""
|
| 334 |
try:
|
| 335 |
-
response =
|
| 336 |
model='gemini-2.5-flash-lite',
|
| 337 |
contents=system_prompt
|
| 338 |
)
|
|
@@ -395,7 +393,7 @@ def find_best_matches_hybrid(entities, data, food_compound_map=None):
|
|
| 395 |
|
| 396 |
results = []
|
| 397 |
for entry in data:
|
| 398 |
-
#
|
| 399 |
if food_compound_map and not validar_seguro_logico(entry, user_foods_list, food_compound_map):
|
| 400 |
continue
|
| 401 |
|
|
@@ -497,7 +495,8 @@ def find_best_foodb_matches(user_foods_es, _foodb_index, _foodb_lower_map, food_
|
|
| 497 |
|
| 498 |
@retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
|
| 499 |
def generate_detailed_analysis(query, match, entities=None, food_compound_map=None, neuro_map=None):
|
| 500 |
-
|
|
|
|
| 501 |
|
| 502 |
compound_context = ""
|
| 503 |
if entities and food_compound_map and neuro_map:
|
|
@@ -528,7 +527,7 @@ def generate_detailed_analysis(query, match, entities=None, food_compound_map=No
|
|
| 528 |
NO incluyas descargos de responsabilidad.
|
| 529 |
"""
|
| 530 |
try:
|
| 531 |
-
response =
|
| 532 |
model='gemini-2.5-flash-lite',
|
| 533 |
contents=prompt
|
| 534 |
)
|
|
@@ -604,7 +603,14 @@ def create_relevance_chart(results):
|
|
| 604 |
x='Relevancia', y=alt.Y('Condición', sort='-x'), tooltip=['Condición', 'Relevancia']
|
| 605 |
).properties(title='Top Coincidencias')
|
| 606 |
|
| 607 |
-
# ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 608 |
col_img1, col_text, col_img2 = st.columns([1, 4, 1])
|
| 609 |
with col_img1:
|
| 610 |
if os.path.exists("imagen.png"): st.image("imagen.png", width=150)
|
|
@@ -614,77 +620,86 @@ with col_text:
|
|
| 614 |
with col_img2:
|
| 615 |
if os.path.exists("buho.png"): st.image("buho.png", width=120)
|
| 616 |
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
submitted = st.form_submit_button("Analizar Caso", type="primary")
|
| 625 |
|
| 626 |
-
if submitted
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
final_symptoms = translate_symptoms_local(reinforced.get("sintomas", []), MASTER_SYMPTOM_MAP)
|
| 635 |
-
final_entities = {"alimentos": reinforced.get("alimentos", []), "sintomas": final_symptoms}
|
| 636 |
-
st.session_state.entities = final_entities
|
| 637 |
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 646 |
|
| 647 |
-
|
|
|
|
| 648 |
results = st.session_state.search_results
|
| 649 |
-
best_match = results[0]['entry']
|
| 650 |
-
|
| 651 |
-
st.success(f"🔎 Coincidencia Principal: **{best_match.get('condicion_asociada')}**")
|
| 652 |
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
)
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
with tab_main:
|
| 665 |
-
st.markdown(st.session_state.analysis_cache[cache_key])
|
| 666 |
-
if len(results) > 1:
|
| 667 |
-
st.altair_chart(create_relevance_chart(results), use_container_width=True)
|
| 668 |
-
|
| 669 |
-
with tab_neuro:
|
| 670 |
-
st.markdown(generate_neuro_report_text(st.session_state.entities, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP))
|
| 671 |
-
|
| 672 |
-
with tab_mol:
|
| 673 |
-
st.markdown(generate_molecular_report_text(
|
| 674 |
-
best_match, st.session_state.entities, foodb_index, foodb_lower_map,
|
| 675 |
-
FOOD_NAME_TO_FOODB_KEY, COMPOUND_SYNONYM_MAP, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP
|
| 676 |
-
))
|
| 677 |
-
|
| 678 |
-
full_report = f"REPORTE\n\n{st.session_state.analysis_cache[cache_key]}"
|
| 679 |
-
word_bytes = generate_word_report_cached(full_report)
|
| 680 |
-
if word_bytes:
|
| 681 |
-
st.download_button(
|
| 682 |
-
label="📄 Descargar Informe",
|
| 683 |
-
data=word_bytes,
|
| 684 |
-
file_name="Reporte.docx",
|
| 685 |
-
mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
| 686 |
-
)
|
| 687 |
-
|
| 688 |
-
elif submitted:
|
| 689 |
-
st.warning("⚠️ No pudimos identificar una causa clara.")
|
| 690 |
-
st.info("Intenta reformular: 'Siento [SÍNTOMA] cuando como [ALIMENTO]'.")
|
|
|
|
| 34 |
**Si tus síntomas son severos, busca ayuda médica urgente.**
|
| 35 |
"""
|
| 36 |
|
| 37 |
+
# --- CONFIGURACIÓN DE LA API GEMINI (google-genai V3.0) ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
@st.cache_resource
|
| 39 |
def get_gemini_client():
|
| 40 |
+
try:
|
| 41 |
+
if 'APIkeydetective' in st.secrets:
|
| 42 |
+
APIkeydetective = st.secrets['APIkeydetective']
|
| 43 |
+
else:
|
| 44 |
+
APIkeydetective = os.getenv("APIkeydetective")
|
| 45 |
+
|
| 46 |
+
if not APIkeydetective:
|
| 47 |
+
st.error("No se encontró la APIkeydetective.")
|
| 48 |
+
return None
|
| 49 |
+
|
| 50 |
+
return genai.Client(
|
| 51 |
+
api_key=APIkeydetective,
|
| 52 |
+
http_options=types.HttpOptions(timeout=30000) # 30 segundos
|
| 53 |
+
)
|
| 54 |
+
except Exception as e:
|
| 55 |
+
st.error(f"❌ Error al configurar Gemini API: {e}")
|
| 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
|
| 64 |
def load_data():
|
| 65 |
try:
|
|
|
|
| 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"],
|
|
|
|
| 124 |
}
|
| 125 |
|
| 126 |
def validar_seguro_logico(entry, user_foods, food_compound_map):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
if not user_foods:
|
| 128 |
return True
|
| 129 |
|
|
|
|
| 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):
|
| 277 |
try:
|
|
|
|
| 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": []}
|
| 322 |
+
|
| 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:
|
|
|
|
| 330 |
Output (JSON estricto):
|
| 331 |
"""
|
| 332 |
try:
|
| 333 |
+
response = gemini_client.models.generate_content(
|
| 334 |
model='gemini-2.5-flash-lite',
|
| 335 |
contents=system_prompt
|
| 336 |
)
|
|
|
|
| 393 |
|
| 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 |
|
|
|
|
| 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 |
+
if not gemini_client: return "Error: IA no disponible."
|
| 500 |
|
| 501 |
compound_context = ""
|
| 502 |
if entities and food_compound_map and neuro_map:
|
|
|
|
| 527 |
NO incluyas descargos de responsabilidad.
|
| 528 |
"""
|
| 529 |
try:
|
| 530 |
+
response = gemini_client.models.generate_content(
|
| 531 |
model='gemini-2.5-flash-lite',
|
| 532 |
contents=prompt
|
| 533 |
)
|
|
|
|
| 603 |
x='Relevancia', y=alt.Y('Condición', sort='-x'), tooltip=['Condición', 'Relevancia']
|
| 604 |
).properties(title='Top Coincidencias')
|
| 605 |
|
| 606 |
+
# --- INICIALIZACIÓN DE ESTADO PERSISTENTE ---
|
| 607 |
+
if 'search_results' not in st.session_state: st.session_state.search_results = None
|
| 608 |
+
if 'user_query' not in st.session_state: st.session_state.user_query = ""
|
| 609 |
+
if 'entities' not in st.session_state: st.session_state.entities = None
|
| 610 |
+
if 'analysis_cache' not in st.session_state: st.session_state.analysis_cache = {}
|
| 611 |
+
if 'has_searched' not in st.session_state: st.session_state.has_searched = False
|
| 612 |
+
|
| 613 |
+
# --- INTERFAZ GRAFICA STREAMLIT ---
|
| 614 |
col_img1, col_text, col_img2 = st.columns([1, 4, 1])
|
| 615 |
with col_img1:
|
| 616 |
if os.path.exists("imagen.png"): st.image("imagen.png", width=150)
|
|
|
|
| 620 |
with col_img2:
|
| 621 |
if os.path.exists("buho.png"): st.image("buho.png", width=120)
|
| 622 |
|
| 623 |
+
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: Cuando tomo mucho jugo de naranja no puedo dormir.",
|
| 628 |
+
key="input_query_text"
|
| 629 |
+
)
|
| 630 |
submitted = st.form_submit_button("Analizar Caso", type="primary")
|
| 631 |
|
| 632 |
+
if submitted:
|
| 633 |
+
if query_input and query_input.strip():
|
| 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 = {} # Limpiar análisis previo
|
| 638 |
+
st.session_state.search_results = None
|
| 639 |
+
st.session_state.entities = None
|
|
|
|
|
|
|
|
|
|
| 640 |
|
| 641 |
+
with st.spinner("🔍 Analizando pistas..."):
|
| 642 |
+
raw_entities = extract_entities_with_gemini(current_query)
|
| 643 |
+
reinforced = reinforce_entities_with_keywords(raw_entities, current_query, FOOD_TO_COMPOUND_MAP, MASTER_SYMPTOM_MAP)
|
| 644 |
+
final_symptoms = translate_symptoms_local(reinforced.get("sintomas", []), MASTER_SYMPTOM_MAP)
|
| 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)
|
| 652 |
+
if local_hypothesis:
|
| 653 |
+
results = [{'entry': local_hypothesis, 'score': {'total': 50}, 'matched_keywords': []}]
|
| 654 |
+
|
| 655 |
+
st.session_state.search_results = results if results else []
|
| 656 |
+
else:
|
| 657 |
+
st.warning("⚠️ Por favor describe tus síntomas o alimentos consumidos antes de analizar.")
|
| 658 |
|
| 659 |
+
# --- DISPLAY DE RESULTADOS INDEPENDIENTE Y REUTILIZABLE ---
|
| 660 |
+
if st.session_state.has_searched:
|
| 661 |
results = st.session_state.search_results
|
|
|
|
|
|
|
|
|
|
| 662 |
|
| 663 |
+
if results and len(results) > 0:
|
| 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..."):
|
| 671 |
+
analysis = generate_detailed_analysis(
|
| 672 |
+
st.session_state.user_query, best_match,
|
| 673 |
+
st.session_state.entities, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP
|
| 674 |
+
)
|
| 675 |
+
st.session_state.analysis_cache[cache_key] = analysis
|
| 676 |
+
|
| 677 |
+
tab_main, tab_neuro, tab_mol = st.tabs(["💡 Interpretación Clínica", "🧠 Efectos Neuropsicológicos", "🔬 Análisis Molecular"])
|
| 678 |
+
|
| 679 |
+
with tab_main:
|
| 680 |
+
st.markdown(st.session_state.analysis_cache[cache_key])
|
| 681 |
+
if len(results) > 1:
|
| 682 |
+
st.altair_chart(create_relevance_chart(results), use_container_width=True)
|
| 683 |
+
|
| 684 |
+
with tab_neuro:
|
| 685 |
+
st.markdown(generate_neuro_report_text(st.session_state.entities, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP))
|
| 686 |
+
|
| 687 |
+
with tab_mol:
|
| 688 |
+
st.markdown(generate_molecular_report_text(
|
| 689 |
+
best_match, st.session_state.entities, foodb_index, foodb_lower_map,
|
| 690 |
+
FOOD_NAME_TO_FOODB_KEY, COMPOUND_SYNONYM_MAP, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP
|
| 691 |
+
))
|
| 692 |
+
|
| 693 |
+
full_report = f"REPORTE\n\n{st.session_state.analysis_cache[cache_key]}"
|
| 694 |
+
word_bytes = generate_word_report_cached(full_report)
|
| 695 |
+
if word_bytes:
|
| 696 |
+
st.download_button(
|
| 697 |
+
label="📄 Descargar Informe",
|
| 698 |
+
data=word_bytes,
|
| 699 |
+
file_name="Reporte.docx",
|
| 700 |
+
mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
| 701 |
+
key=f"dl_btn_{hash(st.session_state.user_query)}"
|
| 702 |
)
|
| 703 |
+
else:
|
| 704 |
+
st.warning("⚠️ No pudimos identificar una causa clara para este caso específico.")
|
| 705 |
+
st.info("Intenta reformular especificando el alimento y el síntoma. Ej: 'Siento [SÍNTOMA] cuando como [ALIMENTO]'.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|