from pydantic import BaseModel, Field from typing import Literal FormatType = Literal[ "Guide", "FullGuide", "Rest", ] from config import DEFAULT_PROVIDER 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 = DEFAULT_PROVIDER ab_step: int | None = Field(default=None, ge=0, le=5) # re-run from board stage (0 lernziele … 5 lesbarkeit); None = full/resume class GuideBoardResetRequest(BaseModel): topic: str = Field(min_length=1, max_length=100) format: FormatType = "Guide" ab_stage: int = Field(ge=0, le=5) # reset cards back to this board stage (no generation) class TopicCreateRequest(BaseModel): name: str = Field(min_length=1, max_length=100) class QaRunRequest(BaseModel): topic: str = Field(min_length=1, max_length=100) llm: bool = True # wie das Gate: Echtheits-/Dubletten-Stichprobe inklusive class RepairRequest(BaseModel): topic: 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 = DEFAULT_PROVIDER source_type: SourceType = "thema" source_location: str = Field(default="", max_length=20000) # link mode: one URL per line research: bool = True # False = Continue: drain the existing kanban queue, no new search qa_force: bool = False # True = übersteuert ein pausierendes QA-Gate („Trotzdem fortsetzen") class BlocksCardRestartRequest(BaseModel): topic: str = Field(min_length=1) card_id: str = Field(min_length=1, max_length=200) class GuideFormatRequest(BaseModel): topic: str = Field(min_length=1) format: str = Field(min_length=1) class PracticeAnswerRequest(BaseModel): topic: str = Field(min_length=1) block_norm: str = Field(min_length=1, max_length=300) sub_norm: str = Field(max_length=300) correct: bool class GuideCardResetRequest(BaseModel): topic: str = Field(min_length=1) format: str = Field(min_length=1) block_norm: str = Field(min_length=1, max_length=200) ab_stage: int = Field(ge=0, le=5) class BlocksResetStageRequest(BaseModel): topic: str = Field(min_length=1, max_length=100) board: Literal["inventory", "artefacts"] stage: str = Field(min_length=1, max_length=40) # kanban column to reset back to class BlocksStatusResponse(BaseModel): ready: bool generating: bool progress: str | None = None error: str | None = None partial: bool = False 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=20000) # link mode: one URL per line 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 = DEFAULT_PROVIDER class GuideChatResponse(BaseModel): reply: 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 = DEFAULT_PROVIDER 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 = DEFAULT_PROVIDER 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 = DEFAULT_PROVIDER 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 = DEFAULT_PROVIDER class BlockUebernehmenResponse(BaseModel): compact: str md: str found: bool