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)