SRDsExercise AIDo-Test APITurn-based (Speak / Improve)
DraftDT-4662Exercise AISRDAPIRESTTurnBasedSpeaking

Do-Test API — Turn-based

Áp dụng cho SPEAK_A_SENTENCEIMPROVE_A_SENTENCE (chung luồng chat theo lượt). Học viên gửi câu trả lời → deterministic pre-check hoặc AI chấm async → verdict + feedback. Tối đa 2 lượt/câu.

Quy ước chung + Common API: xem Tổng quan.

Shape content — ý nghĩa field

TurnBasedProgressContent (DTO: TurnBasedProgressContentDTO) → questions[] = TurnBasedQuestionProgress.

TurnBasedQuestionProgressDTO (kế thừa BaseQuestionProgressDTO: questionId, order, state)

FieldKiểuÝ nghĩa
attemptsLeftintSố lượt còn lại của câu (bắt đầu 2; giảm mỗi verdict sai). Hết lượt → advance.
turnsAnswerTurnDTO[]Lịch sử các lượt trả lời (mỗi lượt 1 bubble chat).

AnswerTurn (entity — 1 lượt trả lời)

FieldKiểuÝ nghĩa
turnIdStringId turn (server sinh) — key cho poll + webhook marking.
clientTurnIdStringId do FE sinh — idempotency key: resubmit cùng key → trả lại turn cũ, không tạo mới.
answerTextStringCâu trả lời học viên gửi.
statusTurnStatusPROCESSING | DONE | FAILED.
verdictSpeakAnswerVerdictKết luận chấm (xem bảng enum).
feedbackStringNhận xét cho học viên (ngôn ngữ theo teacherLanguageType).
matchedStructureStringCấu trúc câu mà câu trả lời khớp (nếu CORRECT).
transitionLineStringCâu dẫn chuyển sang câu kế (khi advance).
markedByMarkedByAi chấm: DETERMINISTIC / AI / FALLBACK — provenance để audit.
sendCountintSố lần đã gửi AI marking (dùng cho cron recovery, chặn vượt maxSendCount).
sentMarkingbooleanĐã fire request AI marking chưa.
createdAtZonedDateTimeThời điểm tạo turn.
markedAtZonedDateTimeThời điểm có kết quả chấm.

AnswerTurnDTO (response GET /turn/{turnId} + trong turns[]) — view FE-facing, thêm field điều hướng:

FieldKiểuÝ nghĩa
turnId, status, verdict, feedback, matchedStructure, transitionLineNhư AnswerTurn.
attemptsLeftIntegerLượt còn lại sau khi chấm turn này.
actionTurnActionFE làm gì tiếp: RETRY (còn lượt, làm lại) | NEXT (sang câu kế) | DONE (xong bài).
nextQuestionIdStringId câu kế (khi action=NEXT).

Endpoint riêng

Command

Method & pathBodyResponseMô tả
PATCH /progress/{progressId}/answerSubmitAnswerRequestDTO (questionId, clientTurnId, answerText)SubmitAnswerResponseDTO (turnId, status)Gửi một lượt trả lời. Idempotent theo clientTurnId (resubmit cùng key → trả lại turn cũ).

Query

Method & pathResponseMô tả
GET /turn/{turnId}AnswerTurnDTOPoll kết quả một turn. Poll tới khi status != PROCESSING. Khi DONEverdict, feedback, matchedStructure, attemptsLeft, action (RETRY/NEXT/DONE), transitionLine, nextQuestionId.

Marking (server-to-server)

Base: /api/v1/exercise-ai/dotest/marking. Internal call, không validator.

Method & pathBodyMô tả
POST /marking/callbackMarkingCallbackRequestDTO (key = turnId, content, usage)Webhook chấm AI: áp kết quả lên turn mang turnId, chuyển PROCESSINGDONE. Idempotent (turn đã DONE → bỏ qua). content/verdict null → fallbackResult (markedBy FALLBACK).
POST /marking/scan-missing-ai-exercise-markingCron recovery (n8n): resend các turn kẹt PROCESSING quá stuckThresholdSeconds, dưới maxSendCount. Trả số turn đã resend.

Luồng chấm

PATCH /answer
  ├─ validate: câu turn-based & ANSWERING & attemptsLeft > 0
  ├─ clientTurnId đã có? → trả lại turn cũ (idempotent)
  ├─ tạo AnswerTurn PROCESSING
  ├─ deterministic pre-check (SpeakAnswerEvaluator)
  │     └─ bắt được lỗi rõ ràng → áp kết quả ngay, trả DONE
  └─ else → AITextService.sendAsync (webhook key = turnId) → trả PROCESSING

   POST /marking/callback ◀──────┘  (AI trả kết quả)
        └─ verdict CORRECT → câu ANSWERED + advance
           verdict sai      → attemptsLeft-- ; hết lượt → advance

FE poll GET /turn/{turnId} hoặc reload GET /{progressId}.

Field reference bổ sung

SubmitAnswerRequestDTO (body /answer)

FieldKiểuBắt buộcÝ nghĩa
questionIdStringCâu đang trả lời (phải turn-based & ANSWERING).
clientTurnIdStringIdempotency key do FE sinh.
answerTextStringNội dung câu trả lời.

SubmitAnswerResponseDTO: turnId (String — poll bằng id này), status (TurnStatusPROCESSING nếu chờ AI, DONE nếu deterministic bắt được ngay).

SpeakMarkingResult (payload AI trong content của callback)

FieldKiểuÝ nghĩa
verdictSpeakAnswerVerdictKết luận chấm.
feedbackStringNhận xét cho học viên.
matchedStructureStringCấu trúc khớp (nếu đúng).
advancebooleanAI đề nghị chuyển câu (thường true khi CORRECT).
transitionLineStringCâu dẫn chuyển câu.

MarkingCallbackRequestDTO (body /marking/callback): key (String = turnId), content (SpeakMarkingResult), usage (Object — token usage, chỉ log). content/verdict null → áp fallbackResult (markedBy=FALLBACK).

Enum — ý nghĩa

EnumGiá trịÝ nghĩa
TurnStatusPROCESSINGĐã tạo turn, đang chờ chấm (poll tiếp).
DONEĐã có verdict + feedback.
FAILEDChấm lỗi không phục hồi.
SpeakAnswerVerdictCORRECTĐúng cấu trúc/nội dung → câu ANSWERED + advance.
NOT_FOLLOW_STRUCTUREChưa theo cấu trúc yêu cầu.
WRONG_FILLĐiền sai chỗ trống/từ khóa.
OFF_TOPICLạc đề.
WRONG_LANGUAGESai ngôn ngữ (không dùng tiếng Anh…).
TOO_SHORTQuá ngắn để chấm.
MarkedByDETERMINISTICBắt bởi pre-check (SpeakAnswerEvaluator), không tốn AI.
AIChấm bởi AI (webhook).
FALLBACKAI fail → kết quả dự phòng.
TurnActionRETRY / NEXT / DONEĐiều hướng FE sau khi turn DONE.

Ví dụ

PATCH /progress/{progressId}/answer

// Request — SubmitAnswerRequestDTO
{
  "questionId": "665fae11b2c3d4e5f6071900",
  "clientTurnId": "c1a2b3-client-uuid-0001",
  "answerText": "I usually go to school by bus."
}
// Response 200 — đang chấm AI (async)
{ "turnId": "6700b1c2d3e4f5061728394a", "status": "PROCESSING" }
// Response 200 — bắt được lỗi ở deterministic pre-check (trả ngay)
{ "turnId": "6700b1c2d3e4f5061728394b", "status": "DONE" }

GET /turn/{turnId}

// Response 200 — AnswerTurnDTO (đang chờ AI)
{ "turnId": "6700b1c2d3e4f5061728394a", "status": "PROCESSING", "verdict": null, "feedback": null, "attemptsLeft": null, "action": null }
// Response 200 — AnswerTurnDTO (đã chấm, đúng)
{
  "turnId": "6700b1c2d3e4f5061728394a",
  "status": "DONE",
  "verdict": "CORRECT",
  "feedback": "Tốt lắm! Câu của bạn đúng cấu trúc.",
  "matchedStructure": "S + V + O",
  "attemptsLeft": 1,
  "action": "NEXT",
  "transitionLine": "Cùng sang câu tiếp theo nhé!",
  "nextQuestionId": "665fae11b2c3d4e5f6071901"
}
// Response 200 — AnswerTurnDTO (sai, còn lượt → RETRY)
{
  "turnId": "6700b1c2d3e4f5061728394c",
  "status": "DONE",
  "verdict": "NOT_FOLLOW_STRUCTURE",
  "feedback": "Câu chưa theo cấu trúc yêu cầu. Thử lại nhé.",
  "matchedStructure": null,
  "attemptsLeft": 1,
  "action": "RETRY",
  "transitionLine": null,
  "nextQuestionId": null
}

POST /marking/callback (server-to-server)

// Request — MarkingCallbackRequestDTO (key = turnId)
{
  "key": "6700b1c2d3e4f5061728394a",
  "content": {
    "verdict": "CORRECT",
    "feedback": "Tốt lắm! Câu của bạn đúng cấu trúc.",
    "matchedStructure": "S + V + O",
    "transitionLine": "Cùng sang câu tiếp theo nhé!",
    "advance": true
  },
  "usage": { "input_tokens": 320, "output_tokens": 48 }
}
// Response 200
OK

GET /{progressId} — content turn-based

{
  "id": "6700a1b2c3d4e5f60718293a",
  "exerciseType": "SPEAK_A_SENTENCE",
  "status": "IN_PROGRESS",
  "currentIndex": 1,
  "totalQuestion": 10,
  "content": {
    "questions": [
      {
        "questionId": "665fae11b2c3d4e5f6071900",
        "order": 0,
        "state": "ANSWERED",
        "attemptsLeft": 1,
        "turns": [
          {
            "turnId": "6700b1c2d3e4f5061728394a",
            "status": "DONE",
            "verdict": "CORRECT",
            "feedback": "Tốt lắm!",
            "matchedStructure": "S + V + O",
            "attemptsLeft": 1,
            "action": "NEXT",
            "transitionLine": "Sang câu tiếp theo nhé!",
            "nextQuestionId": "665fae11b2c3d4e5f6071901"
          }
        ]
      },
      {
        "questionId": "665fae11b2c3d4e5f6071901",
        "order": 1,
        "state": "ANSWERING",
        "attemptsLeft": 2,
        "turns": []
      }
    ]
  }
}