Pipeline de análisis automático de CVs para la Cámara de Estaciones de Servicio y Garajes de Rosario. De 15 minutos de lectura manual a 45 segundos de análisis inteligente por postulante.
CESGAR conecta postulantes con empresas del sector energético-automotriz de Rosario. Con decenas de CVs por semana y criterios de selección complejos, la automatización era imperativa.
El sistema centraliza todas las operaciones de CESGAR: gestión de asociados, noticias, trámites, beneficios y — el componente más innovador — una bolsa de trabajo con screening automático de CVs por IA.
Cuando un postulante sube su CV al formulario web, una señal Django dispara una tarea Celery asíncrona que extrae texto del PDF, lo analiza con un LLM y aplica reglas determinísticas de negocio en Python para generar un puntaje objetivo de 1 a 5, con justificación textual lista para el equipo de RRHH.
El componente de mensajería masiva por WhatsApp complementa el sistema, permitiendo comunicaciones institucionales con alta tasa de apertura (87%) en reemplazo del email tradicional.
# Arquitectura del pipeline de IA Formulario web │ postulante sube CV ▼ Signal post_save (Django) │ dispara tarea async ▼ Celery Worker │ ├─ PDFProcessor │ └─ OCR si es escaneado │ ├─ DeepSeek LLM │ └─ extracción JSON estructurada │ └─ evaluación holística 1-5 │ └─ cv_scoring.py (Python puro) └─ reglas determinísticas └─ descartes automáticos └─ bonificaciones por sector Panel Admin RRHH └─ puntaje + reporte + ranking
Cada componente resuelve un problema concreto del flujo operativo de la cámara.
DeepSeek LLM analiza el PDF del postulante y extrae campos clave: fecha de nacimiento, domicilio, nivel educativo, experiencias laborales con duración, disponibilidad horaria y capacitaciones. Incluye fallback OCR para documentos escaneados.
DeepSeek · OCR · JSONEl LLM asigna una valoración cualitativa holística. Python aplica reglas determinísticas: descarte automático, 5 módulos ponderados, bonos por sector y parámetros configurables por empresa. Resultado: puntaje 1–5 auditable.
Determinístico · Auditable · ConfigurablePlantillas aprobadas por Meta con editor visual, variables personalizables y vista previa en tiempo real. Envío asíncrono a contactos etiquetados con tracking individual por mensaje. Tipos: texto, call-to-action y quick reply.
Twilio · Celery · Tracking en tiempo realCada estación asociada puede especificar criterios propios: zona geográfica, rango etario, experiencia excluyente (debe tener) o prohibida (no debe tener). El mismo motor de scoring produce resultados adaptados a cada contexto.
Multi-tenant · Sin códigoEl equipo ve candidatos ya puntuados y clasificados, con resumen textual generado por el LLM en lenguaje natural. Acceso al detalle por módulo para auditar cualquier puntaje. Reprocesamiento con un clic.
Django Unfold · Admin personalizadoLas reglas de descarte automático se ejecutan en Python, no en el LLM. Garantía del 100% de consistencia: la alucinación del modelo no puede afectar una decisión de descarte.
Cero alucinaciones en descartesArquitectura orientada a costo-eficiencia, confiabilidad y mantenibilidad para un equipo pequeño.
# Señal que desacopla el guardado del análisis con IA # El formulario web responde inmediatamente; el análisis ocurre en background @receiver(post_save, sender=SolicitudEmpleoSimple) def al_guardar_solicitud(sender, instancia, creado, **kwargs): if creado and instancia.cv_r2: analizar_cv.delay(instancia.id) # no bloquea el request HTTP @shared_task def analizar_cv(id_solicitud: int, **parametros): solicitud = SolicitudEmpleoSimple.objects.get(id=id_solicitud) # Leer CV desde R2 con BytesIO (evita fallas en contexto Celery) buffer = BytesIO() with solicitud.cv_r2.open("rb") as archivo: buffer.write(archivo.read()) buffer.seek(0) datos = procesador.procesar_pdf(buffer, "solicitud_simple") puntaje, reporte = calcular_puntaje(datos, **parametros) solicitud.puntaje = puntaje solicitud.reporte = reporte solicitud.save()
# Módulo de educación — patrón aplicado a los 5 módulos # Retorno -1 señaliza descarte automático al orquestador def _modulo_educacion(educacion: dict | None) -> tuple[float, dict]: detalle = {"puntaje": 0, "peso": PESO_EDUCACION} if not educacion: return 0.0, detalle nivel = educacion.get("nivel", "") estado = educacion.get("estado", "") if nivel == "primario" or (nivel == "secundario" and estado != "completo"): return -1.0, detalle # descarte: no puede compensarse puntaje = 100 if estado == "completo" else 60 return puntaje * PESO_EDUCACION, {**detalle, "puntaje": puntaje} def calcular_puntaje(datos: dict, **params) -> tuple[int, str]: item = datos["items"][0] puntaje_llm = item.get("puntaje_final", 3) if item.get("descartado") or _descarte_por_params(item, params): return 0, reporte_descarte(item) mapa = {1: 10, 2: 30, 3: 50, 4: 70, 5: 90} base = mapa[puntaje_llm] base += _bonus_atencion_publica(item.get("experiencias", [])) base += _bonus_comida_rapida(item.get("experiencias", [])) escala = _mapear_escala_5(min(100, base)) return escala, generar_reporte(base, escala, item)
# Envío masivo con fallos parciales aislados # Un mensaje fallido NO cancela toda la campaña @shared_task def enviar_campaña_masiva(id_envio: int): envio = Envio.objects.get(id=id_envio) contactos = set(envio.contactos.all()) for etiqueta in envio.etiquetas.all(): contactos.update(etiqueta.contactos.all()) servicio = ServicioTwilio() for contacto in contactos: envio.refresh_from_db() # verificar cancelación en tiempo real if envio.estado == "cancelado": return try: msg = servicio.enviar_plantilla(contacto, envio.plantilla) MensajeEnvio.objects.create( envio=envio, contacto=contacto, twilio_sid=msg.sid, estado=msg.status or "queued" ) envio.enviados += 1 except TwilioRestException as exc: MensajeEnvio.objects.create( envio=envio, contacto=contacto, estado="failed", codigo_error=str(exc.code), mensaje_error=str(exc.msg) ) envio.fallidos += 1 envio.save() time.sleep(0.1) # respetar rate limit Twilio: ~10 msg/s envio.estado = "completado" envio.completado_en = timezone.now() envio.save()
Estimaciones basadas en benchmarks del sector HR Tech en Latinoamérica.
| Métrica | Proceso manual | Con IA | Mejora |
|---|---|---|---|
| Tiempo de análisis por CV | 15 min | < 45 seg | -94% |
| CVs analizados/día (1 analista) | 32 | 280+ | ×8.8 |
| Consistencia en criterios | ~70% | 100% | +30 puntos porcentuales |
| Tiempo hasta primera respuesta | 48–72 horas | < 2 horas | -96% |
| Costo por CV analizado | $12 USD | $0.80 USD | -93% |
| Falsos positivos (no idóneos convocados) | ~35% | < 8% | -77% |
| Canal | Email (anterior) | Mejora | |
|---|---|---|---|
| Tasa de apertura | 22% | 87% | +295% |
| Tiempo de lectura promedio | 4.2 horas | 8 minutos | -97% |
| Tasa de respuesta | 3% | 38% | +1.167% |
Visualizaciones que complementan el informe técnico en el portfolio.
Flujo completo: Formulario → Signal → Celery → PDFProcessor → DeepSeek → cv_scoring.py → Admin RRHH, con servicios externos (R2, Twilio).
Gráfico comparativo antes/después para tiempo de screening, consistencia y capacidad, con tabla de impacto económico y ROI 8:1 destacado.
Diagrama de decisión del cv_scoring.py: árbol de descartes automáticos, 5 módulos ponderados, bonificaciones aditivas y mapeo a escala 1–5.
Diseño e implemento sistemas de IA aplicada a procesos de negocio reales: automatización de RRHH, análisis de documentos, integración de APIs y comunicación omnicanal.