Yimaru-BackEnd/docs/PRACTICE_CREATION_API_GUIDE.md
Yared Yemane 33355a4b23 feat: PDF_ATTACHMENT stimulus, dynamic question_text rules, admin builder docs
Add PDF_ATTACHMENT stimulus kind and MinIO pdf upload (media_type=pdf) for question-side PDFs.

Reject top-level question_text on DYNAMIC create/update; omit it from API responses and derive stored text from stimulus only.

Expand DYNAMIC_QUESTION_TYPE_BUILDER_ADMIN_INTEGRATION.md with full API request/response reference and workflows.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 11:07:02 -07:00

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_CLIP`, 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.