Spaces:
Sleeping
Sleeping
| # ==================== 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() | |
| 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() | |
| 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) |