"""Pydantic request/response schemas (API contract).""" from __future__ import annotations import uuid from datetime import datetime from pydantic import BaseModel, Field, field_validator class HealthResponse(BaseModel): status: str db: str version: str environment: str class SuggestionList(BaseModel): suggestions: list[str] class ChatRequest(BaseModel): message: str = Field(min_length=1, max_length=4000) class LoginRequest(BaseModel): """``POST /api/login`` body (phase 16): the single admin's password. An empty or wrong password is a 401 with one generic detail — never a 422 that would hint at input-shape differences. """ password: str = "" class WhoamiResponse(BaseModel): """``GET /api/whoami`` (phase 16) — drives all UI gating.""" authenticated: bool role: str # "admin" | "anonymous" class SourceRef(BaseModel): source: str path: str title: str class ChatDoneEvent(BaseModel): """Final SSE event of a chat turn: metadata for the finished answer.""" type: str = "done" deflected: bool sources: list[SourceRef] suggestions: list[str] = [] class ChatErrorEvent(BaseModel): """SSE error event: a turn that cannot complete (PLAN §4). The client's loading-feedback state machine (phase 06) keys off this exact shape — ``{type: "error", detail: str}`` — to flip to the error state and re-enable the send button. """ type: str = "error" detail: str class DocSummary(BaseModel): """One indexed document as shown on the Sources page / API.""" id: str source: str path: str title: str chunks: int indexed_at: str class DocList(BaseModel): """Response of ``GET /api/docs`` (empty list → designed empty state).""" documents: list[DocSummary] class DocContent(BaseModel): """One indexed document's full content (feeds the viewer page, phase 10).""" source: str path: str title: str format: str content: str indexed_at: str chunks: int class SteeringNoteIn(BaseModel): """``POST /api/steering`` body: one tuning instruction (phase 15). The note is trimmed *before* the length constraints run, so a whitespace-only body is a 422 and a 2000-char note with surrounding spaces still passes. """ note: str = Field(min_length=1, max_length=2000) @field_validator("note", mode="before") @classmethod def _trim_note(cls, v: object) -> object: return v.strip() if isinstance(v, str) else v class SteeringNote(BaseModel): """One stored steering note (API shape — ISO-8601 ``created_at``).""" id: uuid.UUID note: str created_at: datetime class SteeringNoteList(BaseModel): """``GET /api/steering`` response: all notes, newest first.""" notes: list[SteeringNote]