from __future__ import annotations from typing import Annotated 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) 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): 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.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