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

11 KiB

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:

{
  "message": "Human-readable message",
  "data": {},
  "success": true,
  "status_code": 200,
  "metadata": null
}

Most errors follow:

{
  "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)

{
  "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)

{
  "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)

{
  "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)

{
  "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.

Endpoint

POST /questions/validate-question-type-definition

Request

{
  "stimulus_component_kinds": ["QUESTION_TEXT", "AUDIO_PROMPT", "IMAGE"],
  "response_component_kinds": ["AUDIO_RESPONSE", "TEXT_INPUT"]
}

Success response

{
  "message": "Question type definition is valid",
  "data": { "valid": true }
}

Error response example

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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

{
  "question_id": 456,
  "display_order": 1
}

Success response example

{
  "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).

{
  "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

{
  "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

{
  "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

{
  "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.