Spaces:
Sleeping
Sleeping
import gradio as gr | |
from google.adk.agents import Agent | |
import os | |
from google.adk.sessions import InMemorySessionService | |
from google.adk.runners import Runner | |
from google.adk.agents import LlmAgent | |
from google.genai import types | |
from typing import Dict, Any | |
from google.adk.models.lite_llm import LiteLlm | |
import uuid | |
import asyncio | |
from datetime import datetime | |
def obtener_fecha_ddmmaaaa(): | |
return datetime.today().strftime("%d/%m/%Y") | |
#download_files_from_drive_tool = FunctionTool(func=download_files_from_drive) | |
# --- Definición de Mod | |
# Definir función para el Agente Revisor | |
# Envolver funciones como herramientas de ADK | |
# Es importante que el nombre de la herramienta sea el mismo que el nombre de la función para el ADK | |
instruction = f"""Eres un auditor médico especializado en la revisión administrativa de órdenes médicas. Tu función es verificar el cumplimiento de requisitos documentales según normativas vigentes. Fecha actual de referencia: **26 de junio de 2025**. | |
Hoy es {obtener_fecha_ddmmaaaa()} | |
## Instrucciones de Procesamiento | |
**IMPORTANTE**: Responde SIEMPRE en español. Analiza únicamente documentos que contengan órdenes médicas. Si el usuario te envia texto, respondele amablemente que te envie las imagenes. | |
## Criterios de Verificación Obligatorios | |
### 0. Validación del Plan del Afiliado (OBLIGATORIO) | |
Siempre debes invocar la herramienta `validar_plan_de_afiliado`. Esta verificación es **obligatoria** y se realiza **aunque todos los demás requisitos estén correctos**. | |
- El valor de entrada debe ser siempre: `"Plata"`, **independientemente del texto en el documento**. | |
- Solo puedes invocar esta herramienta **una única vez** | |
- Recibiras una unica orden. | |
⚠️ **Si no ejecutas esta herramienta, el análisis se considerará incompleto.** | |
--- | |
### 1. Datos del Paciente | |
- **Nombre y apellido completos**: deben estar claramente legibles y sin ambigüedades o abreviaturas confusas. | |
### 2. Información Temporal | |
- **Fecha de emisión**: debe estar presente, legible y dentro de los **últimos 60 días** desde la fecha actual. | |
- Formatos válidos: DD/MM/AAAA, DD-MM-AAAA o fecha escrita completa. | |
- Si la fecha está fuera del rango la orden debe ser rechazada. | |
- Si la fecha es futura la orden debe ser rechazada. | |
### 3. Identificación Profesional | |
- **Número de credencial o matrícula médica**: debe estar presente y ser legible. Validar que corresponda a una credencial médica. | |
### 4. Diagnóstico | |
- **Obligatorio**. Debe estar explícitamente mencionado. Si **falta**, la orden debe ser rechazada sin excepciones. | |
### 5. Práctica solicitada | |
- **Obligatoria**. Debe estar claramente especificada. Si **falta**, la orden debe ser rechazada sin excepciones. | |
### 6. Indicaciones médicas | |
- **Este es un criterio obligatorio y excluyente**. | |
- Debe haber un **detalle clínico que justifique la práctica solicitada**. | |
- ⚠️ **NO se debe asumir, inferir o deducir información si las indicaciones no están presentes explícitamente**. | |
- Si **no hay indicaciones escritas o son insuficientes**, **la orden debe ser RECHAZADA automáticamente**, sin importar el resto del contenido. | |
- Si no se menciona la palabra 'Indicaciones' **la orden debe ser RECHAZADA automáticamente**, sin importar el resto del contenido. | |
✅ Ejemplo válido: “Dolor lumbar irradiado a miembro inferior izquierdo desde hace 2 meses. No responde a medicación. Se solicita resonancia lumbar.” | |
❌ Ejemplo inválido: “Resonancia de columna lumbar”, sin ningún detalle clínico. | |
--- | |
### 7. Firma / Validación Legal | |
- Debe incluir la **firma del profesional**, y esta debe ser legible. | |
- La firma debe permitir identificar al profesional responsable. | |
--- | |
## Formato de Respuesta | |
### Si la orden es ACEPTADA: | |
**"Orden aceptada. Se revisará con auditoría médica y se notificarán los resultados."** | |
**VERIFICACIÓN COMPLETADA:** | |
- ✅ Plan de afiliado: Plan **Plata** tiene cobertura para [insertar práctica solicitada] | |
- ✅ Nombre y apellido: Presente | |
- ✅ Fecha: Presente y dentro del rango válido | |
- ✅ Número de credencial: Presente | |
- ✅ Diagnóstico: Presente | |
- ✅ Práctica solicitada: Presente | |
- ✅ Indicaciones: Presentes y justificadas clínicamente | |
- ✅ Firma: Presente | |
--- | |
### Si la orden es RECHAZADA: | |
**"Orden rechazada por los siguientes motivos:"** | |
**VERIFICACIÓN COMPLETADA:** | |
- [Lista detallada con el estado de cada criterio] | |
- **ELEMENTOS FALTANTES O DEFICIENTES:** | |
- [Descripción clara del problema encontrado] | |
- [Por qué este elemento es obligatorio] | |
--- | |
## Criterios de Calidad | |
- **Legibilidad**: Todo debe ser legible y comprensible. | |
- **Coherencia**: Fechas realistas. Datos consistentes. | |
- **Completitud**: No se aceptan elementos vacíos, incompletos o poco claros. | |
- **No se debe completar con supuestos o inferencias. Todo dato debe estar explícito.** | |
--- | |
## Protocolo de Error | |
En caso de documentos no procesables (archivo corrupto, ilegible o no es una orden médica): | |
**"No se puede procesar el documento. Verifique que sea un archivo válido que contenga una orden médica."** | |
""" | |
def validar_plan_de_afiliado(plan: str, practica: str) -> Dict[str, Any]: | |
""" | |
Verifica si un plan de afiliado específico cubre una práctica médica determinada. | |
Parámetros: | |
- plan (str): El nombre del plan del afiliado (por ejemplo, "Plata"). | |
- practica (str): El nombre de la práctica médica a validar (por ejemplo, "Resonancia de Columna Lumbar"). | |
Retorna: | |
- Dict[str, Any]: Un diccionario con el resultado de la validación. Contiene: | |
- "status" (str): Estado de la validación ("success" o "error"). | |
- "comment" (str): Comentario descriptivo sobre el resultado de la validación. | |
""" | |
return { | |
"status": "success", | |
"comment": f"Validación correcta. El Plan Plata tiene cobertura para la práctica Resonancia de Columna Lumbar." | |
} | |
APP_NAME = "predoc_app" | |
# Definir agente raíz | |
root_agent = LlmAgent( | |
model=LiteLlm(model="openai/gpt-4.1"), | |
generate_content_config=types.GenerateContentConfig(temperature=0.0), | |
name="PreDoc", | |
instruction=instruction, | |
description="Asistente médico para renovación de recetas", | |
tools=[validar_plan_de_afiliado] | |
) | |
session_service = InMemorySessionService() | |
# Esta función se conecta a ChatInterface | |
def respond(message, history): | |
# Detectar si es inicio de conversación (history vacío o con 0 mensajes) | |
print("HISTORY",history) | |
if history is None or len(history) == 0: | |
user_id = str(uuid.uuid4()) | |
session_id = str(uuid.uuid4()) | |
print(f"🔄 Nueva sesión: {session_id}") | |
async def create_session(): | |
await session_service.create_session( | |
app_name=APP_NAME, | |
user_id=user_id, | |
session_id=session_id | |
) | |
asyncio.run(create_session()) | |
# Guardar en algún lado user_id y session_id para usar en las próximas llamadas | |
# Por simplicidad acá lo guardamos en variables globales | |
global CURRENT_USER_ID, CURRENT_SESSION_ID | |
CURRENT_USER_ID = user_id | |
CURRENT_SESSION_ID = session_id | |
else: | |
# Usar IDs existentes | |
user_id = CURRENT_USER_ID | |
session_id = CURRENT_SESSION_ID | |
runner = Runner(agent=root_agent, app_name=APP_NAME, session_service=session_service) | |
def call_agent_text(query): | |
content = types.Content(role='user', parts=[types.Part(text=query)]) | |
events = runner.run(user_id=user_id, session_id=session_id, new_message=content) | |
for event in events: | |
if event.is_final_response(): | |
return event.content.parts[0].text | |
return "No se obtuvo respuesta." | |
def call_agent_image(query): | |
images = [] | |
for q in query: | |
with open(q, 'rb') as f: | |
image_bytes = f.read() | |
images.append(types.Part.from_bytes(data=image_bytes, mime_type='image/jpeg')) | |
content = types.Content(role='user', parts=images) | |
events = runner.run(user_id=user_id, session_id=session_id, new_message=content) | |
for event in events: | |
if event.is_final_response(): | |
return event.content.parts[0].text | |
return "No se obtuvo respuesta." | |
def call_agent_both(image, text): | |
with open(image[0], 'rb') as f: | |
image_bytes = f.read() | |
content = types.Content( | |
role='user', | |
parts=[ | |
types.Part.from_bytes(data=image_bytes, mime_type='image/jpeg'), | |
types.Part(text=text) | |
] | |
) | |
events = runner.run(user_id=user_id, session_id=session_id, new_message=content) | |
for event in events: | |
if event.is_final_response(): | |
return event.content.parts[0].text | |
return "No se obtuvo respuesta." | |
# Dispatcher | |
if message['text'] != '' and len(message['files']) > 0: | |
return call_agent_both(message['files'], message['text']) | |
elif message['text'] == '' and len(message['files']) > 0: | |
return call_agent_image(message['files']) | |
elif message['text'] != '' and len(message['files']) == 0: | |
return call_agent_text(message['text']) | |
else: | |
return "Escribe algo para que pueda contestarte." | |
# Inicializamos demo sin el argumento state | |
demo = gr.ChatInterface(fn=respond, title="Agente Revisor", multimodal=True) | |
demo.launch(debug=True) | |