"""API 请求 / 响应模型。""" from __future__ import annotations from datetime import date, datetime from typing import Any from pydantic import BaseModel, ConfigDict, Field class ORMModel(BaseModel): model_config = ConfigDict(from_attributes=True) class KnowledgeOut(ORMModel): id: int knowledge_id: int name: str mastery: int position: int status: str = "" domain: str = "" category: str = "" class KnowledgeResourceOut(BaseModel): id: int kind: str title: str url: str source_name: str = "" class KnowledgeRelatedOut(BaseModel): id: int name: str relation_type: str direction: str = "out" # out: 当前节点指向对方;in: 对方指向当前节点 mastery: int | None = None class KnowledgeChapterOut(BaseModel): chapter_id: int chapter_name: str textbook_id: int textbook_name: str question_count: int = 0 # 电子书定位:has_ebook 为假时前端不显示「读教材」入口 ebook_page: int = 0 has_ebook: bool = False class KnowledgeDetailOut(BaseModel): id: int name: str domain: str category: str description: str mastery: int status: str related_knowledge_count: int chapter_count: int question_count: int related: list[KnowledgeRelatedOut] = [] chapters: list[KnowledgeChapterOut] = [] resources: list[KnowledgeResourceOut] = [] class KnowledgeResourceIn(BaseModel): kind: str = "video" title: str = "" url: str = "" textbook_id: int | None = None course_id: int | None = None class QuestionOut(BaseModel): id: int stem: str options: list[str] knowledge_name: str class PracticeSessionOut(BaseModel): mode: str title: str chapter_id: int | None = None questions: list[QuestionOut] class AnswerIn(BaseModel): question_id: int selected: int class SubmitIn(BaseModel): mode: str chapter_id: int | None = None answers: list[AnswerIn] = Field(min_length=1) class PerQuestionOut(BaseModel): question_id: int correct: bool selected: int correct_index: int explanation: str class SubmitResultOut(BaseModel): mode: str title: str total: int score: int accuracy: float per_question: list[PerQuestionOut] knowledge: list[KnowledgeOut] class ErrorOut(BaseModel): id: int source_label: str knowledge_name: str diagnosis: str status: str created_on: date attempt_id: int | None = None question_id: int | None = None stem: str = "" options: list[str] = [] selected: int | None = None correct_index: int | None = None explanation: str = "" knowledge_names: list[str] = [] class RecordOut(BaseModel): id: int mode: str title: str total: int correct: int happened_on: date class AttemptItemOut(BaseModel): question_index: int question_id: int | None stem: str options: list[str] selected: int | None correct_index: int correct: bool knowledge_name: str explanation: str knowledge_names: list[str] = [] class RecordDetailOut(RecordOut): items: list[AttemptItemOut] = [] class DailyOut(BaseModel): today: date completed: bool streak: int chapter_id: int | None = None chapter_name: str = "" book_name: str = "" # 今日章节对应的电子书定位,没有电子书时 has_ebook 为假 textbook_id: int | None = None ebook_page: int = 0 has_ebook: bool = False rating: str = "" knowledge_names: list[str] = [] reason: str = "" class DailyCompleteIn(BaseModel): chapter_id: int rating: str = "" class MetricItem(BaseModel): label: str value: str class PlanOut(BaseModel): notice: str focus_names: list[str] steps: list[str] class OverviewOut(BaseModel): metrics: list[MetricItem] knowledge: list[KnowledgeOut] plan: PlanOut class DiagnosisOut(BaseModel): name: str mastery: int error_count: int action: str class AnalyticsOut(BaseModel): metrics: list[MetricItem] diagnosis: list[DiagnosisOut] class ImportPayload(BaseModel): version: int = 1 knowledge: list[dict[str, Any]] = [] records: list[Any] = [] errors: list[Any] = [] daily_completions: list[date] = [] class ImportResult(BaseModel): imported: bool knowledge: int records: int errors: int items: int = 0 message: str class ChapterOut(ORMModel): id: int textbook_id: int name: str position: int summary: str = "" ebook_page: int = 0 question_count: int = 0 knowledge_names: list[str] = [] knowledge_ids: list[int] = [] class EbookProgressOut(ORMModel): textbook_id: int locator: str = "" page: int = 0 total: int = 0 position: float = 0.0 updated_at: datetime | None = None class EbookProgressIn(BaseModel): locator: str = Field(default="", max_length=512) page: int = 0 total: int = 0 position: float = 0.0 class TextbookOut(ORMModel): id: int name: str author: str publisher: str isbn: str edition: str grade: str link: str description: str position: int chapters: list[ChapterOut] = [] # 在线阅读:文件本身走 /api/textbooks/{id}/ebook/file/{file} ebook_file: str = "" ebook_format: str = "" ebook_name: str = "" ebook_size: int = 0 ebook_pages: int = 0 ebook_uploaded_at: datetime | None = None ebook_progress: EbookProgressOut | None = None class CourseOut(ORMModel): id: int name: str provider: str url: str grade: str description: str position: int class QuestionBankItemOut(BaseModel): id: int chapter_id: int | None chapter_name: str = "" textbook_name: str = "" knowledge_name: str stem: str options: list[str] correct_index: int explanation: str difficulty: int is_generated: bool used_count: int knowledge_names: list[str] = [] knowledge_ids: list[int] = [] class QuestionUpdate(BaseModel): stem: str = Field(min_length=1) options: list[str] = Field(min_length=4, max_length=4) correct_index: int = Field(ge=0, le=3) explanation: str = "" knowledge_name: str = Field(min_length=1, max_length=64) difficulty: int = Field(default=1, ge=1, le=3) chapter_id: int | None = None knowledge_names: list[str] = [] knowledge_ids: list[int] = [] class GenerateIn(BaseModel): chapter_id: int count: int = Field(default=3, ge=1, le=10) difficulty: int = Field(default=2, ge=1, le=3) knowledge_name: str = "" instructions: str = "" channel_id: int | None = None knowledge_names: list[str] = [] knowledge_ids: list[int] = [] need_figure: bool = False class LlmProviderOut(BaseModel): id: str label: str base_url: str model: str class LlmChannelOut(BaseModel): id: int name: str provider: str provider_label: str base_url: str model: str temperature: float max_tokens: int | None = None has_api_key: bool api_key_preview: str is_default: bool class LlmSettingsOut(BaseModel): channels: list[LlmChannelOut] = [] providers: list[LlmProviderOut] class LlmChannelCreate(BaseModel): name: str = Field(min_length=1, max_length=128) provider: str = "openai" base_url: str = "" model: str = "" temperature: float = Field(default=0.3, ge=0.0, le=2.0) max_tokens: int | None = Field(default=None, ge=64, le=12000) api_key: str | None = None class LlmChannelUpdate(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=128) provider: str | None = None base_url: str | None = None model: str | None = None temperature: float | None = Field(default=None, ge=0.0, le=2.0) max_tokens: int | None = Field(default=None, ge=64, le=12000) clear_max_tokens: bool = False api_key: str | None = None clear_api_key: bool = False class LlmTestResult(BaseModel): ok: bool message: str class LlmTaskCreate(BaseModel): kind: str params: dict[str, Any] = {} class LlmTaskOut(BaseModel): id: int kind: str status: str progress: int message: str result: dict[str, Any] = {} class LoginIn(BaseModel): username: str password: str class TokenOut(BaseModel): access_token: str token_type: str = "bearer" role: str display_name: str class ChangePasswordIn(BaseModel): old_password: str new_password: str = Field(min_length=6, max_length=128) class RoleOut(ORMModel): id: int code: str name: str description: str class UserOut(BaseModel): id: int username: str display_name: str active: bool roles: list[RoleOut] = [] created_at: datetime | None = None class UserCreate(BaseModel): username: str = Field(min_length=2, max_length=64, pattern=r"^[A-Za-z0-9_.-]+$") display_name: str = Field(default="", max_length=64) password: str = Field(min_length=6, max_length=128) role_code: str = "student" class UserUpdate(BaseModel): display_name: str | None = Field(default=None, max_length=64) active: bool | None = None role_code: str | None = None class ResetPasswordIn(BaseModel): password: str = Field(min_length=6, max_length=128) class TextbookCreate(BaseModel): name: str = Field(min_length=1, max_length=128) author: str = "" publisher: str = "" isbn: str = "" edition: str = "" grade: str = "" link: str = "" description: str = "" class TextbookUpdate(BaseModel): name: str | None = Field(default=None, max_length=128) author: str | None = None publisher: str | None = None isbn: str | None = None edition: str | None = None grade: str | None = None link: str | None = None description: str | None = None class CourseCreate(BaseModel): name: str = Field(min_length=1, max_length=128) provider: str = "" url: str = "" grade: str = "" description: str = "" class CourseUpdate(BaseModel): name: str | None = Field(default=None, max_length=128) provider: str | None = None url: str | None = None grade: str | None = None description: str | None = None class ChapterCreate(BaseModel): name: str = Field(min_length=1, max_length=128) summary: str = "" knowledge_names: list[str] = [] knowledge_ids: list[int] = [] class ChapterUpdate(BaseModel): name: str | None = Field(default=None, max_length=128) summary: str | None = None ebook_page: int | None = None knowledge_names: list[str] | None = None knowledge_ids: list[int] | None = None class ChaptersGenerateIn(BaseModel): count: int | None = Field(default=None, ge=1, le=12) channel_id: int | None = None instructions: str = "" knowledge_ids: list[int] = [] class ChaptersGenerateOut(BaseModel): created: list[ChapterOut] = [] skipped_duplicates: list[str] = [] message: str = "" class ChaptersOrganizeIn(BaseModel): reference_text: str = Field(min_length=1) channel_id: int | None = None class ProfileUpdate(BaseModel): display_name: str = Field(min_length=1, max_length=64) class KnowledgeGraphNode(BaseModel): id: int name: str domain: str category: str description: str chapter_count: int = 0 question_count: int = 0 resources: list[KnowledgeResourceOut] = [] class KnowledgeGraphEdge(BaseModel): source: int target: int relation_type: str class KnowledgeGraphOut(BaseModel): nodes: list[KnowledgeGraphNode] = [] edges: list[KnowledgeGraphEdge] = [] class KnowledgeNodeCreate(BaseModel): name: str = Field(min_length=1, max_length=64) domain: str = "初等" category: str = "" description: str = "" class KnowledgeNodeUpdate(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=64) domain: str | None = None category: str | None = None description: str | None = None class KnowledgeRelationIn(BaseModel): source_id: int target_id: int relation_type: str = "前置"