Skip to content

Differentiate lessons within a curriculum unit - #300

Merged
artcc merged 4 commits into
artcc:developfrom
arqo123:feat/lesson-variety-within-unit
Aug 24, 2026
Merged

Differentiate lessons within a curriculum unit#300
artcc merged 4 commits into
artcc:developfrom
arqo123:feat/lesson-variety-within-unit

Conversation

@arqo123

@arqo123 arqo123 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Closes #295.

Problem

Every lesson of a unit is generated from the same grammar_points and vocabulary_set_ids, the prompt carries no information about the lessons already generated for that unit, and lesson_type appears only as a label echoed back into the output JSON. With no differentiation signal and no memory of the siblings, the model produces the same explanation, the same example sentences and the same "common mistakes" list for lesson after lesson.

Change

Lesson generation receives two additional signals.

Sibling-lesson context. build_previous_lessons_summary() condenses the already generated lessons of the same unit into a capped summary — at most the 6 most recent lessons, each with title, type, a truncated explanation excerpt, up to 3 example sentences, up to 6 vocabulary words and up to 2 common traps, followed by the vocabulary already introduced in the unit. It is injected into the prompt inside a <<<PREVIOUS_LESSONS block described as data only, with explicit instructions not to reuse the example sentences, the explanation angle, the situations or the traps. A unit's first lesson gets no block at all.

The router builds that list from the lessons it already loads for the plan, so GET /today makes no extra query, and it appends lessons generated earlier in the same request so several lessons of one unit generated together still see each other.

Per-type behaviour. lesson_type now selects an instruction block stating what the explanation, the exercise mix and the vocabulary of a grammar, vocabulary, reading, writing, listening or review lesson must emphasise. Unknown types fall back to a generic block. review keeps recycling the unit material by design, so its reuse rule is inverted — recycling is expected, but the sentences, contexts and exercises must be new.

The output JSON schema is untouched; only the instructions around it change.

Tests

  • tests/test_prompts.py — per-type blocks differ, unknown type falls back, the sibling block is delimited and present only when there is history, review keeps the recycling policy.
  • tests/test_lesson_generator.py — summary extraction, caps, vocabulary de-duplication, truncation and malformed-content tolerance, plus the prompt actually receiving the sibling context.
  • tests/test_study_plan.pyGET /today passes the unit's earlier lessons to the generator, including a lesson generated moments earlier in the same request.

Full backend suite: 986 passed, coverage 85%.

Docs

specs/prompts.instructions.md, specs/services.instructions.md and specs/study-plan.instructions.md are updated. CHANGELOG.md and specs/version.md are intentionally left untouched — releases and version bumps look like maintainer territory, and version edits would conflict across parallel PRs.

Not covered here

The "unexpected error" when opening a later lesson of a unit out of sequence, mentioned in the issue, is not addressed. It blocked my verification but is a separate matter.

Every lesson of a unit was generated from the same grammar points and
vocabulary sets, with no knowledge of its siblings and with lesson_type
used only as a label. The model had no differentiation signal, so
consecutive lessons of a unit repeated the same explanation, the same
example sentences and the same common mistakes.

Lesson generation now receives two additional signals:

- The lessons already generated for the same unit are condensed into a
  capped summary (titles, types, explanation excerpts, example sentences,
  vocabulary, common traps) and injected as delimited data the new lesson
  must not reuse. The router builds the list from the lessons it already
  loaded for the plan, so no extra query is needed, and keeps it current
  when several lessons of a unit are generated in one request.
- The declared lesson_type selects an instruction block describing what
  the explanation, the exercise mix and the vocabulary of a grammar,
  vocabulary, reading, writing, listening or review lesson must
  emphasise. Unknown types fall back to a generic block, and review
  lessons keep recycling the unit material but with new sentences and
  contexts.

The output JSON schema is unchanged.
@artcc

artcc commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Thanks for putting this together, the overall approach looks solid. I found four concrete correctness issues that I think should be addressed before merging:

  1. build_previous_lessons_summary() only collects vocabulary from the last six lessons because unit_words is populated inside previous_lessons[-PREVIOUS_LESSONS_LIMIT:]. Therefore, “Vocabulary already introduced in this unit” does not include the whole unit. Please collect unit vocabulary from all previous lessons while keeping the detailed lesson summaries limited to the six most recent ones.

  2. The new lesson-type guidance conflicts with the existing global grammar constraint. Vocabulary lessons require lexical exercises “not grammatical”, while constraint 2 requires at least 70% of exercises to target grammar whenever grammar_points is non-empty. These instructions cannot both be satisfied. The grammar percentage should be scoped or adjusted according to lesson_type.

  3. The prompt says that the student “has already worked through” the supplied lessons, but the router includes every generated sibling without checking is_completed. A supported flow can generate a lesson, skip it, and leave it pending. Please either pass only completed lessons to that block or change the wording and rules so generated-but-incomplete lessons are not treated as known material.

  4. The new summary builder can raise TypeError for content accepted by the current Pydantic schema. LessonContent.explanation and native_explanation are unrestricted dictionaries, so values such as {"examples": 1} or {"common_traps": true} are valid at schema level but are iterated as lists by the summary builder. Please validate these nested values with isinstance(..., list) before iterating, or strengthen the schema accordingly.

The context wiring, unit scoping, same-request context updates, and backward-compatible generator signature otherwise look correct.

Collect the unit vocabulary from every previous lesson instead of only
the six that get a detailed summary. Scope the minimum share of
grammar-targeting exercises to the lesson type, so a vocabulary lesson
can ask for lexical exercises without contradicting the strict
constraints. Stop claiming the student worked through the listed
lessons, since a sibling can still be pending. Skip explanation blocks
that are not lists, which the lesson schema still allows.
@arqo123

arqo123 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

All four addressed.

  1. Unit vocabulary. build_previous_lessons_summary() now walks every previous lesson and
    collects the vocabulary from all of them; the PREVIOUS_LESSONS_LIMIT slice only decides which
    lessons get a detailed entry. UNIT_VOCABULARY_LIMIT still caps the joined list at 40 words.
    The list is in lesson order, so the 40 that survive are the oldest words, the ones that no longer
    appear anywhere else in the summary; what the cap drops comes from the recent lessons, whose own
    "vocabulary taught" lines still list those words.

  2. Grammar percentage. The share is no longer a constant. build_grammar_exercise_ratio() maps
    lesson_type to it — 70% for grammar, review, and unknown types, 30% for vocabulary,
    reading, writing, and listening — and constraint 2 interpolates it. A grammar lesson gets
    exactly the same prompt as before; a vocabulary lesson now has room for the lexical exercises its
    focus block asks for.

  3. Pending siblings. I kept passing every generated sibling and changed the wording instead.
    Filtering on is_completed would drop the lessons the router appends to lessons_by_unit inside
    the same request, which is the case the sibling context exists for. The block now says those
    lessons belong to the unit "whether or not the student has worked through them yet", and the
    instruction to assume the earlier explanations are known is gone — what is left is the
    do-not-repeat rules, which hold for a pending sibling just as well.

  4. Non-list explanation values. Added an _as_list() helper and used it for
    explanation["examples"], content["vocabulary"], and native_explanation["common_traps"], so a
    scalar in any of those positions is skipped instead of raising. I left the schema alone since it
    also types what the model is allowed to return.

New tests cover the unit vocabulary beyond the detailed window, the per-type grammar ratio, the
reworded sibling block, and the non-list values. specs/study-plan.instructions.md and
specs/prompts.instructions.md are updated.

specs/study-plan.instructions.md also no longer describes the sibling list as being "in the order
the student worked through them"; it is plan order, completed or not.

@artcc

artcc commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Thanks!

@artcc
artcc merged commit 06655f7 into artcc:develop Aug 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lessons within the same unit are near-duplicates: generation prompt has no sibling-lesson context and no per-type differentiation

2 participants