Skip to content

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 at LITELLM_BASE_URL (default http://mas-llm-gateway:4000). It mints one virtual sk-... 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 in ask_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-agent reads 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 to mas-class-agent.
  • Credential proxy (src/controllers/credentialProxy.controller.ts) — forwards admin credential CRUD to mas-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 in user_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 via ASKMAS_GATEWAY_MODEL, default smart).
  • Budget exceeded — LiteLLM returns 400/429 with a "budget" message; the client raises LlmBudgetExceededError so 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.tsencryptSecret / 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 (completed is deprecated, kept for back-compat).
  • AiClassroomRequestStatus: pending | approved | rejected.
  • InterestStatus: new | contacted | converted | not_interested.
  • UserLlmKey has 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 attach authMiddleware/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 logginglogQa saves an ask_mas_logs row without await; failures are swallowed (src/services/AskMasService.ts).
  • Classroom publish notification — on the transition into published, the controller fans out a classroom_assigned notification to enrolled students via NotificationService.fromTemplate in a detached async IIFE (non-blocking; re-publish is silent).
  • Self-heal pollinggetAllClassrooms and getClassroomById re-poll mas-class-agent for stale generating rows (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 (503 LLM_GATEWAY_NOT_CONFIGURED when off).
  • If the gateway is off and GROQ_API_KEY is also absent, Ask MAS throws GROQ_NOT_CONFIGURED (503).
  • Classroom create/approve degrade gracefully: if the agent is unreachable, the classroom row is still created with status ready and a "manual setup required" message.
  • getActiveProviders returns 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 (or ASKMAS_GATEWAY_MODEL); the real provider model is resolved gateway-side. The model recorded in ask_mas_logs is the alias when routed via gateway, else the raw Groq model.
  • Budget exceeded is a graceful answer, not an errorLlmBudgetExceededError produces a friendly "usage limit reached" reply with refusalReason: 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/hike only 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/assistant turns (<4000 chars), last 8 — a client cannot inject a system turn 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 any expectedVersion.
  • Singleton config is untyped — the config jsonb 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_KEY or PLATFORM_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-signedgetSignedStudioUrl signs classroomId + userId + timestamp with CLASS_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_interested lead back to new.
  • Admin dashboard depends on the live gateway — every MasGatewayController route returns 503 if the gateway is unconfigured; individual optional reads (activity/spend) fail soft to keep the dashboard alive.