Implementacion
Eventos
Tests
Despliegue
Handlers activos
3
Re-train ยท Slack ยท Audit
Scopes cubiertos
4 / 5
repo, repo.content, repo.config, discussion
Seguridad
100%
Timing-safe compare_digest
๐ Flujo de procesamiento de evento
HF Hub POST
X-Webhook-Secret
โ
Verificacion
compare_digest()
โ
Parse Body
scope + action
โ
Router
match scope.action
โ
Handler
retrain / slack / audit
โ
200 OK
{received: true}
Re-entrenamiento Auto
repo.content ยท update
Detecta nuevos commits en
cultiva-ia/dataset-emails-marketing y encola job de fine-tuning del modelo base.Activo en commits a main
Alerta Slack
discussion ยท create
Envia notificacion a
#mlops-alerts cuando alguien abre una discusion o PR en el modelo modelo-email-gen.Solo en isPullRequest=true
Auditoria Config
repo.config ยท update
Registra en base de datos cada cambio de privacidad, secrets o DOI del Space
space-demo-clientes.Log persistente en PostgreSQL
๐ง Variables de entorno requeridas
HUGGINGFACE_WEBHOOK_SECRET
=
"hf_secret_cultiva_2024"
Configurado en HF Hub โ Settings โ Webhooks
SLACK_WEBHOOK_URL
=
"https://hooks.slack.com/services/T.../B.../..."
Canal #mlops-alerts
DATABASE_URL
=
"postgresql://cultiva:pass@db:5432/mlops"
Audit log de cambios de config
TRAINING_QUEUE_URL
=
"redis://localhost:6379/0"
Cola de jobs de fine-tuning
webhook_server.py
python
๐ Copiar
# Generated with: huggingface-webhooks skill โ Cultiva IA MLOps # https://github.com/hookdeck/webhook-skills/tree/main/skills/huggingface-webhooks import secrets import logging import os from datetime import datetime from typing import Optional import httpx from fastapi import FastAPI, Request, HTTPException, Header from pydantic import BaseModel app = FastAPI(title="Cultiva IA โ HuggingFace Webhook Handler", version="1.0.0") logger = logging.getLogger("cultiva.webhooks.hf") logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s") # โโโโโโโโโโโโโโโโโโโโโโโโโโโ ENV โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ HF_SECRET = os.getenv("HUGGINGFACE_WEBHOOK_SECRET") SLACK_URL = os.getenv("SLACK_WEBHOOK_URL") DB_URL = os.getenv("DATABASE_URL") TRAINING_QUEUE = os.getenv("TRAINING_QUEUE_URL") # Repos de Cultiva IA monitorizados DATASET_REPO = "cultiva-ia/dataset-emails-marketing" MODEL_REPO = "cultiva-ia/modelo-email-gen" SPACE_REPO = "cultiva-ia/space-demo-clientes" # โโโโโโโโโโโโโโโโโโโโโโโโโโโ VERIFICACION โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ def verify_hf_secret(header_secret: Optional[str], env_secret: Optional[str]) -> bool: """ Hugging Face NO usa HMAC. Envia el secreto verbatim en X-Webhook-Secret. Siempre usar secrets.compare_digest para evitar timing attacks. """ if not header_secret or not env_secret: return False return secrets.compare_digest(header_secret, env_secret) # โโโโโโโโโโโโโโโโโโโโโโโโโโโ MODELOS PYDANTIC โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ class HFEvent(BaseModel): action: str scope: str class HFRepo(BaseModel): type: str name: str id: str private: bool headSha: Optional[str] = None class HFWebhookPayload(BaseModel): event: HFEvent repo: HFRepo discussion: Optional[dict] = None comment: Optional[dict] = None updatedRefs: Optional[list] = None updatedConfig: Optional[dict] = None webhook: Optional[dict] = None # โโโโโโโโโโโโโโโโโโโโโโโโโโโ HANDLERS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ async def handle_repo_content_update(payload: HFWebhookPayload): """repo.content.update โ encola fine-tuning si es el dataset de marketing.""" if payload.repo.name != DATASET_REPO: return refs = payload.updatedRefs or [] main_updated = any(r.get("ref") == "refs/heads/main" for r in refs) if main_updated: job = { "type": "finetune", "dataset": payload.repo.name, "base_model": "mistralai/Mistral-7B-Instruct-v0.3", "triggered_by": "webhook", "timestamp": datetime.utcnow().isoformat(), } # Encolar en Redis / Celery / ARQ โ simulado aqui logger.info(f"[RETRAIN] Encolando fine-tuning job: {job}") # await queue.enqueue("tasks.finetune", job) async def handle_discussion_create(payload: HFWebhookPayload): """discussion.create โ alerta Slack solo si es un PR en el modelo.""" if payload.repo.name != MODEL_REPO: return disc = payload.discussion or {} is_pr = disc.get("isPullRequest", False) title = disc.get("title", "Sin titulo") num = disc.get("num", 0) tipo = "PR" if is_pr else "Discussion" msg = { "text": f":robot_face: *Nuevo {tipo} #{num} en {MODEL_REPO}*\n> {title}", } if SLACK_URL: async with httpx.AsyncClient() as client: await client.post(SLACK_URL, json=msg) logger.info(f"[SLACK] {tipo} #{num} notificado: {title}") async def handle_repo_config_update(payload: HFWebhookPayload): """repo.config.update โ log de auditoria para el Space.""" if payload.repo.name != SPACE_REPO: return entry = { "repo": payload.repo.name, "changes": payload.updatedConfig, "repo_private": payload.repo.private, "timestamp": datetime.utcnow().isoformat(), } # await db.execute("INSERT INTO audit_log ...", entry) logger.warning(f"[AUDIT] Config cambiada en Space: {entry}") # โโโโโโโโโโโโโโโโโโโโโโโโโโโ ENDPOINT PRINCIPAL โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ @app.post("/webhooks/huggingface") async def huggingface_webhook( payload: HFWebhookPayload, x_webhook_secret: Optional[str] = Header(None), ): # 1. Verificar secreto โ SIEMPRE primero if not verify_hf_secret(x_webhook_secret, HF_SECRET): logger.error("[SECURITY] Verificacion de secreto fallida") raise HTTPException(status_code=401, detail="Unauthorized") # 2. Enrutar por scope + action scope = payload.event.scope action = payload.event.action logger.info(f"[EVENT] {scope}.{action} en {payload.repo.name} ({payload.repo.type})") if scope == "repo.content": await handle_repo_content_update(payload) elif scope == "repo.config": await handle_repo_config_update(payload) elif scope == "discussion" and action == "create": await handle_discussion_create(payload) else: # Forward-compat: scopes no reconocidos -> log y 200 OK logger.info(f"[SKIP] scope={scope} action={action} sin handler registrado") return {"received": True, "scope": scope, "action": action} # โโโโโโโโโโโโโโโโโโโโโโโโโโโ HEALTHCHECK โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ @app.get("/") def health(): return {"status": "ok", "service": "cultiva-hf-webhooks"}
โก Eventos soportados y handlers mapeados
| event.scope | event.action | Repo objetivo | Handler | Accion |
|---|---|---|---|---|
| repo.content | update | dataset-emails-marketing | handle_repo_content_update | Encola job fine-tuning โ Redis |
| repo.config | update | space-demo-clientes | handle_repo_config_update | INSERT en audit_log (PostgreSQL) |
| discussion | create | modelo-email-gen | handle_discussion_create | POST Slack #mlops-alerts |
| repo | create / update / delete | cualquiera | โ (log + 200 OK) | Forward-compat pasivo |
| discussion.comment | create / update | cualquiera | โ (log + 200 OK) | Extendible en v2 |