Eliot0110 commited on
Commit
cd4408f
·
1 Parent(s): 6c0d50f

feat: 集成persona系统和预算收集

Browse files

✨ 新功能:
- 完整persona支持(planner/social/experiential)
- 预算收集和约束处理
- 动态AI prompt构建

🔧 改进:
- 修正response_generator逻辑主次关系
- 优化信息收集流程
- 增强备用方案生成

📱 API更新:
- 支持persona_key参数传递
- 更新ChatRequest验证器

app.py CHANGED
@@ -89,11 +89,14 @@ async def chat_endpoint(request: ChatRequest):
89
 
90
  try:
91
  log.info(f"收到聊天请求: {request.message[:50]}...")
 
 
92
 
93
  reply, session_id, status_info, history = assistant.chat(
94
  request.message,
95
  request.session_id,
96
- request.history or []
 
97
  )
98
 
99
  log.info(f"聊天响应生成成功,会话ID: {session_id}")
@@ -111,6 +114,13 @@ async def chat_endpoint(request: ChatRequest):
111
  status_code=500,
112
  detail="Internal Server Error: Failed to process chat request."
113
  )
 
 
 
 
 
 
 
114
 
115
  @app.post("/api/reset")
116
  async def reset_session(session_id: str):
 
89
 
90
  try:
91
  log.info(f"收到聊天请求: {request.message[:50]}...")
92
+ if request.persona_key:
93
+ log.info(f"用户选择的persona: {request.persona_key}")
94
 
95
  reply, session_id, status_info, history = assistant.chat(
96
  request.message,
97
  request.session_id,
98
+ request.history or [],
99
+ request.persona_key # 传递persona参数
100
  )
101
 
102
  log.info(f"聊天响应生成成功,会话ID: {session_id}")
 
114
  status_code=500,
115
  detail="Internal Server Error: Failed to process chat request."
116
  )
117
+
118
+ except Exception as e:
119
+ log.error(f"❌ Chat endpoint error: {e}", exc_info=True)
120
+ raise HTTPException(
121
+ status_code=500,
122
+ detail="Internal Server Error: Failed to process chat request."
123
+ )
124
 
125
  @app.post("/api/reset")
126
  async def reset_session(session_id: str):
modules/response_generator.py CHANGED
@@ -1,4 +1,5 @@
1
- # modules/response_generator.py - 修复版本
 
2
  from .ai_model import AIModel
3
  from .knowledge_base import KnowledgeBase
4
  from utils.logger import log
@@ -7,94 +8,254 @@ class ResponseGenerator:
7
  def __init__(self, ai_model: AIModel, knowledge_base: KnowledgeBase):
8
  self.ai_model = ai_model
9
  self.kb = knowledge_base
 
10
 
11
- def generate(self, user_message: str, session_state: dict) -> str:
 
12
  try:
13
- # 1. 优先使用 RAG (检索增强生成)
14
- search_query = user_message
15
- if session_state.get("destination"):
16
- search_query += f" {session_state['destination']['name']}"
17
-
18
- relevant_knowledge = self.kb.search(search_query)
19
- if relevant_knowledge:
20
- context = self._format_knowledge_context(relevant_knowledge)
21
- if self.ai_model.is_available():
22
- return self.ai_model.generate(user_message, context)
23
 
24
- # 2. 如果没有知识库匹配,则使用基于规则的引导式对话
 
 
 
 
25
  if not session_state.get("destination"):
26
  return "听起来很棒!你想去欧洲的哪个城市呢?比如巴黎, 罗马, 巴塞罗那?"
 
 
27
  if not session_state.get("duration"):
28
  return f"好的,{session_state['destination']['name']}是个很棒的选择!你计划玩几天呢?"
 
 
 
 
 
 
 
29
  if not session_state.get("persona"):
30
- return "最后一个问题,这次旅行对你来说什么最重要呢?(例如:美食、艺术、购物、历史)"
31
 
32
- # 3. 如果信息都收集全了,但没触发RAG,让AI生成一个通用计划
33
- if self.ai_model.is_available():
34
- plan_prompt = self._build_plan_prompt(session_state)
35
- return self.ai_model.generate(plan_prompt, context="用户需要一个详细的旅行计划。")
36
- else:
37
- return self._generate_fallback_plan(session_state)
38
 
39
  except Exception as e:
40
  log.error(f"❌ 响应生成失败: {e}", exc_info=True)
41
  return "抱歉,我在处理您的请求时遇到了问题,请稍后再试。"
42
 
43
- def _build_plan_prompt(self, session_state: dict) -> str:
44
- """构建AI生成计划的提示词 - 修复版本"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  destination = session_state.get("destination", {})
46
  duration = session_state.get("duration", {})
 
47
  persona = session_state.get("persona", {})
48
-
49
- prompt = f"请为用户生成一个在 {destination.get('name', '未知城市')} 的 "
50
- prompt += f"{duration.get('days', '')} 天旅行计划。"
51
-
52
- if persona:
53
- # 使用name而不是description
54
- persona_name = persona.get('name', '一般旅行者')
55
- persona_style = persona.get('style', '')
56
- prompt += f" 旅行风格: {persona_name}"
57
- if persona_style:
58
- prompt += f"({persona_style})"
59
- prompt += "。"
60
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  return prompt
62
 
63
- def _generate_fallback_plan(self, session_state: dict) -> str:
64
- """AI不可用时的备用计划生成"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  destination = session_state.get("destination", {})
66
  duration = session_state.get("duration", {})
 
67
  persona = session_state.get("persona", {})
68
 
69
- plan = f"为您推荐 {destination.get('name', '目的地')} {duration.get('days', '几')}天旅行计划:\n\n"
 
 
 
 
 
 
 
 
 
70
 
71
- # 使用cities.json中的信息
 
 
 
 
72
  highlights = destination.get('highlights', '精彩景点等待您的探索')
73
  plan += f"🎯 主要景点:{highlights}\n\n"
74
 
75
  best_season = destination.get('best_season', '全年适宜')
76
  plan += f"🌤️ 最佳旅行时间:{best_season}\n\n"
77
 
78
- budget = destination.get('avg_daily_budget', 100)
79
- plan += f"💰 预算参考:约{budget}欧元/天\n\n"
80
 
81
- if persona:
82
- plan += f"🎨 根据您的{persona.get('name', '旅行')}风格,建议您重点关注相关的景点和体验。\n\n"
 
 
 
 
 
83
 
84
- plan += "详细行程规划请稍后重试,或告诉我您的具体需求!"
 
 
 
 
85
 
 
86
  return plan
87
 
88
  def _format_knowledge_context(self, knowledge_items: list) -> str:
89
- """格式化知识库内容作为上下文 - 完全安全版本"""
90
  if not knowledge_items:
91
- return "没有特定的背景知识。"
92
 
93
  try:
94
  # 只使用最相关的一条知识
95
  item = knowledge_items[0]
96
-
97
- # 安全地获取嵌套数据
98
  knowledge = item.get('knowledge', {})
99
  travel_knowledge = knowledge.get('travel_knowledge', {})
100
 
@@ -110,10 +271,6 @@ class ResponseGenerator:
110
  recommended_duration = destination_info.get('recommended_duration')
111
  if recommended_duration:
112
  context_parts.append(f"推荐天数: {recommended_duration}天")
113
-
114
- travel_theme = destination_info.get('travel_theme')
115
- if travel_theme:
116
- context_parts.append(f"旅行主题: {travel_theme}")
117
 
118
  # 2. 获取预算信息
119
  budget_analysis = travel_knowledge.get('budget_analysis', {})
@@ -126,20 +283,17 @@ class ResponseGenerator:
126
  if daily_average:
127
  context_parts.append(f"日均预算: {daily_average}")
128
 
129
- # 3. 获取专业见解 - 根据实际数据结构
130
  professional_insights = travel_knowledge.get('professional_insights', {})
131
  if professional_insights:
132
- # 常见错误
133
  common_mistakes = professional_insights.get('common_mistakes', [])
134
  if common_mistakes:
135
- context_parts.append(f"注意事项: {', '.join(common_mistakes[:2])}")
136
 
137
- # 内行秘籍
138
  insider_secrets = professional_insights.get('insider_secrets', [])
139
  if insider_secrets:
140
  context_parts.append(f"内行贴士: {', '.join(insider_secrets[:2])}")
141
 
142
- # 最佳月份
143
  seasonal_info = professional_insights.get('seasonal_considerations', {})
144
  if seasonal_info:
145
  best_months = seasonal_info.get('best_months', [])
@@ -150,8 +304,8 @@ class ResponseGenerator:
150
  if context_parts:
151
  return "相关旅游知识:\n- " + "\n- ".join(context_parts)
152
  else:
153
- return "基于旅游知识库的一般建议。"
154
 
155
  except Exception as e:
156
  log.error(f"❌ 格式化知识库上下文失败: {e}", exc_info=True)
157
- return "知识库处理时发生错误,使用一般旅游建议。"
 
1
+ import json
2
+ import os
3
  from .ai_model import AIModel
4
  from .knowledge_base import KnowledgeBase
5
  from utils.logger import log
 
8
  def __init__(self, ai_model: AIModel, knowledge_base: KnowledgeBase):
9
  self.ai_model = ai_model
10
  self.kb = knowledge_base
11
+ self.personas = self._load_personas()
12
 
13
+ def _load_personas(self):
14
+ """加载personas配置"""
15
  try:
16
+ personas_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'personas.json')
17
+ with open(personas_path, 'r', encoding='utf-8') as f:
18
+ data = json.load(f)
19
+ return data.get('personas', {})
20
+ except Exception as e:
21
+ log.error(f"❌ 加载personas.json失败: {e}")
22
+ return {}
 
 
 
23
 
24
+ def generate(self, user_message: str, session_state: dict) -> str:
25
+ try:
26
+ # 【主流程】按顺序收集信息:目的地 → 天数 → 预算 → persona(可选)
27
+
28
+ # 1. 检查是否需要收集目的地
29
  if not session_state.get("destination"):
30
  return "听起来很棒!你想去欧洲的哪个城市呢?比如巴黎, 罗马, 巴塞罗那?"
31
+
32
+ # 2. 检查是否需要收集天数
33
  if not session_state.get("duration"):
34
  return f"好的,{session_state['destination']['name']}是个很棒的选择!你计划玩几天呢?"
35
+
36
+ # 3. 检查是否需要收集预算
37
+ if not session_state.get("budget"):
38
+ return f"了解!{session_state['duration']['days']}天的行程。为了给您更合适的建议,请问您的预算大概是多少呢?(比如:2000欧元,或者经济型/舒适型/豪华型)"
39
+
40
+ # 4. persona由前端传入,这里检查是否存在
41
+ # 注意:如果前端没传persona,可以询问或使用默认值
42
  if not session_state.get("persona"):
43
+ return "请告诉我您更偏向哪种旅行风格:高效规划型、社交分享型,还是深度体验型?"
44
 
45
+ # 【增强阶段】信息收集完毕,现在用知识库和persona增强回答质量
46
+ return self._generate_persona_enhanced_plan(user_message, session_state)
 
 
 
 
47
 
48
  except Exception as e:
49
  log.error(f"❌ 响应生成失败: {e}", exc_info=True)
50
  return "抱歉,我在处理您的请求时遇到了问题,请稍后再试。"
51
 
52
+ def _generate_persona_enhanced_plan(self, user_message: str, session_state: dict) -> str:
53
+ """根据persona和完整信息生成定制化的旅行计划"""
54
+
55
+ # 1. 获取知识库上下文
56
+ search_query = self._build_search_query(session_state)
57
+ relevant_knowledge = self.kb.search(search_query)
58
+ knowledge_context = self._format_knowledge_context(relevant_knowledge)
59
+
60
+ # 2. 根据persona构建定制化prompt
61
+ persona_prompt = self._build_persona_enhanced_prompt(session_state, knowledge_context)
62
+
63
+ # 3. 使用AI生成回答
64
+ if self.ai_model.is_available():
65
+ return self.ai_model.generate(user_message, persona_prompt)
66
+ else:
67
+ return self._generate_fallback_plan(session_state, knowledge_context)
68
+
69
+ def _build_persona_enhanced_prompt(self, session_state: dict, knowledge_context: str = "") -> str:
70
+ """根据persona构建增强的prompt"""
71
  destination = session_state.get("destination", {})
72
  duration = session_state.get("duration", {})
73
+ budget = session_state.get("budget", {})
74
  persona = session_state.get("persona", {})
75
+
76
+ # 基础信息
77
+ location = destination.get('name', '目的地')
78
+ days = duration.get('days', '几')
79
+ budget_info = self._format_budget_info(budget)
80
+
81
+ # 获取persona配置
82
+ persona_key = persona.get('key')
83
+ if not persona_key or persona_key not in self.personas:
84
+ # 如果没有有效persona,使用通用prompt
85
+ return self._build_generic_prompt(session_state, knowledge_context)
86
+
87
+ persona_config = self.personas[persona_key]
88
+
89
+ # 使用personas.json中的prompt_template
90
+ persona_template = persona_config.get('prompt_template', '')
91
+
92
+ # 替换模板中的变量
93
+ enhanced_prompt = persona_template.format(
94
+ location=location,
95
+ days=days,
96
+ date="近期", # 可以从session中获取更具体的日期
97
+ user_tags="", # 可以从用户偏好中提取
98
+ commercial_preference="适中", # 可以从session中获取
99
+ group_description="个人/朋友", # 可以从session中获取
100
+ budget=budget_info, # 使用格式化的预算信息
101
+ tags="" # 可以从session中获取
102
+ )
103
+
104
+ # 添加知识库上下文
105
+ if knowledge_context:
106
+ enhanced_prompt += f"\n\n【背景知识】\n{knowledge_context}"
107
+
108
+ # 添加预算约束信息
109
+ enhanced_prompt += f"\n\n【预算约束】\n用户预算:{budget_info},请确保所有推荐都在预算范围内。"
110
+
111
+ # 添加persona特定的指导原则
112
+ characteristics = persona_config.get('characteristics', [])
113
+ if characteristics:
114
+ enhanced_prompt += f"\n\n【用户特征】\n" + "\n".join([f"- {char}" for char in characteristics])
115
+
116
+ recommendation_strategy = persona_config.get('recommendation_strategy', [])
117
+ if recommendation_strategy:
118
+ enhanced_prompt += f"\n\n【推荐策略】\n" + "\n".join([f"- {strategy}" for strategy in recommendation_strategy])
119
+
120
+ tone_guidelines = persona_config.get('tone', [])
121
+ if tone_guidelines:
122
+ enhanced_prompt += f"\n\n【语言风格】\n" + "\n".join([f"- {tone}" for tone in tone_guidelines])
123
+
124
+ return enhanced_prompt
125
+
126
+ def _format_budget_info(self, budget: dict) -> str:
127
+ """格式化预算信息"""
128
+ if not budget:
129
+ return "中等预算"
130
+
131
+ # 如果有具体金额
132
+ if budget.get('amount') and budget.get('currency'):
133
+ return f"{budget['amount']}{budget['currency']}"
134
+
135
+ # 如果有预算类型
136
+ if budget.get('type'):
137
+ budget_type_map = {
138
+ 'economy': '经济型预算',
139
+ 'comfortable': '舒适型预算',
140
+ 'luxury': '豪华型预算'
141
+ }
142
+ return budget_type_map.get(budget['type'], budget['type'])
143
+
144
+ # 如果有预算范围
145
+ if budget.get('range'):
146
+ return budget['range']
147
+
148
+ return "中等预算"
149
+
150
+ def _build_generic_prompt(self, session_state: dict, knowledge_context: str = "") -> str:
151
+ """构建通用prompt(当没有persona时使用)"""
152
+ destination = session_state.get("destination", {})
153
+ duration = session_state.get("duration", {})
154
+ budget = session_state.get("budget", {})
155
+
156
+ location = destination.get('name', '目的地')
157
+ days = duration.get('days', '几')
158
+ budget_info = self._format_budget_info(budget)
159
+
160
+ prompt = f"""你是一个专业的旅游助手。请为用户生成一个详细的旅行计划。
161
+
162
+ 【基本信息】
163
+ - 目的地:{location}
164
+ - 旅行天数:{days}天
165
+ - 预算:{budget_info}
166
+
167
+ 【要求】
168
+ - 提供具体的景点推荐和路线安排
169
+ - 包含交通、住宿、餐饮建议
170
+ - 确保所有推荐都在预算范围内
171
+ - 提供实用的旅行贴士"""
172
+
173
+ if knowledge_context:
174
+ prompt += f"\n\n【背景信息】\n{knowledge_context}"
175
+
176
+ prompt += "\n\n请生成一份实用、详细的旅行计划。"
177
+
178
  return prompt
179
 
180
+ def _build_search_query(self, session_state: dict) -> str:
181
+ """构建知识库搜索查询"""
182
+ destination = session_state.get("destination", {})
183
+ persona = session_state.get("persona", {})
184
+ budget = session_state.get("budget", {})
185
+
186
+ query_parts = []
187
+
188
+ # 添加目的地
189
+ if destination.get('name'):
190
+ query_parts.append(destination['name'])
191
+
192
+ # 添加persona类型
193
+ if persona.get('key'):
194
+ query_parts.append(persona['key'])
195
+
196
+ # 添加预算类型
197
+ if budget.get('type'):
198
+ query_parts.append(budget['type'])
199
+
200
+ return " ".join(query_parts) if query_parts else "欧洲旅行"
201
+
202
+ def _generate_fallback_plan(self, session_state: dict, knowledge_context: str = "") -> str:
203
+ """AI不可用时的备用计划生成(现在包含预算和persona信息)"""
204
  destination = session_state.get("destination", {})
205
  duration = session_state.get("duration", {})
206
+ budget = session_state.get("budget", {})
207
  persona = session_state.get("persona", {})
208
 
209
+ location = destination.get('name', '目的地')
210
+ days = duration.get('days', '几')
211
+ budget_info = self._format_budget_info(budget)
212
+ persona_name = persona.get('name', '旅行者')
213
+
214
+ plan = f"为您推荐 {location} {days}天旅行计划:\n\n"
215
+
216
+ # 用户信息概述
217
+ plan += f"👤 旅行者类型:{persona_name}\n"
218
+ plan += f"💰 预算范围:{budget_info}\n\n"
219
 
220
+ # 优先使用知识库信息
221
+ if knowledge_context and "相关旅游知识" in knowledge_context:
222
+ plan += f"📚 {knowledge_context}\n\n"
223
+
224
+ # 使用cities.json中的基础信息
225
  highlights = destination.get('highlights', '精彩景点等待您的探索')
226
  plan += f"🎯 主要景点:{highlights}\n\n"
227
 
228
  best_season = destination.get('best_season', '全年适宜')
229
  plan += f"🌤️ 最佳旅行时间:{best_season}\n\n"
230
 
231
+ avg_budget = destination.get('avg_daily_budget', 100)
232
+ plan += f"💰 日均预算参考:约{avg_budget}欧元/天\n\n"
233
 
234
+ # 根据persona调整建议风格
235
+ if persona.get('key') == 'planner':
236
+ plan += "📋 建议制定详细的每日行程表,包含具体时间安排和备选方案。\n\n"
237
+ elif persona.get('key') == 'social':
238
+ plan += "📸 推荐寻找热门打卡点,准备好相机记录美好时光!\n\n"
239
+ elif persona.get('key') == 'experiential':
240
+ plan += "🎨 建议深入当地社区,寻找地道的文化体验和小众景点。\n\n"
241
 
242
+ # 预算相关建议
243
+ if budget.get('type') == 'economy':
244
+ plan += "💡 经济型贴士:考虑使用公共交通,选择青旅或民宿,寻找当地平价美食。\n\n"
245
+ elif budget.get('type') == 'luxury':
246
+ plan += "✨ 豪华体验:可以考虑五星酒店、私人导览和米其林餐厅。\n\n"
247
 
248
+ plan += "如需更详细的个性化规划,请告诉我您的具体需求!"
249
  return plan
250
 
251
  def _format_knowledge_context(self, knowledge_items: list) -> str:
252
+ """格式化知识库内容作为上下文"""
253
  if not knowledge_items:
254
+ return "基于一般旅游建议"
255
 
256
  try:
257
  # 只使用最相关的一条知识
258
  item = knowledge_items[0]
 
 
259
  knowledge = item.get('knowledge', {})
260
  travel_knowledge = knowledge.get('travel_knowledge', {})
261
 
 
271
  recommended_duration = destination_info.get('recommended_duration')
272
  if recommended_duration:
273
  context_parts.append(f"推荐天数: {recommended_duration}天")
 
 
 
 
274
 
275
  # 2. 获取预算信息
276
  budget_analysis = travel_knowledge.get('budget_analysis', {})
 
283
  if daily_average:
284
  context_parts.append(f"日均预算: {daily_average}")
285
 
286
+ # 3. 获取专业见解
287
  professional_insights = travel_knowledge.get('professional_insights', {})
288
  if professional_insights:
 
289
  common_mistakes = professional_insights.get('common_mistakes', [])
290
  if common_mistakes:
291
+ context_parts.append(f"避免误区: {', '.join(common_mistakes[:2])}")
292
 
 
293
  insider_secrets = professional_insights.get('insider_secrets', [])
294
  if insider_secrets:
295
  context_parts.append(f"内行贴士: {', '.join(insider_secrets[:2])}")
296
 
 
297
  seasonal_info = professional_insights.get('seasonal_considerations', {})
298
  if seasonal_info:
299
  best_months = seasonal_info.get('best_months', [])
 
304
  if context_parts:
305
  return "相关旅游知识:\n- " + "\n- ".join(context_parts)
306
  else:
307
+ return "基于专业旅游建议"
308
 
309
  except Exception as e:
310
  log.error(f"❌ 格式化知识库上下文失败: {e}", exc_info=True)
311
+ return "基于一般旅游建议"
modules/travel_assistant.py CHANGED
@@ -1,45 +1,323 @@
1
- # modules/travel_assistant.py
2
- from .config_loader import ConfigLoader
 
3
  from .ai_model import AIModel
4
  from .knowledge_base import KnowledgeBase
5
- from .info_extractor import InfoExtractor
6
- from .session_manager import SessionManager
7
- from .response_generator import ResponseGenerator
8
  from utils.logger import log
9
 
10
- class TravelAssistant:
11
- def __init__(self):
12
- # 依赖注入:在这里实例化所有需要的模块
13
- log.info("开始初始化 Travel Assistant 核心模块...")
14
- self.config = ConfigLoader()
15
- self.kb = KnowledgeBase()
16
- self.ai_model = AIModel()
17
- self.session_manager = SessionManager()
18
- self.info_extractor = InfoExtractor(self.config)
19
- self.response_generator = ResponseGenerator(self.ai_model, self.kb)
20
- log.info("✅ Travel Assistant 核心模块全部初始化完成!")
21
-
22
- def chat(self, message: str, session_id: str, history: list):
23
- # 1. 获取或创建会话
24
- session_state = self.session_manager.get_or_create_session(session_id)
25
- current_session_id = session_state['session_id']
26
-
27
- # 2. 从用户输入中提取信息
28
- extracted_info = self.info_extractor.extract(message)
29
-
30
- # 3. 更新会话状态
31
- if extracted_info:
32
- self.session_manager.update_session(current_session_id, extracted_info)
33
- # 重新获取更新后的状态
34
- session_state = self.session_manager.get_or_create_session(current_session_id)
35
-
36
- # 4. 生成回复
37
- bot_response = self.response_generator.generate(message, session_state)
38
-
39
- # 5. 格式化状态信息用于前端显示
40
- status_info = self.session_manager.format_session_info(session_state)
41
-
42
- # 6. 更新对话历史
43
- new_history = history + [[message, bot_response]]
44
-
45
- return bot_response, current_session_id, status_info, new_history
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/response_generator.py - 完整版本(含persona+预算)
2
+ import json
3
+ import os
4
  from .ai_model import AIModel
5
  from .knowledge_base import KnowledgeBase
 
 
 
6
  from utils.logger import log
7
 
8
+ class ResponseGenerator:
9
+ def __init__(self, ai_model: AIModel, knowledge_base: KnowledgeBase):
10
+ self.ai_model = ai_model
11
+ self.kb = knowledge_base
12
+ self.personas = self._load_personas()
13
+
14
+ def _load_personas(self):
15
+ """加载personas配置"""
16
+ try:
17
+ personas_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'personas.json')
18
+ with open(personas_path, 'r', encoding='utf-8') as f:
19
+ data = json.load(f)
20
+ return data.get('personas', {})
21
+ except Exception as e:
22
+ log.error(f"❌ 加载personas.json失败: {e}")
23
+ return {}
24
+
25
+ def generate(self, user_message: str, session_state: dict) -> str:
26
+ try:
27
+ # 【主流程】按顺序收集信息:目的地 → 天数 → 预算 → persona(可选)
28
+
29
+ # 1. 检查是否需要收集目的地
30
+ if not session_state.get("destination"):
31
+ return "Hi!你想去欧洲的哪个城市呢?比如巴黎, 罗马, 巴塞罗那?"
32
+
33
+ # 2. 检查是否需要收集天数
34
+ if not session_state.get("duration"):
35
+ return f"好的,{session_state['destination']['name']}是个很棒的选择!你计划玩几天呢?"
36
+
37
+ # 3. 检查是否需要收集预算
38
+ if not session_state.get("budget"):
39
+ return f"了解!{session_state['duration']['days']}天的行程。为了给您更合适的建议,请问您的预算大概是多少呢?(比如:2000欧元,或者经济型/舒适型/豪华型)"
40
+
41
+ # 4. persona由前端传入,这里检查是否存在
42
+ # 注意:如果前端没传persona,可以询问或使用默认值
43
+ if not session_state.get("persona"):
44
+ separator = '*' * 60
45
+
46
+ persona_selection_message = f"""请告诉我您更偏向哪种旅行风格:高效规划型、社交分享型,还是深度体验型?
47
+
48
+ {separator}
49
+
50
+ 🗓️ **高效规划型** - 注重时间安排和预算控制,喜欢详细的行程规划
51
+
52
+ 🤝 **社交分享型** - 重视与朋友分享,喜欢拍照打卡和热闹的体验
53
+
54
+ 🎨 **深度体验型** - 追求地道文化体验,避开商业化景点"""
55
+ return persona_selection_message
56
+
57
+ # 【增强阶段】信息收集完毕,现在用知识库和persona增强回答质量
58
+ return self._generate_persona_enhanced_plan(user_message, session_state)
59
+
60
+ except Exception as e:
61
+ log.error(f"❌ 响应生成失败: {e}", exc_info=True)
62
+ return "抱歉,我在处理您的请求时遇到了问题,请稍后再试。"
63
+
64
+ def _generate_persona_enhanced_plan(self, user_message: str, session_state: dict) -> str:
65
+ """根据persona和完整信息生成定制化的旅行计划"""
66
+
67
+ # 1. 获取知识库上下文
68
+ search_query = self._build_search_query(session_state)
69
+ relevant_knowledge = self.kb.search(search_query)
70
+ knowledge_context = self._format_knowledge_context(relevant_knowledge)
71
+
72
+ # 2. 根据persona构建定制化prompt
73
+ persona_prompt = self._build_persona_enhanced_prompt(session_state, knowledge_context)
74
+
75
+ # 3. 使用AI生成回答
76
+ if self.ai_model.is_available():
77
+ return self.ai_model.generate(user_message, persona_prompt)
78
+ else:
79
+ return self._generate_fallback_plan(session_state, knowledge_context)
80
+
81
+ def _build_persona_enhanced_prompt(self, session_state: dict, knowledge_context: str = "") -> str:
82
+ """根据persona构建增强的prompt"""
83
+ destination = session_state.get("destination", {})
84
+ duration = session_state.get("duration", {})
85
+ budget = session_state.get("budget", {})
86
+ persona = session_state.get("persona", {})
87
+
88
+ # 基础信息
89
+ location = destination.get('name', '目的地')
90
+ days = duration.get('days', '几')
91
+ budget_info = self._format_budget_info(budget)
92
+
93
+ # 获取persona配置
94
+ persona_key = persona.get('key')
95
+ if not persona_key or persona_key not in self.personas:
96
+ # 如果没有有效persona,使用通用prompt
97
+ return self._build_generic_prompt(session_state, knowledge_context)
98
+
99
+ persona_config = self.personas[persona_key]
100
+
101
+ # 使用personas.json中的prompt_template
102
+ persona_template = persona_config.get('prompt_template', '')
103
+
104
+ # 替换模板中的变量
105
+ enhanced_prompt = persona_template.format(
106
+ location=location,
107
+ days=days,
108
+ date="近期", # 可以从session中获取更具体的日期
109
+ user_tags="", # 可以从用户偏好中提取
110
+ commercial_preference="适中", # 可以从session中获取
111
+ group_description="个人/朋友", # 可以从session中获取
112
+ budget=budget_info, # 使用格式化的预算信息
113
+ tags="" # 可以从session中获取
114
+ )
115
+
116
+ # 添加知识库上下文
117
+ if knowledge_context:
118
+ enhanced_prompt += f"\n\n【背景知识】\n{knowledge_context}"
119
+
120
+ # 添加预算约束信息
121
+ enhanced_prompt += f"\n\n【预算约束】\n用户预算:{budget_info},请确保所有推荐都在预算范围内。"
122
+
123
+ # 添加persona特定的指导原则
124
+ characteristics = persona_config.get('characteristics', [])
125
+ if characteristics:
126
+ enhanced_prompt += f"\n\n【用户特征】\n" + "\n".join([f"- {char}" for char in characteristics])
127
+
128
+ recommendation_strategy = persona_config.get('recommendation_strategy', [])
129
+ if recommendation_strategy:
130
+ enhanced_prompt += f"\n\n【推荐策略】\n" + "\n".join([f"- {strategy}" for strategy in recommendation_strategy])
131
+
132
+ tone_guidelines = persona_config.get('tone', [])
133
+ if tone_guidelines:
134
+ enhanced_prompt += f"\n\n【语言风格】\n" + "\n".join([f"- {tone}" for tone in tone_guidelines])
135
+
136
+ return enhanced_prompt
137
+
138
+ def _format_budget_info(self, budget: dict) -> str:
139
+ """格式化预算信息"""
140
+ if not budget:
141
+ return "中等预算"
142
+
143
+ # 如果有具体金额
144
+ if budget.get('amount') and budget.get('currency'):
145
+ return f"{budget['amount']}{budget['currency']}"
146
+
147
+ # 如果有预算类型
148
+ if budget.get('type'):
149
+ budget_type_map = {
150
+ 'economy': '经济型预算',
151
+ 'comfortable': '舒适型预算',
152
+ 'luxury': '豪华型预算'
153
+ }
154
+ return budget_type_map.get(budget['type'], budget['type'])
155
+
156
+ # 如果有预算范围
157
+ if budget.get('range'):
158
+ return budget['range']
159
+
160
+ return "中等预算"
161
+
162
+ def _build_generic_prompt(self, session_state: dict, knowledge_context: str = "") -> str:
163
+ """构建通用prompt(当没有persona时使用)"""
164
+ destination = session_state.get("destination", {})
165
+ duration = session_state.get("duration", {})
166
+ budget = session_state.get("budget", {})
167
+
168
+ location = destination.get('name', '目的地')
169
+ days = duration.get('days', '几')
170
+ budget_info = self._format_budget_info(budget)
171
+
172
+ prompt = f"""你是一个专业的旅游助手。请为用户生成一个详细的旅行计划。
173
+
174
+ 【基本信息】
175
+ - 目的地:{location}
176
+ - 旅行天数:{days}天
177
+ - 预算:{budget_info}
178
+
179
+ 【要求】
180
+ - 提供具体的景点推荐和路线安排
181
+ - 包含交通、住宿、餐饮建议
182
+ - 确保所有推荐都在预算范围内
183
+ - 提供实用的旅行贴士"""
184
+
185
+ if knowledge_context:
186
+ prompt += f"\n\n【背景信息】\n{knowledge_context}"
187
+
188
+ prompt += "\n\n请生成一份实用、详细的旅行计划。"
189
+
190
+ return prompt
191
+
192
+ def _build_search_query(self, session_state: dict) -> str:
193
+ """构建知识库搜索查询"""
194
+ destination = session_state.get("destination", {})
195
+ persona = session_state.get("persona", {})
196
+ budget = session_state.get("budget", {})
197
+
198
+ query_parts = []
199
+
200
+ # 添加目的地
201
+ if destination.get('name'):
202
+ query_parts.append(destination['name'])
203
+
204
+ # 添加persona类型
205
+ if persona.get('key'):
206
+ query_parts.append(persona['key'])
207
+
208
+ # 添加预算类型
209
+ if budget.get('type'):
210
+ query_parts.append(budget['type'])
211
+
212
+ return " ".join(query_parts) if query_parts else "欧洲旅行"
213
+
214
+ def _generate_fallback_plan(self, session_state: dict, knowledge_context: str = "") -> str:
215
+ """AI不可用时的备用计划生成(现在包含预算和persona信息)"""
216
+ destination = session_state.get("destination", {})
217
+ duration = session_state.get("duration", {})
218
+ budget = session_state.get("budget", {})
219
+ persona = session_state.get("persona", {})
220
+
221
+ location = destination.get('name', '目的地')
222
+ days = duration.get('days', '几')
223
+ budget_info = self._format_budget_info(budget)
224
+ persona_name = persona.get('name', '旅行者')
225
+
226
+ plan = f"为您推荐 {location} {days}天旅行计划:\n\n"
227
+
228
+ # 用户信息概述
229
+ plan += f"👤 旅行者类型:{persona_name}\n"
230
+ plan += f"💰 预算范围:{budget_info}\n\n"
231
+
232
+ # 优先使用知识库信息
233
+ if knowledge_context and "相关旅游知识" in knowledge_context:
234
+ plan += f"📚 {knowledge_context}\n\n"
235
+
236
+ # 使用cities.json中的基础信息
237
+ highlights = destination.get('highlights', '精彩景点等待您的探索')
238
+ plan += f"🎯 主要景点:{highlights}\n\n"
239
+
240
+ best_season = destination.get('best_season', '全年适宜')
241
+ plan += f"🌤️ 最佳旅行时间:{best_season}\n\n"
242
+
243
+ avg_budget = destination.get('avg_daily_budget', 100)
244
+ plan += f"💰 日均预算参考:约{avg_budget}欧元/天\n\n"
245
+
246
+ # 根据persona调整建议风格
247
+ if persona.get('key') == 'planner':
248
+ plan += "📋 建议制定详细的每日行程表,包含具体时间安排和备选方案。\n\n"
249
+ elif persona.get('key') == 'social':
250
+ plan += "📸 推荐寻找热门打卡点,准备好相机记录美好时光!\n\n"
251
+ elif persona.get('key') == 'experiential':
252
+ plan += "🎨 建议深入当地社区,寻找地道的文化体验和小众景点。\n\n"
253
+
254
+ # 预算相关建议
255
+ if budget.get('type') == 'economy':
256
+ plan += "💡 经济型贴士:考虑使用公共交通,选择青旅或民宿,寻找当地平价美食。\n\n"
257
+ elif budget.get('type') == 'luxury':
258
+ plan += "✨ 豪华体验:可以考虑五星酒店、私人导览和米其林餐厅。\n\n"
259
+
260
+ plan += "如需更详细的个性化规划,请告诉我您的具体需求!"
261
+ return plan
262
+
263
+ def _format_knowledge_context(self, knowledge_items: list) -> str:
264
+ """格式化知识库内容作为上下文"""
265
+ if not knowledge_items:
266
+ return "基于一般旅游建议"
267
+
268
+ try:
269
+ # 只使用最相关的一条知识
270
+ item = knowledge_items[0]
271
+ knowledge = item.get('knowledge', {})
272
+ travel_knowledge = knowledge.get('travel_knowledge', {})
273
+
274
+ context_parts = []
275
+
276
+ # 1. 获取目的地信息
277
+ destination_info = travel_knowledge.get('destination_info', {})
278
+ if destination_info:
279
+ primary_destinations = destination_info.get('primary_destinations', [])
280
+ if primary_destinations:
281
+ context_parts.append(f"目的地: {', '.join(primary_destinations)}")
282
+
283
+ recommended_duration = destination_info.get('recommended_duration')
284
+ if recommended_duration:
285
+ context_parts.append(f"推荐天数: {recommended_duration}天")
286
+
287
+ # 2. 获取预算信息
288
+ budget_analysis = travel_knowledge.get('budget_analysis', {})
289
+ if budget_analysis:
290
+ total_budget = budget_analysis.get('total_budget_range')
291
+ if total_budget:
292
+ context_parts.append(f"预算参考: {total_budget}")
293
+
294
+ daily_average = budget_analysis.get('daily_average')
295
+ if daily_average:
296
+ context_parts.append(f"日均预算: {daily_average}")
297
+
298
+ # 3. 获取专业见解
299
+ professional_insights = travel_knowledge.get('professional_insights', {})
300
+ if professional_insights:
301
+ common_mistakes = professional_insights.get('common_mistakes', [])
302
+ if common_mistakes:
303
+ context_parts.append(f"避免误区: {', '.join(common_mistakes[:2])}")
304
+
305
+ insider_secrets = professional_insights.get('insider_secrets', [])
306
+ if insider_secrets:
307
+ context_parts.append(f"内行贴士: {', '.join(insider_secrets[:2])}")
308
+
309
+ seasonal_info = professional_insights.get('seasonal_considerations', {})
310
+ if seasonal_info:
311
+ best_months = seasonal_info.get('best_months', [])
312
+ if best_months:
313
+ context_parts.append(f"最佳时间: {', '.join(best_months)}")
314
+
315
+ # 4. 拼接结果
316
+ if context_parts:
317
+ return "相关旅游知识:\n- " + "\n- ".join(context_parts)
318
+ else:
319
+ return "基于专业旅游建议"
320
+
321
+ except Exception as e:
322
+ log.error(f"❌ 格式化知识库上下文失败: {e}", exc_info=True)
323
+ return "基于一般旅游建议"
utils/validators.py CHANGED
@@ -1,14 +1,14 @@
1
- # utils/validators.py
2
  from pydantic import BaseModel
3
- from typing import List, Optional
4
 
5
  class ChatRequest(BaseModel):
6
  message: str
7
  session_id: Optional[str] = None
8
  history: Optional[List[List[str]]] = []
 
9
 
10
  class ChatResponse(BaseModel):
11
  reply: str
12
  session_id: str
13
- status_info: str
14
- history: List[List[str]]
 
 
1
  from pydantic import BaseModel
2
+ from typing import List, Optional, Dict, Any
3
 
4
  class ChatRequest(BaseModel):
5
  message: str
6
  session_id: Optional[str] = None
7
  history: Optional[List[List[str]]] = []
8
+ persona_key: Optional[str] = None # 新增:接收前端传来的persona
9
 
10
  class ChatResponse(BaseModel):
11
  reply: str
12
  session_id: str
13
+ status_info: Dict[str, Any]
14
+ history: List[List[str]]