Files
note-zero/app/schemas.py
T
2026-07-29 16:58:59 +08:00

112 lines
3.8 KiB
Python

from __future__ import annotations
from typing import Annotated, Literal
from pydantic import (
BaseModel,
Field,
StringConstraints,
field_validator,
model_validator,
)
Content = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
class LoginRequest(BaseModel):
access_key: str = Field(min_length=1, max_length=512)
class FragmentCreate(BaseModel):
content: Content = Field(max_length=20_000)
class IdeaOverride(BaseModel):
maturity: float | None = Field(default=None, ge=0, le=100)
class DebugSharingUpdate(BaseModel):
enabled: bool
class IdeaAssessment(BaseModel):
idea_id: str | None = Field(
description="Existing idea id, or null only when a genuinely new idea is needed."
)
title: str = Field(min_length=1, max_length=80)
summary: str = Field(min_length=1, max_length=280)
maturity: float = Field(
ge=0,
le=100,
description=(
"Position of the evolving idea itself, never a score of the user or "
"a probability of success."
),
)
confidence: float = Field(ge=0, le=1)
motion: str = Field(min_length=1, max_length=24)
position: str = Field(min_length=1, max_length=120)
tension: str = Field(min_length=1, max_length=240)
trajectory: str = Field(min_length=1, max_length=280)
possible_moves: list[str] = Field(min_length=1, max_length=4)
relevance: float = Field(default=1, ge=0, le=1)
supporting_fragment_ids: list[str] = Field(
default_factory=list,
max_length=6,
description=(
"Ids of earlier fragments from recent_context that are substantive "
"evidence for this same evolving thread. The new fragment is linked "
"automatically and must not be included."
),
)
@field_validator("possible_moves")
@classmethod
def moves_are_brief(cls, moves: list[str]) -> list[str]:
return [move.strip()[:100] for move in moves if move.strip()][:4]
@field_validator("supporting_fragment_ids")
@classmethod
def supporting_fragments_are_unique(
cls, fragment_ids: list[str]
) -> list[str]:
unique: list[str] = []
for fragment_id in fragment_ids:
value = fragment_id.strip()
if value and value not in unique:
unique.append(value)
return unique[:6]
class CuratorDecision(BaseModel):
direction_signal: Literal["explicit", "implicit", "none"] = Field(
description=(
"explicit when the user names this as their goal, idea, project, "
"work, plan, question, inquiry, decision, or commitment; implicit "
"when a future-bearing direction is inferred; none when no such "
"direction is present. Tone, ambition, feasibility, and missing "
"steps must not downgrade an explicit signal."
)
)
standalone: bool = Field(
description="True when the fragment should remain only in the raw stream for now."
)
reasoning_note: str = Field(
max_length=200,
description="Brief audit note for the system, never shown in the capture stream.",
)
assessments: list[IdeaAssessment] = Field(default_factory=list, max_length=2)
@model_validator(mode="after")
def decision_is_coherent(self) -> "CuratorDecision":
if self.direction_signal == "explicit" and self.standalone:
raise ValueError(
"an explicit user-declared direction cannot remain standalone"
)
if self.standalone and self.assessments:
raise ValueError("standalone decisions cannot contain assessments")
if not self.standalone and not self.assessments:
raise ValueError("non-standalone decisions require an assessment")
return self