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=5) # Re-Run ab Guide-Schritt (0 Auswahl … 5 Lese-Prüfung); None = voll/Resume class TopicCreateRequest(BaseModel): name: str = Field(min_length=1, max_length=100) class BausteineCreateRequest(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_ort: str = Field(default="", max_length=2000) ab_phase: int | None = Field(default=None, ge=1, le=6) # Re-Run ab grober Phase (Position in _phasen(topic), 1-based); None = Resume/Fortsetzen ohne Löschen class BausteineStep(BaseModel): label: str state: Literal["done", "active", "pending"] class BausteineStatusResponse(BaseModel): ready: bool generating: bool progress: str | None = None error: str | None = None partial: bool = False steps: list[BausteineStep] = [] class ProjectResponse(BaseModel): name: str class FolderResponse(BaseModel): name: str ort: str # relativer Pfad ab Repo-Root (z.B. "projects/foo") class BausteineQuelleUpdate(BaseModel): topic: str = Field(min_length=1, max_length=100) type: SourceType = "thema" ort: str = Field(default="", max_length=2000) spec: str = Field(default="", max_length=2000) class BausteineQuelleResponse(BaseModel): type: SourceType ort: str spec: str class SubbausteinInfo(BaseModel): titel: str stufe: Literal["einfach", "mittel", "schwer"] relevanz: Literal["relevant", "rand"] | None = None class BausteinUebersicht(BaseModel): num: int titel: str beschreibung: str = "" subbausteine: list[SubbausteinInfo] = [] 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["entfernen", "anpassen", "hinzufuegen"] 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] # --- Baustein-Lernen --- class BausteinChatRequest(BaseModel): topic: str = Field(min_length=1, max_length=100) baustein: str = Field(min_length=1, max_length=200) section: str = Field(default="", max_length=20000) # ausführliche Fassung section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze) messages: list[ChatMessage] = Field(min_length=1) provider: ProviderType = "claude" class BausteinChatResponse(BaseModel): reply: str class BausteinPruefungRequest(BaseModel): topic: str = Field(min_length=1, max_length=100) baustein: str = Field(min_length=1, max_length=200) section: str = Field(default="", max_length=20000) # ausführliche Fassung section_kompakt: str = Field(default="", max_length=20000) # kompakte Fassung (Merksätze) aktion: Literal[ "frage", "diskussion", "antwort", "antwort_pruefen", "quiz_frage", "quiz_antwort", "lueck_frage", "lueck_antwort", ] = "frage" frage: str = Field(default="", max_length=2000) # aktuell geprüfte Frage (für diskussion/antwort); Anker der Basis auswahl: list[int] = [] # Quiz/Lückentext-Auswahl: vom Lerner gewählte Options-Indizes korrekt: list[int] = [] # Quiz/Lückentext-Auswahl: korrekte Indizes (Client hält sie aus der Generierung) loesung: str = Field(default="", max_length=500) # Lückentext frei: erwarteter Begriff alternativen: list[str] = [] # Lückentext frei: akzeptierte Synonyme eingabe: str = Field(default="", max_length=500) # Lückentext frei: getippter Begriff schwer: bool = False # Variante: leicht (+1/−1) vs schwer (+3/−1) letzte_bewertung: str = Field(default="", max_length=2000) # Feedback der letzten Bewertung (Kontext für diskussion) vermeide: list[str] = [] # schon gestellte + vorgemerkte Fragen — sinngemäß nicht wiederholen nachgefragt: bool = False # für diese Frage wurde nachgefragt → Gewinn auf +1 gedeckelt begruendung: str = Field(default="", max_length=2000) # „Gründlich prüfen": warum mit der Bewertung unzufrieden muster: str = Field(default="", max_length=2000) # gezogenes Frage-Muster (Saat); leer → Live-Generierung (Fallback) # Basis wird serverseitig je Frage geführt (offene_frage-Anker) — nicht mehr vom Client. cap: int = Field(default=6, ge=6, le=30) # Score-Deckel = Max des höchsten erstellten Formats (6/12/18/30) messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage provider: ProviderType = "claude" gruendlich: bool = False # „Gründlich prüfen": Bewertung mit starkem Modell (role guide) class QuizOption(BaseModel): text: str korrekt: bool class BausteinPruefungResponse(BaseModel): frage: str | None = None reply: str | None = None feedback: str | None = None punkte: int | None = None # Punkt-Delta dieser Antwort (−2 … +3); schnell = voraussichtlich bewertung: Literal["gut", "neutral", "schlecht"] | None = None # aus Vorzeichen, fürs Einfärben optionen: list[QuizOption] | None = None # Quiz: 4 Optionen + Korrekt-Flags satz: str | None = None # Lückentext: Satz mit Lücke (___) loesung: str | None = None # Lückentext: erwarteter Begriff alternativen: list[str] | None = None # Lückentext: akzeptierte Synonyme gute_antworten: int absolviert: bool verstanden: bool = False gemeistert: bool = False class BausteinLernstand(BaseModel): gute_antworten: int streak: int = 0 absolviert: bool verstanden: bool gemeistert: bool class BausteinLernstandResponse(BaseModel): bausteine: dict[str, BausteinLernstand]