Remove AUDIO_CLIP from the stimulus component catalog and use AUDIO_PROMPT for all question-side audio. Update integration, practice, and Postman docs accordingly. Co-authored-by: Cursor <cursoragent@cursor.com>
545 lines
11 KiB
Markdown
545 lines
11 KiB
Markdown
# Practice Creation API Guide (Lesson Scope)
|
|
|
|
This guide provides the full step-by-step API process to create a lesson practice when using:
|
|
|
|
- system-defined question types (`MCQ`, `TRUE_FALSE`, `SHORT_ANSWER`, `AUDIO`)
|
|
- dynamic question types (`DYNAMIC` with `question_type_definition_id` + `dynamic_payload`)
|
|
|
|
All endpoints below are relative to `/api/v1` and require bearer authentication.
|
|
|
|
---
|
|
|
|
## Standard Response Envelope
|
|
|
|
Most successful responses follow:
|
|
|
|
```json
|
|
{
|
|
"message": "Human-readable message",
|
|
"data": {},
|
|
"success": true,
|
|
"status_code": 200,
|
|
"metadata": null
|
|
}
|
|
```
|
|
|
|
Most errors follow:
|
|
|
|
```json
|
|
{
|
|
"message": "Error summary",
|
|
"error": "Detailed reason"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Required Permissions
|
|
|
|
At minimum, your role should have:
|
|
|
|
- `questions.create`
|
|
- `question_sets.create`
|
|
- `question_set_items.add`
|
|
- `practices.create`
|
|
|
|
If you create/update dynamic definitions:
|
|
|
|
- `questions.update`
|
|
- `questions.delete` (if you also delete definitions)
|
|
|
|
---
|
|
|
|
## End-to-End Flow
|
|
|
|
1. (Optional) Upload media assets
|
|
2. Create question(s):
|
|
- system-defined path, or
|
|
- dynamic path (definition + question)
|
|
3. Create `PRACTICE` question set
|
|
4. Add question(s) to the set
|
|
5. Create lesson practice linked to that set
|
|
6. Verify under lesson
|
|
|
|
---
|
|
|
|
## Step 0 (Optional): Upload Media
|
|
|
|
Use this when question content references audio/image/PDF URLs (e.g. dynamic `IMAGE`, `AUDIO_PROMPT`, or `PDF_ATTACHMENT` stimulus).
|
|
|
|
### Endpoint
|
|
|
|
`POST /files/upload` (multipart form-data)
|
|
|
|
### Form fields
|
|
|
|
- `file`: binary
|
|
- `media_type`: `image`, `audio`, `video`, or `pdf` (PDF is stored in MinIO; response includes presigned `url` and `object_key`)
|
|
|
|
### Example success response (shape)
|
|
|
|
```json
|
|
{
|
|
"message": "Media uploaded successfully",
|
|
"data": {
|
|
"url": "https://your-host/static/uploads/audio/abc.mp3",
|
|
"object_key": "audio/abc.mp3"
|
|
},
|
|
"success": true,
|
|
"status_code": 201
|
|
}
|
|
```
|
|
|
|
### Common errors
|
|
|
|
- `400` invalid media type/content type
|
|
- `500` upload/storage failure
|
|
|
|
Capture and reuse:
|
|
|
|
- `data.url` (or equivalent resolved file URL)
|
|
|
|
---
|
|
|
|
## Step 1A: Create System-Defined Question(s)
|
|
|
|
### Endpoint
|
|
|
|
`POST /questions`
|
|
|
|
### Request example (MCQ)
|
|
|
|
```json
|
|
{
|
|
"question_text": "Choose the correct sentence.",
|
|
"question_type": "MCQ",
|
|
"difficulty_level": "EASY",
|
|
"points": 1,
|
|
"status": "PUBLISHED",
|
|
"options": [
|
|
{ "option_text": "He go to school.", "is_correct": false },
|
|
{ "option_text": "He goes to school.", "is_correct": true }
|
|
]
|
|
}
|
|
```
|
|
|
|
### Request example (SHORT_ANSWER)
|
|
|
|
```json
|
|
{
|
|
"question_text": "Write one sentence using the word 'improve'.",
|
|
"question_type": "SHORT_ANSWER",
|
|
"difficulty_level": "MEDIUM",
|
|
"points": 2,
|
|
"status": "PUBLISHED",
|
|
"short_answers": [
|
|
{ "acceptable_answer": "I want to improve my English.", "match_type": "CASE_INSENSITIVE" }
|
|
]
|
|
}
|
|
```
|
|
|
|
### Example success response (shape)
|
|
|
|
```json
|
|
{
|
|
"message": "Question created successfully",
|
|
"data": {
|
|
"id": 456,
|
|
"question_text": "Choose the correct sentence.",
|
|
"question_type": "MCQ",
|
|
"status": "PUBLISHED"
|
|
},
|
|
"success": true,
|
|
"status_code": 201
|
|
}
|
|
```
|
|
|
|
### Common errors
|
|
|
|
- `400` validation/body errors
|
|
- `500` create failure
|
|
|
|
Capture:
|
|
|
|
- `data.id` as `question_id`
|
|
|
|
---
|
|
|
|
## Step 1B: Dynamic Question Path
|
|
|
|
If you use dynamic questions, follow these sub-steps.
|
|
|
|
### 1B.1 Validate component-kind selection (optional but recommended)
|
|
|
|
#### Endpoint
|
|
|
|
`POST /questions/validate-question-type-definition`
|
|
|
|
#### Request
|
|
|
|
```json
|
|
{
|
|
"stimulus_component_kinds": ["QUESTION_TEXT", "AUDIO_PROMPT", "IMAGE"],
|
|
"response_component_kinds": ["AUDIO_RESPONSE", "TEXT_INPUT"]
|
|
}
|
|
```
|
|
|
|
#### Success response
|
|
|
|
```json
|
|
{
|
|
"message": "Question type definition is valid",
|
|
"data": { "valid": true }
|
|
}
|
|
```
|
|
|
|
#### Error response example
|
|
|
|
```json
|
|
{
|
|
"message": "Invalid question type definition",
|
|
"error": "response: unknown component kind \"AUDIO_PROMPT\""
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 1B.2 Create or reuse a dynamic type definition
|
|
|
|
#### Endpoint
|
|
|
|
`POST /questions/type-definitions`
|
|
|
|
#### Request
|
|
|
|
```json
|
|
{
|
|
"key": "dialogue_audio_avatar_v1",
|
|
"display_name": "Dialogue Audio + Avatar",
|
|
"description": "Question text + prompt audio + two avatar images, with audio/text answer",
|
|
"stimulus_component_kinds": ["QUESTION_TEXT", "AUDIO_PROMPT", "IMAGE"],
|
|
"response_component_kinds": ["AUDIO_RESPONSE", "TEXT_INPUT"],
|
|
"stimulus_schema": [
|
|
{ "id": "question_text", "kind": "QUESTION_TEXT", "required": true },
|
|
{ "id": "prompt_audio", "kind": "AUDIO_PROMPT", "required": true },
|
|
{ "id": "speaker_a_avatar", "kind": "IMAGE", "required": true },
|
|
{ "id": "speaker_b_avatar", "kind": "IMAGE", "required": true }
|
|
],
|
|
"response_schema": [
|
|
{ "id": "answer_audio", "kind": "AUDIO_RESPONSE", "required": true },
|
|
{ "id": "answer_text", "kind": "TEXT_INPUT", "required": true }
|
|
],
|
|
"status": "ACTIVE"
|
|
}
|
|
```
|
|
|
|
#### Success response example
|
|
|
|
```json
|
|
{
|
|
"message": "Question type definition created",
|
|
"data": {
|
|
"id": 123,
|
|
"key": "dialogue_audio_avatar_v1",
|
|
"status": "ACTIVE"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Common errors
|
|
|
|
- `400` invalid schema/kinds/mapping
|
|
- `500` unexpected persistence errors
|
|
|
|
Capture:
|
|
|
|
- `data.id` as `question_type_definition_id`
|
|
|
|
---
|
|
|
|
### 1B.3 Create dynamic question
|
|
|
|
#### Endpoint
|
|
|
|
`POST /questions`
|
|
|
|
#### Request
|
|
|
|
```json
|
|
{
|
|
"question_type": "DYNAMIC",
|
|
"question_type_definition_id": 123,
|
|
"difficulty_level": "MEDIUM",
|
|
"points": 2,
|
|
"status": "PUBLISHED",
|
|
"dynamic_payload": {
|
|
"stimulus": [
|
|
{ "id": "question_text", "kind": "QUESTION_TEXT", "value": "Respond to the conversation." },
|
|
{ "id": "prompt_audio", "kind": "AUDIO_PROMPT", "value": "https://cdn.example.com/audio/prompt-1.mp3" },
|
|
{ "id": "speaker_a_avatar", "kind": "IMAGE", "value": "https://cdn.example.com/images/a.webp" },
|
|
{ "id": "speaker_b_avatar", "kind": "IMAGE", "value": "https://cdn.example.com/images/b.webp" }
|
|
],
|
|
"response": [
|
|
{ "id": "answer_audio", "kind": "AUDIO_RESPONSE", "value": { "instructions": "Record your answer" } },
|
|
{ "id": "answer_text", "kind": "TEXT_INPUT", "value": { "placeholder": "Type your answer" } }
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Success response example
|
|
|
|
```json
|
|
{
|
|
"message": "Question created successfully",
|
|
"data": {
|
|
"id": 789,
|
|
"question_type": "DYNAMIC",
|
|
"question_type_definition_id": 123,
|
|
"status": "PUBLISHED"
|
|
},
|
|
"success": true,
|
|
"status_code": 201
|
|
}
|
|
```
|
|
|
|
#### Common errors
|
|
|
|
- `400` missing/invalid `dynamic_payload`
|
|
- `400` missing `question_type_definition_id`
|
|
- `500` persistence failure
|
|
|
|
Capture:
|
|
|
|
- `data.id` as `question_id`
|
|
|
|
---
|
|
|
|
## Step 2: Create PRACTICE Question Set
|
|
|
|
### Endpoint
|
|
|
|
`POST /question-sets`
|
|
|
|
### Request
|
|
|
|
```json
|
|
{
|
|
"title": "Lesson 12 - Practice Set",
|
|
"description": "Question set for lesson-level practice",
|
|
"set_type": "PRACTICE",
|
|
"owner_type": "LESSON",
|
|
"owner_id": 12,
|
|
"status": "PUBLISHED",
|
|
"shuffle_questions": false
|
|
}
|
|
```
|
|
|
|
### Success response example
|
|
|
|
```json
|
|
{
|
|
"message": "Question set created successfully",
|
|
"data": {
|
|
"id": 55,
|
|
"title": "Lesson 12 - Practice Set",
|
|
"set_type": "PRACTICE",
|
|
"owner_type": "LESSON",
|
|
"owner_id": 12,
|
|
"status": "PUBLISHED"
|
|
},
|
|
"success": true,
|
|
"status_code": 201
|
|
}
|
|
```
|
|
|
|
### Common errors
|
|
|
|
- `400` invalid input
|
|
- `500` create failure
|
|
|
|
Capture:
|
|
|
|
- `data.id` as `set_id`
|
|
|
|
---
|
|
|
|
## Step 3: Add Question(s) to Set
|
|
|
|
Run this once per `question_id`.
|
|
|
|
### Endpoint
|
|
|
|
`POST /question-sets/:setId/questions`
|
|
|
|
### Request
|
|
|
|
```json
|
|
{
|
|
"question_id": 456,
|
|
"display_order": 1
|
|
}
|
|
```
|
|
|
|
### Success response example
|
|
|
|
```json
|
|
{
|
|
"message": "Question added to set successfully",
|
|
"data": {
|
|
"id": 901,
|
|
"set_id": 55,
|
|
"question_id": 456,
|
|
"display_order": 1
|
|
},
|
|
"success": true,
|
|
"status_code": 201
|
|
}
|
|
```
|
|
|
|
### Common errors
|
|
|
|
- `400` invalid `setId` or body
|
|
- `500` link/create failure
|
|
|
|
---
|
|
|
|
## Step 4: Create Lesson Practice
|
|
|
|
This creates the practice record scoped to lesson.
|
|
|
|
### Endpoint
|
|
|
|
`POST /practices`
|
|
|
|
### Request
|
|
|
|
`title` is optional; omit it or use an empty string to create a practice without a display title (stored as empty).
|
|
|
|
Include `publish_status`: `DRAFT` to hide the practice from subscribed learners until you set it to `PUBLISHED` (via create or `PUT /practices/:id`). Omit the field or send `PUBLISHED` to go live immediately (backward compatible).
|
|
|
|
```json
|
|
{
|
|
"parent_kind": "LESSON",
|
|
"parent_id": 12,
|
|
"title": "Lesson 12 Conversation Drill",
|
|
"story_description": "A short two-speaker scenario.",
|
|
"story_image": "https://cdn.example.com/images/story.webp",
|
|
"question_set_id": 55,
|
|
"quick_tips": "Listen carefully before answering.",
|
|
"publish_status": "DRAFT"
|
|
}
|
|
```
|
|
|
|
### Success response example
|
|
|
|
```json
|
|
{
|
|
"message": "Practice created successfully",
|
|
"data": {
|
|
"id": 37,
|
|
"parent_kind": "LESSON",
|
|
"parent_id": 12,
|
|
"title": "Lesson 12 Conversation Drill",
|
|
"question_set_id": 55
|
|
},
|
|
"success": true,
|
|
"status_code": 201
|
|
}
|
|
```
|
|
|
|
### Common errors
|
|
|
|
- `400` validation failed / invalid parent kind
|
|
- `404` lesson not found
|
|
- `404` question set not found
|
|
- `500` create failure
|
|
|
|
Capture:
|
|
|
|
- `data.id` as `practice_id`
|
|
|
|
---
|
|
|
|
## Step 5: Verify Practice Under Lesson
|
|
|
|
### Endpoint
|
|
|
|
`GET /lessons/:id/practices`
|
|
|
|
Example:
|
|
|
|
`GET /lessons/12/practices`
|
|
|
|
### Success response example
|
|
|
|
```json
|
|
{
|
|
"message": "Practices retrieved successfully",
|
|
"data": {
|
|
"practices": [
|
|
{
|
|
"id": 37,
|
|
"parent_kind": "LESSON",
|
|
"parent_id": 12,
|
|
"title": "Lesson 12 Conversation Drill",
|
|
"question_set_id": 55
|
|
}
|
|
],
|
|
"total_count": 1,
|
|
"limit": 20,
|
|
"offset": 0
|
|
},
|
|
"success": true,
|
|
"status_code": 200
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Optional Learner Completion Step
|
|
|
|
### Endpoint
|
|
|
|
`POST /progress/practices/:id/complete`
|
|
|
|
Use `practice_id` as `:id` for current behavior.
|
|
|
|
### Success response example
|
|
|
|
```json
|
|
{
|
|
"message": "Practice completed",
|
|
"success": true,
|
|
"status_code": 200
|
|
}
|
|
```
|
|
|
|
### Common errors
|
|
|
|
- `403` sequence gating violation
|
|
- `404` practice not found
|
|
- `500` completion/persistence failure
|
|
|
|
---
|
|
|
|
## Quick Checklist (IDs to Carry Forward)
|
|
|
|
- From question create: `question_id`
|
|
- From dynamic definition create (if used): `question_type_definition_id`
|
|
- From question set create: `set_id`
|
|
- From practice create: `practice_id`
|
|
|
|
---
|
|
|
|
## Notes and Pitfalls
|
|
|
|
- For dynamic questions, `question_type` must be `DYNAMIC`.
|
|
- For dynamic questions, both `question_type_definition_id` and `dynamic_payload` are required.
|
|
- `AUDIO_PROMPT` is stimulus-side; response-side audio uses `AUDIO_RESPONSE`.
|
|
- `question_set_id` in `POST /practices` must reference an existing set.
|
|
- For lesson practice always use:
|
|
- `parent_kind = "LESSON"`
|
|
- `parent_id = <lesson_id>`
|
|
- Publish questions and question set (`status = "PUBLISHED"`) if learners must complete immediately.
|