268 lines
8.9 KiB
Python
268 lines
8.9 KiB
Python
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 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 = "claude"
|
||
source_type: SourceType = "thema"
|
||
source_location: str = Field(default="", max_length=2000)
|
||
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 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 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
|
||
|
||
|
||
# --- 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
|