Added an image and updated the Tech Stack section.
16 KiB
GenExámenes IA
Overview: What is this project meant
GenExámenes IA está pensado para flujos docentes en los que hay que crear evaluaciones estructuradas, apoyarse en un LLM y exportar el resultado a Moodle.
Capacidades principales:
- Registro e inicio de sesión (email/contraseña y Google opcional)
- Creación de plantillas de examen (tipos de pregunta, dificultad, barajar, feedback)
- Subida de Material IA (PDF, DOCX, TXT, MD…) con extracción de texto para contexto
- Subida de imágenes para preguntas visuales (embebidas en Moodle XML)
- Generación de preguntas: automática (LLM en servidor), solo prompt o importar JSON/TXT
- Revisión, vinculación imagen–pregunta y exportación Moodle XML, TXT o JSON
- Cupo de almacenamiento por examen y controles de seguridad en producción
Note
El despliegue en Sinbad2 (UJA) sigue el patrón orcid2sword: HTTPS en Apache, contenedores en HTTP en puertos internos (
8069frontend,8068backend). Ver deploy/DESPLIEGUE_SINBAD2.md.
URL pública (producción): https://sinbad2.ujaen.es/generadorexamenesllm/
Guía ampliada de API, flujo y errores: GUIA_API_Y_FLUJO.md
Tech Stack
Backend
- FastAPI
- SQLAlchemy
- PostgreSQL
- python-jose (JWT)
- bcrypt (contraseñas)
- httpx (cliente LLM Sinbad2IA)
- pypdf / python-docx / Pillow / pytesseract (extracción de materiales)
Frontend
- React 18
- Vite 5
- React Router 6
- Axios
- CSS propio (UI responsive, menú móvil)
Infrastructure
- Docker / Docker Compose
- Nginx (contenedor frontend: SPA + proxy
/authy/exam) - Apache reverse proxy (HTTPS en Sinbad2, fuera del repo)
IA
- Sinbad2IA UJA —
POST {LLM_BASE_URL}/api/chat(modelo por defectoqwen3.5:35b)
Quick Start
Desde la raíz del proyecto:
cp backend/.env.example backend/.env
# Edita backend/.env: LLM_BASE_URL y LLM_API_KEY (solo en el servidor, no en Git)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
URLs por defecto con Docker Compose:
| Servicio | URL local |
|---|---|
| Frontend | http://localhost:8069 |
| Frontend (ruta pública) | http://localhost:8069/generadorexamenesllm/ |
| Backend | http://localhost:8068 |
| PostgreSQL | localhost:5432 |
Desarrollo del frontend sin Docker (solo UI):
cd frontend
npm install
npm run dev
Vite escucha en http://localhost:8075 (configurable en vite.config.js).
Important
Las rutas bajo
/examrequieren JWT de usuario (Authorization: Bearer <token>). El material y las imágenes son opcionales para generar; basta el campo «Tema / instrucciones para la IA» (mínimo 5 caracteres).
Important
Seguridad del LLM: la URL y la clave del servicio de IA no están en el repositorio.
POST /exam/generatesolo funciona si el servidor tieneLLM_BASE_URLyLLM_API_KEYenbackend/.env. El backend no se publica en el host en producción (solo nginx en:8069). Quien clone el repo no puede llamar al LLM sin esas variables en el despliegue.
Tip
Si la generación automática devuelve 503 (Automatic AI generation is not available), faltan variables LLM en el servidor o el servicio no es alcanzable desde el contenedor. Usa Solo prompt → LLM externo → Pegar respuesta IA.
Environment Configuration
Backend
- Archivo principal:
backend/.env - Referencia:
backend/.env.example
Frontend
- Build Docker / ejemplo:
frontend/.env.example - Variables inyectadas en build:
VITE_APP_BASE_PATH,VITE_API_URL,VITE_GOOGLE_CLIENT_ID
Variables backend importantes:
| Variable | Descripción |
|---|---|
JWT_SECRET_KEY |
Secreto JWT (≥ 32 caracteres) |
JWT_EXPIRE_MINUTES |
Duración del token |
GOOGLE_CLIENT_ID |
OAuth Google (POST /auth/google) |
DATABASE_URL |
PostgreSQL |
ALLOWED_ORIGINS |
Orígenes CORS (HTTPS Sinbad2) |
TRUSTED_HOSTS |
Hosts permitidos (TrustedHostMiddleware) |
PUBLIC_BASE_URL |
URL pública con prefijo de despliegue |
LLM_BASE_URL |
URL interna del LLM (solo en .env del servidor, no en el repo) |
LLM_API_KEY |
Clave para autorizar llamadas al LLM (obligatoria para generación automática) |
LLM_MODEL |
Modelo (p. ej. qwen3.5:35b) |
LLM_TIMEOUT_SECONDS |
Timeout llamada LLM (180 s por defecto) |
LLM_GENERATE_RATE_LIMIT_* |
Cuota de POST /exam/generate por usuario |
MAX_STORAGE_BYTES_PER_TEMPLATE |
Cupo materiales + imágenes por examen |
Variables frontend importantes:
| Variable | Descripción |
|---|---|
VITE_APP_BASE_PATH |
Prefijo SPA (/generadorexamenesllm/ en producción) |
VITE_API_URL |
Vacío en producción: API en el mismo origen vía nginx |
VITE_GOOGLE_CLIENT_ID |
Debe coincidir con GOOGLE_CLIENT_ID del backend |
Warning
No subas secretos reales al repositorio. Rota
JWT_SECRET_KEYy credenciales de Google antes de producción.
API Endpoints
Base backend (Docker local): http://localhost:8068
Todas las rutas /exam/* requieren Bearer JWT salvo que se indique lo contrario.
| Módulo | Método | Endpoint | Auth | Notas |
|---|---|---|---|---|
| Health | GET |
/health |
Ninguna | Liveness |
| Auth | POST |
/auth/register |
Ninguna | Email + contraseña |
| Auth | POST |
/auth/login |
Ninguna | Devuelve access_token |
| Auth | POST |
/auth/google |
Ninguna | Body: { "id_token": "..." } |
| Auth | GET |
/auth/me |
Bearer | Usuario actual |
| Plantillas | POST |
/exam/templates |
Bearer | Crear examen |
| Plantillas | GET |
/exam/templates |
Bearer | Listar del usuario |
| Plantillas | GET |
/exam/templates/{id} |
Bearer | Detalle |
| Plantillas | GET |
/exam/templates/{id}/storage |
Bearer | Uso de almacenamiento |
| Materiales | POST |
/exam/templates/{id}/materials |
Bearer | multipart/form-data |
| Materiales | GET |
/exam/templates/{id}/materials |
Bearer | Listado |
| Imágenes | POST |
/exam/templates/{id}/images |
Bearer | Imagen para preguntas |
| Imágenes | GET |
/exam/images/{image_id}/content |
Bearer | Contenido binario |
| Preguntas | GET |
/exam/templates/{id}/questions |
Bearer | Listado |
| Preguntas | PATCH |
/exam/questions/{id}/image |
Bearer | Vincular imagen |
| IA | POST |
/exam/prompts/{template_id} |
Bearer | Construir prompt |
| IA | POST |
/exam/generate |
Bearer | LLM + guardar preguntas |
| IA | POST |
/exam/parse |
Bearer | Importar JSON/TXT externo |
| Export | GET |
/exam/export/xml/{template_id} |
Bearer | Moodle XML |
| Export | GET |
/exam/export/txt/{template_id} |
Bearer | Texto plano |
| Export | GET |
/exam/export/json/{template_id} |
Bearer | JSON |
| Historial | GET |
/exam/history |
Bearer | Resumen de exámenes |
Request Examples
Health
curl http://localhost:8068/health
Registro e inicio de sesión
curl -X POST "http://localhost:8068/auth/register" \
-H "Content-Type: application/json" \
-d "{\"email\":\"docente@ujaen.es\",\"password\":\"MiClaveSegura1\",\"password_confirm\":\"MiClaveSegura1\"}"
curl -X POST "http://localhost:8068/auth/login" \
-H "Content-Type: application/json" \
-d "{\"email\":\"docente@ujaen.es\",\"password\":\"MiClaveSegura1\"}"
Crear plantilla de examen
curl -X POST "http://localhost:8068/exam/templates" \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"Examen Tema 3\",
\"subject\": \"Sistemas Operativos\",
\"educational_level\": \"Grado\",
\"language\": \"es\",
\"settings\": {
\"question_types\": [{\"type\": \"multichoice\", \"count\": 5, \"score\": 1, \"penalty\": 0}],
\"shuffle_questions\": true,
\"shuffle_answers\": true,
\"include_feedback\": true
},
\"difficulty_profile\": {\"easy\": 2, \"medium\": 3, \"hard\": 0, \"very_hard\": 0}
}"
Generar preguntas (solo con tema, sin materiales)
curl -X POST "http://localhost:8068/exam/generate" \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d "{
\"template_id\": \"TEMPLATE_UUID\",
\"topic_prompt\": \"Genera preguntas sobre aritmética y trigonometría\",
\"material_ids\": null
}"
Exportar Moodle XML
curl "http://localhost:8068/exam/export/xml/TEMPLATE_UUID" \
-H "Authorization: Bearer YOUR_JWT" \
-o examen_moodle.xml
Security Controls
Controles implementados en backend:
- CORS con lista de orígenes permitidos
- filtrado de hosts de confianza (
TrustedHostMiddleware) - cabeceras de seguridad (HSTS en producción)
- límite de tamaño de petición
- rate limiting por cliente
- LLM solo desde el servidor: credenciales en
.env, sin URL en el código público POST /exam/generate: exige JWT +LLM_API_KEYconfigurada + límite por usuario- backend no expuesto en
docker-compose.yml(solo red interna Docker; Apache → frontend) - contraseñas con bcrypt
- JWT por usuario; cada plantilla pertenece a un único usuario
- sanitización de prompts y texto persistido
- contenedores sin privilegios innecesarios
Warning
En producción, fuerza HTTPS detrás de Apache y mantén
ALLOWED_ORIGINSyTRUSTED_HOSTSalineados conhttps://sinbad2.ujaen.es. El servicio LLM de la UJA debería aceptar peticiones solo desde la red del backend y exigirLLM_API_KEY(firewall + clave en el propio Sinbad2IA).
Frontend Details
SPA React con rutas protegidas y cliente API centralizado (frontend/src/api/client.js).
Rutas principales
| Ruta | Página |
|---|---|
/login |
Inicio de sesión |
/registro |
Alta de usuario |
/ |
Panel «Mis exámenes» |
/plantillas/nueva |
Asistente «Nuevo examen» |
/plantillas/:templateId |
Detalle con pestañas: Resumen, Material IA, Imágenes, Generar, Preguntas, Exportar |
Pestaña Generar (tres modos)
- Generación automática:
POST /exam/generate— llama al LLM del servidor. - Solo prompt:
POST /exam/prompts/{id}— copiar prompt a un LLM externo. - Pegar respuesta IA:
POST /exam/parse— importar JSON o TXT.
Cliente API (frontend/src/api/)
client.js— Axios, JWT enlocalStorage, errores normalizados (ApiError)auth.js,templates.js,materials.js,images.js,generation.js,exports.js,questions.js
Nginx en Docker (frontend/nginx.conf)
- Sirve la SPA bajo
/y/generadorexamenesllm/ - Proxy de
/auth/y/exam/al backend (backend:8074en red interna) - Redirección de raíz sin barra final al login
Project Structure
03_GeneradorExamenes/
├── backend/
│ ├── app/
│ │ ├── api/
│ │ │ ├── routes/
│ │ │ │ ├── auth.py
│ │ │ │ ├── templates.py
│ │ │ │ ├── generation.py
│ │ │ │ ├── materials.py
│ │ │ │ ├── images.py
│ │ │ │ ├── questions.py
│ │ │ │ ├── exports.py
│ │ │ │ └── history.py
│ │ │ └── dependencies.py
│ │ ├── core/
│ │ │ ├── config.py
│ │ │ ├── auth.py
│ │ │ ├── errors.py
│ │ │ ├── middleware.py
│ │ │ └── security_headers.py
│ │ ├── db/
│ │ ├── models/
│ │ ├── schemas/
│ │ ├── services/
│ │ │ ├── exam_service.py
│ │ │ ├── llm.py
│ │ │ ├── prompt_builder.py
│ │ │ ├── parser.py
│ │ │ ├── material_service.py
│ │ │ ├── image_service.py
│ │ │ └── moodle_exporter.py
│ │ └── main.py
│ ├── .env.example
│ ├── Dockerfile
│ └── requirements.txt
├── frontend/
│ ├── src/
│ │ ├── api/
│ │ ├── components/
│ │ ├── context/
│ │ ├── pages/
│ │ │ ├── DashboardPage.jsx
│ │ │ ├── CreateTemplatePage.jsx
│ │ │ ├── TemplateDetailPage.jsx
│ │ │ └── template/
│ │ │ ├── GenerateTab.jsx
│ │ │ ├── MaterialsTab.jsx
│ │ │ └── ExportTab.jsx
│ │ ├── App.jsx
│ │ └── main.jsx
│ ├── nginx.conf
│ ├── .env.example
│ ├── package.json
│ └── vite.config.js
├── deploy/
│ ├── DESPLIEGUE_SINBAD2.md
│ └── apache-reverse-proxy.conf
├── docs/
│ └── Manual_de_Usuario_GenExamenes_IA.docx
├── scripts/
│ └── generar_manual_usuario.py
├── docker-compose.yml
├── GUIA_API_Y_FLUJO.md
└── README.md
Production Checklist
ENVIRONMENT=production- Rotar
JWT_SECRET_KEYyGOOGLE_CLIENT_IDde desarrollo ALLOWED_ORIGINScon orígenes HTTPS realesTRUSTED_HOSTSconsinbad2.ujaen.esPUBLIC_BASE_URL=https://sinbad2.ujaen.es/generadorexamenesllm- Apache
ProxyPassa puerto 8069 (frontend), no al backend directo - Reglas Apache antes de WordPress en Sinbad2
LLM_BASE_URLyLLM_API_KEYsolo enbackend/.envdel servidor (no en Git)- Comprobar conectividad backend → LLM (red interna UJA)
- Despliegue con
docker compose upsindocker-compose.dev.yml(backend no publicado en host) - Volumen
uploads_datay backups de PostgreSQL - Manual de usuario actualizado en
docs/
Authors and Team
Este proyecto es el resultado de la colaboración con la Universidad de Jaén (grupo Sinbad²).
| Rol | Developer | GitHub |
|---|---|---|
| Frontend | Alexis López Moral | @AlexisLopez-Dev |
| Backend | Mireya Cueto Garrido | @MireyaCueto |
Direction
- Project Supervisor: Luis Martínez López
Built with professional care and ❤️ for AI-assisted exam workflows at the University of Jaén.