travel_request.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. from __future__ import annotations
  2. """用户旅行需求模型:从自然语言解析出的结构化TravelRequest,包含偏好、日期、预算等所有维度。"""
  3. from datetime import date, time
  4. from typing import Literal
  5. from pydantic import Field, field_validator
  6. from app.schemas.common import AppModel
  7. TravelPace = Literal[
  8. "relaxed",
  9. "normal",
  10. "intensive",
  11. ]
  12. TravelPriority = Literal[
  13. "price",
  14. "convenience",
  15. "balanced",
  16. ]
  17. class FlightPreferences(AppModel):
  18. """用户对航班的偏好。
  19. 这些字段全部是软偏好或可选限制。
  20. 用户没有明确表达时,不主动添加限制。
  21. """
  22. priority: TravelPriority = "balanced"
  23. preferred_departure_airports: list[str] = Field(
  24. default_factory=list
  25. )
  26. preferred_arrival_airports: list[str] = Field(
  27. default_factory=list
  28. )
  29. earliest_departure_time: time | None = None
  30. latest_departure_time: time | None = None
  31. earliest_arrival_time: time | None = None
  32. latest_arrival_time: time | None = None
  33. # 0表示只接受直飞;None表示不限制。
  34. max_stops: int | None = Field(
  35. default=None,
  36. ge=0,
  37. le=3,
  38. )
  39. @field_validator(
  40. "preferred_departure_airports",
  41. "preferred_arrival_airports",
  42. )
  43. @classmethod
  44. def normalize_airports(
  45. cls,
  46. value: list[str],
  47. ) -> list[str]:
  48. result: list[str] = []
  49. for airport in value:
  50. code = airport.strip().upper()
  51. if code and code not in result:
  52. result.append(code)
  53. return result
  54. class HotelPreferences(AppModel):
  55. """用户对酒店的偏好。"""
  56. max_price_per_night: float | None = Field(
  57. default=None,
  58. gt=0,
  59. )
  60. minimum_rating: float | None = Field(
  61. default=None,
  62. ge=0,
  63. le=5,
  64. )
  65. hotel_classes: list[int] = Field(
  66. default_factory=list,
  67. )
  68. @field_validator("hotel_classes")
  69. @classmethod
  70. def validate_hotel_classes(
  71. cls,
  72. value: list[int],
  73. ) -> list[int]:
  74. for item in value:
  75. if item not in (2, 3, 4, 5):
  76. raise ValueError(
  77. f"酒店星级必须在 2-5 之间,实际: {item}"
  78. )
  79. return value
  80. near_subway: bool | None = None
  81. preferred_areas: list[str] = Field(
  82. default_factory=list
  83. )
  84. amenities: list[str] = Field(
  85. default_factory=list
  86. )
  87. class TravelRequest(AppModel):
  88. """从用户自然语言中提取的完整旅行需求。"""
  89. origin_city: str | None = None
  90. destination_city: str | None = None
  91. departure_date: date | None = None
  92. return_date: date | None = None
  93. # “4天3晚”等信息先保留下来,
  94. # 后续确定性代码可以据此推算返程日期。
  95. trip_days: int | None = Field(
  96. default=None,
  97. ge=1,
  98. le=30,
  99. )
  100. nights: int | None = Field(
  101. default=None,
  102. ge=0,
  103. le=29,
  104. )
  105. adults: int = Field(
  106. default=1,
  107. ge=1,
  108. le=9,
  109. )
  110. children: int = Field(
  111. default=0,
  112. ge=0,
  113. le=8,
  114. )
  115. @field_validator(
  116. "children",
  117. mode="before",
  118. )
  119. @classmethod
  120. def normalize_children(
  121. cls,
  122. value: object,
  123. ) -> int:
  124. """LLM 可能误将 children 输出为 [],防御性转换为 int。"""
  125. if value is None:
  126. return 0
  127. if isinstance(value, bool):
  128. return 1 if value else 0
  129. if isinstance(value, (list, tuple)):
  130. return len(value)
  131. return int(value)
  132. total_budget: float | None = Field(
  133. default=None,
  134. gt=0,
  135. )
  136. currency: str = "CNY"
  137. interests: list[str] = Field(
  138. default_factory=list
  139. )
  140. pace: TravelPace = "normal"
  141. flight_preferences: FlightPreferences = Field(
  142. default_factory=FlightPreferences
  143. )
  144. hotel_preferences: HotelPreferences = Field(
  145. default_factory=HotelPreferences
  146. )
  147. special_requirements: list[str] = Field(
  148. default_factory=list
  149. )
  150. @field_validator(
  151. "origin_city",
  152. "destination_city",
  153. mode="before",
  154. )
  155. @classmethod
  156. def normalize_city(
  157. cls,
  158. value: object,
  159. ) -> str | None:
  160. if value is None:
  161. return None
  162. text = str(value).strip()
  163. return text or None
  164. @field_validator("currency")
  165. @classmethod
  166. def normalize_currency(
  167. cls,
  168. value: str,
  169. ) -> str:
  170. return value.strip().upper()