Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
-
# ==================== El Detective de Alimentos
|
| 2 |
-
#
|
| 3 |
-
# Por: JAIRO CESAR ALEXANDER E. MD DIANA MILENA SOLER MARTINEZ PSI. ESP. U JUAN N CORPAS
|
| 4 |
import streamlit as st
|
| 5 |
import google.generativeai as genai
|
| 6 |
import google.api_core.exceptions
|
|
@@ -14,7 +13,6 @@ from datetime import datetime
|
|
| 14 |
from tenacity import retry, stop_after_attempt, wait_random_exponential
|
| 15 |
from io import BytesIO
|
| 16 |
import docx
|
| 17 |
-
|
| 18 |
st.set_page_config(page_title="El Detective de Alimentos", page_icon="🍎", layout="wide")
|
| 19 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 20 |
logger = logging.getLogger("food_detective_app")
|
|
@@ -76,13 +74,11 @@ KNOWN_TRIGGERS_MAP = {
|
|
| 76 |
"Phenylethylamine": ["dolor de cabeza", "migraña"],
|
| 77 |
"Cadaverine": ["inflamación", "dolor de cabeza"],
|
| 78 |
"Putrescine": ["inflamación", "dolor de cabeza"],
|
| 79 |
-
|
| 80 |
# --- Compuestos de Gluten y Lácteos ---
|
| 81 |
"Gluten": ["niebla mental", "dolor abdominal", "diarrea", "inflamación", "dolor articular", "ataxia", "neuropatía periférica"],
|
| 82 |
"Gliadin": ["niebla mental", "dolor abdominal", "diarrea", "inflamación"], # Componente principal del Gluten
|
| 83 |
"Casein": ["niebla mental", "inflamación", "acné", "congestión nasal", "estreñimiento"],
|
| 84 |
"Lactose": ["hinchazón", "gases", "diarrea", "dolor abdominal"],
|
| 85 |
-
|
| 86 |
# --- Antinutrientes y Compuestos de Defensa Vegetal ---
|
| 87 |
"Oxalates": ["dolor articular", "dolor muscular", "cálculos renales"],
|
| 88 |
"Lectins": ["inflamación", "dolor articular", "hinchazón", "dolor abdominal", "erupción"],
|
|
@@ -92,7 +88,6 @@ KNOWN_TRIGGERS_MAP = {
|
|
| 92 |
"Solanine": ["dolor articular", "inflamación", "dolor muscular"],
|
| 93 |
"Avenin": ["inflamación", "dolor abdominal"], # En avena, similar al gluten
|
| 94 |
"Quercetin": ["dolor de cabeza", "migraña"], # Específicamente en el contexto del vino
|
| 95 |
-
|
| 96 |
# --- FODMAPs (Oligosacáridos, Disacáridos, Monosacáridos y Polioles Fermentables) ---
|
| 97 |
"Fructans": ["hinchazón", "gases", "dolor abdominal", "diarrea", "estreñimiento"],
|
| 98 |
"GOS (Galactooligosaccharides)": ["hinchazón", "gases", "dolor abdominal"],
|
|
@@ -100,14 +95,12 @@ KNOWN_TRIGGERS_MAP = {
|
|
| 100 |
"Polyols": ["diarrea", "hinchazón", "gases"], # Incluye Sorbitol, Manitol, Xilitol
|
| 101 |
"Sorbitol": ["diarrea", "hinchazón", "gases"],
|
| 102 |
"Mannitol": ["diarrea", "hinchazón", "gases"],
|
| 103 |
-
|
| 104 |
# --- Estimulantes, Alcaloides y Compuestos del Sistema Nervioso ---
|
| 105 |
"Caffeine": ["ansiedad", "dolor de cabeza", "insomnio", "palpitaciones", "acidez"],
|
| 106 |
"Theobromine": ["ansiedad", "dolor de cabeza", "insomnio"], # Similar a la cafeína, en chocolate
|
| 107 |
"Capsaicin": ["dolor", "acidez", "ardor"],
|
| 108 |
"Alcohol": ["dolor de cabeza", "niebla mental", "inflamación", "fatiga", "náuseas"],
|
| 109 |
"Acetaldehyde": ["dolor de cabeza", "náuseas", "enrojecimiento facial"], # Metabolito del alcohol
|
| 110 |
-
|
| 111 |
# --- Aditivos Alimentarios y Compuestos de Procesamiento ---
|
| 112 |
"Glutamate (MSG)": ["dolor de cabeza", "náuseas", "debilidad muscular", "palpitaciones"],
|
| 113 |
"Sulfites": ["dolor de cabeza", "sibilancias", "erupción", "congestión nasal"],
|
|
@@ -119,14 +112,12 @@ KNOWN_TRIGGERS_MAP = {
|
|
| 119 |
"Artificial colorings": ["erupción", "hiperactividad"], # Ej. Tartrazine
|
| 120 |
"Benzoates": ["erupción", "asma", "hiperactividad"],
|
| 121 |
"Acrylamide": ["neuropatía periférica", "debilidad muscular"], # Formado en frituras
|
| 122 |
-
|
| 123 |
# --- Toxinas Naturales y Compuestos Específicos ---
|
| 124 |
"Aflatoxins": ["fatiga", "náuseas", "dolor abdominal"], # De mohos en frutos secos/granos
|
| 125 |
"Cyanogenic glycosides": ["mareo/vértigo", "dolor de cabeza", "náuseas", "vómito"], # En almendras amargas, yuca
|
| 126 |
"Arsenic": ["fatiga", "náuseas", "neuropatía periférica"], # En arroz
|
| 127 |
"Mercury": ["fatiga", "niebla mental", "neuropatía periférica"], # En pescados grandes
|
| 128 |
"Alpha-Gal": ["erupción", "hinchazón", "náuseas"], # Alérgeno de carne roja
|
| 129 |
-
|
| 130 |
# --- Minerales y Elementos (en contexto de exceso o sensibilidad) ---
|
| 131 |
"Nickel": ["erupción", "dermatitis", "dolor abdominal", "hinchazón"],
|
| 132 |
"Iodine": ["acné", "erupción", "fatiga"], # En personas con sensibilidad o problemas tiroideos
|
|
@@ -619,25 +610,17 @@ def sanitize_text(text):
|
|
| 619 |
return re.sub(r'[.,;()]', '', text).lower().strip()
|
| 620 |
|
| 621 |
@retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(3))
|
|
|
|
| 622 |
def extract_entities_with_gemini(query):
|
| 623 |
-
|
| 624 |
-
logger.info("Intentando extracción de entidades con Gemini...")
|
| 625 |
-
system_prompt = f"""
|
| 626 |
-
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:
|
| 627 |
-
1. `alimentos`: Una lista exhaustiva de todos los alimentos, bebidas o ingredientes consumidos mencionados.
|
| 628 |
-
2. `sintomas`: Una lista exhaustiva de todos los síntomas, sensaciones o signos clínicos descritos.
|
| 629 |
-
Devuelve la respuesta ÚNICAMENTE en formato JSON estricto. No incluyas explicaciones ni texto adicional.
|
| 630 |
-
Consulta: "{query}"
|
| 631 |
-
"""
|
| 632 |
try:
|
| 633 |
response = model.generate_content(system_prompt)
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
json_text = re.search(r'\{.*\}', response.text, re.DOTALL).group(0)
|
| 639 |
logger.info("Extracción con Gemini exitosa.")
|
| 640 |
-
return
|
| 641 |
except (Exception, google.api_core.exceptions.GoogleAPICallError) as e:
|
| 642 |
logger.error(f"Error en la extracción con Gemini (puede ser reintentado): {e}")
|
| 643 |
raise e
|
|
@@ -677,78 +660,129 @@ def reinforce_entities_with_keywords(entities, query, food_map, master_symptom_m
|
|
| 677 |
entities["sintomas"] = list(set(current_symptoms))
|
| 678 |
return entities
|
| 679 |
|
|
|
|
| 680 |
def find_best_matches_hybrid(entities, data):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 681 |
if not entities or not data: return []
|
| 682 |
-
|
| 683 |
-
|
|
|
|
|
|
|
| 684 |
|
| 685 |
-
# --- LISTA DE CONDICIONES RARAS (Centralizada para fácil mantenimiento) ---
|
| 686 |
-
# Cualquier condición en esta lista recibirá una penalización en su puntuación.
|
| 687 |
-
# El resto se considerará de probabilidad normal.
|
| 688 |
RARE_CONDITIONS = [
|
| 689 |
-
"Porfiria Aguda Intermitente (PAI).",
|
| 690 |
-
"
|
| 691 |
-
"Ataxia por Gluten.",
|
| 692 |
-
"Encefalopatía por Gluten.",
|
| 693 |
-
"Enfermedad de Wilson.",
|
| 694 |
-
"Aciduria Argininosuccínica.",
|
| 695 |
-
"Síndrome de Alagille."
|
| 696 |
-
# Añade aquí otras enfermedades raras a medida que las incorpores a tu JSON.
|
| 697 |
]
|
| 698 |
|
| 699 |
-
candidate_terms = set(user_foods)
|
| 700 |
-
for food in user_foods:
|
| 701 |
-
if food in FOOD_TO_COMPOUND_MAP:
|
| 702 |
-
candidate_terms.update(c.lower() for c in FOOD_TO_COMPOUND_MAP[food])
|
| 703 |
-
|
| 704 |
results = []
|
| 705 |
for entry in data:
|
| 706 |
-
|
| 707 |
-
|
|
|
|
|
|
|
| 708 |
|
| 709 |
-
|
| 710 |
-
continue
|
| 711 |
-
|
| 712 |
-
score_details = {'food': 20, 'symptoms': 0}
|
| 713 |
|
| 714 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 715 |
symptom_score = 0
|
| 716 |
matched_symptoms = []
|
| 717 |
for user_symptom in user_symptoms:
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
matched_symptoms.append(key)
|
| 722 |
-
break
|
| 723 |
-
|
| 724 |
score_details['symptoms'] = symptom_score
|
| 725 |
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
score_details['total'] = int(final_score)
|
| 740 |
-
|
| 741 |
results.append({
|
| 742 |
'entry': entry,
|
| 743 |
'score': score_details,
|
| 744 |
-
'matched_symptoms':
|
| 745 |
})
|
| 746 |
|
| 747 |
if not results: return []
|
| 748 |
return sorted(results, key=lambda x: x['score']['total'], reverse=True)
|
| 749 |
|
| 750 |
-
|
| 751 |
@retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(3))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 752 |
|
| 753 |
|
| 754 |
def find_best_foodb_matches(user_foods_es, foodb_index_keys, food_name_map, limit=3):
|
|
@@ -1042,26 +1076,47 @@ if st.session_state.start_analysis:
|
|
| 1042 |
elif alimentos_data is None:
|
| 1043 |
st.error("La base de datos de alimentos no está disponible.")
|
| 1044 |
else:
|
| 1045 |
-
entities = None
|
| 1046 |
with st.spinner("🧠 Interpretando tu caso y buscando pistas..."):
|
|
|
|
|
|
|
| 1047 |
try:
|
| 1048 |
-
|
| 1049 |
except Exception as e:
|
| 1050 |
logger.warning(f"La extracción con Gemini falló; se usará el sistema de respaldo: {e}")
|
| 1051 |
|
| 1052 |
-
#
|
| 1053 |
-
|
| 1054 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1055 |
|
| 1056 |
-
if
|
| 1057 |
-
info_str = f"Pistas identificadas - Alimentos: {', '.join(
|
| 1058 |
st.info(info_str)
|
| 1059 |
with st.spinner("🔬 Cruzando información y calculando relevancia..."):
|
| 1060 |
-
results = find_best_matches_hybrid(
|
| 1061 |
st.session_state.search_results = results
|
| 1062 |
else:
|
| 1063 |
st.error("No se pudieron identificar alimentos o síntomas claros en tu descripción. Intenta ser más específico.")
|
| 1064 |
st.session_state.search_results = []
|
|
|
|
|
|
|
| 1065 |
if st.session_state.search_results is not None:
|
| 1066 |
results = st.session_state.search_results
|
| 1067 |
|
|
|
|
| 1 |
+
# ==================== El Detective de Alimentos
|
| 2 |
+
# JAIRO CESAR ALEXANDER E. MD DIANA MILENA SOLER MARTINEZ PSI. ESP. U JUAN N CORPAS
|
|
|
|
| 3 |
import streamlit as st
|
| 4 |
import google.generativeai as genai
|
| 5 |
import google.api_core.exceptions
|
|
|
|
| 13 |
from tenacity import retry, stop_after_attempt, wait_random_exponential
|
| 14 |
from io import BytesIO
|
| 15 |
import docx
|
|
|
|
| 16 |
st.set_page_config(page_title="El Detective de Alimentos", page_icon="🍎", layout="wide")
|
| 17 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 18 |
logger = logging.getLogger("food_detective_app")
|
|
|
|
| 74 |
"Phenylethylamine": ["dolor de cabeza", "migraña"],
|
| 75 |
"Cadaverine": ["inflamación", "dolor de cabeza"],
|
| 76 |
"Putrescine": ["inflamación", "dolor de cabeza"],
|
|
|
|
| 77 |
# --- Compuestos de Gluten y Lácteos ---
|
| 78 |
"Gluten": ["niebla mental", "dolor abdominal", "diarrea", "inflamación", "dolor articular", "ataxia", "neuropatía periférica"],
|
| 79 |
"Gliadin": ["niebla mental", "dolor abdominal", "diarrea", "inflamación"], # Componente principal del Gluten
|
| 80 |
"Casein": ["niebla mental", "inflamación", "acné", "congestión nasal", "estreñimiento"],
|
| 81 |
"Lactose": ["hinchazón", "gases", "diarrea", "dolor abdominal"],
|
|
|
|
| 82 |
# --- Antinutrientes y Compuestos de Defensa Vegetal ---
|
| 83 |
"Oxalates": ["dolor articular", "dolor muscular", "cálculos renales"],
|
| 84 |
"Lectins": ["inflamación", "dolor articular", "hinchazón", "dolor abdominal", "erupción"],
|
|
|
|
| 88 |
"Solanine": ["dolor articular", "inflamación", "dolor muscular"],
|
| 89 |
"Avenin": ["inflamación", "dolor abdominal"], # En avena, similar al gluten
|
| 90 |
"Quercetin": ["dolor de cabeza", "migraña"], # Específicamente en el contexto del vino
|
|
|
|
| 91 |
# --- FODMAPs (Oligosacáridos, Disacáridos, Monosacáridos y Polioles Fermentables) ---
|
| 92 |
"Fructans": ["hinchazón", "gases", "dolor abdominal", "diarrea", "estreñimiento"],
|
| 93 |
"GOS (Galactooligosaccharides)": ["hinchazón", "gases", "dolor abdominal"],
|
|
|
|
| 95 |
"Polyols": ["diarrea", "hinchazón", "gases"], # Incluye Sorbitol, Manitol, Xilitol
|
| 96 |
"Sorbitol": ["diarrea", "hinchazón", "gases"],
|
| 97 |
"Mannitol": ["diarrea", "hinchazón", "gases"],
|
|
|
|
| 98 |
# --- Estimulantes, Alcaloides y Compuestos del Sistema Nervioso ---
|
| 99 |
"Caffeine": ["ansiedad", "dolor de cabeza", "insomnio", "palpitaciones", "acidez"],
|
| 100 |
"Theobromine": ["ansiedad", "dolor de cabeza", "insomnio"], # Similar a la cafeína, en chocolate
|
| 101 |
"Capsaicin": ["dolor", "acidez", "ardor"],
|
| 102 |
"Alcohol": ["dolor de cabeza", "niebla mental", "inflamación", "fatiga", "náuseas"],
|
| 103 |
"Acetaldehyde": ["dolor de cabeza", "náuseas", "enrojecimiento facial"], # Metabolito del alcohol
|
|
|
|
| 104 |
# --- Aditivos Alimentarios y Compuestos de Procesamiento ---
|
| 105 |
"Glutamate (MSG)": ["dolor de cabeza", "náuseas", "debilidad muscular", "palpitaciones"],
|
| 106 |
"Sulfites": ["dolor de cabeza", "sibilancias", "erupción", "congestión nasal"],
|
|
|
|
| 112 |
"Artificial colorings": ["erupción", "hiperactividad"], # Ej. Tartrazine
|
| 113 |
"Benzoates": ["erupción", "asma", "hiperactividad"],
|
| 114 |
"Acrylamide": ["neuropatía periférica", "debilidad muscular"], # Formado en frituras
|
|
|
|
| 115 |
# --- Toxinas Naturales y Compuestos Específicos ---
|
| 116 |
"Aflatoxins": ["fatiga", "náuseas", "dolor abdominal"], # De mohos en frutos secos/granos
|
| 117 |
"Cyanogenic glycosides": ["mareo/vértigo", "dolor de cabeza", "náuseas", "vómito"], # En almendras amargas, yuca
|
| 118 |
"Arsenic": ["fatiga", "náuseas", "neuropatía periférica"], # En arroz
|
| 119 |
"Mercury": ["fatiga", "niebla mental", "neuropatía periférica"], # En pescados grandes
|
| 120 |
"Alpha-Gal": ["erupción", "hinchazón", "náuseas"], # Alérgeno de carne roja
|
|
|
|
| 121 |
# --- Minerales y Elementos (en contexto de exceso o sensibilidad) ---
|
| 122 |
"Nickel": ["erupción", "dermatitis", "dolor abdominal", "hinchazón"],
|
| 123 |
"Iodine": ["acné", "erupción", "fatiga"], # En personas con sensibilidad o problemas tiroideos
|
|
|
|
| 610 |
return re.sub(r'[.,;()]', '', text).lower().strip()
|
| 611 |
|
| 612 |
@retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(3))
|
| 613 |
+
|
| 614 |
def extract_entities_with_gemini(query):
|
| 615 |
+
# ... (el prompt es el mismo)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 616 |
try:
|
| 617 |
response = model.generate_content(system_prompt)
|
| 618 |
+
# ... (lógica de extracción de JSON es la misma)
|
| 619 |
+
extracted_data = json.loads(json_text)
|
| 620 |
+
# Guardar los síntomas originales para la traducción posterior
|
| 621 |
+
extracted_data['sintomas_originales_ia'] = extracted_data.get('sintomas', [])
|
|
|
|
| 622 |
logger.info("Extracción con Gemini exitosa.")
|
| 623 |
+
return extracted_data
|
| 624 |
except (Exception, google.api_core.exceptions.GoogleAPICallError) as e:
|
| 625 |
logger.error(f"Error en la extracción con Gemini (puede ser reintentado): {e}")
|
| 626 |
raise e
|
|
|
|
| 660 |
entities["sintomas"] = list(set(current_symptoms))
|
| 661 |
return entities
|
| 662 |
|
| 663 |
+
|
| 664 |
def find_best_matches_hybrid(entities, data):
|
| 665 |
+
"""
|
| 666 |
+
Nueva función de búsqueda flexible y ponderada.
|
| 667 |
+
No descarta coincidencias y añade un bonus por confianza en el alimento.
|
| 668 |
+
"""
|
| 669 |
if not entities or not data: return []
|
| 670 |
+
|
| 671 |
+
user_symptoms = set(s.lower() for s in entities.get("sintomas", []))
|
| 672 |
+
user_foods_text = " ".join(entities.get("alimentos", [])).lower()
|
| 673 |
+
user_food_keywords = set(re.findall(r'\b\w+\b', user_foods_text))
|
| 674 |
|
|
|
|
|
|
|
|
|
|
| 675 |
RARE_CONDITIONS = [
|
| 676 |
+
"Porfiria Aguda Intermitente (PAI).", "Enfermedad de Refsum del Adulto.",
|
| 677 |
+
"Ataxia por Gluten.", "Encefalopatía por Gluten.", "Enfermedad de Wilson."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 678 |
]
|
| 679 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 680 |
results = []
|
| 681 |
for entry in data:
|
| 682 |
+
# Puntuación de Alimento con Bonus de Confianza
|
| 683 |
+
score_details = {'food': 0, 'symptoms': 0, 'bonus': 0}
|
| 684 |
+
db_food_text = entry.get("compuesto_alimento", "").lower()
|
| 685 |
+
db_food_keywords = set(re.findall(r'\b\w+\b', re.sub(r'\(.*?\)', '', db_food_text)))
|
| 686 |
|
| 687 |
+
common_food_keywords = user_food_keywords.intersection(db_food_keywords)
|
|
|
|
|
|
|
|
|
|
| 688 |
|
| 689 |
+
if common_food_keywords:
|
| 690 |
+
score_details['food'] = 20 # Puntuación base por encontrar cualquier coincidencia
|
| 691 |
+
# Bonus por alta confianza: si más del 50% de las palabras del usuario coinciden
|
| 692 |
+
if len(common_food_keywords) / len(user_food_keywords) > 0.5:
|
| 693 |
+
score_details['bonus'] = 30
|
| 694 |
+
else:
|
| 695 |
+
# Si no hay ninguna coincidencia de palabras clave de alimento, saltar esta entrada
|
| 696 |
+
continue
|
| 697 |
+
|
| 698 |
+
# Puntuación de Síntomas
|
| 699 |
+
db_symptoms_keys = set(s.lower() for s in entry.get("sintomas_clave", []))
|
| 700 |
symptom_score = 0
|
| 701 |
matched_symptoms = []
|
| 702 |
for user_symptom in user_symptoms:
|
| 703 |
+
if user_symptom in db_symptoms_keys:
|
| 704 |
+
symptom_score += 30
|
| 705 |
+
matched_symptoms.append(user_symptom)
|
|
|
|
|
|
|
|
|
|
| 706 |
score_details['symptoms'] = symptom_score
|
| 707 |
|
| 708 |
+
# Calcular puntuación total ponderada
|
| 709 |
+
base_score = score_details['food'] + score_details['symptoms'] + score_details['bonus']
|
| 710 |
+
|
| 711 |
+
condition_name = entry.get("condicion_asociada", "")
|
| 712 |
+
if condition_name in RARE_CONDITIONS:
|
| 713 |
+
final_score = base_score * 0.4
|
| 714 |
+
else:
|
| 715 |
+
final_score = base_score * 1.0
|
| 716 |
+
|
| 717 |
+
score_details['total'] = int(final_score)
|
| 718 |
+
|
| 719 |
+
# Añadir a resultados si tiene una puntuación mínima
|
| 720 |
+
if score_details['total'] > 10:
|
|
|
|
|
|
|
| 721 |
results.append({
|
| 722 |
'entry': entry,
|
| 723 |
'score': score_details,
|
| 724 |
+
'matched_symptoms': matched_symptoms
|
| 725 |
})
|
| 726 |
|
| 727 |
if not results: return []
|
| 728 |
return sorted(results, key=lambda x: x['score']['total'], reverse=True)
|
| 729 |
|
|
|
|
| 730 |
@retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(3))
|
| 731 |
+
@retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(3))
|
| 732 |
+
def translate_symptoms_with_gemini(symptoms_list, master_symptom_map):
|
| 733 |
+
"""
|
| 734 |
+
Usa la IA para traducir síntomas coloquiales a términos clínicos estandarizados
|
| 735 |
+
de nuestro MASTER_SYMPTOM_MAP.
|
| 736 |
+
"""
|
| 737 |
+
if not symptoms_list or not model:
|
| 738 |
+
return []
|
| 739 |
+
|
| 740 |
+
# Crear una lista de los términos clínicos que la IA puede elegir
|
| 741 |
+
clinical_terms = list(master_symptom_map.keys())
|
| 742 |
+
|
| 743 |
+
# Crear una descripción para cada término clínico para darle más contexto a la IA
|
| 744 |
+
contextual_terms = []
|
| 745 |
+
for term in clinical_terms:
|
| 746 |
+
description = ", ".join(master_symptom_map[term].get("frases_es", []))
|
| 747 |
+
contextual_terms.append(f"- {term}: (descrito como: {description})")
|
| 748 |
+
|
| 749 |
+
contextual_terms_str = "\n".join(contextual_terms)
|
| 750 |
+
symptoms_str = ", ".join(symptoms_list)
|
| 751 |
+
|
| 752 |
+
system_prompt = f"""
|
| 753 |
+
Eres un experto en terminología médica. Tu única tarea es mapear una lista de síntomas descritos por un usuario a una lista de términos clínicos estandarizados.
|
| 754 |
+
|
| 755 |
+
LISTA DE TÉRMINOS CLÍNICOS POSIBLES:
|
| 756 |
+
{contextual_terms_str}
|
| 757 |
+
|
| 758 |
+
SÍNTOMAS DEL USUARIO A ANALIZAR:
|
| 759 |
+
"{symptoms_str}"
|
| 760 |
+
|
| 761 |
+
INSTRUCCIONES:
|
| 762 |
+
1. Lee cada síntoma del usuario.
|
| 763 |
+
2. Encuentra el término clínico más apropiado de la lista proporcionada.
|
| 764 |
+
3. Si un síntoma del usuario ya es un término clínico, simplemente inclúyelo.
|
| 765 |
+
4. Si no encuentras una coincidencia clara para un síntoma, ignóralo.
|
| 766 |
+
5. Devuelve ÚNICAMENTE una lista JSON con los términos clínicos estandarizados.
|
| 767 |
+
|
| 768 |
+
Ejemplo:
|
| 769 |
+
Si los síntomas del usuario son ["crecimiento de un bulto en el cuello", "cansancio"], la respuesta debe ser:
|
| 770 |
+
["bocio", "fatiga"]
|
| 771 |
+
"""
|
| 772 |
+
|
| 773 |
+
try:
|
| 774 |
+
response = model.generate_content(system_prompt)
|
| 775 |
+
# Extraer la lista JSON de la respuesta
|
| 776 |
+
match = re.search(r'\[.*?\]', response.text.replace("'", '"'))
|
| 777 |
+
if match:
|
| 778 |
+
translated_list = json.loads(match.group(0))
|
| 779 |
+
logger.info(f"Síntomas traducidos por IA: {symptoms_list} -> {translated_list}")
|
| 780 |
+
return translated_list
|
| 781 |
+
except (Exception, google.api_core.exceptions.GoogleAPICallError) as e:
|
| 782 |
+
logger.error(f"Error en la traducción de síntomas con Gemini: {e}")
|
| 783 |
+
raise e # Para que tenacity reintente
|
| 784 |
+
|
| 785 |
+
return []
|
| 786 |
|
| 787 |
|
| 788 |
def find_best_foodb_matches(user_foods_es, foodb_index_keys, food_name_map, limit=3):
|
|
|
|
| 1076 |
elif alimentos_data is None:
|
| 1077 |
st.error("La base de datos de alimentos no está disponible.")
|
| 1078 |
else:
|
|
|
|
| 1079 |
with st.spinner("🧠 Interpretando tu caso y buscando pistas..."):
|
| 1080 |
+
# Paso A: Extracción inicial
|
| 1081 |
+
initial_entities = None
|
| 1082 |
try:
|
| 1083 |
+
initial_entities = extract_entities_with_gemini(query_to_analyze)
|
| 1084 |
except Exception as e:
|
| 1085 |
logger.warning(f"La extracción con Gemini falló; se usará el sistema de respaldo: {e}")
|
| 1086 |
|
| 1087 |
+
# Paso B: Refuerzo y normalización con el sistema de respaldo
|
| 1088 |
+
reinforced_entities = reinforce_entities_with_keywords(initial_entities, query_to_analyze, FOOD_TO_COMPOUND_MAP, MASTER_SYMPTOM_MAP)
|
| 1089 |
+
|
| 1090 |
+
# --- PASO C: TRADUCCIÓN DE SÍNTOMAS CON IA (NUEVA LÓGICA) ---
|
| 1091 |
+
final_symptoms = set(reinforced_entities.get("sintomas", []))
|
| 1092 |
+
untranslated_symptoms = reinforced_entities.get("sintomas_originales_ia", reinforced_entities.get("sintomas", []))
|
| 1093 |
+
|
| 1094 |
+
if untranslated_symptoms:
|
| 1095 |
+
try:
|
| 1096 |
+
with st.spinner("🧠 Profundizando en la interpretación de los síntomas..."):
|
| 1097 |
+
translated_symptoms = translate_symptoms_with_gemini(untranslated_symptoms, MASTER_SYMPTOM_MAP)
|
| 1098 |
+
final_symptoms.update(translated_symptoms)
|
| 1099 |
+
except Exception as e:
|
| 1100 |
+
logger.error("La traducción de síntomas con IA falló después de varios intentos.")
|
| 1101 |
+
|
| 1102 |
+
# Unir todo en la entidad final
|
| 1103 |
+
final_entities = {
|
| 1104 |
+
"alimentos": reinforced_entities.get("alimentos", []),
|
| 1105 |
+
"sintomas": list(final_symptoms)
|
| 1106 |
+
}
|
| 1107 |
+
st.session_state.entities = final_entities
|
| 1108 |
|
| 1109 |
+
if final_entities and (final_entities.get("alimentos") or final_entities.get("sintomas")):
|
| 1110 |
+
info_str = f"Pistas identificadas - Alimentos: {', '.join(final_entities.get('alimentos',[])) or 'Ninguno'}, Síntomas: {', '.join(final_entities.get('sintomas',[])) or 'Ninguno'}"
|
| 1111 |
st.info(info_str)
|
| 1112 |
with st.spinner("🔬 Cruzando información y calculando relevancia..."):
|
| 1113 |
+
results = find_best_matches_hybrid(final_entities, alimentos_data)
|
| 1114 |
st.session_state.search_results = results
|
| 1115 |
else:
|
| 1116 |
st.error("No se pudieron identificar alimentos o síntomas claros en tu descripción. Intenta ser más específico.")
|
| 1117 |
st.session_state.search_results = []
|
| 1118 |
+
|
| 1119 |
+
|
| 1120 |
if st.session_state.search_results is not None:
|
| 1121 |
results = st.session_state.search_results
|
| 1122 |
|