Appearance
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:649exposesPOST /api/questions/ai-classify/suggestions.node-platform/apps/api/src/modules/questions/questions.controller.ts:668exposesPOST /api/questions/ai-classify/jobs.node-platform/apps/api/src/modules/questions/questions.controller.ts:683exposesGET /api/questions/ai-classify/jobs.node-platform/apps/api/src/modules/questions/questions.controller.ts:703exposesGET /api/questions/ai-classify/jobs/:id.node-platform/apps/api/src/modules/questions/questions.controller.ts:717exposesGET /api/questions/ai-classify/jobs/:id/errors.node-platform/apps/api/src/modules/questions/questions.controller.ts:740streams SSE events withsnapshot,heartbeat,job.updated, anderror.node-platform/apps/api/src/modules/questions/questions.controller.ts:807exposesPOST /api/questions/ai-classify/jobs/:id/cancel.node-platform/apps/api/src/modules/questions/questions.controller.ts:558exposesPOST /api/questions/ai-generation/document-preview.node-platform/apps/api/src/modules/questions/questions.controller.ts:577exposesPOST /api/questions/ai-generation/generate.node-platform/apps/api/src/modules/questions/questions.controller.ts:493exposesPOST /api/questions/ai-solutions/editor.node-platform/apps/api/src/modules/questions/questions.controller.ts:598exposesPOST /api/questions/ai-solutions/apply.node-platform/apps/api/src/modules/attempts/attempts.controller.ts:41exposesPOST /api/attempts/:attemptId/submit.node-platform/apps/api/src/modules/app-data/app-data.exams-attempts.ts:558callsgradeQuestion(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:639implements objectivegradeQuestionscoring from by-value question snapshots and answer payloads.node-platform/apps/api/src/modules/app-data/app-data.shared.ts:364defines coreESSAYasTự luận,answerMode: "essay_rubric", andscoringRule.mode: "MANUAL".node-platform/apps/api/src/modules/questions/question-classification.queue.tsuses BullMQ job idquestion-classification-{jobId}, 3 attempts, exponential backoff, andREDIS_URL.node-platform/apps/api/src/modules/questions/question-classification.service.tsdefines 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.tssanitizes 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.tsestimates question-solution credit burn, records provider usage forquestion-solution.editor, returns editor solution previews, and applies solution writes only through explicit question mutation paths.node-platform/apps/api/prisma/schema.prismadefinesQuestionClassificationJob,AiProviderSetting, andAiProviderUsageEvent.
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:
AITaskRequestEnvelopeandAITaskResultEnvelopecoverclassify,generate,explain, andgrade.- 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-serviceandwallet-serviceowner 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 explicitquestion-servicehandoff. 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, andsimilarmodes.similaraccepts at most fivereferenceQuestionIds, 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 withchargePolicy=preview_only_no_consumeby default. - P6-047 adds an opt-in generation-only owner-service adapter. With
AI_GENERATION_USAGE_WALLET_ENABLED=1, AI checks the legacy-equivalentcredits_monthly,credits_weekly,credits_hourly, andtokens_monthlylimits through usage-service and reads wallet balance before the provider call. A usable result then idempotently consumes those usage counters and writes one walletUSAGE_DEBITfor3 * generatedQuestionCountcredits by default.chargePolicy=usage_wallet_settledis 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.
singleflightdedupes 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=1claims due pending rows withFOR 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 aquestion-solutionprovider usage event on success or failure, and keeps canonical solution writes inquestion-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 anessay-gradeprovider usage event without Usage quota consumption or Wallet movement, and keeps stored attempt grade writes inattempt-service. - P6-043 adds owner
GET /v1/ai/providers/statusfor admin-serviceai.providerssource 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/settingswith anADMINactor. 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-summaryfor admin operations queue-depth/failure projections andPOST /v1/ai-classifier/jobs/{id}/retryfor failed/cancelled native job retry. Admin-service and BFF must call those owner APIs instead of readingclassification_jobsdirectly. - 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, andPOST /api/attempts/ai-essay-grade/preview; they forward only to internal no-write preview endpoints after Gateway auth,ADMIN/TEACHERrole, and organization checks. The essay-grade preview never calls an attempt handoff or writes a stored score. These routes are available only inroutes.teacher-ai-preview-native-*.json;routes.jsoncontinues to send the broad question routes to legacy and has no dedicated essay-grade route.
| Method | Path | Purpose |
|---|---|---|
GET | /v1/ai/providers/status | Report AI provider runtime readiness for admin source snapshots without exposing secrets. |
GET | /v1/admin/ai/providers/settings | Read the AI-owned default provider selection and active runtime status; requires ADMIN. |
PUT | /v1/admin/ai/providers/settings | Validate and save a non-secret default provider selection, then switch the active runtime; requires ADMIN. |
GET | /v1/admin/ai-classifier/queue-summary | Report service-owned AI classification job counts, pending/running depth, recent failures, and owner command routes. |
POST | /v1/ai/generation/preview | Return no-write generated question previews with question-service as the canonical write handoff. |
POST | /v1/ai/explanation/preview | Return no-write editor solution previews; question-service remains the canonical solution write owner. |
POST | /v1/ai/essay-grade/preview | Return no-write rubric score previews; attempt-service remains the canonical stored grade owner. |
POST | /v1/ai-classifier/jobs | Create or reuse a pending/running classification job for the same actor, organization, filters, fields, mode, overwrite, and threshold. |
GET | /v1/ai-classifier/jobs | List 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}/errors | Read paged errors from the job result payload. |
GET | /v1/ai-classifier/jobs/{id}/events | Stream a legacy-shaped SSE snapshot, heartbeat, and job update. |
POST | /v1/ai-classifier/jobs/{id}/cancel | Cancel a pending/running job. |
POST | /v1/ai-classifier/jobs/{id}/retry | Retry 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, andX-Organization-Idheaders. The service rejects missing actor or organization scope, and accepts onlyADMINorTEACHERfor preview/job operations as a defense in depth behind Gateway RBAC. Queue summary additionally requiresADMIN; 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 toai-classifier-service; they still return mock no-write previews and do not promote canonical question or solution writes. - Public
/api/attempts/:attemptId/submitremains 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-summaryreturnssourceService=ai-classifier-service, status counts, a native queue sample overPENDING/RUNNING, recent failed jobs capped byrecentFailuresLimit(1..50, default10), and the owner retry/cancel command paths. It is the owner projection for adminqueue.depths; it does not promote public/api/questions/ai-classify/*routes.POST /v1/ai-classifier/jobs/{id}/retryaccepts onlyFAILEDorCANCELLEDnative 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 byAI_CLASSIFIER_MAX_RETRIES.- Deferred jobs use native worker claim fields on the job payload:
attempts,maxAttempts,nextRunAt,lockedUntil, andworkerId. 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, andAI_CLASSIFIER_WORKER_RETRY_BASE_DELAY_MS. AI_CLASSIFIER_PROVIDERcontrols the environment fallback provider runtime. An enabled AI-ownedprovider_settingsrow for featuredefaultinstead selects the provider/model/base URL at startup and is reported asconfigurationSource=database; no enabled row is reported asconfigurationSource=environment. Supported values aremock,openai,9router,gemini,local, andollama; the environment default ismock.AI_CLASSIFIER_API_KEY,AI_CLASSIFIER_BASE_URL, andAI_CLASSIFIER_MODELoverride provider-specific env such asOPENAI_API_KEY,GEMINI_API_KEY,OLLAMA_BASE_URL, andNINE_ROUTER_MODEL.- A persisted
apiKeyRefmay name onlyAI_CLASSIFIER_API_KEYor 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_MSremains the legacy-compatible timeout default source.AI_CLASSIFIER_TIMEOUT_MS,AI_CLASSIFIER_MAX_RETRIES,AI_CLASSIFIER_CIRCUIT_FAILURES, andAI_CLASSIFIER_CIRCUIT_COOLDOWN_MSprovide native runtime overrides.GET /v1/ai/providers/statusreturnsstatus=okfor usable mock or credentialed provider config andstatus=degradedwhen provider configuration is missing or unsupported. It includesconfigurationSource=environment|databaseand onlyapiKeyConfigured=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 sendsquestionIds, requested fields, mode, overwrite, and owner headers to question-bank-service, stores the returnedquestionContextin 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 toprovider_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/previewandPOST /v1/ai/explanation/preview; grade is available only as internalPOST /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 remainspreview_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-draftsthrough an exact opt-in Gateway route. AI service never performs that write; Question Bank forces the accepted candidate toDRAFTand owns accepted preview idempotency. A reviewed explanation preview follows the same boundary throughPOST /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-guardverifies 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,POSTmethod, 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.