from pydantic import BaseModel, Field from typing import Literal FormatType = Literal[ "Guide", "FullGuide", "Rest", ] ProviderType = Literal["claude", "minimax", "lokal"] SourceType = Literal["thema", "projekt", "uni", "link"] class GuideCreateRequest(BaseModel): topic: str = Field(min_length=1, max_length=100) format: FormatType instructions: str = Field(default="", max_length=2000) provider: ProviderType = "claude" ab_step: int | None = Field(default=None, ge=0, le=4) # re-run from guide step (0 outline … 4 read-exam); None = full/resume class TopicCreateRequest(BaseModel): name: str = Field(min_length=1, max_length=100) class BlocksCreateRequest(BaseModel): topic: str = Field(min_length=1, max_length=100) instructions: str = Field(default="", max_length=2000) provider: ProviderType = "claude" source_type: SourceType = "thema" source_location: str = Field(default="", max_length=2000) ab_phase: int | None = Field(default=None, ge=1, le=9) # re-run from a coarse phase (position in _phasen(topic), 1-based; up to 9: …outline/questions/artifacts); None = resume/continue without deleting ab_step: int | None = Field(default=None, ge=0) # re-run from a fine sub-step (0-based index into _blocks_steps); takes precedence over ab_phase to_step: int | None = Field(default=None, ge=0) # stop AFTER this fine sub-step (0-based index into _blocks_steps); None = run to the end class BlocksResetStepRequest(BaseModel): topic: str = Field(min_length=1, max_length=100) ab_step: int = Field(ge=0) # ONLY reset from this sub-step (no regeneration) class BlocksStep(BaseModel): label: str state: Literal["done", "active", "pending"] class BlocksFineStep(BaseModel): label: str phase: str = "" state: Literal["done", "active", "pending"] class BlocksStatusResponse(BaseModel): ready: bool generating: bool progress: str | None = None error: str | None = None partial: bool = False steps: list[BlocksStep] = [] feine_steps: list[BlocksFineStep] = [] class ProjectResponse(BaseModel): name: str class FolderResponse(BaseModel): name: str location: str # path relative to the repo root (e.g. "projects/foo") class BlocksSourceUpdate(BaseModel): topic: str = Field(min_length=1, max_length=100) type: SourceType = "thema" location: str = Field(default="", max_length=2000) spec: str = Field(default="", max_length=2000) class BlocksSourceResponse(BaseModel): type: SourceType location: str spec: str class SubblockInfo(BaseModel): title: str level: Literal["beginner", "advanced", "expert", "easy", "medium", "hard"] relevance: Literal["relevant", "peripheral"] | None = None class BlockOverview(BaseModel): num: int title: str description: str = "" subblocks: list[SubblockInfo] = [] class ProviderInfo(BaseModel): id: str available: bool class GuideResponse(BaseModel): id: str topic: str format: str status: str progress: str | None = None step: int | None = None error_msg: str | None = None created_at: str updated_at: str class ChatMessage(BaseModel): role: Literal["user", "assistant"] content: str = Field(min_length=1, max_length=8000) class GuideChatRequest(BaseModel): section: str = Field(default="", max_length=20000) outline: str = Field(default="", max_length=8000) messages: list[ChatMessage] = Field(min_length=1) provider: ProviderType = "claude" class GuideChatResponse(BaseModel): reply: str class ElementResponse(BaseModel): id: str topic: str title: str description: str = "" examples: list[str] = [] hints: list[str] = [] created_at: str updated_at: str class ElementCreateRequest(BaseModel): topic: str = Field(min_length=1, max_length=100) hint: str = Field(default="", max_length=500) provider: ProviderType = "claude" class ElementUpdateRequest(BaseModel): title: str | None = Field(default=None, max_length=200) description: str | None = None examples: list[str] | None = None hints: list[str] | None = None class ElementCheckRequest(BaseModel): provider: ProviderType = "claude" class ElementSuggestion(BaseModel): text: str target: Literal["description", "examples", "hints"] content: str class ElementCheckResponse(BaseModel): suggestions: list[ElementSuggestion] class ElementStyleChange(BaseModel): text: str action: Literal["remove", "adjust", "add"] target: Literal["title", "description", "examples", "hints"] index: int | None = None content: str = "" class ElementStyleResponse(BaseModel): changes: list[ElementStyleChange] class ElementChatRequest(BaseModel): messages: list[ChatMessage] = Field(min_length=1) provider: ProviderType = "claude" class ElementChatResponse(BaseModel): reply: str changes: list[ElementStyleChange] = [] class ElementRefineRequest(BaseModel): suggestion: ElementStyleChange instruction: str = Field(min_length=1, max_length=2000) provider: ProviderType = "claude" class ElementRefineResponse(BaseModel): change: ElementStyleChange class ProgressUpdate(BaseModel): chapter: str = Field(min_length=1, max_length=100) done: bool class ProgressResponse(BaseModel): chapters: list[str] # --- Block learning --- class BlockChatRequest(BaseModel): topic: str = Field(min_length=1, max_length=100) block: str = Field(min_length=1, max_length=200) section: str = Field(default="", max_length=20000) # detailed version section_compact: str = Field(default="", max_length=20000) # compact version (mnemonics) messages: list[ChatMessage] = Field(min_length=1) provider: ProviderType = "claude" class BlockChatResponse(BaseModel): reply: str class BlockExamRequest(BaseModel): topic: str = Field(min_length=1, max_length=100) block: str = Field(min_length=1, max_length=200) section: str = Field(default="", max_length=20000) # detailed version section_compact: str = Field(default="", max_length=20000) # compact version (mnemonics) action: Literal[ "question", "discussion", "answer", "answer_check", "quiz_question", "quiz_answer", "gap_question", "gap_answer", ] = "question" question: str = Field(default="", max_length=2000) # currently checked question (for discussion/answer); base anchor selection: list[int] = [] # quiz/gap-text choice: option indices picked by the learner correct: list[int] = [] # quiz/gap-text choice: correct indices (the client keeps them from generation) solution: str = Field(default="", max_length=500) # gap text free: expected term alternatives: list[str] = [] # gap text free: accepted synonyms input: str = Field(default="", max_length=500) # gap text free: typed term schwer: bool = False # variant: easy (+1/−1) vs hard (+3/−1) last_rating: str = Field(default="", max_length=2000) # feedback of the last rating (context for discussion) avoid: list[str] = [] # already-asked + earmarked questions — don't repeat them in substance asked_again: bool = False # asked_again was used for this question → gain capped at +1 reason: str = Field(default="", max_length=2000) # "thorough check": why dissatisfied with the rating pattern: str = Field(default="", max_length=2000) # drawn question pattern (seed); empty → live generation (fallback) # Base + cap are kept server-side (anchor / subs×25) — the client cap is only a hint. cap: int = Field(default=10, ge=1, le=10000) # score cap = unlocked subblocks × 25 messages: list[ChatMessage] = [] # dialog so far; empty = first question provider: ProviderType = "claude" thorough: bool = False # "thorough check": rating with a strong model (role guide) class QuizOption(BaseModel): text: str correct: bool class BlockExamResponse(BaseModel): question: str | None = None reply: str | None = None feedback: str | None = None points: int | None = None # points delta of this answer (−2 … +3); fast = expected rating: Literal["gut", "neutral", "schlecht"] | None = None # from the sign, for coloring options: list[QuizOption] | None = None # quiz: 4 options + correct flags sentence: str | None = None # gap text: sentence with a gap (___) solution: str | None = None # gap text: expected term alternatives: list[str] | None = None # gap text: accepted synonyms good_answers: int streak: int = 0 # current run of correct answers (per block) cap: int = 10 # cap_final = all subs × 25 — the frontend derives the learning level class BlockLearnState(BaseModel): good_answers: int streak: int = 0 cap: int = 0 # cap_final = all subblocks × 25 cap_aktuell: int = 0 # reachable cap of the currently unlocked level freie_level: int = 1 # 1=A · 2=F · 3=E · 4=V class BlockLearnStateResponse(BaseModel): blocks: dict[str, BlockLearnState] # --- Block content: check + apply one section on demand (focus, right-click) --- class BlockPruefenRequest(BaseModel): block: str = Field(min_length=1, max_length=200) spot: str = "ausführlich" # "compact" | "ausführlich" (displayed field) snippet: str = Field(min_length=1, max_length=20000) # raw markdown block hint: str = Field(default="", max_length=2000) # optional addition (✏️) provider: ProviderType = "claude" class BlockPruefenResponse(BaseModel): revised: str # corrected block as markdown class BlockUebernehmenRequest(BaseModel): block: str = Field(min_length=1, max_length=200) spot: str = "ausführlich" alt: str = Field(min_length=1, max_length=20000) revised: str = Field(default="", max_length=20000) provider: ProviderType = "claude" class BlockUebernehmenResponse(BaseModel): compact: str md: str found: bool