File size: 17,238 Bytes
5693371 14e0302 50b7e58 a362398 21a807d 29d0842 a362398 e0ecd33 2f59c3d e4f7e04 50b7e58 8ae4598 29d0842 9bff8cd 53a4ac5 50b7e58 93dbc98 2f59c3d 53a4ac5 c52e98f 2f59c3d 2108de0 50b7e58 8ae4598 50b7e58 dbadfd8 50b7e58 14e0302 50b7e58 8ae4598 50b7e58 14e0302 50b7e58 14e0302 50b7e58 2f59c3d a362398 29d0842 a362398 14e0302 21a807d 14e0302 a362398 14e0302 29d0842 14e0302 7de5686 2f59c3d dbadfd8 14e0302 dbadfd8 14e0302 dbadfd8 14e0302 dbadfd8 ea9d0eb 2f59c3d ea9d0eb 2f59c3d ea9d0eb dbadfd8 3502efb dbadfd8 ea9d0eb dbadfd8 3502efb ea9d0eb dbadfd8 ea9d0eb 2f59c3d 3502efb dbadfd8 2f59c3d 29d0842 14e0302 2f59c3d a362398 9bff8cd 2f59c3d 14e0302 2f59c3d bde368b a362398 bde368b 29d0842 a362398 14e0302 01fb88d 29d0842 14e0302 93dbc98 a362398 9bff8cd 93dbc98 14e0302 93dbc98 a362398 93dbc98 a362398 14e0302 50b7e58 29607cf 3502efb 93dbc98 3502efb 50b7e58 d6b8172 50b7e58 8bf2098 29607cf 984602c 8bf2098 29607cf 984602c 8bf2098 50b7e58 8bf2098 dbadfd8 8bf2098 dbadfd8 8bf2098 dbadfd8 8bf2098 dbadfd8 8bf2098 2be1203 8123a33 8bf2098 29607cf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 | # ==================== Buscador de Eventos de Notificación SIVIGILA - Colombia 2025 =================
# JAIRO ALEXANDER ERASO MD U Nacional de Colombia.
# DIANA MILENA SOLER MARTINEZ U Juan N. Corpas
# Actualizado a google-genai V1.0
import streamlit as st
from google import genai
from google.genai import types
import os
import json
import logging
import datetime
import pandas as pd
import io
import difflib
st.set_page_config(
page_title="Plataforma SIVIGILA Inteligente",
page_icon="buho.png",
layout="wide"
)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("sivigila_app")
@st.cache_data
def load_data():
"""Carga y procesa los archivos JSON, creando mapas optimizados para búsqueda rápida."""
try:
path_codigos = os.path.join('PLANTILLAS', 'DIC_NOMBRES_CODIGOS_FICHAS.json')
path_notificacion = os.path.join('PLANTILLAS', 'DATOS_NOTIFICACION.json')
with open(path_codigos, 'r', encoding='utf-8') as f: data_codigos = json.load(f)
with open(path_notificacion, 'r', encoding='utf-8') as f: data_notificacion = json.load(f)
notificacion_map = {item['FICHA']: item for item in data_notificacion}
dx_to_info_map = {}
for item in data_codigos:
ficha = item.get("FICHA")
if ficha and ficha != "No aplica":
evento_info = notificacion_map.get(ficha)
if evento_info:
info = {"ficha": ficha, "evento": evento_info.get("Evento", "N/A"), "protocolo": item.get("NOMBRES DE PROTOCOLOS", "N/A")}
cie10 = item.get("CIE 10")
cie11 = item.get("CIE 11")
if cie10: dx_to_info_map[cie10] = info
if cie11: dx_to_info_map[cie11] = info
nombres_eventos_limpios = sorted(list(set(item['Evento'] for item in data_notificacion)))
logger.info("✅ Datos cargados y mapas de búsqueda optimizados creados.")
return data_codigos, data_notificacion, notificacion_map, nombres_eventos_limpios, dx_to_info_map
except Exception as e:
st.error(f"❌ Error crítico al cargar o procesar los datos: {e}")
return None, None, None, None, None
data_codigos, data_notificacion, notificacion_map, nombres_eventos_limpios, dx_to_info_map = load_data()
try:
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
if not GEMINI_API_KEY:
st.error("No se encontró la variable de entorno GEMINI_API_KEY. Por favor, configúrala en los Secrets del Space.")
st.stop()
client = genai.Client(api_key=GEMINI_API_KEY)
except Exception as e:
st.error(f"❌ Error al configurar Gemini API: {e}")
st.stop()
@st.cache_resource
def get_gemini_client():
return client
def search_local_fuzzy(query, event_list, cutoff=0.6):
"""
Busca coincidencias aproximadas usando CPU local (0 tokens).
Retorna una lista de nombres de eventos si encuentra similitud alta.
"""
query_lower = query.lower()
matches = difflib.get_close_matches(query, event_list, n=3, cutoff=cutoff)
if not matches:
matches = [event for event in event_list if query_lower in event.lower()]
return matches
def search_direct(query, data, notif_map):
query_lower = query.lower().strip()
final_results = []
informative_results = []
for item in data:
if query_lower in item.get("NOMBRES DE PROTOCOLOS", "").lower() and item.get("FICHA") == "No aplica":
info_notif = {
"Evento": item.get("NOMBRES DE PROTOCOLOS").split(":")[0],
"FICHA": "Informativo",
"PROTOCOLO": item.get("PROTOCOLO"),
"is_informative": True
}
informative_results.append({
"info_notificacion": info_notif,
"detalles_codigos": [item]
})
if informative_results:
return informative_results
matching_items = []
for item in data:
if (query_lower == item.get("CIE 10", "").lower().strip() or
query_lower == item.get("CIE 11", "").lower().strip() or
query_lower == item.get("FICHA", "").lower().strip() or
query_lower in item.get("NOMBRES DE PROTOCOLOS", "").lower()):
matching_items.append(item)
if not matching_items: return []
grouped_by_ficha = {}
for item in matching_items:
ficha = item.get("FICHA")
if ficha and ficha != "No aplica":
if ficha not in grouped_by_ficha: grouped_by_ficha[ficha] = []
grouped_by_ficha[ficha].append(item)
for ficha, matched_details in grouped_by_ficha.items():
info_notificacion = notif_map.get(ficha)
if info_notificacion:
final_results.append({"info_notificacion": info_notificacion, "detalles_codigos": matched_details})
return final_results
@st.cache_data(show_spinner=False)
def search_with_gemini(query, event_list):
if not client: return "Error: Modelo Gemini no disponible."
system_prompt = f"""
Actúa como clasificador CIE-10/CIE-11 para Colombia.
Lista de eventos oficiales: {'; '.join(event_list)}
Consulta: "{query}"
Instrucción: Retorna ÚNICAMENTE el nombre exacto del evento de la lista que mejor corresponda semánticamente (ej: 'mordedura culebra' -> 'Accidente Ofídico').
Si hay varios posibles, sepáralos con ';'. Si no hay coincidencia clara, responde "NO_ENCONTRADO".
"""
try:
# NUEVA SINTAXIS DE LLAMADA
response = client.models.generate_content(
model='gemini-2.5-flash-lite',
contents=system_prompt
)
result_text = response.text.strip()
if "NO_ENCONTRADO" in result_text: return []
return [name.strip() for name in result_text.split(';') if name.strip()]
except Exception as e:
if "429" in str(e):
return "Límite de API alcanzado."
return f"Error API: {e}"
@st.cache_data(show_spinner=False)
def analyze_query_with_gemini(query, definition, evento_name, ficha_number):
if not client: return "Error: Modelo Gemini no disponible."
analysis_prompt = f"""
Rol: Auditor SIVIGILA.
Evento: "{evento_name}" (Ficha {ficha_number}).
Definición Caso: "{definition}"
Consulta: "{query}"
Tarea: Compara la consulta con la definición.
Responde ESTRICTAMENTE en este formato:
**Decisión:** [NOTIFICAR | NO CUMPLE CRITERIOS]
**Justificación:** [Explica brevísimamente qué criterios cumple o faltan]
**Recomendación:** [Si NOTIFICAR: indicar proceder con ficha {ficha_number}. Si NO: revisar guías INS.]
"""
try:
# NUEVA SINTAXIS DE LLAMADA
response = client.models.generate_content(
model='gemini-2.5-flash-lite',
contents=analysis_prompt
)
return response.text
except Exception as e:
if "429" in str(e):
return "Límite de consultas para análisis alcanzado."
return f"Error durante el análisis de IA: {e}"
# --- NUEVA FUNCIÓN CON FRAGMENTO PARA EVITAR EL SALTO DE PESTAÑA ---
@st.fragment
def seccion_analisis_ia(info, query):
"""
Esta función maneja el botón y la visualización del análisis de forma aislada.
Al usar @st.fragment, cuando se hace clic, solo se recarga esta parte,
evitando que las pestañas (tabs) se reinicien.
"""
definicion_de_caso = info.get("Definición de caso", "").strip()
ficha = info.get("FICHA", "N/A")
evento = info.get("Evento", "N/A")
if not definicion_de_caso:
st.warning("No hay definición de caso para analizar.")
return
# Usamos una clave única en session_state para guardar el resultado
key_analisis = f"analisis_ia_result_{ficha}"
if st.button(f"Analizar caso con IA para ficha {ficha}", key=f"btn_analisis_{ficha}"):
with st.spinner("Analizando..."):
resultado = analyze_query_with_gemini(query, definicion_de_caso, evento, ficha)
st.session_state[key_analisis] = resultado
if key_analisis in st.session_state:
st.markdown(st.session_state[key_analisis])
# -------------------------------------------------------------------
col_img, col_text = st.columns([1, 5], gap="medium")
with col_img: st.image("buho.png", width=100)
with col_text:
st.title("Plataforma Inteligente SIVIGILA")
st.markdown("Herramienta experimental de apoyo para la notificación de eventos de salud pública en Colombia actualizado a CIE 10 - CIE 11. por Diana Soler, Jairo Alexander")
# COMENTAMOS LA CREACIÓN DE PESTAÑAS PRINCIPALES
# tab_individual, tab_hc, tab_db, tab_sitrep = st.tabs(["Búsqueda Individual", "Análisis por HC (pdf)", "Conexión a bases de datos", "SITREP"])
# === INICIO CONTENIDO DE BÚSQUEDA INDIVIDUAL (SIN TAB) ===
st.header("Consulta de Evento Específico")
if 'search_results' not in st.session_state:
st.session_state.search_results = None
if 'last_query' not in st.session_state:
st.session_state.last_query = ""
def clear_search_state():
st.session_state.search_results = None
st.session_state.last_query = ""
# Limpieza profunda de análisis previos
keys_to_delete = [k for k in st.session_state.keys() if k.startswith("analisis_ia_result_")]
for k in keys_to_delete:
del st.session_state[k]
with st.form(key="search_form"):
query = st.text_input("Ingrese su búsqueda:", placeholder="Ej: Sereno, Lepra, T630, mordedura de serpiente...", help="Busque por nombre, sinónimo o código.")
submitted = st.form_submit_button("Buscar", type="primary")
if submitted:
if not query:
st.warning("Por favor, ingrese un término de búsqueda.")
elif data_codigos is None:
st.error("Los datos no se pudieron cargar.")
else:
# Limpiar resultados anteriores al hacer una nueva búsqueda
keys_to_delete = [k for k in st.session_state.keys() if k.startswith("analisis_ia_result_")]
for k in keys_to_delete:
del st.session_state[k]
with st.spinner("Buscando..."):
results = []
# 1. Búsqueda exacta (Coste: 0 tokens)
direct_results = search_direct(query, data_codigos, notificacion_map)
if direct_results:
results = direct_results
else:
# 2. Búsqueda Local Difusa (Fuzzy) (Coste: 0 tokens)
event_names = search_local_fuzzy(query, nombres_eventos_limpios)
# 3. Solo si Fuzzy falla, llamar a Gemini (Coste: Tokens, pero con caché)
if not event_names:
gemini_response = search_with_gemini(query, nombres_eventos_limpios)
if isinstance(gemini_response, list):
event_names = gemini_response
elif isinstance(gemini_response, str):
if "Error" in gemini_response or "Límite" in gemini_response:
st.warning(f"IA: {gemini_response}")
event_names = []
if event_names:
fichas_encontradas = {item['FICHA'] for name in event_names for item in data_notificacion if item['Evento'] == name}
query_keywords = [kw for kw in query.lower().split() if len(kw) > 2]
for ficha in fichas_encontradas:
info_notif = notificacion_map.get(ficha)
all_details_for_ficha = [d for d in data_codigos if d.get("FICHA") == ficha]
# Filtro visual simple
filtered_details = [detail for detail in all_details_for_ficha if any(keyword in detail.get("NOMBRES DE PROTOCOLOS", "").lower() for keyword in query_keywords)] if query_keywords else []
final_details = filtered_details if filtered_details else all_details_for_ficha
if info_notif:
results.append({"info_notificacion": info_notif, "detalles_codigos": final_details})
st.session_state.search_results = results
st.session_state.last_query = query
if st.session_state.search_results is not None:
st.button("Limpiar Búsqueda", on_click=clear_search_state)
st.markdown("---")
results = st.session_state.search_results
if not results:
st.info(f"No se encontraron eventos. Intente reformular su búsqueda: '{st.session_state.last_query}'.")
else:
if results[0].get("info_notificacion", {}).get("is_informative"):
st.success(f"Resultado informativo: '{st.session_state.last_query}'")
else:
st.success(f"Se encontraron {len(results)} evento(s): '{st.session_state.last_query}'")
for result in results:
info = result["info_notificacion"]
codigos = result["detalles_codigos"]
if info.get("is_informative"):
with st.expander(f"**Término Informativo: {info.get('Evento', 'N/A')}**", expanded=True):
st.markdown("**Este término corresponde a una creencia cultural y NO es un evento de notificación obligatoria.**")
st.markdown("---")
st.markdown(f"**Explicación:** {info.get('PROTOCOLO', 'No disponible.')}")
st.markdown(f"**CIE-10:** `{codigos[0].get('CIE 10', 'N/A')}` | **CIE-11:** `{codigos[0].get('CIE 11', 'N/A')}`")
else:
with st.expander(f"**Evento: {info.get('Evento', 'Sin nombre')} (Ficha: {info.get('FICHA', 'N/A')})**", expanded=True):
tab_info, tab_def, tab_analysis = st.tabs(["Información General", "📖 Definición de Caso", "🤖 Análisis por IA"])
with tab_info:
st.subheader("Información de Notificación")
notif_super = info.get("Notificación superinmedita", "NO") == "SI"
if notif_super: st.error(f"**¡ATENCIÓN! Requiere Notificación SUPERINMEDIATA:** {info.get('Descripción superinmedita', '')}")
elif info.get("Notificación Inmediata", "NO") == "SI": st.warning("**Requiere Notificación Inmediata (dentro de las 24 horas).**")
else: st.info("**Notificación Semanal Individual.**")
col1, col2 = st.columns(2)
col1.markdown(f"**Fichas:** {info.get('Fichas a Utilizar', 'No especificado')}")
col2.markdown(f"**Requisito:** {info.get('REQUISITO', 'No especificado')}")
st.markdown("---")
st.caption("Para ver detalles de edad, clasificación y condición final, consulte los protocolos completos del INS.")
st.subheader("Códigos Relacionados")
for codigo in codigos[:5]:
st.markdown(f"- `{codigo.get('CIE 10', 'N/A')}` / `{codigo.get('CIE 11', 'N/A')}`: {codigo.get('NOMBRES DE PROTOCOLOS', 'N/A')}")
if len(codigos) > 5: st.caption(f"... y {len(codigos)-5} códigos más.")
with tab_def:
texto_caso = info.get("Definición de caso", "No disponible.").strip()
# 1. Truco para mejorar las listas: forzamos saltos de línea antes de viñetas comunes
# Si tus datos usan otros símbolos como '•' o '', agrégalos aquí.
texto_caso = texto_caso.replace("•", "\n- ").replace("", "\n- ").replace("- ", "\n- ")
# 2. Renderizamos usando HTML para poder usar "text-align: justify"
# replace('\n', '<br>') asegura que los saltos de línea del JSON se respeten
st.markdown(
f"""
<div style="text-align: justify; font-size: 16px; line-height: 1.6; padding-right: 10px;">
{texto_caso.replace(chr(10), '<br>')}
</div>
""",
unsafe_allow_html=True
)
with tab_analysis:
# LLAMADA A LA FUNCIÓN FRAGMENTADA - SOLUCIÓN DEL SALTO
seccion_analisis_ia(info, st.session_state.last_query)
# === SECCIONES COMENTADAS ===
# with tab_hc:
# st.header("Análisis por Historia Clínica (PDF)")
# st.warning("🚧 Esta funcionalidad se encuentra actualmente en desarrollo.")
# with tab_db:
# st.header("Conexión a Bases de Datos")
# st.warning("🚧 Esta funcionalidad se encuentra actualmente en desarrollo.")
# with tab_sitrep:
# st.header("SITREP (Situation Report)")
# st.warning("🚧 Esta funcionalidad se encuentra actualmente en desarrollo.") |