from __future__ import annotations from typing import Literal from pydantic import BaseModel, ConfigDict, Field, field_validator ForestStyle = Literal[ "surprise", "watercolor", "paper_cut", "moonlit_gouache", "botanical_ink", ] ArcRole = Literal["arrive", "steady", "widen", "step", "carry"] class StrictModel(BaseModel): model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) class FactAnchor(StrictModel): source_phrase: str = Field(min_length=1, max_length=240) meaning: str = Field(min_length=3, max_length=300) class SituationPlan(StrictModel): faithful_summary: str = Field(min_length=12, max_length=500) fact_anchors: list[FactAnchor] = Field(min_length=1, max_length=4) central_uncertainty: str = Field(min_length=3, max_length=300) desired_direction: str = Field(min_length=3, max_length=300) class Clearing(StrictModel): arc_role: ArcRole source_phrase: str = Field(min_length=1, max_length=240) scene_title: str = Field(min_length=3, max_length=80) scene_intro: str = Field(min_length=12, max_length=240) narration: str = Field(min_length=80, max_length=720) strength: str = Field(min_length=3, max_length=100) reflection: str = Field(min_length=12, max_length=260) spell: str = Field(min_length=3, max_length=80) image_prompt: str = Field(min_length=8, max_length=300) @field_validator("spell") @classmethod def validate_spell(cls, value: str) -> str: if not value.lower().startswith(("i ", "i'm ", "i am ")): raise ValueError("spell must be a first-person present-tense mantra") if len(value.split()) > 12: raise ValueError("spell must contain at most 12 words") return value class IntakeTurn(StrictModel): question: str = Field(min_length=4, max_length=240) answer: str = Field(min_length=1, max_length=240) class IntakeQuestion(StrictModel): question: str = Field(min_length=4, max_length=240) options: list[str] = Field(min_length=3, max_length=4) rationale: str = Field(default="", max_length=2000) @field_validator("options") @classmethod def validate_unique_options(cls, values: list[str]) -> list[str]: normalized = {value.casefold() for value in values} if len(normalized) != len(values): raise ValueError("options must be unique") return values class ForestDraft(StrictModel): forest_title: str = Field(min_length=3, max_length=120) proposed_strengths: list[str] = Field(min_length=3, max_length=6) clearings: list[Clearing] = Field(min_length=1, max_length=6) @field_validator("proposed_strengths") @classmethod def validate_unique_strengths(cls, values: list[str]) -> list[str]: normalized = {value.casefold() for value in values} if len(normalized) != len(values): raise ValueError("proposed strengths must be unique") return values class CriticScore(StrictModel): index: int = Field(ge=0, le=5) specificity: int = Field(ge=1, le=5) warmth: int = Field(ge=1, le=5) non_genericness: int = Field(ge=1, le=5) non_toxic_positivity: int = Field(ge=1, le=5) reason: str = Field(min_length=3, max_length=240) class CriticDecision(StrictModel): keep_indices: list[int] = Field(min_length=1, max_length=6) revise_indices: list[int] = Field(default_factory=list, max_length=6) reasons: dict[str, str] = Field(default_factory=dict) scores: list[CriticScore] = Field(default_factory=list, max_length=6) @field_validator("keep_indices", "revise_indices") @classmethod def validate_unique_indices(cls, values: list[int]) -> list[int]: if len(set(values)) != len(values): raise ValueError("indices must be unique") if any(index < 0 or index > 5 for index in values): raise ValueError("indices must be between zero and five") return values class GuardResult(StrictModel): allowed: bool category: Literal["ok", "invalid", "crisis", "abuse", "medical"] message: str = "" class StreamEvent(StrictModel): type: Literal[ "status", "support", "forest", "clearing", "soundscape", "complete", "error", ] message: str = "" data: dict[str, object] = Field(default_factory=dict)