"""Pydantic schemas for API request/response.""" from __future__ import annotations from datetime import datetime from typing import Any, Dict, List, Optional from uuid import UUID from pydantic import BaseModel, ConfigDict, EmailStr, Field # === Auth === class UserRegister(BaseModel): email: EmailStr username: str = Field(min_length=3, max_length=64) password: str = Field(min_length=6, max_length=128) class UserLogin(BaseModel): # Accepts either email or username — the backend resolves it. login: str password: str class UserOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID email: EmailStr username: str is_admin: bool is_active: bool preferred_language: str created_at: datetime class TokenOut(BaseModel): access_token: str token_type: str = "bearer" user: UserOut class AdminSetupRequest(BaseModel): token: str email: EmailStr username: str = Field(min_length=3, max_length=64) password: str = Field(min_length=6, max_length=128) # === Settings === class SettingsUpdate(BaseModel): values: Dict[str, Any] class SettingsOut(BaseModel): values: Dict[str, Any] editable_keys: List[str] # === Presets === class PresetOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID slug: str title: str description: Optional[str] = None language: str is_public: bool is_builtin: bool payload: Dict[str, Any] created_at: datetime class PresetCreate(BaseModel): slug: str title: str description: Optional[str] = None language: str = "en" is_public: bool = True payload: Dict[str, Any] # === Worlds === class WorldCreate(BaseModel): name: str = Field(min_length=1, max_length=255) language: str = "en" preset_id: Optional[UUID] = None class WorldDefinition(BaseModel): setting_description: str = "" rules: Dict[str, Any] = Field(default_factory=dict) world_schema: Dict[str, Any] = Field(default_factory=dict) plot_rails: Dict[str, Any] = Field(default_factory=dict) initial_state: Dict[str, Any] = Field(default_factory=dict) initial_time: Optional[str] = None class WorldOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID owner_id: UUID name: str language: str definition: Dict[str, Any] state: Dict[str, Any] current_time: Optional[str] status: str preset_id: Optional[UUID] = None created_at: datetime updated_at: datetime class WorldUpdate(BaseModel): name: Optional[str] = None definition: Optional[Dict[str, Any]] = None state: Optional[Dict[str, Any]] = None current_time: Optional[str] = None status: Optional[str] = None # === World Builder === class WorldBuilderStart(BaseModel): """Kick off a new world-building conversation.""" world_name: str = Field(min_length=1, max_length=255) language: str = "en" # Either pick a preset to start from, or fill the freeform brief. preset_id: Optional[UUID] = None setting_brief: str = "" character_brief: str = "" rules_brief: str = "" notes: str = "" class WorldBuilderMessage(BaseModel): """User reply in the world-builder dialogue.""" session_id: UUID message: str class WorldBuilderReply(BaseModel): """AI reply in the world-builder dialogue.""" session_id: UUID turn: int ai_message: str proposed_definition: Optional[WorldDefinition] = None is_final: bool = False # True when AI thinks world is ready to commit followup_questions: List[str] = Field(default_factory=list) class WorldBuilderCommit(BaseModel): """User accepts the proposed world definition and creates the world.""" session_id: UUID name: Optional[str] = None # === Sessions === class SessionOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID world_id: UUID title: str is_active: bool created_at: datetime last_played_at: Optional[datetime] = None class SessionCreate(BaseModel): world_id: UUID title: Optional[str] = None # === Messages === class MessageOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID seq: int role: str kind: str content: str payload: Dict[str, Any] is_pinned: bool hidden: bool created_at: datetime # === Iteration === class IterationRequest(BaseModel): """Player submits an action/choice for the next iteration.""" session_id: UUID action_text: str = Field(min_length=1, max_length=4000) class IterationEvent(BaseModel): """SSE event sent to the frontend during an iteration.""" type: str # status / plan / tool_call / tool_result / narrative_chunk / step_complete / error / done data: Dict[str, Any] = Field(default_factory=dict) # === Glossary === class GlossaryEntryOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID kind: str name: str description: str payload: Dict[str, Any] # === LLM Logs === class LlmLogOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID purpose: str model: str base_url: str prompt_tokens: Optional[int] completion_tokens: Optional[int] total_tokens: Optional[int] latency_ms: Optional[int] error: Optional[str] created_at: datetime # === Triggers === class TriggerOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: UUID session_id: UUID fire_at: str description: str payload: Dict[str, Any] fired: bool created_at: datetime class TriggerCreate(BaseModel): session_id: UUID fire_at: str description: str payload: Dict[str, Any] = Field(default_factory=dict) # === World Editor (AI-assisted editing of an existing world) === class WorldEditorChatRequest(BaseModel): message: str = Field(min_length=1, max_length=4000) class WorldEditorChatReply(BaseModel): ai_message: str definition: Optional[Dict[str, Any]] = None changed: bool = False