schemas.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. """Pydantic 契约(D4)— 前后端对齐的请求/响应模型。"""
  2. from typing import List, Literal, Optional
  3. from pydantic import BaseModel, Field
  4. # ============ 枚举字面量(与前端表单对齐) ============
  5. Mood = Literal["放松", "欢乐", "虐心", "烧脑", "紧张刺激", "温馨"]
  6. PartyType = Literal["独自", "情侣", "家庭", "朋友"]
  7. RegionPreference = Literal["华语", "好莱坞", "日韩", "欧洲", "不限"]
  8. YearPreference = Literal["不限", "近5年", "近10年", "经典"]
  9. # ============ 请求模型 ============
  10. class RecommendRequest(BaseModel):
  11. """观影偏好 / 智能推荐请求(F1)"""
  12. mood: Mood = Field(..., description="当前心情")
  13. party_type: PartyType = Field(..., description="观影人群")
  14. genres: List[str] = Field(default_factory=list, description="偏好类型标签")
  15. max_runtime_minutes: Optional[int] = Field(
  16. default=None,
  17. description="最大时长(分钟);null=不限",
  18. examples=[120],
  19. )
  20. region_preference: RegionPreference = Field(default="不限", description="地区偏好")
  21. year_preference: YearPreference = Field(default="不限", description="年代偏好")
  22. exclude_titles: List[str] = Field(default_factory=list, description="已看过片名")
  23. spoilers_ok: bool = Field(default=False, description="是否允许剧透")
  24. free_text: str = Field(default="", description="额外自由文本要求")
  25. exclude_ids: List[int] = Field(
  26. default_factory=list,
  27. description="换一批时排除的 TMDB 电影 id",
  28. )
  29. taste_profile: Optional["TasteProfile"] = Field(
  30. default=None,
  31. description="若传入则跳过画像 Agent(换一批复用)",
  32. )
  33. model_config = {
  34. "json_schema_extra": {
  35. "example": {
  36. "mood": "放松",
  37. "party_type": "独自",
  38. "genres": ["剧情", "喜剧"],
  39. "max_runtime_minutes": 120,
  40. "region_preference": "不限",
  41. "year_preference": "近10年",
  42. "exclude_titles": [],
  43. "spoilers_ok": False,
  44. "free_text": "不要太沉重",
  45. "exclude_ids": [],
  46. }
  47. }
  48. }
  49. # ============ 领域子模型 ============
  50. class TasteProfile(BaseModel):
  51. """画像 Agent 结构化输出(内部契约,后续 Agent 使用)"""
  52. summary: str = Field(default="", description="口味摘要")
  53. genre_hints: List[str] = Field(default_factory=list, description="类型倾向")
  54. language_hints: List[str] = Field(default_factory=list, description="语言/地区倾向")
  55. avoid: List[str] = Field(default_factory=list, description="禁忌/规避项")
  56. discover_notes: str = Field(default="", description="discover 友好检索条件说明")
  57. class CandidateMovie(BaseModel):
  58. """检索 Agent / MovieService 候选片"""
  59. id: int = Field(..., description="TMDB movie id")
  60. title: str
  61. year: Optional[int] = None
  62. genres: List[str] = Field(default_factory=list)
  63. runtime: Optional[int] = Field(default=None, description="片长(分钟)")
  64. rating: Optional[float] = None
  65. poster_url: Optional[str] = None
  66. overview: Optional[str] = None
  67. class MovieDetail(CandidateMovie):
  68. """电影详情(TMDB /movie/{id} + credits)"""
  69. tagline: Optional[str] = None
  70. original_title: Optional[str] = None
  71. vote_count: Optional[int] = None
  72. original_language: Optional[str] = None
  73. countries: List[str] = Field(default_factory=list)
  74. directors: List[str] = Field(default_factory=list)
  75. cast: List[str] = Field(default_factory=list)
  76. tmdb_url: Optional[str] = None
  77. class MovieCard(BaseModel):
  78. """推荐结果卡片(F2 / F3)"""
  79. id: int = Field(..., description="TMDB movie id")
  80. title: str
  81. year: Optional[int] = None
  82. genres: List[str] = Field(default_factory=list)
  83. runtime: Optional[int] = None
  84. rating: Optional[float] = None
  85. poster_url: Optional[str] = None
  86. why: str = Field(default="", description="推荐理由")
  87. vibe_tags: List[str] = Field(default_factory=list)
  88. caution: Optional[str] = Field(default=None, description="适看提示")
  89. overview_safe: str = Field(default="", description="安全简介(遵守 spoilers_ok)")
  90. class RecommendResult(BaseModel):
  91. """推荐结果主体"""
  92. playlist_name: str = ""
  93. profile_summary: str = ""
  94. movies: List[MovieCard] = Field(default_factory=list)
  95. is_fallback: bool = Field(default=False, description="是否为降级结果(D5)")
  96. taste_profile: Optional[TasteProfile] = Field(
  97. default=None,
  98. description="本次使用的画像;换一批时可原样回传以跳过画像 Agent",
  99. )
  100. # ============ 响应包装 ============
  101. class RecommendResponse(BaseModel):
  102. success: bool
  103. message: str = ""
  104. data: Optional[RecommendResult] = None
  105. class MovieListResponse(BaseModel):
  106. """确定性搜片列表响应(search / discover)"""
  107. success: bool
  108. message: str = ""
  109. data: List[CandidateMovie] = Field(default_factory=list)
  110. class MovieDetailResponse(BaseModel):
  111. """电影详情响应"""
  112. success: bool
  113. message: str = ""
  114. data: Optional[MovieDetail] = None
  115. class ErrorResponse(BaseModel):
  116. success: bool = False
  117. message: str
  118. error_code: Optional[str] = None
  119. RecommendRequest.model_rebuild()
  120. RecommendResult.model_rebuild()