Skip to content

AI Service API (ai-classifier-service alias)

Status: P6-045 teacher AI preview Gateway rehearsal with default legacy rollback

Legacy Evidence

  • node-platform/apps/api/src/modules/questions/questions.controller.ts:649 exposes POST /api/questions/ai-classify/suggestions.
  • node-platform/apps/api/src/modules/questions/questions.controller.ts:668 exposes POST /api/questions/ai-classify/jobs.
  • node-platform/apps/api/src/modules/questions/questions.controller.ts:683 exposes GET /api/questions/ai-classify/jobs.
  • node-platform/apps/api/src/modules/questions/questions.controller.ts:703 exposes GET /api/questions/ai-classify/jobs/:id.
  • node-platform/apps/api/src/modules/questions/questions.controller.ts:717 exposes GET /api/questions/ai-classify/jobs/:id/errors.
  • node-platform/apps/api/src/modules/questions/questions.controller.ts:740 streams SSE events with snapshot, heartbeat, job.updated, and error.
  • node-platform/apps/api/src/modules/questions/questions.controller.ts:807 exposes POST /api/questions/ai-classify/jobs/:id/cancel.
  • node-platform/apps/api/src/modules/questions/questions.controller.ts:558 exposes POST /api/questions/ai-generation/document-preview.
  • node-platform/apps/api/src/modules/questions/questions.controller.ts:577 exposes POST /api/questions/ai-generation/generate.
  • node-platform/apps/api/src/modules/questions/questions.controller.ts:493 exposes POST /api/questions/ai-solutions/editor.
  • node-platform/apps/api/src/modules/questions/questions.controller.ts:598 exposes POST /api/questions/ai-solutions/apply.
  • node-platform/apps/api/src/modules/attempts/attempts.controller.ts:41 exposes POST /api/attempts/:attemptId/submit.
  • node-platform/apps/api/src/modules/app-data/app-data.exams-attempts.ts:558 calls gradeQuestion(snapshot, answer, score) during attempt submit and persists stored answer/attempt scores.
  • node-platform/apps/api/src/modules/app-data/app-data.exam-runtime-core.ts:639 implements objective gradeQuestion scoring from by-value question snapshots and answer payloads.
  • node-platform/apps/api/src/modules/app-data/app-data.shared.ts:364 defines core ESSAY as Tự luận, answerMode: "essay_rubric", and scoringRule.mode: "MANUAL".
  • node-platform/apps/api/src/modules/questions/question-classification.queue.ts uses BullMQ job id question-classification-{jobId}, 3 attempts, exponential backoff, and REDIS_URL.
  • node-platform/apps/api/src/modules/questions/question-classification.service.ts defines classification fields, job status values, progress/result shapes, idempotency, batch/concurrency limits, provider timeout, and provider fallback rules.
  • node-platform/apps/api/src/modules/questions/question-generation.service.ts sanitizes public generation inputs, estimates credit/token burn from count, supports source document references, and returns generated question previews before later apply/persistence flow.
  • node-platform/apps/api/src/modules/questions/question-solution.service.ts estimates question-solution credit burn, records provider usage for question-solution.editor, returns editor solution previews, and applies solution writes only through explicit question mutation paths.
  • node-platform/apps/api/prisma/schema.prisma defines QuestionClassificationJob, AiProviderSetting, and AiProviderUsageEvent.

Native Foundation

P6-005 implements internal /v1/ai-classifier/* endpoints for a service-owned job foundation. P6-006 adds pgx-backed persistence for jobs and provider usage events when DATABASE_URL is configured and healthy, with in-memory fallback for local/dev runs without Postgres. P6-037 adds env-configured provider adapters while keeping mock as the default. P6-038 adds the question-bank service-owned /v1/questions/ai-classify/context endpoint, and the current AI classifier runtime consumes it through an optional service-to-service client when QUESTION_BANK_SERVICE_BASE_URL or QUESTION_BANK_SERVICE_URL is set. That client supplies bounded question prompt rows and taxonomy candidates to classifier providers before the provider call. SVC-015 adds native queue worker semantics for deferred classification jobs: the Postgres store can atomically claim due PENDING jobs with row locks and SKIP LOCKED, persist attempts, maxAttempts, nextRunAt, lockedUntil, and workerId, then process provider calls in fixed batches of 5 question IDs. AI_CLASSIFIER_BATCH_CONCURRENCY controls provider fan-out within one job independently of queue-worker concurrency. It defaults to one and is bounded to five; when increased, completed provider batches are still persisted as ordered results, usage events, and progress snapshots. Worker-owned failures requeue with exponential backoff beginning at 3000 ms until the configured max attempts is reached. The worker is opt-in through AI_CLASSIFIER_WORKER_ENABLED=1; request-triggered inline processing remains available for internal non-deferred jobs.

P6-039 adds the internal contract-first envelope for the next AI-owned task families before adding public routes:

  • AITaskRequestEnvelope and AITaskResultEnvelope cover classify, generate, explain, and grade.
  • Every request envelope carries one matching typed payload, an idempotency key, cancellation key, provider feature flags, usage event fields, and rollback notes.
  • Generation results are preview/handoff payloads for question-service; they do not create canonical questions directly.
  • Explanation results are editor previews and do not mutate question data in AI service. A teacher may later submit a reviewed preview to Question Bank's separate versioned owner-write route.
  • Essay grading accepts attempt/exam/question snapshots by value and returns a future handoff payload for attempt-service; it does not write attempt results directly.
  • AI records usage telemetry and charge policy. The opt-in generation adapter calls usage-service and wallet-service owner APIs, but AI-service never moves credits or enforces plan logic locally.
  • P6-040 adds internal POST /v1/ai/generation/preview. It normalizes a generate payload into the same AI task envelope, uses a configured non-mock adapter for provider-backed preview questions when available, records a provider usage event on success or failure, and carries an explicit question-service handoff. Mock remains deterministic for local development. It does not create, update, or apply canonical question records.
  • P6-046 extends generation preview with typed fresh, advanced, and similar modes. similar accepts at most five referenceQuestionIds, then loads their bounded content/options through question-bank-service using the current organization and actor headers. Caller-supplied reference content is never trusted. The result remains a no-write preview with chargePolicy=preview_only_no_consume by default.
  • P6-047 adds an opt-in generation-only owner-service adapter. With AI_GENERATION_USAGE_WALLET_ENABLED=1, AI checks the legacy-equivalent credits_monthly, credits_weekly, credits_hourly, and tokens_monthly limits through usage-service and reads wallet balance before the provider call. A usable result then idempotently consumes those usage counters and writes one wallet USAGE_DEBIT for 3 * generatedQuestionCount credits by default. chargePolicy=usage_wallet_settled is returned only after both owner-service operations succeed. AI never accesses their databases, and the default flag remains off.
  • P6-048 adds a service-owned replay cache only while P6-047 is enabled. After provider output, AI stores a 24-hour pending replay under the normalized payload hash already used as the idempotency key plus organization and actor. If settlement or successful usage-event recording fails, a retry with that same key bypasses Question Bank reference loading, preflight, and provider execution, then retries owner settlement from the persisted provider usage. Only a replay whose settlement and telemetry both succeed becomes a settled cache response with an explicit warning. Provider or pending-replay save failures return an error and can re-enter the provider on a later retry. Recovery relies on owner-service idempotency keys and does not compensate a partially applied wallet/usage sequence. singleflight dedupes simultaneous identical requests only within one service instance; the Postgres cache covers later retries and restarts, not an in-flight duplicate across pods.
  • P6-049 adds opt-in durable forward reconciliation for an unsettled P6-048 replay. AI_GENERATION_SETTLEMENT_WORKER_ENABLED=1 claims due pending rows with FOR UPDATE SKIP LOCKED, a persisted lease, attempt count, retry time, and last error. It keeps a pending result claimable after preview-cache expiry until owner settlement succeeds, then uses the original wallet and usage idempotency keys. The worker never reruns a provider request, preflight, or Question Bank context/write. It remains disabled by default, changes no public route, and is forward reconciliation rather than compensation.
  • P6-041 adds internal POST /v1/ai/explanation/preview. It normalizes an editor solution request into the same AI task envelope, uses a configured non-mock adapter for provider-backed solution text/steps when available, records a question-solution provider usage event on success or failure, and keeps canonical solution writes in question-service. Mock remains deterministic for local development.
  • P6-042 adds internal POST /v1/ai/essay-grade/preview. It normalizes attempt/question/answer snapshots and rubric criteria into the same AI task envelope. A configured OpenAI-compatible, Gemini, local, or Ollama adapter returns rubric-constrained advisory scores; the deterministic mock remains the local fallback. It records an essay-grade provider usage event without Usage quota consumption or Wallet movement, and keeps stored attempt grade writes in attempt-service.
  • P6-043 adds owner GET /v1/ai/providers/status for admin-service ai.providers source snapshots. It reports environment or database-selected provider/model readiness and native AI slice feature flags without returning API keys or calling the live provider network.
  • The AI-owned default provider selection is readable and writable only through GET/PUT /v1/admin/ai/providers/settings with an ADMIN actor. It stores provider, model, base URL, and an allowlisted secret environment-variable reference, then switches the in-process provider after validating the non-secret setting. Raw provider API keys are neither accepted nor returned.
  • The service now exposes owner GET /v1/admin/ai-classifier/queue-summary for admin operations queue-depth/failure projections and POST /v1/ai-classifier/jobs/{id}/retry for failed/cancelled native job retry. Admin-service and BFF must call those owner APIs instead of reading classification_jobs directly.
  • P6-045 adds a non-default Gateway rehearsal for teacher generation, explanation, and essay-grade previews. The exact public paths are POST /api/questions/ai-generation/generate, POST /api/questions/ai-solutions/editor, and POST /api/attempts/ai-essay-grade/preview; they forward only to internal no-write preview endpoints after Gateway auth, ADMIN/TEACHER role, and organization checks. The essay-grade preview never calls an attempt handoff or writes a stored score. These routes are available only in routes.teacher-ai-preview-native-*.json; routes.json continues to send the broad question routes to legacy and has no dedicated essay-grade route.
MethodPathPurpose
GET/v1/ai/providers/statusReport AI provider runtime readiness for admin source snapshots without exposing secrets.
GET/v1/admin/ai/providers/settingsRead the AI-owned default provider selection and active runtime status; requires ADMIN.
PUT/v1/admin/ai/providers/settingsValidate and save a non-secret default provider selection, then switch the active runtime; requires ADMIN.
GET/v1/admin/ai-classifier/queue-summaryReport service-owned AI classification job counts, pending/running depth, recent failures, and owner command routes.
POST/v1/ai/generation/previewReturn no-write generated question previews with question-service as the canonical write handoff.
POST/v1/ai/explanation/previewReturn no-write editor solution previews; question-service remains the canonical solution write owner.
POST/v1/ai/essay-grade/previewReturn no-write rubric score previews; attempt-service remains the canonical stored grade owner.
POST/v1/ai-classifier/jobsCreate or reuse a pending/running classification job for the same actor, organization, filters, fields, mode, overwrite, and threshold.
GET/v1/ai-classifier/jobsList jobs by optional status with limit/offset pagination.
GET/v1/ai-classifier/jobs/{id}Read one job scoped by actor/organization headers.
GET/v1/ai-classifier/jobs/{id}/errorsRead paged errors from the job result payload.
GET/v1/ai-classifier/jobs/{id}/eventsStream a legacy-shaped SSE snapshot, heartbeat, and job update.
POST/v1/ai-classifier/jobs/{id}/cancelCancel a pending/running job.
POST/v1/ai-classifier/jobs/{id}/retryRetry a failed/cancelled native AI classification job through the configured provider.

Compatibility Notes

  • Interactive AI preview and classification-job owner APIs require Gateway-authenticated X-Actor-Id, X-Actor-Role, and X-Organization-Id headers. The service rejects missing actor or organization scope, and accepts only ADMIN or TEACHER for preview/job operations as a defense in depth behind Gateway RBAC. Queue summary additionally requires ADMIN; provider status remains an internal admin-service source snapshot. These headers are Gateway-owned and callers must not trust or forward browser-supplied values.
  • Public /api/questions/ai-classify/* remains legacy-proxied.
  • Default public /api/questions/ai-generation/* and /api/questions/ai-solutions/* remain legacy-proxied. The P6-045 non-default manifests narrow the two teacher preview paths above to ai-classifier-service; they still return mock no-write previews and do not promote canonical question or solution writes.
  • Public /api/attempts/:attemptId/submit remains legacy-proxied.
  • The native foundation returns the existing success envelope shape used by other Go services: { success, data, message }.
  • Job statuses preserve legacy values: PENDING, RUNNING, COMPLETED, FAILED, CANCELLED.
  • GET /v1/admin/ai-classifier/queue-summary returns sourceService=ai-classifier-service, status counts, a native queue sample over PENDING/RUNNING, recent failed jobs capped by recentFailuresLimit (1..50, default 10), and the owner retry/cancel command paths. It is the owner projection for admin queue.depths; it does not promote public /api/questions/ai-classify/* routes.
  • POST /v1/ai-classifier/jobs/{id}/retry accepts only FAILED or CANCELLED native jobs, resets result/error/runtime timestamps, and processes the same job id again. It also clears native worker claim fields and attempt counters before reprocessing. Provider-level HTTP retry remains separate and controlled by AI_CLASSIFIER_MAX_RETRIES.
  • Deferred jobs use native worker claim fields on the job payload: attempts, maxAttempts, nextRunAt, lockedUntil, and workerId. These fields are internal runtime state and are not browser-facing API commitments.
  • Native worker runtime env: AI_CLASSIFIER_WORKER_ENABLED, AI_CLASSIFIER_WORKER_CONCURRENCY, AI_CLASSIFIER_BATCH_CONCURRENCY, AI_CLASSIFIER_WORKER_POLL_INTERVAL_MS, AI_CLASSIFIER_WORKER_LEASE_MS, AI_CLASSIFIER_WORKER_MAX_ATTEMPTS, and AI_CLASSIFIER_WORKER_RETRY_BASE_DELAY_MS.
  • AI_CLASSIFIER_PROVIDER controls the environment fallback provider runtime. An enabled AI-owned provider_settings row for feature default instead selects the provider/model/base URL at startup and is reported as configurationSource=database; no enabled row is reported as configurationSource=environment. Supported values are mock, openai, 9router, gemini, local, and ollama; the environment default is mock.
  • AI_CLASSIFIER_API_KEY, AI_CLASSIFIER_BASE_URL, and AI_CLASSIFIER_MODEL override provider-specific env such as OPENAI_API_KEY, GEMINI_API_KEY, OLLAMA_BASE_URL, and NINE_ROUTER_MODEL.
  • A persisted apiKeyRef may name only AI_CLASSIFIER_API_KEY or the selected provider's documented secret environment variable. The service resolves that reference at runtime, rejects unrelated names, and never stores or exposes a raw secret. Provider settings are internal owner APIs and do not add a default Gateway route.
  • AI_PROVIDER_TIMEOUT_MS remains the legacy-compatible timeout default source. AI_CLASSIFIER_TIMEOUT_MS, AI_CLASSIFIER_MAX_RETRIES, AI_CLASSIFIER_CIRCUIT_FAILURES, and AI_CLASSIFIER_CIRCUIT_COOLDOWN_MS provide native runtime overrides.
  • GET /v1/ai/providers/status returns status=ok for usable mock or credentialed provider config and status=degraded when provider configuration is missing or unsupported. It includes configurationSource=environment|database and only apiKeyConfigured=true/false, never secret values.
  • Non-mock providers record success/failure usage telemetry with provider, base URL, model, operation, latency, token counts when returned by the provider, and retry/circuit metadata.
  • Native question prompt input is provided by question-bank-service POST /v1/questions/ai-classify/context. When the context client is configured, ai-classifier-service sends questionIds, requested fields, mode, overwrite, and owner headers to question-bank-service, stores the returned questionContext in job scope, and includes it in provider prompts. If the configured context client fails, the job fails instead of silently falling back to an ids-only provider payload.
  • Job scope/progress/result payloads persist to service-owned JSONB columns in classification_jobs; provider telemetry persists to provider_usage_events.
  • Applying classification results to questions remains a later question-bank adapter; this slice does not mutate question-bank-service data.
  • Generate and explain are available only as internal POST /v1/ai/generation/preview and POST /v1/ai/explanation/preview; grade is available only as internal POST /v1/ai/essay-grade/preview. Generation and explanation use the configured OpenAI-compatible, Gemini, local, or Ollama adapter when selected; mock is deterministic otherwise. Grade follows the same provider/fallback rule but remains preview_only_no_consume. AI returns only previews or explicit owner handoff payloads; it never writes Question Bank or attempt data itself.
  • A reviewed generation candidate may now be sent by the teacher UI to the separate Question Bank owner route POST /v1/questions/ai-preview-drafts through an exact opt-in Gateway route. AI service never performs that write; Question Bank forces the accepted candidate to DRAFT and owns accepted preview idempotency. A reviewed explanation preview follows the same boundary through POST /v1/questions/ai-preview-solutions: Question Bank appends a canonical replacement version and owns tenant scope, provenance, and (organizationId, actorId, previewId) replay handling. AI remains preview-only in both flows.
  • A charged generation retry first reads the 24-hour settled-result cache, scoped by organization, actor, and normalized request hash. A cache hit returns the original no-write preview without provider, usage, or wallet I/O; cache misses retain the existing idempotent owner-service settlement. This is still an exact opt-in route rehearsal rather than a default public charge path, and it does not coordinate simultaneous requests across service pods.
  • make test-ai-classification-job-route-guard verifies gateway route tables do not accidentally expose public job, suggestion, SSE, error, cancel, or broad AI routes to this internal foundation. It allows only the exact P6-045 teacher preview carve-outs with their fixed paths, POST method, native-write state, auth/role/org guards, and ai-classifier targets; the existing exact apply route remains allowed because it targets question-bank-service instead.
  • Full public cutover still requires BullMQ/job worker parity, gateway RBAC/maintenance behavior, and browser parity.

Rollback

Keep or set /api/questions/ai-classify/* and /api/questions/ai-generation/*, /api/questions/ai-solutions/*, and /api/attempts/:attemptId/submit gateway routes to legacy_proxy in the default route table. P6-045 rollback is returning the Gateway to routes.json or removing the two non-default teacher preview entries. The P6-006, P6-040, P6-041, P6-042, and P6-043 native endpoints remain internal-only outside that opt-in rehearsal.

Go-platform documentation is generated from repository Markdown.