Spaces:
Sleeping
Sleeping
File size: 17,133 Bytes
8b414e4 abab40a ec9da27 8b414e4 2108de0 21a807d 2f59c3d 93dbc98 e0ecd33 2f59c3d e4f7e04 74bb11e e4f7e04 53a4ac5 7de5686 93dbc98 2f59c3d 53a4ac5 c52e98f 2f59c3d 2108de0 2f59c3d 2108de0 2f59c3d 7de5686 2f59c3d 2108de0 2f59c3d 2108de0 b72b5ea 2f59c3d b72b5ea 2f59c3d 2108de0 2f59c3d 21a807d 2f59c3d 28a4514 2f59c3d 7de5686 2f59c3d 3502efb 7de5686 3502efb 7de5686 2f59c3d 3502efb 2f59c3d 3502efb 2f59c3d 3502efb 2f59c3d ea9d0eb 2f59c3d ea9d0eb 2f59c3d ea9d0eb 3502efb ea9d0eb 3502efb ea9d0eb 2f59c3d ea9d0eb 2f59c3d 3502efb 2f59c3d 40736f3 2f59c3d 3502efb 2f59c3d 3502efb 2f59c3d 3502efb 2f59c3d 3502efb 2f59c3d bde368b 2f59c3d 2108de0 2f59c3d 3502efb 2f59c3d 2108de0 2f59c3d 3502efb bde368b 3502efb 01fb88d c83d53a 93dbc98 7de5686 93dbc98 7de5686 93dbc98 7de5686 93dbc98 3502efb 93dbc98 3502efb 7de5686 301a79a 3502efb a7da8ed ef010e2 a7da8ed ef010e2 d5cfc55 ef010e2 bde368b ef010e2 3502efb e4f7e04 3502efb ef010e2 7de5686 ef010e2 7de5686 3502efb 7de5686 3502efb 7de5686 3502efb 93dbc98 7de5686 3502efb 7de5686 ef010e2 7de5686 a7da8ed 40736f3 3502efb a7da8ed ef010e2 a7da8ed 93dbc98 6f1d7ce 3502efb 6f1d7ce 3502efb 6f1d7ce 3502efb 6f1d7ce 3502efb 28a4514 3502efb 28a4514 3502efb 7de5686 93dbc98 c83d53a 7de5686 93dbc98 c83d53a | 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 | # ==================== Buscador de Eventos para Notificación SIVIGILA - Colombia 2025 =====================================
# JAIRO ALEXANDER ERASO MD U Nacional de Colombia.
# DIANA MILENA SOLER MARTINEZ Psc.U Juan N. Corpas, U. Bosque.
import streamlit as st
import google.generativeai as genai
import google.api_core.exceptions
import os
import json
import logging
import datetime
st.set_page_config(
page_title="Buscador Inteligente SIVIGILA Colombia",
page_icon="buho.png",
layout="wide"
)
# Configurar logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("sivigila_app")
# --- CONFIGURACIÓN DE GEMINI ---
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 tu archivo .env o en los Secrets.")
logger.error("GEMINI_API_KEY no encontrada.")
st.stop()
genai.configure(api_key=GEMINI_API_KEY)
logger.info("✅ Configuración de Gemini API realizada.")
except Exception as e:
st.error(f"❌ Error al configurar Gemini API: {e}")
logger.error(f"Error al configurar Gemini API: {e}")
st.stop()
@st.cache_resource
def get_gemini_model():
logger.info("🔄 Cargando modelo Gemini (gemini-2.0-flash-lite)...")
try:
return genai.GenerativeModel("gemini-2.0-flash-lite")
except Exception as e:
st.error(f"❌ No se pudo cargar el modelo Gemini: {e}")
logger.error(f"Error cargando modelo Gemini: {e}")
return None
model = get_gemini_model()
@st.cache_data
def load_data():
"""Carga y procesa los archivos JSON, uniéndolos para un acceso rápido."""
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}
nombres_eventos_limpios = sorted(list(set(item['Evento'] for item in data_notificacion)))
logger.info(f"✅ Datos cargados.")
return data_codigos, data_notificacion, notificacion_map, nombres_eventos_limpios
except FileNotFoundError as e:
st.error(f"❌ Error: No se encontró el archivo {e.filename}. Asegúrate de que los archivos JSON estén en la carpeta 'PLANTILLAS'.")
return None, None, None, None
except json.JSONDecodeError as e:
st.error(f"❌ Error: El archivo JSON tiene un formato incorrecto: {e}")
return None, None, None, None
data_codigos, data_notificacion, notificacion_map, nombres_eventos_limpios = load_data()
# --- LÓGICA DE BÚSQUEDA ---
def search_direct(query, data, notif_map):
query_lower = query.lower().strip()
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:
if ficha not in grouped_by_ficha: grouped_by_ficha[ficha] = []
grouped_by_ficha[ficha].append(item)
final_results = []
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
def search_with_gemini(query, event_list):
if not model: return "Error: Modelo Gemini no disponible."
system_prompt = f"""
Eres un asesor experto en SIVIGILA, el sistema de vigilancia en salud pública de Colombia.
Tu tarea es interpretar la consulta de un profesional de la salud, que puede ser un término coloquial, un sinónimo o una descripción, y asociarla al nombre del evento oficial más relevante de una lista.
LISTA DE EVENTOS OFICIALES:
{'; '.join(event_list)}
Analiza la siguiente consulta y devuelve ÚNICAMENTE el nombre exacto del evento oficial de la lista que mejor corresponda.
Por ejemplo, si la consulta es 'mordedura de culebra', la respuesta debe ser 'Accidente Ofídico'.
Si encuentras varios, sepáralos con un punto y coma (;).
Si la consulta no corresponde a ningún evento de la lista, responde con "NO_ENCONTRADO".
No añadas explicaciones ni texto adicional.
Consulta del usuario: "{query}"
"""
try:
response = model.generate_content(system_prompt)
if response.parts:
result_text = response.text.strip()
if result_text == "NO_ENCONTRADO": return []
return [name.strip() for name in result_text.split(';') if name.strip()]
else:
return "Respuesta bloqueada por filtros de seguridad."
except google.api_core.exceptions.ResourceExhausted as e:
return "Se ha alcanzado el límite de consultas. Por favor, espere un minuto y vuelva a intentarlo."
except Exception as e:
return f"Error en la comunicación con la API de Gemini: {e}"
def analyze_query_with_gemini(query, definition, evento_name, ficha_number):
if not model: return "Error: Modelo Gemini no disponible."
analysis_prompt = f"""
Eres un auditor experto del sistema SIVIGILA de Colombia. Tu única misión es determinar si un caso descrito por un profesional de la salud cumple con los criterios de la definición de caso oficial para ser de notificación obligatoria.
**Reglas Estrictas:**
1. Tu decisión final solo puede ser "NOTIFICAR" o "NO CUMPLE CRITERIOS".
2. Debes basar tu justificación únicamente en la comparación entre la consulta y la definición de caso proporcionada.
3. No debes inventar información ni hacer suposiciones clínicas.
**CONTEXTO DEL EVENTO YA IDENTIFICADO:**
- Nombre del Evento: "{evento_name}"
- Ficha de Notificación: "{ficha_number}"
**CONSULTA DEL USUARIO:**
"{query}"
**DEFINICIÓN DE CASO OFICIAL:**
"{definition}"
**TU ANÁLISIS ESTRUCTURADO:**
Basado en la comparación, responde OBLIGATORIAMENTE en el siguiente formato, sin añadir texto adicional:
**Decisión:** [Escribe aquí "NOTIFICAR" o "NO CUMPLE CRITERIOS"]
**Justificación:**
[Si la decisión es NOTIFICAR, explica brevemente qué criterios de la definición de caso se cumplen en la consulta del usuario. Si la decisión es NO CUMPLE CRITERIOS, explica qué criterios clave faltan o se contradicen explícitamente.]
**Recomendación:**
[Si la decisión es NOTIFICAR, redacta una recomendación para proceder con la notificación, mencionando explícitamente el número de Ficha y el nombre del Evento proporcionados en el contexto. Si la decisión es NO CUMPLE CRITERIOS, escribe: "Se recomienda revisar las guías y protocolos del Instituto Nacional de Salud de Colombia para determinar el manejo adecuado del caso."]
"""
try:
response = model.generate_content(analysis_prompt)
if response.parts: return response.text
else: return "El análisis no pudo ser completado. La respuesta de la IA fue bloqueada por filtros de seguridad."
except google.api_core.exceptions.ResourceExhausted:
return "Se ha alcanzado el límite de consultas para el análisis. Por favor, espere un minuto y vuelva a intentarlo."
except Exception as e:
return f"Ocurrió un error durante el análisis de la IA: {e}"
# --- INTERFAZ DE USUARIO (UI) ---
col_img, col_text = st.columns([1, 5], gap="medium")
with col_img: st.image("buho.png", width=100)
with col_text:
st.title("Buscador Inteligente SIVIGILA")
st.markdown("Herramienta de apoyo para la notificación de eventos de salud pública en Colombia por Diana Soler")
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 = ""
with st.form(key="search_form"):
query = st.text_input("Ingrese su búsqueda:", placeholder="Ej: Lepra, T630, mordedura de culebra, inhalación de canela, niño de 4 años y 3 kilos..", help="Puede buscar por nombre de la enfermedad, palabras clave, o códigos CIE.")
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:
with st.spinner("Buscando con IA (Gemini)..."):
results = []
direct_results = search_direct(query, data_codigos, notificacion_map)
if not direct_results:
logger.info(f"Búsqueda directa para '{query}' no arrojó resultados. Usando IA (Gemini)...")
event_names = search_with_gemini(query, nombres_eventos_limpios)
if isinstance(event_names, str):
st.error(event_names)
event_names = []
if event_names:
fichas_encontradas = set()
for name in event_names:
for item in data_notificacion:
if item['Evento'] == name: fichas_encontradas.add(item['FICHA'])
query_keywords = [kw for kw in query.lower().split() if len(kw) > 2] # Keywords de la búsqueda original
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]
filtered_details = []
if query_keywords:
for detail in all_details_for_ficha:
protocol_name_lower = detail.get("NOMBRES DE PROTOCOLOS", "").lower()
if any(keyword in protocol_name_lower for keyword in query_keywords):
filtered_details.append(detail)
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
})
else:
results = direct_results
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("---")
if st.session_state.search_results is not None:
results = st.session_state.search_results
if not results:
st.info(f"No se encontraron eventos de notificación obligatoria para su búsqueda: '{st.session_state.last_query}'.")
else:
st.success(f"Se encontraron {len(results)} evento(s) relacionado(s) con su búsqueda: '{st.session_state.last_query}'")
for result in results:
info = result["info_notificacion"]
codigos = result["detalles_codigos"]
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.**")
st.markdown(f"**Requisito:** {info.get('REQUISITO', 'No especificado')}")
fichas_texto = info.get('Fichas a Utilizar', 'No especificado').replace('+', '\n+ ')
st.markdown(f"**Fichas a utilizar:** {fichas_texto}")
st.markdown("---")
st.subheader("Clasificación de Caso Permitida")
classification_fields=[("Sospechoso","Clasificación Permitida Sospechoso"),("Probable","Clasificación Permitida Probable"),("Conf. Clínica","Clasificación Permitida Conf. Clínica"),("Conf. Laboratorio","Clasificación Permitida Conf. Laboratorio"),("Conf. Nexo Epi.","Clasificación Permitida Conf. Nexo Epi."),("Descartado","Clasificación Permitida Descartado")]
col1,col2=st.columns(2)
for i,(display_name,field_key) in enumerate(classification_fields):
value=info.get(field_key,"NO")
icon="✅" if value=="SI" else "❌"
md_string=f"{icon} **{display_name}**"
if i<len(classification_fields)/2: col1.markdown(md_string)
else: col2.markdown(md_string)
st.markdown("---")
st.subheader("Rango de Edad Permitido para Notificación")
age_fields=[("Menor de 1 año","Menor de 1 año"),("De 1 a 4 años","De 1 a 4 años"),("De 5 a 14 años","De 5 a 14 años"),("De 15 a 44 años","De 15 a 44 años"),("De 45 a 59 años","De 45 a 59 años"),("De 60 y más","De 60 y más")]
age_col1,age_col2=st.columns(2)
for i,(display_name,field_key) in enumerate(age_fields):
value=info.get(field_key,"NO")
icon="✅" if value=="SI" else "❌"
md_string=f"{icon} **{display_name}**"
if i<len(age_fields)/2: age_col1.markdown(md_string)
else: age_col2.markdown(md_string)
st.markdown("---")
st.subheader("Condición Final Permitida")
final_condition_fields=[("Vivo","Condición final Vivo"),("Muerto","Condición final Muerto")]
cond_col1,cond_col2=st.columns(2)
for i,(display_name,field_key) in enumerate(final_condition_fields):
value=info.get(field_key,"NO")
icon="✅" if value=="SI" else "❌"
md_string=f"{icon} **{display_name}**"
if i==0: cond_col1.markdown(md_string)
else: cond_col2.markdown(md_string)
st.subheader("Códigos Relacionados (CIE-10 y CIE-11)")
for codigo in codigos:
st.markdown(f"""- **Nombre Protocolo:** `{codigo.get('NOMBRES DE PROTOCOLOS', 'N/A')}`\n - **CIE-10:** `{codigo.get('CIE 10', 'N/A')}`\n - **CIE-11:** `{codigo.get('CIE 11', 'N/A')}` | **Nombre CIE-11:** *{codigo.get('NOMBRES DE CIE 11', 'N/A')}*""")
with tab_def:
definicion_de_caso = info.get("Definición de caso", "").strip()
if definicion_de_caso: st.markdown(definicion_de_caso.replace('\n', ' \n'))
else: st.info("No se encontró una definición de caso oficial para este evento.")
with tab_analysis:
st.info("Este análisis compara su búsqueda original con la definición oficial del evento encontrado.")
definicion_de_caso_para_analisis = info.get("Definición de caso", "").strip()
if not definicion_de_caso_para_analisis:
st.warning("No se puede realizar el análisis porque no hay una definición de caso disponible para este evento.")
else:
with st.spinner("Realizando análisis con IA..."):
evento_name = info.get("Evento", "N/A")
ficha_number = info.get("FICHA", "N/A")
analysis_result = analyze_query_with_gemini(st.session_state.last_query, definicion_de_caso_para_analisis, evento_name, ficha_number)
st.markdown(analysis_result) |