Filosofía
La educación en línea con menores y contextos sensibles (depresión, ansiedad preuniversitaria, bullying) exige moderación IA real, no solo disclaimers. Studeia adopta:
- Moderación en background, no gatekeeping — el supervisor analiza tras la respuesta, no bloquea el stream. El alumno recibe la respuesta normal y el supervisor actúa si es necesario en los turnos siguientes.
- Self-harm como crisis, no infracción — nunca penalizar al alumno en sufrimiento.
- Cascada de configuración — el admin puede deshabilitar por tenant o por curso cuando el contexto lo requiere (anatomía, farmacología, psicología).
- Auditoría completa — cada incidente, transición de estado, cuarentena y apelación queda registrado en AdminAuditLog.
Modelo de datos
AiSupervisorIncident
id, userId, tenantId, courseId?
severity: low | medium | high | critical | safety
categories: [tipos]
status: open | acknowledged | resolved | dismissed | auto_resolved
messagesSnapshot: JSON (PII — retención 2 años vía cron)
supervisorReasoning: string
countedAsStrike: boolean
detectedAt, resolvedAt
appealText: string (máx. 500 chars, 1 por cuarentena)
AiTutorQuarantine
id, userId, tenantId
reason: string
expiresAt: timestamp
liftedBy: userId? (admin que liberó manualmente)
Pipeline supervisor
Turno de chat completo
↓ (after())
SupervisorAgent.run({
userId, tenantId, courseId,
messages: lastNTurns,
isMinor: user.isMinor,
courseContext: { title, description } // whitelist contextual
})
↓
LLM (Haiku) clasifica: severity + categories + reasoning
↓
decideAction({ severity, categories, recentStrikes, isMinor, isSafety })
↓
Acciones posibles:
- none (no registra)
- warn (notificación in-app)
- register (crea incident, countedAsStrike)
- quarantine (crea AiTutorQuarantine 48h)
- safety (cooldown Redis 24h + acogida + admin URGENTE)
Reglas de severidad
| Severity | Categoría típica | Acción 1.ª infracción | Acción 2.ª+ infracción |
|---|---|---|---|
| low | lenguaje inapropiado leve | warn | strike +1; 3 strikes = cuarentena 48h |
| medium | off-topic persistente, jailbreak | warn + register | strike +1; 3 strikes = cuarentena 48h |
| high | violencia, sexual, ilegal | cuarentena 48h | cuarentena 7 días |
| critical | amenaza a otros, contenido extremo | cuarentena 7 días | cuarentena indefinida + admin review |
| safety | self_harm | NUNCA cuarentena — cooldown 24h + acogida + admin URGENTE | igual |
Self-harm: tratamiento especial
Cuando severity === "safety":
- El stream del tutor se interrumpe inmediatamente — el tutor no responde algo inapropiado ante una crisis
- Mensaje de acogida mostrado al alumno:
"Estoy aquí contigo. Si estás pasando por un momento difícil, por favor busca ayuda:
- CVV 188 (24h, llamada gratuita, anónima)
- SAMU 192
- Centro de Valorização da Vida — chat online No estás solo/a."
- Cooldown Redis
tutor-safety-cooldown:{userId}con TTL configurable (SUPERVISOR_SAFETY_COOLDOWN_HOURS, default 24h) - Email URGENTE inmediato al admin institucional vía template
ai_supervisor_safety_urgent - Incidente creado en estado 'open' — el admin DEBE revisar
- NUNCA strike (countedAsStrike=false), NUNCA cuarentena, NUNCA penalización
Apelación de cuarentena
El alumno en cuarentena ve el componente QuarantineNotice (web + equivalente móvil):
- Explica el motivo (severity + categoría, sin exponer el reasoning interno del supervisor)
- Muestra cuenta regresiva hasta la expiración
- Formulario de apelación: máx. 500 caracteres, 1 por cuarentena
- El envío crea
appealTexten el incidente + notifica al admin institucional - El admin puede: acknowledge, dismiss (libera la cuarentena), resolve, o ignorar (la cuarentena expira sola)
Configuración
Cascada de habilitación
Course.supervisorEnabled (null = inherit)
↓ si null
Tenant.supervisorEnabled (null = inherit)
↓ si null
default = true para B2B (con tenant)
Caché Redis versionado: supervisor-flag-version:{tenantId} + clave supervisor-enabled:v{N}:{tenantId}:{courseId}. Toda mutación llama a bumpSupervisorFlagVersion(tenantId) que incrementa la versión — invalida lógicamente todas las claves sin SCAN+DEL.
Solo el admin global puede editar
PATCH /api/admin/tenants/[id]/supervisor— toggle por tenantPATCH /api/admin/courses/[id]/supervisor— toggle por curso- Ambas requieren
role === "admin"global + auditadas en AdminAuditLog
Prompt del supervisor
Editado ÚNICAMENTE por admin global (regla crítica 141): PromptTemplate con taskType = chat_supervisor acepta ÚNICAMENTE tenantId = null. Los endpoints /api/institution/prompts/* rechazan este taskType con 403.
Auditoría + retención
- Cada incidente registrado con
messagesSnapshot(PII) - Cron diario
/api/cron/supervisor-maintenance:- Auto-expira cuarentenas vencidas
- Purga
messagesSnapshot=[]+appealText=nulltras 2 años (regla crítica 145) - Envía digest
ai_supervisor_digestal admin agrupando incidentes open/acknowledged de las últimas 24h
- AdminAuditLog:
ai_supervisor.incident.created/acknowledged/dismissed/resolved,quarantine.lift,prompt.update,tenant.toggle,course.toggle
Protección de datos
GET /api/user/data-exportincluyeaiSupervisor.{incidents, quarantines}del usuarioDELETE /api/user/accountanonimizamessagesSnapshot=[]+appealText=nullmanteniendo severity/categories para retención fiscal- Los listados (
/api/institution/ai-supervisor/incidents) usanselectexplícito que OMITE messagesSnapshot y reasoning — solo la ruta de detalle los expone
Limitaciones conocidas
- Falso positivo en contexto médico/farmacología: el contexto del curso (courseContext.title) se envía al supervisor para whitelist. Sin embargo, puede fallar en casos extremos. Solución: deshabilitar el supervisor para cursos específicos.
- Idioma: el prompt del supervisor está localizado (4 idiomas), pero la clasificación puede presentar pequeñas variaciones de calidad entre PT-BR y EN-US.
- Jailbreak sofisticado: ataques de prompt injection muy elaborados pueden pasar. Mitigación: defensa en capas (system prompt + supervisor + rate limit).
- Equilibrio privacidad vs. seguridad: messagesSnapshot es PII. Retención máxima 2 años. El admin global lo ve en /admin/ai-supervisor/incidents/[id] — auditado.