| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- """Pydantic 契约(D4)— 前后端对齐的请求/响应模型。"""
- from typing import List, Literal, Optional
- from pydantic import BaseModel, Field
- # ============ 枚举字面量(与前端表单对齐) ============
- Mood = Literal["放松", "欢乐", "虐心", "烧脑", "紧张刺激", "温馨"]
- PartyType = Literal["独自", "情侣", "家庭", "朋友"]
- RegionPreference = Literal["华语", "好莱坞", "日韩", "欧洲", "不限"]
- YearPreference = Literal["不限", "近5年", "近10年", "经典"]
- # ============ 请求模型 ============
- class RecommendRequest(BaseModel):
- """观影偏好 / 智能推荐请求(F1)"""
- mood: Mood = Field(..., description="当前心情")
- party_type: PartyType = Field(..., description="观影人群")
- genres: List[str] = Field(default_factory=list, description="偏好类型标签")
- max_runtime_minutes: Optional[int] = Field(
- default=None,
- description="最大时长(分钟);null=不限",
- examples=[120],
- )
- region_preference: RegionPreference = Field(default="不限", description="地区偏好")
- year_preference: YearPreference = Field(default="不限", description="年代偏好")
- exclude_titles: List[str] = Field(default_factory=list, description="已看过片名")
- spoilers_ok: bool = Field(default=False, description="是否允许剧透")
- free_text: str = Field(default="", description="额外自由文本要求")
- exclude_ids: List[int] = Field(
- default_factory=list,
- description="换一批时排除的 TMDB 电影 id",
- )
- taste_profile: Optional["TasteProfile"] = Field(
- default=None,
- description="若传入则跳过画像 Agent(换一批复用)",
- )
- model_config = {
- "json_schema_extra": {
- "example": {
- "mood": "放松",
- "party_type": "独自",
- "genres": ["剧情", "喜剧"],
- "max_runtime_minutes": 120,
- "region_preference": "不限",
- "year_preference": "近10年",
- "exclude_titles": [],
- "spoilers_ok": False,
- "free_text": "不要太沉重",
- "exclude_ids": [],
- }
- }
- }
- # ============ 领域子模型 ============
- class TasteProfile(BaseModel):
- """画像 Agent 结构化输出(内部契约,后续 Agent 使用)"""
- summary: str = Field(default="", description="口味摘要")
- genre_hints: List[str] = Field(default_factory=list, description="类型倾向")
- language_hints: List[str] = Field(default_factory=list, description="语言/地区倾向")
- avoid: List[str] = Field(default_factory=list, description="禁忌/规避项")
- discover_notes: str = Field(default="", description="discover 友好检索条件说明")
- class CandidateMovie(BaseModel):
- """检索 Agent / MovieService 候选片"""
- id: int = Field(..., description="TMDB movie id")
- title: str
- year: Optional[int] = None
- genres: List[str] = Field(default_factory=list)
- runtime: Optional[int] = Field(default=None, description="片长(分钟)")
- rating: Optional[float] = None
- poster_url: Optional[str] = None
- overview: Optional[str] = None
- class MovieDetail(CandidateMovie):
- """电影详情(TMDB /movie/{id} + credits)"""
- tagline: Optional[str] = None
- original_title: Optional[str] = None
- vote_count: Optional[int] = None
- original_language: Optional[str] = None
- countries: List[str] = Field(default_factory=list)
- directors: List[str] = Field(default_factory=list)
- cast: List[str] = Field(default_factory=list)
- tmdb_url: Optional[str] = None
- class MovieCard(BaseModel):
- """推荐结果卡片(F2 / F3)"""
- id: int = Field(..., description="TMDB movie id")
- title: str
- year: Optional[int] = None
- genres: List[str] = Field(default_factory=list)
- runtime: Optional[int] = None
- rating: Optional[float] = None
- poster_url: Optional[str] = None
- why: str = Field(default="", description="推荐理由")
- vibe_tags: List[str] = Field(default_factory=list)
- caution: Optional[str] = Field(default=None, description="适看提示")
- overview_safe: str = Field(default="", description="安全简介(遵守 spoilers_ok)")
- class RecommendResult(BaseModel):
- """推荐结果主体"""
- playlist_name: str = ""
- profile_summary: str = ""
- movies: List[MovieCard] = Field(default_factory=list)
- is_fallback: bool = Field(default=False, description="是否为降级结果(D5)")
- taste_profile: Optional[TasteProfile] = Field(
- default=None,
- description="本次使用的画像;换一批时可原样回传以跳过画像 Agent",
- )
- # ============ 响应包装 ============
- class RecommendResponse(BaseModel):
- success: bool
- message: str = ""
- data: Optional[RecommendResult] = None
- class MovieListResponse(BaseModel):
- """确定性搜片列表响应(search / discover)"""
- success: bool
- message: str = ""
- data: List[CandidateMovie] = Field(default_factory=list)
- class MovieDetailResponse(BaseModel):
- """电影详情响应"""
- success: bool
- message: str = ""
- data: Optional[MovieDetail] = None
- class ErrorResponse(BaseModel):
- success: bool = False
- message: str
- error_code: Optional[str] = None
- RecommendRequest.model_rebuild()
- RecommendResult.model_rebuild()
|