Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
-
# ==================== Buscador de Eventos
|
|
|
|
| 2 |
|
| 3 |
# JAIRO ALEXANDER ERASO MD U Nacional de Colombia.
|
| 4 |
# DIANA MILENA SOLER MARTINEZ U Juan N. Corpas
|
|
@@ -12,8 +13,9 @@ import logging
|
|
| 12 |
import datetime
|
| 13 |
|
| 14 |
|
|
|
|
| 15 |
st.set_page_config(
|
| 16 |
-
page_title="Buscador Inteligente
|
| 17 |
page_icon="buho.png",
|
| 18 |
layout="wide"
|
| 19 |
)
|
|
@@ -26,7 +28,7 @@ logger = logging.getLogger("sivigila_app")
|
|
| 26 |
try:
|
| 27 |
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 28 |
if not GEMINI_API_KEY:
|
| 29 |
-
st.error("No se encontró la variable de entorno GEMINI_API_KEY. Por favor, configúrala en tu archivo .env.")
|
| 30 |
logger.error("GEMINI_API_KEY no encontrada.")
|
| 31 |
st.stop()
|
| 32 |
genai.configure(api_key=GEMINI_API_KEY)
|
|
@@ -54,12 +56,19 @@ def load_data():
|
|
| 54 |
try:
|
| 55 |
path_codigos = os.path.join('PLANTILLAS', 'DIC_NOMBRES_CODIGOS_FICHAS.json')
|
| 56 |
path_notificacion = os.path.join('PLANTILLAS', 'DATOS_NOTIFICACION.json')
|
| 57 |
-
|
| 58 |
-
with open(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
notificacion_map = {item['FICHA']: item for item in data_notificacion}
|
| 60 |
nombres_eventos_limpios = sorted(list(set(item['Evento'] for item in data_notificacion)))
|
| 61 |
-
|
|
|
|
| 62 |
return data_codigos, data_notificacion, notificacion_map, nombres_eventos_limpios
|
|
|
|
| 63 |
except FileNotFoundError as e:
|
| 64 |
st.error(f"❌ Error: No se encontró el archivo {e.filename}. Asegúrate de que los archivos JSON estén en la carpeta 'PLANTILLAS'.")
|
| 65 |
return None, None, None, None
|
|
@@ -125,57 +134,43 @@ def search_with_gemini(query, event_list):
|
|
| 125 |
|
| 126 |
|
| 127 |
def analyze_query_with_gemini(query, definition, evento_name, ficha_number):
|
| 128 |
-
"""
|
| 129 |
-
Usa la API de Gemini para analizar la consulta y emitir una recomendación
|
| 130 |
-
prescriptiva sobre la notificación.
|
| 131 |
-
"""
|
| 132 |
if not model: return "Error: Modelo Gemini no disponible."
|
| 133 |
-
|
| 134 |
analysis_prompt = f"""
|
| 135 |
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.
|
| 136 |
-
|
| 137 |
**Reglas Estrictas:**
|
| 138 |
-
1.
|
| 139 |
-
2.
|
| 140 |
-
3.
|
| 141 |
-
|
| 142 |
**CONTEXTO DEL EVENTO YA IDENTIFICADO:**
|
| 143 |
- Nombre del Evento: "{evento_name}"
|
| 144 |
- Ficha de Notificación: "{ficha_number}"
|
| 145 |
-
|
| 146 |
**CONSULTA DEL USUARIO:**
|
| 147 |
"{query}"
|
| 148 |
-
|
| 149 |
**DEFINICIÓN DE CASO OFICIAL:**
|
| 150 |
"{definition}"
|
| 151 |
-
|
| 152 |
**TU ANÁLISIS ESTRUCTURADO:**
|
| 153 |
Basado en la comparación, responde OBLIGATORIAMENTE en el siguiente formato, sin añadir texto adicional:
|
| 154 |
-
|
| 155 |
**Decisión:** [Escribe aquí "NOTIFICAR" o "NO CUMPLE CRITERIOS"]
|
| 156 |
-
|
| 157 |
**Justificación:**
|
| 158 |
[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.]
|
| 159 |
-
|
| 160 |
**Recomendación:**
|
| 161 |
[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."]
|
| 162 |
"""
|
| 163 |
try:
|
| 164 |
response = model.generate_content(analysis_prompt)
|
| 165 |
-
if response.parts:
|
| 166 |
-
|
| 167 |
-
else:
|
| 168 |
-
return "El análisis no pudo ser completado. La respuesta de la IA fue bloqueada por filtros de seguridad."
|
| 169 |
except google.api_core.exceptions.ResourceExhausted:
|
| 170 |
return "Se ha alcanzado el límite de consultas para el análisis. Por favor, espere un minuto y vuelva a intentarlo."
|
| 171 |
except Exception as e:
|
| 172 |
return f"Ocurrió un error durante el análisis de la IA: {e}"
|
| 173 |
|
|
|
|
| 174 |
# --- INTERFAZ DE USUARIO (UI) ---
|
| 175 |
col_img, col_text = st.columns([1, 5], gap="medium")
|
| 176 |
with col_img: st.image("buho.png", width=100)
|
| 177 |
with col_text:
|
| 178 |
-
st.title("Buscador Inteligente
|
| 179 |
st.markdown("Herramienta de apoyo para la notificación de eventos de salud pública en Colombia.")
|
| 180 |
|
| 181 |
if 'search_results' not in st.session_state: st.session_state.search_results = None
|
|
@@ -186,7 +181,7 @@ def clear_search_state():
|
|
| 186 |
st.session_state.last_query = ""
|
| 187 |
|
| 188 |
with st.form(key="search_form"):
|
| 189 |
-
query = st.text_input("Ingrese su búsqueda:", placeholder="Ej: Lepra, T630, mordedura de serpiente,
|
| 190 |
submitted = st.form_submit_button("Buscar", type="primary")
|
| 191 |
|
| 192 |
if submitted:
|
|
@@ -196,22 +191,44 @@ if submitted:
|
|
| 196 |
with st.spinner("Buscando con IA (Gemini)..."):
|
| 197 |
results = []
|
| 198 |
direct_results = search_direct(query, data_codigos, notificacion_map)
|
|
|
|
| 199 |
if not direct_results:
|
|
|
|
| 200 |
event_names = search_with_gemini(query, nombres_eventos_limpios)
|
|
|
|
| 201 |
if isinstance(event_names, str):
|
| 202 |
st.error(event_names)
|
| 203 |
event_names = []
|
|
|
|
| 204 |
if event_names:
|
| 205 |
fichas_encontradas = set()
|
| 206 |
for name in event_names:
|
| 207 |
for item in data_notificacion:
|
| 208 |
if item['Evento'] == name: fichas_encontradas.add(item['FICHA'])
|
|
|
|
|
|
|
|
|
|
| 209 |
for ficha in fichas_encontradas:
|
| 210 |
info_notif = notificacion_map.get(ficha)
|
| 211 |
-
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
else:
|
| 214 |
results = direct_results
|
|
|
|
| 215 |
st.session_state.search_results = results
|
| 216 |
st.session_state.last_query = query
|
| 217 |
|
|
@@ -228,12 +245,8 @@ if st.session_state.search_results is not None:
|
|
| 228 |
info = result["info_notificacion"]
|
| 229 |
codigos = result["detalles_codigos"]
|
| 230 |
with st.expander(f"**Evento: {info.get('Evento', 'Sin nombre')} (Ficha: {info.get('FICHA', 'N/A')})**", expanded=True):
|
| 231 |
-
|
| 232 |
-
# Se mantienen las 3 pestañas
|
| 233 |
tab_info, tab_def, tab_analysis = st.tabs(["Información General", "📖 Definición de Caso", "🤖 Análisis por IA"])
|
| 234 |
-
|
| 235 |
with tab_info:
|
| 236 |
-
# El contenido de esta pestaña no cambia
|
| 237 |
st.subheader("Información de Notificación")
|
| 238 |
notif_super = info.get("Notificación superinmedita", "NO") == "SI"
|
| 239 |
if notif_super: st.error(f"**¡ATENCIÓN! Requiere Notificación SUPERINMEDIATA:** {info.get('Descripción superinmedita', '')}")
|
|
@@ -275,7 +288,7 @@ if st.session_state.search_results is not None:
|
|
| 275 |
st.subheader("Códigos Relacionados (CIE-10 y CIE-11)")
|
| 276 |
for codigo in codigos:
|
| 277 |
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')}*""")
|
| 278 |
-
|
| 279 |
with tab_def:
|
| 280 |
definicion_de_caso = info.get("Definición de caso", "").strip()
|
| 281 |
if definicion_de_caso: st.markdown(definicion_de_caso.replace('\n', ' \n'))
|
|
@@ -284,15 +297,12 @@ if st.session_state.search_results is not None:
|
|
| 284 |
with tab_analysis:
|
| 285 |
st.info("Este análisis compara su búsqueda original con la definición oficial del evento encontrado.")
|
| 286 |
definicion_de_caso_para_analisis = info.get("Definición de caso", "").strip()
|
| 287 |
-
|
| 288 |
if not definicion_de_caso_para_analisis:
|
| 289 |
st.warning("No se puede realizar el análisis porque no hay una definición de caso disponible para este evento.")
|
| 290 |
else:
|
| 291 |
-
with st.spinner("Realizando análisis con IA..."):
|
| 292 |
|
| 293 |
-
|
| 294 |
evento_name = info.get("Evento", "N/A")
|
| 295 |
ficha_number = info.get("FICHA", "N/A")
|
| 296 |
-
|
| 297 |
analysis_result = analyze_query_with_gemini(st.session_state.last_query, definicion_de_caso_para_analisis, evento_name, ficha_number)
|
| 298 |
st.markdown(analysis_result)
|
|
|
|
| 1 |
+
# ==================== Buscador de Eventos de Notificación SIVIGILA - Colombia 2025 =====================================
|
| 2 |
+
# Versión API Web Final con Lógica de IA Mejorada y Segundo Filtro
|
| 3 |
|
| 4 |
# JAIRO ALEXANDER ERASO MD U Nacional de Colombia.
|
| 5 |
# DIANA MILENA SOLER MARTINEZ U Juan N. Corpas
|
|
|
|
| 13 |
import datetime
|
| 14 |
|
| 15 |
|
| 16 |
+
# --- CONFIGURACIÓN BÁSICA ---
|
| 17 |
st.set_page_config(
|
| 18 |
+
page_title="Buscador Inteligente SIVIGILA Colombia",
|
| 19 |
page_icon="buho.png",
|
| 20 |
layout="wide"
|
| 21 |
)
|
|
|
|
| 28 |
try:
|
| 29 |
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 30 |
if not GEMINI_API_KEY:
|
| 31 |
+
st.error("No se encontró la variable de entorno GEMINI_API_KEY. Por favor, configúrala en tu archivo .env o en los Secrets.")
|
| 32 |
logger.error("GEMINI_API_KEY no encontrada.")
|
| 33 |
st.stop()
|
| 34 |
genai.configure(api_key=GEMINI_API_KEY)
|
|
|
|
| 56 |
try:
|
| 57 |
path_codigos = os.path.join('PLANTILLAS', 'DIC_NOMBRES_CODIGOS_FICHAS.json')
|
| 58 |
path_notificacion = os.path.join('PLANTILLAS', 'DATOS_NOTIFICACION.json')
|
| 59 |
+
|
| 60 |
+
with open(path_codigos, 'r', encoding='utf-8') as f:
|
| 61 |
+
data_codigos = json.load(f)
|
| 62 |
+
|
| 63 |
+
with open(path_notificacion, 'r', encoding='utf-8') as f:
|
| 64 |
+
data_notificacion = json.load(f)
|
| 65 |
+
|
| 66 |
notificacion_map = {item['FICHA']: item for item in data_notificacion}
|
| 67 |
nombres_eventos_limpios = sorted(list(set(item['Evento'] for item in data_notificacion)))
|
| 68 |
+
|
| 69 |
+
logger.info(f"✅ Datos cargados.")
|
| 70 |
return data_codigos, data_notificacion, notificacion_map, nombres_eventos_limpios
|
| 71 |
+
|
| 72 |
except FileNotFoundError as e:
|
| 73 |
st.error(f"❌ Error: No se encontró el archivo {e.filename}. Asegúrate de que los archivos JSON estén en la carpeta 'PLANTILLAS'.")
|
| 74 |
return None, None, None, None
|
|
|
|
| 134 |
|
| 135 |
|
| 136 |
def analyze_query_with_gemini(query, definition, evento_name, ficha_number):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
if not model: return "Error: Modelo Gemini no disponible."
|
|
|
|
| 138 |
analysis_prompt = f"""
|
| 139 |
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.
|
|
|
|
| 140 |
**Reglas Estrictas:**
|
| 141 |
+
1. Tu decisión final solo puede ser "NOTIFICAR" o "NO CUMPLE CRITERIOS".
|
| 142 |
+
2. Debes basar tu justificación únicamente en la comparación entre la consulta y la definición de caso proporcionada.
|
| 143 |
+
3. No debes inventar información ni hacer suposiciones clínicas.
|
|
|
|
| 144 |
**CONTEXTO DEL EVENTO YA IDENTIFICADO:**
|
| 145 |
- Nombre del Evento: "{evento_name}"
|
| 146 |
- Ficha de Notificación: "{ficha_number}"
|
|
|
|
| 147 |
**CONSULTA DEL USUARIO:**
|
| 148 |
"{query}"
|
|
|
|
| 149 |
**DEFINICIÓN DE CASO OFICIAL:**
|
| 150 |
"{definition}"
|
|
|
|
| 151 |
**TU ANÁLISIS ESTRUCTURADO:**
|
| 152 |
Basado en la comparación, responde OBLIGATORIAMENTE en el siguiente formato, sin añadir texto adicional:
|
|
|
|
| 153 |
**Decisión:** [Escribe aquí "NOTIFICAR" o "NO CUMPLE CRITERIOS"]
|
|
|
|
| 154 |
**Justificación:**
|
| 155 |
[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.]
|
|
|
|
| 156 |
**Recomendación:**
|
| 157 |
[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."]
|
| 158 |
"""
|
| 159 |
try:
|
| 160 |
response = model.generate_content(analysis_prompt)
|
| 161 |
+
if response.parts: return response.text
|
| 162 |
+
else: return "El análisis no pudo ser completado. La respuesta de la IA fue bloqueada por filtros de seguridad."
|
|
|
|
|
|
|
| 163 |
except google.api_core.exceptions.ResourceExhausted:
|
| 164 |
return "Se ha alcanzado el límite de consultas para el análisis. Por favor, espere un minuto y vuelva a intentarlo."
|
| 165 |
except Exception as e:
|
| 166 |
return f"Ocurrió un error durante el análisis de la IA: {e}"
|
| 167 |
|
| 168 |
+
|
| 169 |
# --- INTERFAZ DE USUARIO (UI) ---
|
| 170 |
col_img, col_text = st.columns([1, 5], gap="medium")
|
| 171 |
with col_img: st.image("buho.png", width=100)
|
| 172 |
with col_text:
|
| 173 |
+
st.title("Buscador Inteligente SIVIGILA")
|
| 174 |
st.markdown("Herramienta de apoyo para la notificación de eventos de salud pública en Colombia.")
|
| 175 |
|
| 176 |
if 'search_results' not in st.session_state: st.session_state.search_results = None
|
|
|
|
| 181 |
st.session_state.last_query = ""
|
| 182 |
|
| 183 |
with st.form(key="search_form"):
|
| 184 |
+
query = st.text_input("Ingrese su búsqueda:", placeholder="Ej: Lepra, T630, mordedura de serpiente, 3MC...", help="Puede buscar por nombre de la enfermedad, palabras clave, o códigos CIE.")
|
| 185 |
submitted = st.form_submit_button("Buscar", type="primary")
|
| 186 |
|
| 187 |
if submitted:
|
|
|
|
| 191 |
with st.spinner("Buscando con IA (Gemini)..."):
|
| 192 |
results = []
|
| 193 |
direct_results = search_direct(query, data_codigos, notificacion_map)
|
| 194 |
+
|
| 195 |
if not direct_results:
|
| 196 |
+
logger.info(f"Búsqueda directa para '{query}' no arrojó resultados. Usando IA (Gemini)...")
|
| 197 |
event_names = search_with_gemini(query, nombres_eventos_limpios)
|
| 198 |
+
|
| 199 |
if isinstance(event_names, str):
|
| 200 |
st.error(event_names)
|
| 201 |
event_names = []
|
| 202 |
+
|
| 203 |
if event_names:
|
| 204 |
fichas_encontradas = set()
|
| 205 |
for name in event_names:
|
| 206 |
for item in data_notificacion:
|
| 207 |
if item['Evento'] == name: fichas_encontradas.add(item['FICHA'])
|
| 208 |
+
|
| 209 |
+
query_keywords = [kw for kw in query.lower().split() if len(kw) > 2] # Keywords de la búsqueda original
|
| 210 |
+
|
| 211 |
for ficha in fichas_encontradas:
|
| 212 |
info_notif = notificacion_map.get(ficha)
|
| 213 |
+
all_details_for_ficha = [d for d in data_codigos if d.get("FICHA") == ficha]
|
| 214 |
+
|
| 215 |
+
filtered_details = []
|
| 216 |
+
if query_keywords:
|
| 217 |
+
for detail in all_details_for_ficha:
|
| 218 |
+
protocol_name_lower = detail.get("NOMBRES DE PROTOCOLOS", "").lower()
|
| 219 |
+
if any(keyword in protocol_name_lower for keyword in query_keywords):
|
| 220 |
+
filtered_details.append(detail)
|
| 221 |
+
|
| 222 |
+
final_details = filtered_details if filtered_details else all_details_for_ficha
|
| 223 |
+
|
| 224 |
+
if info_notif:
|
| 225 |
+
results.append({
|
| 226 |
+
"info_notificacion": info_notif,
|
| 227 |
+
"detalles_codigos": final_details
|
| 228 |
+
})
|
| 229 |
else:
|
| 230 |
results = direct_results
|
| 231 |
+
|
| 232 |
st.session_state.search_results = results
|
| 233 |
st.session_state.last_query = query
|
| 234 |
|
|
|
|
| 245 |
info = result["info_notificacion"]
|
| 246 |
codigos = result["detalles_codigos"]
|
| 247 |
with st.expander(f"**Evento: {info.get('Evento', 'Sin nombre')} (Ficha: {info.get('FICHA', 'N/A')})**", expanded=True):
|
|
|
|
|
|
|
| 248 |
tab_info, tab_def, tab_analysis = st.tabs(["Información General", "📖 Definición de Caso", "🤖 Análisis por IA"])
|
|
|
|
| 249 |
with tab_info:
|
|
|
|
| 250 |
st.subheader("Información de Notificación")
|
| 251 |
notif_super = info.get("Notificación superinmedita", "NO") == "SI"
|
| 252 |
if notif_super: st.error(f"**¡ATENCIÓN! Requiere Notificación SUPERINMEDIATA:** {info.get('Descripción superinmedita', '')}")
|
|
|
|
| 288 |
st.subheader("Códigos Relacionados (CIE-10 y CIE-11)")
|
| 289 |
for codigo in codigos:
|
| 290 |
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')}*""")
|
| 291 |
+
|
| 292 |
with tab_def:
|
| 293 |
definicion_de_caso = info.get("Definición de caso", "").strip()
|
| 294 |
if definicion_de_caso: st.markdown(definicion_de_caso.replace('\n', ' \n'))
|
|
|
|
| 297 |
with tab_analysis:
|
| 298 |
st.info("Este análisis compara su búsqueda original con la definición oficial del evento encontrado.")
|
| 299 |
definicion_de_caso_para_analisis = info.get("Definición de caso", "").strip()
|
|
|
|
| 300 |
if not definicion_de_caso_para_analisis:
|
| 301 |
st.warning("No se puede realizar el análisis porque no hay una definición de caso disponible para este evento.")
|
| 302 |
else:
|
|
|
|
| 303 |
|
| 304 |
+
with st.spinner("Realizando análisis con IA..."):
|
| 305 |
evento_name = info.get("Evento", "N/A")
|
| 306 |
ficha_number = info.get("FICHA", "N/A")
|
|
|
|
| 307 |
analysis_result = analyze_query_with_gemini(st.session_state.last_query, definicion_de_caso_para_analisis, evento_name, ficha_number)
|
| 308 |
st.markdown(analysis_result)
|