| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- from __future__ import annotations
- """行程规划结果模型:ItineraryPlan多日行程、FlightSelection航班选择、HotelSelection酒店选择及每日活动。"""
- from datetime import date, time
- from typing import Literal
- from pydantic import Field, field_validator, model_validator
- from app.schemas.common import AppModel
- ActivityType = Literal[
- "flight",
- "hotel_check_in",
- "hotel_check_out",
- "attraction",
- "restaurant",
- "transport",
- "free_time",
- "other",
- ]
- TransportMode = Literal[
- "flight",
- "walking",
- "public_transit",
- "driving",
- "taxi",
- "unknown",
- ]
- class FlightSelection(AppModel):
- """规划角色选择的航班方案。
- 这里保存候选ID,后续Validator将检查这些ID
- 是否真实存在于候选结果中。
- """
- flight_type: Literal[
- "one_way",
- "round_trip",
- ]
- combination_id: str | None = None
- outbound_option_id: str
- return_option_id: str | None = None
- # 接口报价(不是去程+返程的加总)。
- quoted_price: float | None = Field(
- default=None,
- ge=0,
- )
- currency: str = "CNY"
- outbound_flight_numbers: list[str] = Field(
- default_factory=list
- )
- return_flight_numbers: list[str] = Field(
- default_factory=list
- )
- outbound_departure_time: str | None = None
- outbound_arrival_time: str | None = None
- return_departure_time: str | None = None
- return_arrival_time: str | None = None
- selection_reason: str
- @model_validator(mode="after")
- def validate_round_trip(
- self,
- ) -> "FlightSelection":
- if self.flight_type == "round_trip":
- if not self.combination_id:
- raise ValueError(
- "往返方案必须提供combination_id。"
- )
- if not self.return_option_id:
- raise ValueError(
- "往返方案必须提供return_option_id。"
- )
- return self
- class HotelSelection(AppModel):
- """规划角色选择的住宿方案。"""
- hotel_id: str
- name: str
- check_in_date: date
- check_out_date: date
- nights: int = Field(ge=1)
- price_per_night: float | None = Field(
- default=None,
- ge=0,
- )
- estimated_total_price: float | None = Field(
- default=None,
- ge=0,
- )
- currency: str = "CNY"
- address: str | None = None
- location: str | None = None
- selection_reason: str
- @field_validator("location", mode="before")
- @classmethod
- def _coerce_location_to_str(
- cls,
- value: object,
- ) -> object:
- """LLM 可能将 coordinates dict 误填到 location,
- 自动转换为 "longitude,latitude" 字符串。"""
- if isinstance(value, dict):
- lat = value.get("latitude")
- lng = value.get("longitude")
- if lat is not None and lng is not None:
- return f"{lng},{lat}"
- return value
- class ItineraryActivity(AppModel):
- """一天中的一个具体活动。"""
- sequence: int = Field(ge=1)
- activity_type: ActivityType
- # 景点或餐厅对应的真实POI ID。
- # 航班、入住等活动允许为空。
- reference_id: str | None = None
- name: str
- start_time: time
- end_time: time
- address: str | None = None
- location: str | None = None
- transport_mode: TransportMode | None = None
- estimated_transport_minutes: int | None = Field(
- default=None,
- ge=0,
- )
- estimated_cost: float | None = Field(
- default=None,
- ge=0,
- )
- notes: str | None = None
- class DailyItinerary(AppModel):
- """单日行程。"""
- day_index: int = Field(
- ge=1,
- le=30,
- )
- date: date
- theme: str
- activities: list[ItineraryActivity] = Field(
- min_length=1
- )
- daily_notes: list[str] = Field(
- default_factory=list
- )
- class ItineraryPlan(AppModel):
- """行程规划角色生成的完整结构化方案。"""
- title: str
- overview: str
- selected_flight: FlightSelection
- selected_hotel: HotelSelection
- days: list[DailyItinerary] = Field(
- min_length=1
- )
- highlights: list[str] = Field(
- default_factory=list
- )
- assumptions: list[str] = Field(
- default_factory=list
- )
- warnings: list[str] = Field(
- default_factory=list
- )
- @model_validator(mode="after")
- def validate_days(
- self,
- ) -> "ItineraryPlan":
- day_indexes = [
- item.day_index
- for item in self.days
- ]
- dates = [
- item.date
- for item in self.days
- ]
- if len(day_indexes) != len(set(day_indexes)):
- raise ValueError(
- "每日行程的day_index不能重复。"
- )
- if len(dates) != len(set(dates)):
- raise ValueError(
- "每日行程日期不能重复。"
- )
- return self
|