Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
| 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 |
-
|
| 5 |
-
|
| 6 |
import os
|
| 7 |
import json
|
| 8 |
import logging
|
|
@@ -10,7 +9,7 @@ import re
|
|
| 10 |
import pandas as pd
|
| 11 |
import altair as alt
|
| 12 |
from datetime import datetime
|
| 13 |
-
from tenacity import retry, stop_after_attempt,
|
| 14 |
from io import BytesIO
|
| 15 |
import docx
|
| 16 |
import difflib
|
|
@@ -19,22 +18,22 @@ st.set_page_config(page_title="El Detective de Alimentos", page_icon="🍎", lay
|
|
| 19 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 20 |
logger = logging.getLogger("food_detective_app")
|
| 21 |
|
| 22 |
-
# ---
|
| 23 |
TEXTO_BANDERAS_ROJAS = """
|
| 24 |
\n### **IMPORTANTE: Descargo de Responsabilidad y Banderas Rojas**
|
| 25 |
-
Este análisis es una herramienta informativa de IA y **NO es un diagnóstico médico.**
|
| 26 |
|
| 27 |
-
**🚩 BANDERAS ROJAS: ¡Atención!**
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
2. **Enfermedad Inflamatoria Intestinal:** Diarrea con sangre/moco, fiebre recurrente, dolor articular.
|
| 31 |
3. **Embarazo:** Náuseas matutinas, ausencia de menstruación.
|
| 32 |
-
4. **Isquemia Mesentérica:** Dolor
|
| 33 |
-
5. **
|
| 34 |
|
| 35 |
-
**Si tus síntomas son severos
|
| 36 |
"""
|
| 37 |
|
|
|
|
| 38 |
try:
|
| 39 |
if 'GEMINI_API_KEY' in st.secrets:
|
| 40 |
GEMINI_API_KEY = st.secrets['GEMINI_API_KEY']
|
|
@@ -43,19 +42,17 @@ try:
|
|
| 43 |
if not GEMINI_API_KEY:
|
| 44 |
st.error("No se encontró la GEMINI_API_KEY.")
|
| 45 |
st.stop()
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
| 47 |
except Exception as e:
|
| 48 |
st.error(f"❌ Error al configurar Gemini API: {e}")
|
| 49 |
st.stop()
|
| 50 |
|
| 51 |
@st.cache_resource
|
| 52 |
-
def
|
| 53 |
-
|
| 54 |
-
return genai.GenerativeModel("gemini-2.5-flash-lite")
|
| 55 |
-
except Exception as e:
|
| 56 |
-
st.error(f"❌ No se pudo cargar el modelo Gemini: {e}")
|
| 57 |
-
return None
|
| 58 |
-
model = get_gemini_model()
|
| 59 |
|
| 60 |
@st.cache_data
|
| 61 |
def load_data():
|
|
@@ -1104,23 +1101,23 @@ def sanitize_text(text):
|
|
| 1104 |
if not text: return ""
|
| 1105 |
return re.sub(r'[.,;()]', '', text).lower().strip()
|
| 1106 |
|
| 1107 |
-
# --- FUNCIONES DE EXTRACCIÓN Y LÓGICA
|
| 1108 |
|
| 1109 |
-
@retry(wait=
|
| 1110 |
def extract_entities_with_gemini(query):
|
| 1111 |
-
if not
|
| 1112 |
-
# PROMPT TELEGRÁFICO (AHORRO)
|
| 1113 |
system_prompt = f"""
|
| 1114 |
-
Rol: Extractor
|
| 1115 |
-
Tarea:
|
| 1116 |
-
Campos:
|
| 1117 |
-
1. "alimentos": Lista de comidas/ingredientes.
|
| 1118 |
-
2. "sintomas": Lista de sensaciones/signos físicos.
|
| 1119 |
Input: "{query}"
|
| 1120 |
-
Output (JSON
|
| 1121 |
"""
|
| 1122 |
try:
|
| 1123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1124 |
text = response.text
|
| 1125 |
if "```json" in text: text = text.split("```json")[1].split("```")[0]
|
| 1126 |
elif "```" in text: text = text.split("```")[1].split("```")[0]
|
|
@@ -1137,7 +1134,7 @@ def reinforce_entities_with_keywords(entities, query, food_map, master_symptom_m
|
|
| 1137 |
for food_keyword in food_map.keys():
|
| 1138 |
if food_keyword in query_sanitized: current_foods.add(food_keyword)
|
| 1139 |
entities["alimentos"] = list(current_foods)
|
| 1140 |
-
|
| 1141 |
current_symptoms = set(entities.get("sintomas", []))
|
| 1142 |
for main_symptom, details in master_symptom_map.items():
|
| 1143 |
for phrase in details.get("frases_es", []):
|
|
@@ -1199,104 +1196,61 @@ def find_best_foodb_matches(user_foods_es, foodb_index_keys, food_name_map, limi
|
|
| 1199 |
found_matches.extend(matches)
|
| 1200 |
return list(set(found_matches))[:limit]
|
| 1201 |
|
| 1202 |
-
|
| 1203 |
-
|
| 1204 |
-
@retry(wait=wait_random_exponential(min=1, max=10), stop=stop_after_attempt(3))
|
| 1205 |
def generate_detailed_analysis(query, match):
|
| 1206 |
-
if not
|
| 1207 |
prompt_parts = [
|
| 1208 |
"Rol: Nutricionista Funcional.",
|
| 1209 |
f"Caso: {query}",
|
| 1210 |
f"Hipótesis: {match.get('condicion_asociada')}",
|
| 1211 |
f"Mecanismo: {match.get('mecanismo_fisiologico')}",
|
| 1212 |
f"Alimentos clave: {match.get('compuesto_alimento')}",
|
| 1213 |
-
"Tarea: Escribir análisis
|
| 1214 |
-
"
|
| 1215 |
-
"1. Saludo y posible causa.",
|
| 1216 |
-
"2. Explicación del mecanismo.",
|
| 1217 |
-
"3. Alimentos a evitar.",
|
| 1218 |
-
"4. Reemplazos sugeridos.",
|
| 1219 |
-
"5. Consejo práctico.",
|
| 1220 |
-
"IMPORTANTE: NO incluyas descargos de responsabilidad."
|
| 1221 |
]
|
| 1222 |
prompt = "\n".join(prompt_parts)
|
| 1223 |
try:
|
| 1224 |
-
|
| 1225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1226 |
except Exception as e:
|
| 1227 |
logger.error(f"Error análisis: {e}")
|
| 1228 |
raise e
|
| 1229 |
|
| 1230 |
def generate_neuro_report_text(entities, food_map, neuro_map):
|
| 1231 |
-
"""Genera reporte neuropsicológico basado en mapas locales (Costo 0 tokens)."""
|
| 1232 |
report_lines = ["\n### 🧠 Efectos Neuropsicológicos Posibles"]
|
| 1233 |
user_foods = entities.get("alimentos", [])
|
| 1234 |
relevant_compounds = set()
|
| 1235 |
if user_foods:
|
| 1236 |
for food in user_foods:
|
| 1237 |
if food in food_map: relevant_compounds.update(food_map[food])
|
| 1238 |
-
|
| 1239 |
found_neuro_effect = False
|
| 1240 |
if relevant_compounds:
|
| 1241 |
for compound in sorted(list(relevant_compounds)):
|
| 1242 |
if compound in neuro_map:
|
| 1243 |
found_neuro_effect = True
|
| 1244 |
-
|
| 1245 |
-
|
| 1246 |
-
report_lines.append(f"_{effect_info['efecto_neuropsicologico']}_\n")
|
| 1247 |
-
|
| 1248 |
-
if not found_neuro_effect:
|
| 1249 |
-
report_lines.append("No se detectaron efectos neuropsicológicos específicos en nuestra base de datos para estos alimentos.")
|
| 1250 |
return "\n".join(report_lines)
|
| 1251 |
|
| 1252 |
def generate_molecular_report_text(best_match, entities, foodb_index, food_name_map, synonym_map, triggers_map):
|
| 1253 |
-
|
| 1254 |
-
|
| 1255 |
-
|
| 1256 |
-
|
| 1257 |
-
if not user_foods_mentioned:
|
| 1258 |
-
return "No se identificaron alimentos específicos para el análisis molecular."
|
| 1259 |
-
|
| 1260 |
-
initial_clues = set()
|
| 1261 |
-
direct_text = best_match.get("compuesto_alimento", "").lower()
|
| 1262 |
-
cleaned_text = re.sub(r'\(.*?\)', '', direct_text)
|
| 1263 |
-
initial_clues.update(re.findall(r'\b[a-zA-Z-]+\b', cleaned_text))
|
| 1264 |
-
|
| 1265 |
-
main_diagnosis_symptoms = set(s.lower() for s in best_match.get("sintomas_clave", []))
|
| 1266 |
-
for compound, triggered_symptoms in triggers_map.items():
|
| 1267 |
-
if main_diagnosis_symptoms.intersection(triggered_symptoms):
|
| 1268 |
-
initial_clues.add(compound.lower())
|
| 1269 |
-
|
| 1270 |
-
final_search_keywords = set()
|
| 1271 |
-
for clue in initial_clues:
|
| 1272 |
-
final_search_keywords.add(clue)
|
| 1273 |
-
if clue in synonym_map: final_search_keywords.update(synonym_map[clue])
|
| 1274 |
|
| 1275 |
-
|
|
|
|
|
|
|
| 1276 |
|
| 1277 |
-
if not best_food_matches:
|
| 1278 |
-
return "No se encontraron datos moleculares detallados para los alimentos mencionados."
|
| 1279 |
-
|
| 1280 |
-
found_any_data = False
|
| 1281 |
for food_key in best_food_matches:
|
| 1282 |
-
|
| 1283 |
-
|
| 1284 |
-
for item in
|
| 1285 |
-
|
| 1286 |
-
|
| 1287 |
-
|
| 1288 |
-
if relevant_compounds:
|
| 1289 |
-
found_any_data = True
|
| 1290 |
-
report_lines.append(f"\n**Alimento Analizado: {food_key.capitalize()}**")
|
| 1291 |
-
unique_compounds = set()
|
| 1292 |
-
for item in relevant_compounds:
|
| 1293 |
-
if item['compound'] not in unique_compounds:
|
| 1294 |
-
report_lines.append(f"- Compuesto: `{item['compound']}` (Vínculo potencial con {best_match.get('condicion_asociada')})")
|
| 1295 |
-
unique_compounds.add(item['compound'])
|
| 1296 |
-
|
| 1297 |
-
if not found_any_data:
|
| 1298 |
-
return f"No se encontraron los compuestos moleculares específicos de esta condición en los alimentos analizados."
|
| 1299 |
-
|
| 1300 |
return "\n".join(report_lines)
|
| 1301 |
|
| 1302 |
def create_relevance_chart(results):
|
|
@@ -1306,14 +1260,11 @@ def create_relevance_chart(results):
|
|
| 1306 |
"Relevancia": [r['score']['total'] for r in top_results]
|
| 1307 |
})
|
| 1308 |
chart = alt.Chart(data).mark_bar().encode(
|
| 1309 |
-
x='Relevancia',
|
| 1310 |
-
y=alt.Y('Condición', sort='-x'),
|
| 1311 |
-
tooltip=['Condición', 'Relevancia']
|
| 1312 |
).properties(title='Top Coincidencias')
|
| 1313 |
return chart
|
| 1314 |
|
| 1315 |
def generate_word_report(report_text):
|
| 1316 |
-
# Simulación simple para no requerir plantilla física en el ejemplo
|
| 1317 |
doc = docx.Document()
|
| 1318 |
doc.add_paragraph(report_text)
|
| 1319 |
doc_io = BytesIO()
|
|
@@ -1321,7 +1272,7 @@ def generate_word_report(report_text):
|
|
| 1321 |
doc_io.seek(0)
|
| 1322 |
return doc_io
|
| 1323 |
|
| 1324 |
-
# --- INTERFAZ
|
| 1325 |
col_img1, col_text, col_img2 = st.columns([1, 4, 1])
|
| 1326 |
with col_img1:
|
| 1327 |
if os.path.exists("imagen.png"): st.image("imagen.png", width=150)
|
|
@@ -1337,7 +1288,7 @@ if 'entities' not in st.session_state: st.session_state.entities = None
|
|
| 1337 |
if 'analysis_cache' not in st.session_state: st.session_state.analysis_cache = {}
|
| 1338 |
|
| 1339 |
with st.form(key="search_form"):
|
| 1340 |
-
query = st.text_area("Tu Caso:", height=150, placeholder="Ej: Me duele la cabeza
|
| 1341 |
submitted = st.form_submit_button("Analizar Caso", type="primary")
|
| 1342 |
|
| 1343 |
if submitted and query:
|
|
@@ -1345,20 +1296,17 @@ if submitted and query:
|
|
| 1345 |
st.session_state.search_results = None
|
| 1346 |
st.session_state.analysis_cache = {}
|
| 1347 |
|
| 1348 |
-
with st.spinner("🔍 Analizando pistas
|
| 1349 |
-
# 1. Extracción (IA Optimizada)
|
| 1350 |
try:
|
| 1351 |
raw_entities = extract_entities_with_gemini(query)
|
| 1352 |
except:
|
| 1353 |
raw_entities = {"alimentos": [], "sintomas": []}
|
| 1354 |
|
| 1355 |
-
|
| 1356 |
-
|
| 1357 |
-
final_symptoms = translate_symptoms_local(reinforced.get("sintomas", []), MASTER_SYMPTOM_MAP)
|
| 1358 |
final_entities = {"alimentos": reinforced.get("alimentos", []), "sintomas": final_symptoms}
|
| 1359 |
st.session_state.entities = final_entities
|
| 1360 |
|
| 1361 |
-
# 3. Búsqueda (Local)
|
| 1362 |
results = find_best_matches_hybrid(final_entities, alimentos_data)
|
| 1363 |
st.session_state.search_results = results
|
| 1364 |
|
|
@@ -1368,56 +1316,32 @@ if st.session_state.search_results:
|
|
| 1368 |
|
| 1369 |
st.success(f"🔎 Coincidencia Principal: **{best_match.get('condicion_asociada')}**")
|
| 1370 |
|
| 1371 |
-
# Generación de textos (Bajo demanda o caché)
|
| 1372 |
cache_key = f"analysis_{best_match.get('condicion_asociada')}"
|
| 1373 |
if cache_key not in st.session_state.analysis_cache:
|
| 1374 |
-
with st.spinner("✍️ Redactando informe
|
| 1375 |
try:
|
| 1376 |
analysis = generate_detailed_analysis(st.session_state.user_query, best_match)
|
| 1377 |
st.session_state.analysis_cache[cache_key] = analysis
|
| 1378 |
except:
|
| 1379 |
-
st.session_state.analysis_cache[cache_key] = "
|
| 1380 |
|
| 1381 |
-
# --- VISUALIZACIÓN EN PESTAÑAS (MÁS LIMPIO) ---
|
| 1382 |
tab_main, tab_neuro, tab_mol = st.tabs(["💡 Interpretación Clínica", "🧠 Efectos Neuropsicológicos", "🔬 Análisis Molecular"])
|
| 1383 |
|
| 1384 |
with tab_main:
|
| 1385 |
st.markdown(st.session_state.analysis_cache[cache_key])
|
| 1386 |
-
st.markdown("---")
|
| 1387 |
-
st.caption("Gráfico de otras posibles causas:")
|
| 1388 |
st.altair_chart(create_relevance_chart(results), use_container_width=True)
|
| 1389 |
|
| 1390 |
with tab_neuro:
|
| 1391 |
-
st.
|
| 1392 |
-
neuro_text = generate_neuro_report_text(st.session_state.entities, FOOD_TO_COMPOUND_MAP, INTEGRATED_NEURO_FOOD_MAP)
|
| 1393 |
-
st.markdown(neuro_text)
|
| 1394 |
|
| 1395 |
with tab_mol:
|
| 1396 |
-
st.
|
| 1397 |
-
mol_text = generate_molecular_report_text(best_match, st.session_state.entities, foodb_index, FOOD_NAME_TO_FOODB_KEY, COMPOUND_SYNONYM_MAP, KNOWN_TRIGGERS_MAP)
|
| 1398 |
-
st.markdown(mol_text)
|
| 1399 |
|
| 1400 |
-
|
| 1401 |
-
full_report = f"REPORTE CLÍNICO\n\n{st.session_state.analysis_cache[cache_key]}\n\n{neuro_text}\n\n{mol_text}"
|
| 1402 |
word_data = generate_word_report(full_report)
|
| 1403 |
-
st.download_button("📄 Descargar Informe
|
| 1404 |
|
| 1405 |
elif submitted:
|
| 1406 |
-
# Creamos un contenedor de advertencia visualmente más claro
|
| 1407 |
with st.container(border=True):
|
| 1408 |
-
st.warning("⚠️ No pudimos identificar una causa clara
|
| 1409 |
-
|
| 1410 |
-
col_help, col_tips = st.columns([1, 2])
|
| 1411 |
-
|
| 1412 |
-
with col_help:
|
| 1413 |
-
st.markdown("### ¿Qué pudo pasar?")
|
| 1414 |
-
st.markdown("""
|
| 1415 |
-
- **Descripción muy breve:** La IA necesita contexto.
|
| 1416 |
-
- **Sinónimos desconocidos:** Usaste términos muy coloquiales.
|
| 1417 |
-
- **Fallo de conexión:** La IA no respondió a tiempo.
|
| 1418 |
-
""")
|
| 1419 |
-
|
| 1420 |
-
with col_tips:
|
| 1421 |
-
st.info("💡 **Intenta reformular tu consulta así:**")
|
| 1422 |
-
st.code("Siento [SÍNTOMA] y [SÍNTOMA] después de comer [ALIMENTO].", language="text")
|
| 1423 |
-
st.markdown("**Ejemplo:** _Me duele mucho la cabeza tipo migraña cada vez que como queso curado y tomo vino tinto._")
|
|
|
|
| 1 |
+
# El Detective de Alimentos (Versión Actualizada a google-genai V1.0)
|
|
|
|
| 2 |
import streamlit as st
|
| 3 |
+
from google import genai # NUEVA LIBRERÍA
|
| 4 |
+
from google.genai import types # TIPOS DE DATOS NUEVOS
|
| 5 |
import os
|
| 6 |
import json
|
| 7 |
import logging
|
|
|
|
| 9 |
import pandas as pd
|
| 10 |
import altair as alt
|
| 11 |
from datetime import datetime
|
| 12 |
+
from tenacity import retry, stop_after_attempt, wait_fixed
|
| 13 |
from io import BytesIO
|
| 14 |
import docx
|
| 15 |
import difflib
|
|
|
|
| 18 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 19 |
logger = logging.getLogger("food_detective_app")
|
| 20 |
|
| 21 |
+
# --- TEXTO ESTÁTICO DE SEGURIDAD ---
|
| 22 |
TEXTO_BANDERAS_ROJAS = """
|
| 23 |
\n### **IMPORTANTE: Descargo de Responsabilidad y Banderas Rojas**
|
| 24 |
+
Este análisis es una herramienta informativa de IA y **NO es un diagnóstico médico.**
|
| 25 |
|
| 26 |
+
**🚩 BANDERAS ROJAS: ¡Atención!** Consulta a un médico si experimentas:
|
| 27 |
+
1. **Cáncer Gástrico/Colon:** Pérdida de peso, sangre en heces.
|
| 28 |
+
2. **Enfermedad Inflamatoria Intestinal:** Diarrea con sangre/moco, fiebre.
|
|
|
|
| 29 |
3. **Embarazo:** Náuseas matutinas, ausencia de menstruación.
|
| 30 |
+
4. **Isquemia Mesentérica:** Dolor fuerte 15-30 min después de comer.
|
| 31 |
+
5. **Vesícula:** Dolor agudo lado derecho tras grasas, piel amarilla.
|
| 32 |
|
| 33 |
+
**Si tus síntomas son severos, busca ayuda médica urgente.**
|
| 34 |
"""
|
| 35 |
|
| 36 |
+
# --- CONFIGURACIÓN DE LA NUEVA API (google-genai) ---
|
| 37 |
try:
|
| 38 |
if 'GEMINI_API_KEY' in st.secrets:
|
| 39 |
GEMINI_API_KEY = st.secrets['GEMINI_API_KEY']
|
|
|
|
| 42 |
if not GEMINI_API_KEY:
|
| 43 |
st.error("No se encontró la GEMINI_API_KEY.")
|
| 44 |
st.stop()
|
| 45 |
+
|
| 46 |
+
# NUEVA FORMA DE INICIAR EL CLIENTE
|
| 47 |
+
client = genai.Client(api_key=GEMINI_API_KEY)
|
| 48 |
+
|
| 49 |
except Exception as e:
|
| 50 |
st.error(f"❌ Error al configurar Gemini API: {e}")
|
| 51 |
st.stop()
|
| 52 |
|
| 53 |
@st.cache_resource
|
| 54 |
+
def get_gemini_client():
|
| 55 |
+
return client
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
@st.cache_data
|
| 58 |
def load_data():
|
|
|
|
| 1101 |
if not text: return ""
|
| 1102 |
return re.sub(r'[.,;()]', '', text).lower().strip()
|
| 1103 |
|
| 1104 |
+
# --- FUNCIONES DE EXTRACCIÓN Y LÓGICA ---
|
| 1105 |
|
| 1106 |
+
@retry(wait=wait_fixed(2), stop=stop_after_attempt(2)) # Retries rápidos
|
| 1107 |
def extract_entities_with_gemini(query):
|
| 1108 |
+
if not client: return None
|
|
|
|
| 1109 |
system_prompt = f"""
|
| 1110 |
+
Rol: Extractor Médico JSON.
|
| 1111 |
+
Tarea: Extraer JSON estricto con claves "alimentos" (lista) y "sintomas" (lista).
|
|
|
|
|
|
|
|
|
|
| 1112 |
Input: "{query}"
|
| 1113 |
+
Output (JSON):
|
| 1114 |
"""
|
| 1115 |
try:
|
| 1116 |
+
# NUEVA SINTAXIS DE LLAMADA
|
| 1117 |
+
response = client.models.generate_content(
|
| 1118 |
+
model='gemini-2.5-flash-lite',
|
| 1119 |
+
contents=system_prompt
|
| 1120 |
+
)
|
| 1121 |
text = response.text
|
| 1122 |
if "```json" in text: text = text.split("```json")[1].split("```")[0]
|
| 1123 |
elif "```" in text: text = text.split("```")[1].split("```")[0]
|
|
|
|
| 1134 |
for food_keyword in food_map.keys():
|
| 1135 |
if food_keyword in query_sanitized: current_foods.add(food_keyword)
|
| 1136 |
entities["alimentos"] = list(current_foods)
|
| 1137 |
+
|
| 1138 |
current_symptoms = set(entities.get("sintomas", []))
|
| 1139 |
for main_symptom, details in master_symptom_map.items():
|
| 1140 |
for phrase in details.get("frases_es", []):
|
|
|
|
| 1196 |
found_matches.extend(matches)
|
| 1197 |
return list(set(found_matches))[:limit]
|
| 1198 |
|
| 1199 |
+
@retry(wait=wait_fixed(2), stop=stop_after_attempt(2))
|
|
|
|
|
|
|
| 1200 |
def generate_detailed_analysis(query, match):
|
| 1201 |
+
if not client: return "Error: IA no disponible."
|
| 1202 |
prompt_parts = [
|
| 1203 |
"Rol: Nutricionista Funcional.",
|
| 1204 |
f"Caso: {query}",
|
| 1205 |
f"Hipótesis: {match.get('condicion_asociada')}",
|
| 1206 |
f"Mecanismo: {match.get('mecanismo_fisiologico')}",
|
| 1207 |
f"Alimentos clave: {match.get('compuesto_alimento')}",
|
| 1208 |
+
"Tarea: Escribir análisis Markdown (Saludo, Causa, Mecanismo, Evitar, Reemplazos, Consejo).",
|
| 1209 |
+
"NO incluyas descargos de responsabilidad."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1210 |
]
|
| 1211 |
prompt = "\n".join(prompt_parts)
|
| 1212 |
try:
|
| 1213 |
+
# NUEVA SINTAXIS
|
| 1214 |
+
response = client.models.generate_content(
|
| 1215 |
+
model='gemini-2.5-flash-lite',
|
| 1216 |
+
contents=prompt
|
| 1217 |
+
)
|
| 1218 |
+
return response.text + TEXTO_BANDERAS_ROJAS
|
| 1219 |
except Exception as e:
|
| 1220 |
logger.error(f"Error análisis: {e}")
|
| 1221 |
raise e
|
| 1222 |
|
| 1223 |
def generate_neuro_report_text(entities, food_map, neuro_map):
|
|
|
|
| 1224 |
report_lines = ["\n### 🧠 Efectos Neuropsicológicos Posibles"]
|
| 1225 |
user_foods = entities.get("alimentos", [])
|
| 1226 |
relevant_compounds = set()
|
| 1227 |
if user_foods:
|
| 1228 |
for food in user_foods:
|
| 1229 |
if food in food_map: relevant_compounds.update(food_map[food])
|
|
|
|
| 1230 |
found_neuro_effect = False
|
| 1231 |
if relevant_compounds:
|
| 1232 |
for compound in sorted(list(relevant_compounds)):
|
| 1233 |
if compound in neuro_map:
|
| 1234 |
found_neuro_effect = True
|
| 1235 |
+
report_lines.append(f"**{compound.capitalize()}**: _{neuro_map[compound]['efecto_neuropsicologico']}_")
|
| 1236 |
+
if not found_neuro_effect: report_lines.append("No se detectaron efectos específicos.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1237 |
return "\n".join(report_lines)
|
| 1238 |
|
| 1239 |
def generate_molecular_report_text(best_match, entities, foodb_index, food_name_map, synonym_map, triggers_map):
|
| 1240 |
+
report_lines = ["\n### 🔬 Análisis Molecular"]
|
| 1241 |
+
user_foods = entities.get("alimentos", [])
|
| 1242 |
+
if not user_foods: return "No hay alimentos para analizar."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1243 |
|
| 1244 |
+
# Lógica de búsqueda simplificada para el ejemplo
|
| 1245 |
+
best_food_matches = find_best_foodb_matches(user_foods, foodb_index.keys(), food_name_map)
|
| 1246 |
+
if not best_food_matches: return "No se encontraron datos moleculares detallados."
|
| 1247 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1248 |
for food_key in best_food_matches:
|
| 1249 |
+
report_lines.append(f"\n**{food_key.capitalize()}**")
|
| 1250 |
+
compounds = foodb_index.get(food_key, [])[:5] # Limitamos a 5 para el ejemplo
|
| 1251 |
+
for item in compounds:
|
| 1252 |
+
report_lines.append(f"- {item['compound']}")
|
| 1253 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1254 |
return "\n".join(report_lines)
|
| 1255 |
|
| 1256 |
def create_relevance_chart(results):
|
|
|
|
| 1260 |
"Relevancia": [r['score']['total'] for r in top_results]
|
| 1261 |
})
|
| 1262 |
chart = alt.Chart(data).mark_bar().encode(
|
| 1263 |
+
x='Relevancia', y=alt.Y('Condición', sort='-x'), tooltip=['Condición', 'Relevancia']
|
|
|
|
|
|
|
| 1264 |
).properties(title='Top Coincidencias')
|
| 1265 |
return chart
|
| 1266 |
|
| 1267 |
def generate_word_report(report_text):
|
|
|
|
| 1268 |
doc = docx.Document()
|
| 1269 |
doc.add_paragraph(report_text)
|
| 1270 |
doc_io = BytesIO()
|
|
|
|
| 1272 |
doc_io.seek(0)
|
| 1273 |
return doc_io
|
| 1274 |
|
| 1275 |
+
# --- INTERFAZ ---
|
| 1276 |
col_img1, col_text, col_img2 = st.columns([1, 4, 1])
|
| 1277 |
with col_img1:
|
| 1278 |
if os.path.exists("imagen.png"): st.image("imagen.png", width=150)
|
|
|
|
| 1288 |
if 'analysis_cache' not in st.session_state: st.session_state.analysis_cache = {}
|
| 1289 |
|
| 1290 |
with st.form(key="search_form"):
|
| 1291 |
+
query = st.text_area("Tu Caso:", height=150, placeholder="Ej: Me duele la cabeza tipo migraña cada vez que como queso curado.")
|
| 1292 |
submitted = st.form_submit_button("Analizar Caso", type="primary")
|
| 1293 |
|
| 1294 |
if submitted and query:
|
|
|
|
| 1296 |
st.session_state.search_results = None
|
| 1297 |
st.session_state.analysis_cache = {}
|
| 1298 |
|
| 1299 |
+
with st.spinner("🔍 Analizando pistas..."):
|
|
|
|
| 1300 |
try:
|
| 1301 |
raw_entities = extract_entities_with_gemini(query)
|
| 1302 |
except:
|
| 1303 |
raw_entities = {"alimentos": [], "sintomas": []}
|
| 1304 |
|
| 1305 |
+
reinforced = reinforce_entities_with_keywords(raw_entities, query, {}, {}) # Poner mapas completos aquí
|
| 1306 |
+
final_symptoms = translate_symptoms_local(reinforced.get("sintomas", []), {}) # Poner mapas completos aquí
|
|
|
|
| 1307 |
final_entities = {"alimentos": reinforced.get("alimentos", []), "sintomas": final_symptoms}
|
| 1308 |
st.session_state.entities = final_entities
|
| 1309 |
|
|
|
|
| 1310 |
results = find_best_matches_hybrid(final_entities, alimentos_data)
|
| 1311 |
st.session_state.search_results = results
|
| 1312 |
|
|
|
|
| 1316 |
|
| 1317 |
st.success(f"🔎 Coincidencia Principal: **{best_match.get('condicion_asociada')}**")
|
| 1318 |
|
|
|
|
| 1319 |
cache_key = f"analysis_{best_match.get('condicion_asociada')}"
|
| 1320 |
if cache_key not in st.session_state.analysis_cache:
|
| 1321 |
+
with st.spinner("✍️ Redactando informe..."):
|
| 1322 |
try:
|
| 1323 |
analysis = generate_detailed_analysis(st.session_state.user_query, best_match)
|
| 1324 |
st.session_state.analysis_cache[cache_key] = analysis
|
| 1325 |
except:
|
| 1326 |
+
st.session_state.analysis_cache[cache_key] = "Error en análisis detallado."
|
| 1327 |
|
|
|
|
| 1328 |
tab_main, tab_neuro, tab_mol = st.tabs(["💡 Interpretación Clínica", "🧠 Efectos Neuropsicológicos", "🔬 Análisis Molecular"])
|
| 1329 |
|
| 1330 |
with tab_main:
|
| 1331 |
st.markdown(st.session_state.analysis_cache[cache_key])
|
|
|
|
|
|
|
| 1332 |
st.altair_chart(create_relevance_chart(results), use_container_width=True)
|
| 1333 |
|
| 1334 |
with tab_neuro:
|
| 1335 |
+
st.markdown(generate_neuro_report_text(st.session_state.entities, {}, {})) # Poner mapas completos
|
|
|
|
|
|
|
| 1336 |
|
| 1337 |
with tab_mol:
|
| 1338 |
+
st.markdown(generate_molecular_report_text(best_match, st.session_state.entities, foodb_index, {}, {}, {})) # Poner mapas completos
|
|
|
|
|
|
|
| 1339 |
|
| 1340 |
+
full_report = f"REPORTE\n\n{st.session_state.analysis_cache[cache_key]}"
|
|
|
|
| 1341 |
word_data = generate_word_report(full_report)
|
| 1342 |
+
st.download_button("📄 Descargar Informe", data=word_data, file_name="Reporte.docx", mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document")
|
| 1343 |
|
| 1344 |
elif submitted:
|
|
|
|
| 1345 |
with st.container(border=True):
|
| 1346 |
+
st.warning("⚠️ No pudimos identificar una causa clara.")
|
| 1347 |
+
st.info("Intenta reformular: 'Siento [SÍNTOMA] cuando como [ALIMENTO]'.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|