"""Pydantic schemas for structured model responses.""" from pydantic import BaseModel, ConfigDict, Field, model_validator from gcmd_classifier.models import SupportType class CandidateDecision(BaseModel): """One model-selected application-supplied candidate.""" model_config = ConfigDict(extra="forbid", frozen=True) candidate_id: str = Field(min_length=1) confidence: float | None = Field(default=None, ge=0.0, le=1.0) evidence: str = Field(min_length=1) support_type: SupportType reason: str | None = None class TopicResponse(BaseModel): """Structured multi-label Topic routing response.""" model_config = ConfigDict(extra="forbid", frozen=True) selected: list[CandidateDecision] = Field(default_factory=list) ambiguous_alternatives: list[str] = Field(default_factory=list) no_selection_reason: str | None = None class _ChildSelectionResponse(BaseModel): """Shared response invariants for Term and Variable-level descent decisions.""" model_config = ConfigDict(extra="forbid", frozen=True) selected: list[CandidateDecision] = Field(default_factory=list) stop_at_parent: bool stop_reason: str | None = None ambiguous_alternatives: list[str] = Field(default_factory=list) @model_validator(mode="after") def validate_stop_and_selection_invariants(self) -> "_ChildSelectionResponse": """Reject contradictory child-selection responses before orchestration can use them.""" if self.stop_at_parent and self.selected: raise ValueError("stop_at_parent=true requires selected to be empty") if self.selected and self.stop_at_parent: raise ValueError("non-empty selected requires stop_at_parent=false") if self.stop_at_parent and not self.stop_reason: raise ValueError("stop_at_parent=true requires a non-empty stop_reason") return self class TermResponse(_ChildSelectionResponse): """Structured Term routing response beneath a selected Topic.""" class VariableResponse(_ChildSelectionResponse): """Structured Variable-level descent response beneath a selected parent."""