Skip to content

Arquitectura del AI Backend

apps/ai-backend/src/
├── main.ts ← Arranca en puerto 3001
├── app.module.ts ← Módulo raíz
├── common/
│ ├── enums/
│ │ └── ai.enums.ts ← AIProvider, AIUseCase
│ └── guards/
│ └── internal-auth.guard.ts ← Valida x-internal-key
└── modules/
├── credentials/ ← Gestión de API keys cifradas
├── providers/ ← Adapters LLM (Anthropic/OpenAI/Gemini)
├── router/ ← Enrutamiento + circuit breaker
├── vector-db/ ← Qdrant + embeddings
├── metrics/ ← Registro de uso y costes
├── health/ ← Health check de proveedores
├── admin/ ← Endpoints de administración
└── usecases/
├── tutor/ ← Tutor conversacional
├── adaptive/ ← Motor adaptativo
├── fluency-test/ ← Scoring del test de fluencia
├── content-gen/ ← Generación de quizzes/resúmenes/casos
├── evaluation/ ← Evaluación de respuestas abiertas
├── churn/ ← Predicción de abandono
└── indexing/ ← Indexación en Qdrant
MóduloResponsabilidad
CredentialsModuleCifrado AES-256-GCM y caché de API keys
ProvidersModuleLos 3 adapters LLM con interfaz común
RouterModuleResolución de proveedor/modelo por caso de uso + circuit breaker
VectorDbModuleQdrantService + EmbeddingService + IndexingService
MetricsModuleRegistro de AIUsageMetric con estimación de coste
HealthModulePing a los 3 proveedores + estado de Qdrant
AdminModuleEndpoints /admin/metrics, /admin/router, /admin/vector-db
TutorModulePOST /usecases/tutor/chat con streaming SSE
AdaptiveModulePOST /usecases/adaptive/recommend
FluencyTestModulePOST /usecases/fluency-test/score
ContentGenModule`POST /usecases/content-gen/quiz
EvaluationModulePOST /usecases/evaluation/grade
ChurnModulePOST /usecases/churn/predict
IndexingModulePOST /usecases/indexing/index-lesson

Todos los endpoints del AI Backend están protegidos con:

apps/ai-backend/src/common/guards/internal-auth.guard.ts
@Injectable()
export class InternalAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const key = request.headers['x-internal-key'];
if (key !== process.env.AIBACKEND_API_KEY) {
throw new UnauthorizedException('Invalid internal API key');
}
return true;
}
}

El secret AIBACKEND_API_KEY debe estar en el .env de ambas apps (api y ai-backend).

sequenceDiagram
participant W as apps/web
participant A as apps/api
participant AI as apps/ai-backend
participant P as LLM Provider
participant Q as Qdrant
W->>A: POST /tutor/chat
A->>AI: POST /usecases/tutor/chat (x-internal-key)
AI->>AI: InternalAuthGuard valida key
AI->>AI: RouterService.resolveProvider(TUTOR_CHAT)
AI->>AI: CircuitBreaker.isOpen(anthropic) → false
AI->>AI: CredentialsService.getKey(anthropic) → API key descifrada
AI->>Q: Buscar contexto relevante para el usuario
Q-->>AI: Top-K chunks similares
AI->>P: Anthropic.messages.stream(...)
P-->>AI: Tokens en streaming
AI-->>A: SSE chunks
A-->>W: SSE chunks
AI->>AI: MetricsService.record(useCase, tokens, latency, cost)

Al arrancar, RouterService.onModuleInit() carga los AIRouteConfig desde apps/api:

async onModuleInit(): Promise<void> {
await this.loadRoutesFromApi(); // GET /internal/ai-routes
}

Si la API no está disponible, usa DEFAULT_ROUTE_CONFIG (hardcoded) como fallback.