Architecture¶
A map of the code: the packages, the rules between them, the processes they run as, and where each piece of state lives. Subsystems have their own pages: Repetition schedule, Storage and identity, Tracing, Telegram bot, AI-generated content and Web frontend. CLAUDE.md holds short rules for AI agents that point to these pages. Why significant decisions were made is recorded in Architecture decision records.
This page says learner; in code, a learner is a student (StudentID, StudentRecord).
Packages¶
flowchart TD
bot["wiederholen.bot<br>Telegram"] --> school
web["wiederholen.web<br>web widget"] --> school
bot --> tracing
web --> tracing
school["wiederholen.school<br>learning, transport-agnostic"]
tracing["wiederholen.tracing<br>OpenTelemetry setup"]
wiederholen.schoolis everything about learning: the course, the repetition schedule, the learners' records. It knows nothing about Telegram or HTTP, so every frontend shares it.wiederholen.botandwiederholen.webare two frontends to it. They depend onschool, never the other way around, and never on each other.wiederholen.tracingis the OpenTelemetry setup shared by both frontends. It's a separate package so thatwebdoesn't have to import it frombot.
wiederholen.school¶
| Module | What it holds |
|---|---|
curriculum |
the course model — Exercise, Recall, TopicGroup, Course — and loading it from YAML (see Course format) |
tutoring |
the repetition engine: Tutor picks the next exercise, checks answers and decides on reminders, working on a StudentRecord; SelectablePairs chooses the word and topic; events reports what happened to the current trace |
authoring |
content made by generative AI: shadow_exercises (questions rewritten by Claude) and cues (illustrations from Workers AI, with the generator as cues/__main__.py) |
student_record_book |
StudentRecordBook, storage for learners' records, and its Redis implementation |
student_identity_store |
StudentIdentityStore, which maps logins (a Telegram chat, a browser) to learners |
cue_store |
CueStore, where illustrations are stored and looked up (Cloudflare R2), with an in-process cache |
i18n |
the supported languages |
wiederholen.school re-exports its public API from __init__.py, so frontends import from wiederholen.school directly.
wiederholen.bot¶
| Module | What it holds |
|---|---|
__init__ |
the aiogram dispatcher: middlewares and routers, in an order that matters — the tracing middleware comes first, so its span also covers loading the chat state |
commands/ |
one module per command: /start, /wiederholen (exercises, answers, recalls), /progress, /topics, /reminders, /language, /reset, /ai |
__main__, bootstrap |
startup: reading the configuration, building the stores, setting the bot's Telegram profile, polling |
reminder, reminder_settings |
the reminder process and each chat's reminder interval |
chat_membership |
noticing that a learner blocked or unblocked the bot |
pending_buttons |
removing inline buttons from older messages once they no longer apply |
topic_selection |
narrowing the course to the topic groups a chat picked |
feature_flags |
enabling a feature for specific chats |
l10n |
the bot's texts in Russian and English |
tracing |
a span per Telegram update |
wiederholen.web¶
A Litestar app with a JSON API and two static frontends: the embeddable <german-exercise-widget> (static/widget.js) and a standalone page built on it (static/app/). See Web frontend.
Rules¶
schooldoesn't know about frontends. Nothing in it importsbotorweb, deals with chat ids or HTTP, or reads environment variables — frontends pass everything in. The one exception is the cue generator's__main__, a command-line entry point nothing imports.tutoringdoes no I/O.Tutorworks on a plaindicthanded to it and never touches Redis, the network or the environment, so its tests need no mocks. Besides the standard library, it uses only pydantic (to validate records) and the OpenTelemetry API (which does nothing unless tracing is configured).- Frontends don't depend on each other. Code both need goes into
school(or, for tracing, intowiederholen.tracing). - AI calls live in
authoring. It's a sibling oftutoring, not part of it, so the scheduling engine never gains a network dependency.
Processes and state¶
flowchart LR
tg((Telegram)) <--> bot
tg <--> reminder
browser((Browser)) <--> web
bot --> records[(student records)]
bot --> identities[(identities)]
bot --> fsm[(chat state)]
reminder --> records
reminder --> identities
reminder --> fsm
web --> records
web --> identities
web --> sessions[(web sessions)]
bot -. lookups .-> r2[(R2: illustrations)]
tg -. fetches images .-> r2
generator[cue generator] -.-> r2
generator -.-> ai((Workers AI))
bot -.-> claude((Anthropic))
Dashed lines are optional integrations. The bot only checks whether an illustration exists and sends Telegram its public URL; Telegram fetches the image itself. How to run and configure each process is in Configuration.
| State | Store | Shared by |
|---|---|---|
| a learner's repetition schedule and progress | StudentRecordBook |
every frontend |
| which logins belong to which learner | StudentIdentityStore |
every frontend |
| a Telegram chat's settings (language, topics, reminders) and the exercise being answered | aiogram's FSM storage | bot and reminder |
| the exercise shown in a web session | WebSessionStore |
web only |
The split follows one question: would another frontend need this? A learner's progress, yes — someone who practices in Telegram and then logs in on the web should continue where they left off. A chat's language or the message with pending buttons, no.
How an answer is handled¶
What happens when a learner answers an exercise in Telegram:
TracingMiddlewareopens a span for the update; aiogram loads the chat's FSM state, which holds the exercise that was shown.- The handler resolves the chat to a learner through
StudentIdentityStore. - It checks out the learner's record from
StudentRecordBookand builds aTutoron it. Tutor.check_answer()marks the answer, reschedules the pair and schedules linked topics, recording what happened as span events.- The handler replies with the explanation and the next step (a recall or a "next" button).
- It clears the chat's conversation state but keeps the chat's settings (language,
/aimode, selected topics) — a bare clear would reset them after every exercise. - When the check-out block ends, the record is saved — but only if it changed.
The web app follows the same path with three differences: an HTTP request instead of a Telegram update, traced by Litestar's OpenTelemetry plugin; a browser cookie instead of a chat id; and WebSessionStore instead of aiogram's FSM storage for the exercise being shown.
Decisions¶
The full list is in the ADR index. Among them:
- ADR 0002: the repetition engine reports events on the current span.
- ADR 0003: learners are separate from the ways they log in.
- ADR 0007: a student record is saved only when it changes.
- ADR 0008: the student record is kept apart from chat state.
- ADR 0009: one environment variable per store.
- ADR 0010: reminders run as a separate process.