from __future__ import annotations import time import uuid from collections import OrderedDict from dataclasses import dataclass, field from typing import Optional from .knowledge import KnowledgeBase from .llm import LLMService from .schemas import ( GeneratedItinerary, Itinerary, PlanResponse, Source, TravelPreferences, ) @dataclass class Session: preferences: TravelPreferences itinerary: GeneratedItinerary created_at: float history: list[str] = field(default_factory=list) class SessionStore: def __init__(self, max_size: int = 100, ttl_seconds: int = 7200) -> None: self.max_size = max_size self.ttl_seconds = ttl_seconds self.sessions: OrderedDict[str, Session] = OrderedDict() def put(self, session: Session) -> str: self.cleanup() while len(self.sessions) >= self.max_size: self.sessions.popitem(last=False) session_id = str(uuid.uuid4()) self.sessions[session_id] = session return session_id def get(self, session_id: str) -> Optional[Session]: self.cleanup() session = self.sessions.get(session_id) if session: self.sessions.move_to_end(session_id) return session def cleanup(self) -> None: deadline = time.time() - self.ttl_seconds expired = [ key for key, session in self.sessions.items() if session.created_at < deadline ] for key in expired: self.sessions.pop(key, None) class PlannerService: def __init__( self, knowledge: KnowledgeBase, llm: LLMService, store: SessionStore, ) -> None: self.knowledge = knowledge self.llm = llm self.store = store async def create(self, preferences: TravelPreferences) -> PlanResponse: candidates = self.knowledge.retrieve(preferences) if not candidates: raise LookupError("没有找到符合当前条件的光明区地点") generated = await self.llm.generate(preferences, candidates) generated = self._canonicalize(generated, candidates) session_id = self.store.put( Session(preferences=preferences, itinerary=generated, created_at=time.time()) ) return PlanResponse( conversation_id=session_id, assistant_message="已根据你的偏好生成光明区行程。", itinerary=self._with_sources(generated), ) async def adjust(self, session_id: str, message: str) -> PlanResponse: session = self.store.get(session_id) if not session: raise LookupError("会话不存在或已过期,请重新生成行程") updated_preferences = session.preferences.model_copy( update={ "extra_requirements": ( session.preferences.extra_requirements + ";" + message ).strip(";") } ) candidates = self.knowledge.retrieve(updated_preferences) if not candidates: raise LookupError("没有找到符合调整条件的光明区地点") generated = await self.llm.generate( updated_preferences, candidates, session.itinerary, message ) generated = self._canonicalize(generated, candidates) session.preferences = updated_preferences session.itinerary = generated session.history.append(message) return PlanResponse( conversation_id=session_id, assistant_message="已按你的补充要求调整行程。", change_summary=f"已根据“{message}”更新相关安排。", itinerary=self._with_sources(generated), ) def _canonicalize( self, itinerary: GeneratedItinerary, candidates: list[dict], ) -> GeneratedItinerary: candidate_map = {place["id"]: place for place in candidates} for item in itinerary.items: place = candidate_map.get(item.place_id) if not place: raise ValueError(f"行程包含知识库外地点:{item.place_id}") item.place_name = place["name"] item.tips = [ tip for tip in item.tips if tip in place.get("tips", []) ] or place.get("tips", [])[:2] return itinerary def _with_sources(self, generated: GeneratedItinerary) -> Itinerary: seen: set[str] = set() sources: list[Source] = [] for item in generated.items: if item.place_id in seen: continue seen.add(item.place_id) place = self.knowledge.by_id(item.place_id) if place: sources.append( Source( place_id=place["id"], source_name=place["sourceName"], source_url=place["sourceUrl"], updated_at=place["updatedAt"], ) ) return Itinerary( **generated.model_dump(), sources=sources, )