| 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) |
|
|