112 lines
3.8 KiB
Python
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
|