Do-Test API — Turn-based
Áp dụng cho SPEAK_A_SENTENCE và IMPROVE_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)
| Field | Kiểu | Ý nghĩa |
|---|---|---|
attemptsLeft | int | Số 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. |
turns | AnswerTurnDTO[] | 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)
| Field | Kiểu | Ý nghĩa |
|---|---|---|
turnId | String | Id turn (server sinh) — key cho poll + webhook marking. |
clientTurnId | String | Id do FE sinh — idempotency key: resubmit cùng key → trả lại turn cũ, không tạo mới. |
answerText | String | Câu trả lời học viên gửi. |
status | TurnStatus | PROCESSING | DONE | FAILED. |
verdict | SpeakAnswerVerdict | Kết luận chấm (xem bảng enum). |
feedback | String | Nhận xét cho học viên (ngôn ngữ theo teacherLanguageType). |
matchedStructure | String | Cấu trúc câu mà câu trả lời khớp (nếu CORRECT). |
transitionLine | String | Câu dẫn chuyển sang câu kế (khi advance). |
markedBy | MarkedBy | Ai chấm: DETERMINISTIC / AI / FALLBACK — provenance để audit. |
sendCount | int | Số lần đã gửi AI marking (dùng cho cron recovery, chặn vượt maxSendCount). |
sentMarking | boolean | Đã fire request AI marking chưa. |
createdAt | ZonedDateTime | Thời điểm tạo turn. |
markedAt | ZonedDateTime | Thờ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:
| Field | Kiểu | Ý nghĩa |
|---|---|---|
turnId, status, verdict, feedback, matchedStructure, transitionLine | Như AnswerTurn. | |
attemptsLeft | Integer | Lượt còn lại sau khi chấm turn này. |
action | TurnAction | FE làm gì tiếp: RETRY (còn lượt, làm lại) | NEXT (sang câu kế) | DONE (xong bài). |
nextQuestionId | String | Id câu kế (khi action=NEXT). |
Endpoint riêng
Command
| Method & path | Body | Response | Mô tả |
|---|---|---|---|
PATCH /progress/{progressId}/answer | SubmitAnswerRequestDTO (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 & path | Response | Mô tả |
|---|---|---|
GET /turn/{turnId} | AnswerTurnDTO | Poll kết quả một turn. Poll tới khi status != PROCESSING. Khi DONE có verdict, 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 & path | Body | Mô tả |
|---|---|---|
POST /marking/callback | MarkingCallbackRequestDTO (key = turnId, content, usage) | Webhook chấm AI: áp kết quả lên turn mang turnId, chuyển PROCESSING → DONE. Idempotent (turn đã DONE → bỏ qua). content/verdict null → fallbackResult (markedBy FALLBACK). |
POST /marking/scan-missing-ai-exercise-marking | — | Cron 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 → advanceFE poll GET /turn/{turnId} hoặc reload GET /{progressId}.
Field reference bổ sung
SubmitAnswerRequestDTO (body /answer)
| Field | Kiểu | Bắt buộc | Ý nghĩa |
|---|---|---|---|
questionId | String | ✅ | Câu đang trả lời (phải turn-based & ANSWERING). |
clientTurnId | String | ✅ | Idempotency key do FE sinh. |
answerText | String | ✅ | Nội dung câu trả lời. |
SubmitAnswerResponseDTO: turnId (String — poll bằng id này), status (TurnStatus — PROCESSING nếu chờ AI, DONE nếu deterministic bắt được ngay).
SpeakMarkingResult (payload AI trong content của callback)
| Field | Kiểu | Ý nghĩa |
|---|---|---|
verdict | SpeakAnswerVerdict | Kết luận chấm. |
feedback | String | Nhận xét cho học viên. |
matchedStructure | String | Cấu trúc khớp (nếu đúng). |
advance | boolean | AI đề nghị chuyển câu (thường true khi CORRECT). |
transitionLine | String | Câ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
| Enum | Giá trị | Ý nghĩa |
|---|---|---|
TurnStatus | PROCESSING | Đã tạo turn, đang chờ chấm (poll tiếp). |
DONE | Đã có verdict + feedback. | |
FAILED | Chấm lỗi không phục hồi. | |
SpeakAnswerVerdict | CORRECT | Đúng cấu trúc/nội dung → câu ANSWERED + advance. |
NOT_FOLLOW_STRUCTURE | Chưa theo cấu trúc yêu cầu. | |
WRONG_FILL | Điền sai chỗ trống/từ khóa. | |
OFF_TOPIC | Lạc đề. | |
WRONG_LANGUAGE | Sai ngôn ngữ (không dùng tiếng Anh…). | |
TOO_SHORT | Quá ngắn để chấm. | |
MarkedBy | DETERMINISTIC | Bắt bởi pre-check (SpeakAnswerEvaluator), không tốn AI. |
AI | Chấm bởi AI (webhook). | |
FALLBACK | AI fail → kết quả dự phòng. | |
TurnAction | RETRY / 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
OKGET /{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": []
}
]
}
}