59 lines
2.0 KiB
Python
59 lines
2.0 KiB
Python
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
|