feat(chat): save and view chat history — admin-only saved_chats, History page, open-a-chat return
This commit is contained in:
+101
-1
@@ -5,7 +5,7 @@ import uuid
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
@@ -282,3 +282,103 @@ class UploadOut(BaseModel):
|
||||
errors: int
|
||||
chunks: int
|
||||
overview: bool
|
||||
|
||||
|
||||
class ToolCall(BaseModel):
|
||||
"""One agent tool-call record (the phase-37 ``tools`` record shape).
|
||||
|
||||
Mirrors the ``{name, argument}`` pair the SSE ``tool`` frames carry
|
||||
(PLAN §4 extension): ``argument`` is the read document's
|
||||
``"source/path"`` for ``read_document`` and null otherwise. Stored
|
||||
inside :class:`ChatMessage.tools` so a saved chat restores the
|
||||
"calling tool" lines pixel-identical (phase 50).
|
||||
"""
|
||||
|
||||
name: str
|
||||
argument: str | None = None
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
"""One conversation record in the ``bor.chat.v1`` localStorage shape
|
||||
(phase 14) — the stored ``messages`` payload of a saved chat (phase 50).
|
||||
|
||||
``{who, text, sources?, deflected?, suggestions?, thinking?, tools?,
|
||||
stopped?}`` — raw text, never HTML, so a saved chat restores
|
||||
pixel-identical through the existing ``renderStoredMessage`` path.
|
||||
``extra="forbid"`` rejects unknown keys (a corrupted or HTML-shaped
|
||||
payload, e.g. a stray ``<b>``-ish extra key) at the boundary with a
|
||||
422, so nothing outside this shape can poison a restored
|
||||
conversation.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
who: Literal["user", "brain"]
|
||||
text: str = Field(min_length=1)
|
||||
sources: list[SourceRef] | None = None
|
||||
deflected: bool | None = None
|
||||
suggestions: list[str] | None = None
|
||||
thinking: str | None = None
|
||||
tools: list[ToolCall] | None = None
|
||||
stopped: bool | None = None
|
||||
|
||||
|
||||
class SavedChatCreate(BaseModel):
|
||||
"""``POST /api/chats`` body (phase 50, task 02).
|
||||
|
||||
``title`` is optional: when absent or blank the API auto-titles the
|
||||
row (the first user message's text, whitespace-collapsed, truncated
|
||||
to 120 chars — the owner-locked convention). ``messages`` must be
|
||||
non-empty — a saved chat with nothing to restore is meaningless.
|
||||
"""
|
||||
|
||||
title: str | None = Field(default=None, max_length=500)
|
||||
messages: list[ChatMessage] = Field(min_length=1)
|
||||
|
||||
|
||||
class SavedChatUpdate(BaseModel):
|
||||
"""``PUT /api/chats/{chat_id}`` body (phase 50, task 02).
|
||||
|
||||
``messages`` is a full replacement (the re-Save upsert semantics —
|
||||
re-Saving the same conversation updates the same row, never a new
|
||||
one). ``title`` is replaced only when supplied — an absent (or
|
||||
blank) ``title`` keeps the row's current title.
|
||||
"""
|
||||
|
||||
title: str | None = Field(default=None, max_length=500)
|
||||
messages: list[ChatMessage] = Field(min_length=1)
|
||||
|
||||
|
||||
class SavedChatOut(BaseModel):
|
||||
"""One saved chat, full payload (create/get/put response, phase 50).
|
||||
|
||||
``messages`` round-trips the ``bor.chat.v1`` record list losslessly
|
||||
— the restore path is pixel-identical by construction.
|
||||
"""
|
||||
|
||||
id: uuid.UUID
|
||||
title: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
message_count: int
|
||||
messages: list[ChatMessage]
|
||||
|
||||
|
||||
class SavedChatRow(BaseModel):
|
||||
"""One row of ``GET /api/chats`` (the History page's list shape).
|
||||
|
||||
No payloads in the list — the row carries only what the table needs
|
||||
(Title, Messages count, Updated).
|
||||
"""
|
||||
|
||||
id: uuid.UUID
|
||||
title: str
|
||||
updated_at: datetime
|
||||
message_count: int
|
||||
|
||||
|
||||
class SavedChatList(BaseModel):
|
||||
"""``GET /api/chats`` response: saved chats, latest activity first
|
||||
(``updated_at desc, id desc``)."""
|
||||
|
||||
chats: list[SavedChatRow]
|
||||
|
||||
Reference in New Issue
Block a user