141 lines
3.3 KiB
Python
141 lines
3.3 KiB
Python
"""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 ChatThinkingEvent(BaseModel):
|
|
"""SSE thinking event: one chunk of the model's reasoning (phase 17).
|
|
|
|
PLAN §4 extension (A15, owner permission 2026-08-23): frames of the
|
|
shape ``{type: "thinking", text: str}`` stream ahead of the
|
|
``delta`` frames in practice (the model reasons before it answers). The
|
|
client renders them in a collapsible "Thinking" block; the ``done``
|
|
event shape is unchanged and thinking text never travels on it.
|
|
Sibling of :class:`ChatErrorEvent`.
|
|
"""
|
|
|
|
type: str = "thinking"
|
|
text: 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]
|