"""Pydantic schemas (request/response) for the API layer. These are NOT the same as the world's JSON-schema — see `app/core/state_validator` for world-schema validation. Pydantic here only handles HTTP boundary validation. """ from __future__ import annotations import uuid from datetime import datetime from typing import Any, Literal from pydantic import BaseModel, EmailStr, Field, field_validator # --------------------------------------------------------------------------- # # Auth # --------------------------------------------------------------------------- # class RegisterRequest(BaseModel): email: EmailStr username: str = Field(min_length=3, max_length=64, pattern=r"^[a-zA-Z0-9_]+$") password: str = Field(min_length=8, max_length=128) password_confirm: str = Field(min_length=8, max_length=128) @field_validator("password_confirm") @classmethod def _match(cls, v, info): if "password" in info.data and v != info.data["password"]: raise ValueError("password and password_confirm do not match") return v class AdminRegisterRequest(BaseModel): token: str email: EmailStr username: str = Field(min_length=3, max_length=64, pattern=r"^[a-zA-Z0-9_]+$") password: str = Field(min_length=8, max_length=128) password_confirm: str = Field(min_length=8, max_length=128) @field_validator("password_confirm") @classmethod def _match(cls, v, info): if "password" in info.data and v != info.data["password"]: raise ValueError("password and password_confirm do not match") return v class LoginRequest(BaseModel): login: str = Field(min_length=1, max_length=255) # email OR username password: str = Field(min_length=1, max_length=128) class TokenResponse(BaseModel): access_token: str refresh_token: str token_type: str = "bearer" expires_in: int = 60 * 24 user: "UserPublic" class UserPublic(BaseModel): id: uuid.UUID email: EmailStr username: str is_admin: bool is_active: bool created_at: datetime last_login_at: datetime | None = None model_config = {"from_attributes": True} # --------------------------------------------------------------------------- # # Worlds # --------------------------------------------------------------------------- # class WorldSummary(BaseModel): id: uuid.UUID name: str description: str | None language: str status: str last_played_at: datetime | None current_time: str created_at: datetime preview_player_name: str | None = None model_config = {"from_attributes": True} class WorldFull(BaseModel): id: uuid.UUID owner_id: uuid.UUID preset_id: uuid.UUID | None name: str description: str | None language: str rules: list time_schema: dict schemas: list environment_schema: list environment: dict plot_rails: dict current_time: str status: str intro_scene: str | None created_at: datetime updated_at: datetime last_played_at: datetime | None model_config = {"from_attributes": True} class WorldCreateRequest(BaseModel): mode: Literal["preset", "form"] preset_id: uuid.UUID | None = None form_data: dict | None = None name: str = Field(min_length=1, max_length=255) language: str = Field(min_length=2, max_length=8, default="en") player_name: str = Field(min_length=1, max_length=128) notes: str | None = None class WorldPatchRequest(BaseModel): name: str | None = None description: str | None = None rules: list | None = None schemas: list | None = None environment_schema: list | None = None environment: dict | None = None plot_rails: dict | None = None time_schema: dict | None = None current_time: str | None = None intro_scene: str | None = None status: str | None = None updated_at: datetime | None = None # for optimistic locking class WorldEditRequest(BaseModel): instruction: str = Field(min_length=1, max_length=4000) # --------------------------------------------------------------------------- # # Sessions # --------------------------------------------------------------------------- # class IterateRequest(BaseModel): action: str = Field(min_length=1, max_length=4000) action_source: Literal["custom", "suggested"] = "custom" class AnswerRequest(BaseModel): text: str = Field(min_length=1, max_length=4000) class SessionState(BaseModel): world: dict environment: dict recent_steps: list[dict] next_actions: list[str] # --------------------------------------------------------------------------- # # Presets # --------------------------------------------------------------------------- # class PresetSummary(BaseModel): id: uuid.UUID name: str description: str | None language: str is_public: bool status: str version: int created_at: datetime model_config = {"from_attributes": True} class PresetFull(BaseModel): id: uuid.UUID owner_id: uuid.UUID name: str description: str | None language: str rules: list time_schema: dict schemas: list environment_schema: list environment_initial: dict status: str is_public: bool version: int created_at: datetime updated_at: datetime model_config = {"from_attributes": True} class PresetCreateRequest(BaseModel): name: str = Field(min_length=1, max_length=255) description: str | None = None language: str = "en" rules: list = Field(default_factory=list) time_schema: dict = Field(default_factory=lambda: {"hours_in_day": 24, "initial_date": "day_1_hour_8"}) schemas: list = Field(default_factory=list) environment_schema: list = Field(default_factory=list) environment_initial: dict = Field(default_factory=dict) is_public: bool = False # --------------------------------------------------------------------------- # # Admin # --------------------------------------------------------------------------- # class SettingsPatchRequest(BaseModel): """A flat dict of {setting_key: value} to upsert.""" settings: dict[str, Any] class LlmLogOut(BaseModel): id: uuid.UUID stage: str model: str status: str latency_ms: int | None prompt_tokens: int | None completion_tokens: int | None error_message: str | None created_at: datetime model_config = {"from_attributes": True} class LlmLogDetail(BaseModel): id: uuid.UUID user_id: uuid.UUID | None world_id: uuid.UUID | None step_id: uuid.UUID | None stage: str model: str request_messages: list request_tools: list | None response_message: dict tool_calls: list | None prompt_tokens: int | None completion_tokens: int | None latency_ms: int | None temperature: float | None status: str error_message: str | None created_at: datetime model_config = {"from_attributes": True} class TestLlmRequest(BaseModel): api_url: str | None = None api_key: str | None = None model: str | None = None class TestEmbeddingsRequest(BaseModel): api_url: str | None = None api_key: str | None = None model: str | None = None provider: str | None = None # --------------------------------------------------------------------------- # # Misc # --------------------------------------------------------------------------- # class HealthResponse(BaseModel): status: str db: bool qdrant: bool llm: bool embeddings: bool version: str class ErrorOut(BaseModel): error: dict[str, Any] # --------------------------------------------------------------------------- # # Forward refs # --------------------------------------------------------------------------- # TokenResponse.model_rebuild()