from typing import List, Literal from pydantic import BaseModel, Field, field_validator class Pick(BaseModel): numbers: List[int] = Field(min_length=6, max_length=6) risk_tag: Literal["안정", "균형", "공격"] reason: str = Field(max_length=80) @field_validator("numbers") @classmethod def _check_numbers(cls, v): if len(set(v)) != 6: raise ValueError("numbers must be 6 unique integers") if any(n < 1 or n > 45 for n in v): raise ValueError("numbers must be within 1..45") return sorted(v) class TierRationale(BaseModel): bonus: str = Field(max_length=40) extended: str = Field(max_length=40) pool: str = Field(max_length=40) class Narrative(BaseModel): headline: str summary_3lines: List[str] = Field(min_length=3, max_length=3) hot_cold_comment: str = "" warnings: str = "" retrospective: str = Field(default="", max_length=80) class CuratorOutput(BaseModel): core_picks: List[Pick] = Field(min_length=5, max_length=5) bonus_picks: List[Pick] = Field(min_length=5, max_length=5) extended_picks: List[Pick] = Field(min_length=5, max_length=5) pool_picks: List[Pick] = Field(min_length=5, max_length=5) tier_rationale: TierRationale narrative: Narrative confidence: int = Field(ge=0, le=100) def validate_response(data: dict, candidate_numbers: List[List[int]]) -> CuratorOutput: out = CuratorOutput.model_validate(data) candidate_set = {tuple(sorted(c)) for c in candidate_numbers} all_picks = ( out.core_picks + out.bonus_picks + out.extended_picks + out.pool_picks ) # 중복 픽 검증 pick_keys = [tuple(p.numbers) for p in all_picks] if len(pick_keys) != len(set(pick_keys)): raise ValueError("duplicate picks across tiers") # 후보에 없는 번호 조합 금지 for p in all_picks: if tuple(p.numbers) not in candidate_set: raise ValueError(f"pick {p.numbers} not in candidates") return out