AI Platform — LLM Gateway, Agents & Ask MAS¶
This document describes the backend's "AI platform" domain: the central LLM Gateway (a thin client over a self-hosted LiteLLM proxy) that mints per-user virtual API keys and meters spend, the Ask MAS student assistant that routes through it with RAG over the student's own dashboard, the singleton Agent Configuration that the external mas-class-agent reads to pick providers, the AI Classroom surface that proxies generation/playback to mas-class-agent, the credential proxy that brokers provider keys, and the lightweight AI-for-Everyone interest capture. It is the shared LLM plumbing that resume screening, voice interviews, and quiz generation either reuse or sit beside.
Status: documented from source on this branch.
Overview¶
The AI platform is several loosely-coupled features that share a common goal — putting LLM-backed capability in front of students and admins while keeping cost, access, and provider choice under central control.
- LLM Gateway (
src/services/LlmGatewayService.ts) — a thin TypeScript client for a self-hosted LiteLLM proxy (OpenAI-compatible) reached over the internal Docker network atLITELLM_BASE_URL(defaulthttp://mas-llm-gateway:4000). It mints one virtualsk-...key per(MAS user, product)so the gateway enforces that user's budget + rate limits and tracks spend per student. The master key is used only for admin operations (mint/update/revoke/read) and never leaves the backend; chat traffic always uses the user's key. When the gateway is not wired up, callers fall back cleanly. - Ask MAS (
src/services/AskMasService.ts) — an in-app student assistant. It builds a personalised system prompt from the student's own dashboard signals (test scores, streak, weak skills, mentor-token balance, supermentor feedback, warnings), runs lightweight RAG against published Smart Classrooms, and routes the completion through the gateway (per-user key) or — if the gateway is unconfigured — directly to Groq. Every answer (including refusals) is audited inask_mas_logs. - Agent Configuration (
src/services/AgentConfigurationService.ts) — a single versioned row describing which providers (LLM, image, video, TTS, ASR, PDF, web-search) the platform should use. The admin UI edits it with optimistic concurrency;mas-class-agentreads it via an internal endpoint. - AI Classroom (
src/controllers/aiClassroom.controller.ts) — admin authors and students consume AI-generated "Smart Classrooms". The backend owns the metadata/progress/request rows and proxies all generation, scene actions, and playback tomas-class-agent. - Credential proxy (
src/controllers/credentialProxy.controller.ts) — forwards admin credential CRUD tomas-class-agent, which owns provider-key encryption and storage. - AI for Everyone (
src/services/AiForEveryoneService.ts) — a public interest-capture form (lead funnel) for the "AI for Everyone" course.
Personas¶
| Persona | Uses |
|---|---|
Student (USER, enrolled) |
Ask MAS chat + suggestions, AI Classroom playback, classroom course requests |
Admin / Superadmin (ADMIN, SUPERADMIN) |
Agent configuration, LLM Gateway dashboard (spend/budgets/block/mint), classroom authoring + review, provider credentials, AI-for-Everyone leads |
Service consumer (mas-class-agent) |
Reads active agent configuration via internal-token endpoint; receives generation jobs + credential operations |
| Public visitor | Submits the AI-for-Everyone interest form |
Place in the suite¶
mas-class-agent is a separate service (default http://localhost:3001, env CLASS_AGENT_URL) that actually generates classrooms and holds provider credentials; this backend is its control plane and proxy. The LLM Gateway is the shared metering layer — Ask MAS is its first consumer, and the same gateway/keys mechanism is available to other LLM-shaped features.
Key concepts & entities¶
Glossary
- LiteLLM gateway — OpenAI-compatible proxy. Exposes admin APIs (
/key/*,/spend/logs,/global/activity,/model/info) and a chat API (/v1/chat/completions). Source of truth for live spend + limits. - Virtual key — a
sk-...bearer secret minted per(user, product)on the gateway. Stored AES-encrypted inuser_llm_keys; decrypted only at call time. - Model alias — a gateway-side logical model name. Ask MAS uses aliases like
fast | smart | cheap | moderation(configurable viaASKMAS_GATEWAY_MODEL, defaultsmart). - Budget exceeded — LiteLLM returns 400/429 with a "budget" message; the client raises
LlmBudgetExceededErrorso callers can answer gracefully. - Agent configuration — the singleton provider-selection blob consumed by
mas-class-agent. - Smart Classroom — an AI-generated multi-scene lesson. Generation/playback live in
mas-class-agent; the backend stores metadata + per-student progress. - Refusal — Ask MAS pre-filters and post-filters out-of-scope topics (resume/career/placement/salary) via regex patterns.
Entities (file paths)
| Entity | Table | File | Purpose |
|---|---|---|---|
UserLlmKey |
user_llm_keys |
src/entities/UserLlmKey.ts |
Maps (userId, product) → encrypted gateway sk- key (unique constraint) |
AgentConfiguration |
agent_configurations |
src/entities/AgentConfiguration.ts |
Singleton (id=1) provider config blob + @VersionColumn |
AgentConfigurationHistory |
agent_configuration_history |
src/entities/AgentConfigurationHistory.ts |
Append-only snapshots of previous configs |
AskMasLog |
ask_mas_logs |
src/entities/AskMasLog.ts |
One audit row per produced answer (incl. refusals) |
AiClassroom |
ai_classrooms |
src/entities/AiClassroom.ts |
Smart Classroom metadata + generation/publish status |
AiClassroomProgress |
ai_classroom_progress |
src/entities/AiClassroomProgress.ts |
Per-student progress, quiz scores, review state |
AiClassroomRequest |
ai_classroom_requests |
src/entities/AiClassroomRequest.ts |
Student "please make a course on X" requests |
AiForEveryoneInterest |
ai_for_everyone_interest |
src/entities/AiForEveryoneInterest.ts |
Public interest-form leads |
Services & utils
src/services/LlmGatewayService.ts— gateway client (mint/update/revoke/block, list/activity/spend/models, chat).src/services/MasGatewayService.ts— admin dashboard read+control layer (joins gateway data with DB users +ask_mas_logs).src/services/AskMasService.ts+src/services/askMas.helpers.ts— assistant logic + pure refusal/topic/history helpers.src/services/AgentConfigurationService.ts— versioned config read/write + Redis cache.src/services/AiForEveryoneService.ts— interest CRUD + stats.src/utils/courseKnowledgeBase.ts— normalises/validates knowledge-base URLs and builds the generation prompt fragment.src/utils/encryption.ts—encryptSecret/decryptSecret(AES, format<ivHex>:<cipherHex>).
Architecture¶
flowchart TD
subgraph Clients["Clients"]
Student["Student portal"]
Admin["Admin dashboard"]
Public["Public website"]
Agent["mas-class-agent (service)"]
end
subgraph Routes["Express routes"]
RStudent["/api/student/* (auth + enrolled)"]
RAdmin["/api/admin/mas/* (auth + admin)"]
RAgentCfg["/api/mas/agent-configuration/active (internal token)"]
RAi["/api/ai/students/:id/profile"]
RAife["/api/ai-for-everyone/*"]
end
subgraph Controllers["Controllers"]
CAsk["AskMasController"]
CGw["MasGatewayController"]
CCfg["AgentConfigurationController"]
CCred["CredentialProxyController"]
CClass["AiClassroomController"]
CStud["AiStudentController"]
CAife["AiForEveryoneController"]
end
subgraph Services["Services"]
SAsk["AskMasService"]
SGw["LlmGatewayService"]
SMas["MasGatewayService"]
SCfg["AgentConfigurationService"]
SAife["AiForEveryoneService"]
end
subgraph Data["PostgreSQL + Redis"]
DKeys["user_llm_keys"]
DLogs["ask_mas_logs"]
DCfg["agent_configurations + history"]
DClass["ai_classrooms / progress / requests"]
DAife["ai_for_everyone_interest"]
Redis["Redis (rate limits, config cache)"]
end
subgraph External["External"]
LiteLLM["LiteLLM gateway (mas-llm-gateway:4000)"]
Groq["Groq API (fallback)"]
Providers["LLM / image / TTS providers"]
end
Student --> RStudent --> CAsk --> SAsk
Student --> RStudent --> CClass
Admin --> RAdmin --> CGw --> SMas
Admin --> RAdmin --> CCfg --> SCfg
Admin --> RAdmin --> CCred --> CClass
Agent --> RAgentCfg --> CCfg
Student --> RAi --> CStud
Public --> RAife --> CAife --> SAife
SAsk --> SGw
SMas --> SGw
SGw --> DKeys
SAsk --> DLogs
SMas --> DLogs
SCfg --> DCfg
SCfg --> Redis
SAsk --> Redis
SAife --> DAife
CClass --> DClass
SGw -->|"per-user key + master key"| LiteLLM
SAsk -->|"fallback when gateway off"| Groq
LiteLLM --> Providers
CClass -->|"X-Internal-API-Key"| Agent
CCred -->|"X-Internal-API-Key"| Agent
Agent -->|"reads providers"| Providers
Data model¶
erDiagram
USER ||--o{ USER_LLM_KEYS : "has"
USER ||--o{ ASK_MAS_LOGS : "asks"
USER ||--o{ AI_CLASSROOM_PROGRESS : "tracks"
USER ||--o{ AI_CLASSROOM_REQUESTS : "requests"
AI_CLASSROOMS ||--o{ AI_CLASSROOM_PROGRESS : "progressed by"
COURSE ||--o{ AI_CLASSROOMS : "optionally scopes"
AGENT_CONFIGURATIONS ||--o{ AGENT_CONFIGURATION_HISTORY : "snapshots"
USER_LLM_KEYS {
uuid id PK
uuid userId FK
varchar product "default ask-mas"
varchar litellmUserId
varchar keyAlias
text litellmKeyEncrypted "AES ivHex:cipherHex"
timestamp createdAt
}
ASK_MAS_LOGS {
uuid id PK
uuid userId
text question
text reply
boolean refused
varchar refusalReason "pre_filter|post_filter|budget_exceeded"
varchar model
int tokensUsed
timestamp createdAt
}
AGENT_CONFIGURATIONS {
int id PK "always 1"
jsonb config
int version "VersionColumn optimistic lock"
uuid updatedBy
timestamp updatedAt
}
AGENT_CONFIGURATION_HISTORY {
uuid id PK
jsonb config
int version
uuid updatedBy
timestamp createdAt
}
AI_CLASSROOMS {
uuid id PK
varchar classroomId UK "agent-side id"
varchar title
text topic
jsonb knowledgeBaseUrls
int sceneCount
enum status "generating|ready|published|archived"
uuid courseId FK
uuid moduleId
varchar generationJobId
uuid createdById
timestamp publishedAt
}
AI_CLASSROOM_PROGRESS {
uuid id PK
uuid userId FK
uuid aiClassroomId FK
int currentScene
int totalScenes
json completedScenes
json quizScores
enum status "not_started|in_progress|submitted|reviewed|completed"
timestamp submittedAt
timestamp reviewedAt
uuid reviewedById
}
AI_CLASSROOM_REQUESTS {
uuid id PK
text topic
varchar title
enum status "pending|approved|rejected"
uuid requestedById FK
uuid reviewedById
uuid generatedClassroomId
}
AI_FOR_EVERYONE_INTEREST {
uuid id PK
varchar name
varchar email
varchar phone
varchar college
enum status "new|contacted|converted|not_interested"
varchar source
uuid assignedTo
}
Notable enums:
AiClassroomStatus:generating | ready | published | archived(src/entities/AiClassroom.ts).AiClassroomProgressStatus:not_started | in_progress | submitted | reviewed | completed(completedis deprecated, kept for back-compat).AiClassroomRequestStatus:pending | approved | rejected.InterestStatus:new | contacted | converted | not_interested.UserLlmKeyhas a unique constraint on(userId, product)— one key per product per user.
API surface¶
Paths below are the full mounted paths. Mount prefixes (from src/routes/index.ts): student routes at /api/student, admin routes at /api/admin, AI-for-Everyone / agent-configuration / credential-proxy at /api, and AI student profile at /api/ai.
Ask MAS (student)¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| POST | /api/student/ask-mas |
auth + enrolled | Ask a question; returns reply + refusal flag + model + tokens |
| GET | /api/student/ask-mas/suggestions |
auth + enrolled | Up to 8 personalised quick-prompts for the empty state |
LLM Gateway dashboard (admin)¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| GET | /api/admin/mas/llm-gateway/overview |
auth + admin | KPIs, daily activity, spend-by-model, top spenders |
| GET | /api/admin/mas/llm-gateway/users |
auth + admin | Per-student spend/limits/usage table (search + paginate) |
| GET | /api/admin/mas/llm-gateway/users/:userId |
auth + admin | One student's detail + recent Q&A |
| PATCH | /api/admin/mas/llm-gateway/users/:userId/limits |
auth + admin | Update budget / duration / rpm / tpm / models |
| POST | /api/admin/mas/llm-gateway/users/:userId/block |
auth + admin | Block / unblock a user's key |
| POST | /api/admin/mas/llm-gateway/users/:userId/key |
auth + admin | Eagerly mint a key for a user |
| DELETE | /api/admin/mas/llm-gateway/users/:userId/key |
auth + admin | Revoke a user's key |
| GET | /api/admin/mas/llm-gateway/logs |
auth + admin | Recent per-request spend logs |
| GET | /api/admin/mas/llm-gateway/models |
auth + admin | Model catalog + per-token pricing |
Agent configuration¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| GET | /api/mas/agent-configuration/active |
internal token (x-internal-token == INTERNAL_SERVICE_TOKEN; falls open if unset) |
Read active config for mas-class-agent |
| GET | /api/admin/mas/agent-configuration |
auth + admin | Load current config (ETag / 304 supported) |
| PUT | /api/admin/mas/agent-configuration |
auth + admin | Upsert with { config, expectedVersion }; 409 on version conflict |
| GET | /api/admin/mas/agent-configuration/history |
auth + admin | Last N config snapshots (limit ≤ 200) |
| GET | /api/admin/mas/agent-configuration/active-providers |
auth + admin | Proxy mas-class-agent active-providers (filters dropdowns) |
Provider credentials proxy (admin → mas-class-agent)¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| GET | /api/admin/mas/credentials |
auth + admin | List provider credentials |
| POST | /api/admin/mas/credentials |
auth + admin | Create a credential |
| POST | /api/admin/mas/credentials/upload-google-json |
auth + admin | Multipart upload of a Google service-account JSON (≤ 64KB) |
| GET | /api/admin/mas/credentials/:id |
auth + admin | Get one credential |
| PUT | /api/admin/mas/credentials/:id |
auth + admin | Update a credential |
| DELETE | /api/admin/mas/credentials/:id |
auth + admin | Delete a credential |
| POST | /api/admin/mas/credentials/:id/test |
auth + admin | Test a credential |
| POST | /api/admin/mas/credentials/:id/set-default |
auth + admin | Mark a credential default |
AI Classroom — admin¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| GET | /api/admin/mas/ai-classrooms |
auth + admin | List/filter classrooms (self-heals stale GENERATING rows) |
| POST | /api/admin/mas/ai-classrooms |
auth + admin | Create classroom + trigger generation |
| GET | /api/admin/mas/ai-classrooms/:id |
auth + admin | Detail + scenes (self-heals status) |
| GET | /api/admin/mas/ai-classrooms/:id/status |
auth + admin | Poll generation status from agent |
| PUT | /api/admin/mas/ai-classrooms/:id |
auth + admin | Update metadata |
| DELETE | /api/admin/mas/ai-classrooms/:id |
auth + admin | Delete classroom + progress |
| POST | /api/admin/mas/ai-classrooms/:id/publish |
auth + admin | Publish + notify enrolled students |
| POST | /api/admin/mas/ai-classrooms/:id/link |
auth + admin | Link to course/module |
| GET | /api/admin/mas/ai-classrooms/:id/submissions |
auth + admin | List per-student progress (review queue) |
| PATCH | /api/admin/mas/ai-classrooms/:id/submissions/:userId/review |
auth + admin | Mark a submission reviewed |
| POST | /api/admin/mas/ai-classrooms/sync |
auth + admin | Bulk upsert classrooms by classroomId |
| POST | /api/admin/mas/ai-classrooms/:id/scenes/:sceneIndex/action |
auth + admin | Proxy scene action (regen/TTS) to agent |
| POST | /api/admin/mas/ai-classrooms/check-duplicate |
auth + admin | LLM-assisted duplicate check (via agent /api/admin/llm) |
| POST | /api/admin/mas/ai-classrooms/enhance-prompt |
auth + admin | LLM prompt enhancement (via agent) |
| POST | /api/admin/mas/ai-classrooms/generate-meta |
auth + admin | LLM title/description generation (via agent) |
| GET/POST | /api/admin/mas/ai-classrooms-agent/sync |
auth + admin | Proxy agent storage sync |
| POST | /api/admin/mas/ai-classrooms-agent/delete-storage |
auth + admin | Proxy agent storage delete |
| GET | /api/admin/mas/ai-classrooms/:id/studio-url |
auth + admin | HMAC-signed studio embed URL |
| GET | /api/admin/mas/ai-classroom-requests |
auth + admin | List all course requests |
| POST | /api/admin/mas/ai-classroom-requests/:id/approve |
auth + admin | Approve + trigger generation |
| POST | /api/admin/mas/ai-classroom-requests/:id/reject |
auth + admin | Reject a request |
AI Classroom — student¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| GET | /api/student/ai-classrooms |
auth | List published classrooms visible to the student (enrollment-scoped) |
| GET | /api/student/ai-classrooms/:id |
auth | Get one classroom + scenes + progress (creates progress row) |
| POST | /api/student/ai-classrooms/:id/progress |
auth | Update progress (postMessage bridge from iframe) |
| POST | /api/student/ai-classrooms/:id/quiz |
auth | Submit a scene quiz score |
| POST | /api/student/ai-classroom-requests |
auth | Submit a course request |
| GET | /api/student/ai-classroom-requests |
auth | List my requests |
AI student profile & AI for Everyone¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| GET | /api/ai/students/:identifier/profile |
(route-level open) | Consolidated student profile for an agent (accepts UUID/email/roll no.) |
| POST | /api/ai-for-everyone/interest |
public | Submit interest form (upserts by email) |
| GET | /api/ai-for-everyone/interest |
admin (intended) | List interests (paginated; limit ≤ 10000 for export) |
| GET | /api/ai-for-everyone/interest/stats |
admin (intended) | Totals + by-college breakdown |
| GET | /api/ai-for-everyone/interest/:id |
admin (intended) | Get one interest |
| PATCH | /api/ai-for-everyone/interest/:id/status |
admin (intended) | Update status (+ notes) |
| DELETE | /api/ai-for-everyone/interest/:id |
admin (intended) | Delete interest |
Note: the AI-for-Everyone GET/PATCH/DELETE routes are documented "admin only" in code comments but the route file (
src/routes/aiForEveryone.routes.ts) does not attachauthMiddleware/adminMiddleware— admin gating is not enforced at the router level here (inferred gotcha; see Edge cases).
User journeys¶
1. A feature calls the LLM gateway (model routing, BYO/per-user key, usage logging)¶
Ask MAS is the canonical consumer. On each request it lazily mints (or reuses) the student's gateway key, calls the OpenAI-compatible chat endpoint, and the gateway meters spend against that key.
sequenceDiagram
participant Svc as AskMasService
participant Gw as LlmGatewayService
participant DB as user_llm_keys
participant LiteLLM as LiteLLM gateway
Svc->>Gw: getOrCreateUserKey(userId, "ask-mas", limits)
Gw->>DB: findOne(userId, product)
alt key already stored
DB-->>Gw: encrypted sk-key
Gw-->>Svc: decrypted sk-key
else first use
Gw->>LiteLLM: POST /key/generate (master key + budget + rpm/tpm)
LiteLLM-->>Gw: { key: sk-... }
Gw->>DB: save encrypted key (guard unique race)
Gw-->>Svc: sk-key
end
Svc->>Gw: chatCompletion(sk-key, model alias, messages)
Gw->>LiteLLM: POST /v1/chat/completions (Bearer sk-key)
alt budget spent
LiteLLM-->>Gw: 400 or 429 budget error
Gw-->>Svc: throw LlmBudgetExceededError
else ok
LiteLLM-->>Gw: choices + usage.total_tokens
Gw-->>Svc: content + model + totalTokens
end
Key points: the master key never touches chat traffic. If two concurrent first-requests race the unique (userId, product) insert, the loser re-reads the winning row. The model passed to chat is a gateway alias (smart by default), not a raw provider model.
2. Ask MAS end-to-end (pre-filter then RAG then completion then log)¶
sequenceDiagram
participant Student
participant Ctrl as AskMasController
participant Svc as AskMasService
participant Redis
participant DB as Postgres
participant Gw as LlmGatewayService
participant Log as ask_mas_logs
Student->>Ctrl: POST /api/student/ask-mas { message, history }
Ctrl->>Svc: ask(userId, input)
Svc->>Redis: INCR rate-limit bucket (10 per 60s)
alt over limit
Svc-->>Ctrl: throw RATE_LIMITED
Ctrl-->>Student: 429 too quickly
else within limit
Svc->>Svc: matchesRefusal(message) pre-filter
alt out of scope
Svc->>Log: logQa refused pre_filter
Svc-->>Ctrl: refusal reply
else in scope
Svc->>DB: build student context (15 queries in parallel)
Svc->>DB: findRelevantClassrooms (ILIKE on published)
Svc->>Gw: runCompletion via gateway key
alt budget exceeded
Gw-->>Svc: LlmBudgetExceededError
Svc->>Log: logQa refused budget_exceeded
Svc-->>Ctrl: budget reply
else completion ok
Gw-->>Svc: raw content + tokens
Svc->>Svc: stripReasoningBlocks then post-filter
Svc->>Log: logQa answered
Svc-->>Ctrl: reply + model + tokensUsed
end
end
Ctrl-->>Student: 200 { reply, refused, model }
end
Alternate: when LITELLM_BASE_URL/LITELLM_MASTER_KEY are unset, runCompletion calls Groq directly (callGroqDirect) using GROQ_API_KEY; all Groq failures normalise to LLM_UPSTREAM_ERROR (502). Logging is best-effort and never blocks or fails the request.
3. Configure an AI agent + versioned history (optimistic concurrency)¶
sequenceDiagram
participant Admin
participant Ctrl as AgentConfigurationController
participant Svc as AgentConfigurationService
participant Redis
participant DB as agent_configurations
Admin->>Ctrl: GET /api/admin/mas/agent-configuration
Ctrl->>Svc: getActive()
Svc->>Redis: read cache agent-config:active:v1
alt cache hit
Redis-->>Svc: snapshot
else miss
Svc->>DB: find singleton id=1 (create empty if absent)
DB-->>Svc: row
Svc->>Redis: write cache (300s)
end
Svc-->>Ctrl: snapshot (config + version + etag)
Ctrl-->>Admin: 200 + ETag header
Admin->>Ctrl: PUT config + expectedVersion
Ctrl->>Svc: upsert(config, expectedVersion, adminId)
Svc->>DB: begin transaction
alt expectedVersion matches current
Svc->>DB: snapshot previous to history
Svc->>DB: save new config (version auto-increments)
Svc->>Redis: invalidate cache
Svc-->>Ctrl: new snapshot
Ctrl-->>Admin: 200 + new ETag
else version moved on
Svc-->>Ctrl: throw VersionConflictError
Ctrl-->>Admin: 409 VERSION_CONFLICT + current config
end
A service consumer (mas-class-agent) reads the same snapshot via GET /api/mas/agent-configuration/active, presenting x-internal-token; the handler enforces it only when INTERNAL_SERVICE_TOKEN is set.
4. AI classroom request then generation then session + progress¶
sequenceDiagram
participant Student
participant Admin
participant Ctrl as AiClassroomController
participant DB as Postgres
participant Agent as mas-class-agent
Student->>Ctrl: POST /api/student/ai-classroom-requests { topic }
Ctrl->>DB: save AiClassroomRequest (pending)
Admin->>Ctrl: POST .../ai-classroom-requests/:id/approve
Ctrl->>Agent: POST /api/generate-classroom { requirement }
Agent-->>Ctrl: { jobId }
Ctrl->>DB: create AiClassroom (generating) + mark request approved
loop until terminal
Admin->>Ctrl: GET .../ai-classrooms/:id/status
Ctrl->>Agent: GET /api/generate-classroom/:jobId
Agent-->>Ctrl: status + result.classroomId
Ctrl->>DB: on succeeded set ready + classroomId + sceneCount
end
Admin->>Ctrl: POST .../ai-classrooms/:id/publish
Ctrl->>DB: status published + publishedAt
Ctrl->>Ctrl: notify enrolled students (non-blocking)
Student->>Ctrl: GET /api/student/ai-classrooms/:id
Ctrl->>DB: enrollment gate then get-or-create progress row
Ctrl->>Agent: GET /api/classroom?id=classroomId
Agent-->>Ctrl: scenes
Ctrl-->>Student: classroom + scenes + progress
Student->>Ctrl: POST .../ai-classrooms/:id/progress { completedScenes }
Ctrl->>DB: update progress (in_progress then submitted when all scenes done)
Admins can also author directly via POST /api/admin/mas/ai-classrooms (same generation trigger, skipping the request step). List and detail views self-heal: a row stuck in generating is re-polled against the agent (bounded 2s per request) and flipped to ready/archived so the UI reflects reality without a manual open.
5. AI-for-Everyone interest capture (public funnel)¶
sequenceDiagram
participant Visitor
participant Ctrl as AiForEveryoneController
participant Svc as AiForEveryoneService
participant DB as ai_for_everyone_interest
Visitor->>Ctrl: POST /api/ai-for-everyone/interest { name, email, phone, college }
Ctrl->>Ctrl: validate name, email regex, 10-digit phone, college
Ctrl->>Svc: createInterest(data)
Svc->>DB: findOne by email
alt email exists
Svc->>DB: update details (reset NOT_INTERESTED to NEW)
else new
Svc->>DB: insert (status NEW, source default website)
end
Svc-->>Ctrl: interest
Ctrl-->>Visitor: 201 thank-you
6. Admin LLM-Gateway dashboard (spend + controls)¶
sequenceDiagram
participant Admin
participant Ctrl as MasGatewayController
participant Svc as MasGatewayService
participant Gw as LlmGatewayService
participant LiteLLM as LiteLLM gateway
participant DB as Postgres
Admin->>Ctrl: GET /api/admin/mas/llm-gateway/overview
Ctrl->>Svc: overview(range)
Svc->>DB: load user_llm_keys (product ask-mas)
Svc->>Gw: listKeys + getActivity + getSpendLogs
Gw->>LiteLLM: GET /key/list /global/activity /spend/logs (master key)
LiteLLM-->>Gw: spend + limits + activity
Svc->>DB: join users + ask_mas_logs aggregates
Svc-->>Ctrl: KPIs + activity + byModel + topUsers
Ctrl-->>Admin: 200 dashboard data
Admin->>Ctrl: PATCH .../users/:userId/limits
Ctrl->>Svc: updateUserLimits
Svc->>Gw: updateUserKey
Gw->>LiteLLM: POST /key/update
LiteLLM-->>Gw: ok
Ctrl-->>Admin: 200 limits updated
Background jobs & async¶
This domain is mostly request/response, with a few fire-and-forget paths:
- Ask MAS audit logging —
logQasaves anask_mas_logsrow withoutawait; failures are swallowed (src/services/AskMasService.ts). - Classroom publish notification — on the transition into
published, the controller fans out aclassroom_assignednotification to enrolled students viaNotificationService.fromTemplatein a detached async IIFE (non-blocking; re-publish is silent). - Self-heal polling —
getAllClassroomsandgetClassroomByIdre-pollmas-class-agentfor stalegeneratingrows (Promise.allSettled, 2s timeout each) and patch terminal statuses. - Generation — the actual long-running classroom generation runs inside
mas-class-agent; the backend only fires the job and polls. No BullMQ queue is used in this domain. - Redis — used for Ask MAS per-user rate limiting (INCR + EXPIRE pipeline, fails open) and suggestion caching (300s), and for the agent-configuration read cache (
agent-config:active:v1, 300s, invalidated on PUT).
No webhooks or Socket.IO events are part of this domain.
External integrations¶
| Integration | How | Env vars |
|---|---|---|
| LiteLLM gateway | OpenAI-compatible proxy over internal Docker network; admin ops with master key, chat with per-user key | LITELLM_BASE_URL (default http://mas-llm-gateway:4000), LITELLM_MASTER_KEY, LITELLM_ADMIN_TIMEOUT_MS (default 10000) |
| Groq (fallback) | Direct OpenAI-compatible call when gateway unconfigured | GROQ_API_KEY, GROQ_TEXT_ENDPOINT, GROQ_TEXT_MODEL (default llama-3.3-70b-versatile), GROQ_TIMEOUT_MS (default 25000) |
| mas-class-agent | Generation, scene actions, classroom playback, provider credentials, LLM helpers, active-providers | CLASS_AGENT_URL (default http://localhost:3001), CLASS_AGENT_API_KEY (sent as X-Internal-API-Key; must equal the agent's INTERNAL_API_KEY) |
| Secret encryption | AES encrypt/decrypt of stored sk- keys |
LLM_GATEWAY_ENCRYPTION_KEY or PLATFORM_ENCRYPTION_KEY |
| Service auth (inbound) | mas-class-agent reading agent config |
INTERNAL_SERVICE_TOKEN (matched against x-internal-token; falls open if unset) |
| Ask MAS budgets | Per-student monthly budget + limits applied at key mint | ASKMAS_GATEWAY_MODEL (default smart), ASKMAS_USER_MONTHLY_BUDGET (default 2 USD), ASKMAS_USER_BUDGET_DURATION (default 30d), ASKMAS_USER_RPM (default 15), ASKMAS_USER_TPM (default 60000) |
| Classroom knowledge base | Default authoritative URLs when admin supplies none | n/a — defaults baked into src/utils/courseKnowledgeBase.ts (arxiv, paperswithcode, MIT OCW, Stanford CS, Google Research) |
Feature flags / fallbacks
LlmGatewayService.isConfigured()(both base URL + master key present) decides gateway vs Groq for Ask MAS, and gates the entire admin dashboard (503LLM_GATEWAY_NOT_CONFIGUREDwhen off).- If the gateway is off and
GROQ_API_KEYis also absent, Ask MAS throwsGROQ_NOT_CONFIGURED(503). - Classroom create/approve degrade gracefully: if the agent is unreachable, the classroom row is still created with status
readyand a "manual setup required" message. getActiveProvidersreturns empty arrays on agent failure so the frontend "shows everything" instead of blocking.
Status lifecycles¶
AiClassroom¶
stateDiagram-v2
[*] --> generating: create with agent jobId
[*] --> ready: create when agent unreachable
generating --> ready: agent reports succeeded
generating --> archived: agent reports failed
ready --> published: admin publishes
published --> published: re-publish (silent)
ready --> archived: (manual)
published --> archived: (manual)
archived --> [*]
AiClassroomProgress¶
stateDiagram-v2
[*] --> not_started: progress row created
not_started --> in_progress: first access or write
in_progress --> submitted: all scenes completed
submitted --> reviewed: batch lead reviews
completed --> reviewed: legacy rows reviewable
reviewed --> [*]
note right of submitted
submitted and reviewed are terminal
progress writes never demote them
end note
AiClassroomRequest¶
stateDiagram-v2
[*] --> pending: student submits
pending --> approved: admin approves then generation starts
pending --> rejected: admin rejects
approved --> [*]
rejected --> [*]
AiForEveryoneInterest¶
stateDiagram-v2
[*] --> new: form submitted
new --> contacted: admin sets contacted
contacted --> converted: lead converts
contacted --> not_interested: lead declines
not_interested --> new: resubmits form
converted --> [*]
Edge cases, limits & gotchas¶
- Gateway aliases, not models — Ask MAS sends
smart(orASKMAS_GATEWAY_MODEL); the real provider model is resolved gateway-side. The model recorded inask_mas_logsis the alias when routed via gateway, else the raw Groq model. - Budget exceeded is a graceful answer, not an error —
LlmBudgetExceededErrorproduces a friendly "usage limit reached" reply withrefusalReason: budget_exceeded, logged like any other answer. - Two-layer refusal — out-of-scope topics (resume/career/salary/placement) are caught pre-LLM (saves a call) and post-LLM (catches leaks).
package/hikeonly trip a refusal in a pay context so "Python package" stays in scope. - Reasoning blocks stripped —
<think>/<reasoning>blocks (Qwen/DeepSeek-R1 style), including orphaned openers/closers, are removed before display. - History injection guard — client-supplied chat history is sanitised to only
user/assistantturns (<4000 chars), last 8 — a client cannot inject asystemturn to override scope. - Rate limits fail open — if Redis is unreachable the Ask MAS rate-limit check allows the request rather than 500-ing. Ask = 10/60s, suggestions recompute = 20/60s.
- Message bounds — empty message →
EMPTY_MESSAGE(400); >2000 chars →MESSAGE_TOO_LONG(400). Suggestions are capped at 8 and cached 300s. - Optimistic concurrency — agent-config PUT requires
expectedVersion; a stale version returns 409 with the current config so the admin can re-apply. First-ever write accepts anyexpectedVersion. - Singleton config is untyped — the
configjsonb shape is owned by the frontend; adding a provider category needs no backend migration. - Per-user key uniqueness + race —
(userId, product)is unique; concurrent first-requests are handled by re-reading the winning row. - Encryption key required — minting/reading keys needs
LLM_GATEWAY_ENCRYPTION_KEYorPLATFORM_ENCRYPTION_KEY; without it decrypt/encrypt fails. - Classroom enrollment gate — classrooms attached to a course are visible/writable only to students enrolled in that course; standalone (no
courseId) classrooms are open to any authenticated student. - Studio URL is HMAC-signed —
getSignedStudioUrlsignsclassroomId + userId + timestampwithCLASS_AGENT_API_KEY(HMAC-SHA256) so the agent can verify embed requests. - AI-for-Everyone admin endpoints lack router-level auth — comments say "admin only" but no middleware is attached in
aiForEveryone.routes.ts(inferred; verify before treating these as protected). - Interest upsert by email — re-submitting an existing email updates the row rather than creating a duplicate, and revives a
not_interestedlead back tonew. - Admin dashboard depends on the live gateway — every
MasGatewayControllerroute returns 503 if the gateway is unconfigured; individual optional reads (activity/spend) fail soft to keep the dashboard alive.
Related docs¶
- Mr Hire — AI Resume Screening — another LLM-shaped pipeline; consumes AI providers.
- Mr Hire — Voice Interviews & AI Calling — AI-driven phone interviews.
- Assessments — Quizzes & Assignments — quiz generation and classroom quiz scoring.
- Education — LMS & Courses — courses/modules that scope AI classrooms.
- Student Engagement & Gamification — the dashboard signals Ask MAS reads (streak, XP, ratings, warnings).
- Student Portal & Profile — student-facing surfaces.
- Admin Ops & Config — admin dashboards including the LLM Gateway and agent-configuration UIs.
- Comms — Email & Notifications —
classroom_assignednotification template.