Eliot0110 commited on
Commit
f2dcd91
·
1 Parent(s): 9fc53c5

improve: chat logic

Browse files
Files changed (1) hide show
  1. modules/response_generator.py +136 -322
modules/response_generator.py CHANGED
@@ -1,340 +1,154 @@
1
  import json
2
- import os
3
  from .ai_model import AIModel
4
- from .knowledge_base import KnowledgeBase
5
  from utils.logger import log
 
6
 
7
  class ResponseGenerator:
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
- personas_path = "./config/personas.json" # 简化路径
16
- with open(personas_path, 'r', encoding='utf-8') as f:
17
  data = json.load(f)
 
18
  return data.get('personas', {})
19
-
20
- def generate(self, user_message: str, session_state: dict, extracted_info: dict) -> str:
21
- """生成智能响应 - 修复重复询问问题"""
 
 
22
  try:
23
- response_parts = []
24
-
25
- # 1. 生成对新提取信息的确认反馈
26
- acknowledgement = self._generate_acknowledgement(extracted_info)
27
- if acknowledgement:
28
- response_parts.append(acknowledgement)
29
 
30
- # 2. 检查信息完整性,按优先级询问缺失信息
31
- next_question = self._get_next_question(session_state)
32
  if next_question:
33
- response_parts.append(next_question)
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- # 3. 如果所有信息都齐全,生成旅行计划
36
- if not next_question:
37
- plan = self._generate_persona_enhanced_plan(user_message, session_state)
38
- response_parts.append(plan)
39
-
40
- # 组合回复
41
- return " ".join(response_parts) if response_parts else "我理解了,请继续告诉我您的需求。"
42
-
43
  except Exception as e:
44
- log.error(f"❌ 响应生成失败: {e}", exc_info=True)
45
- return "抱歉,我在处理您的请求时遇到了问题,请稍后再试。"
46
-
47
- def _get_next_question(self, session_state: dict) -> str:
48
- """获取下一个需要询问的问题 - 修复版"""
49
-
50
- # 按顺序检查缺失信息
51
- if not session_state.get("destination"):
52
- return "你想去欧洲的哪个城市呢?比如巴黎、罗马、巴塞罗那?"
53
-
54
- if not session_state.get("duration"):
55
- destination_name = session_state.get('destination', {}).get('name', '目的地')
56
- return f"好的,{destination_name}是个很棒的选择!你计划玩几天呢?"
57
-
58
- if not session_state.get("budget"):
59
- days = session_state.get('duration', {}).get('days', '几')
60
- return f"了解!{days}天的行程。预算大概多少呢?(比如:2000欧元,或经济型/舒适型/豪华型)"
61
-
62
- # 所有信息都已收集完毕
63
- return ""
64
-
65
- def _generate_acknowledgement(self, extracted_info: dict) -> str:
66
- """生成确认信息 - 简化版"""
67
- if not extracted_info:
68
- return ""
69
-
70
- ack_parts = []
71
-
72
- if "destination" in extracted_info:
73
- name = extracted_info['destination'].get('name', '目的地')
74
- ack_parts.append(f"目的地已设置为{name}")
75
-
76
- if "duration" in extracted_info:
77
- days = extracted_info['duration'].get('days', '几')
78
- ack_parts.append(f"行程天数已设置为{days}天")
79
-
80
- if "budget" in extracted_info:
81
- budget_desc = self._format_budget_info(extracted_info['budget'])
82
- ack_parts.append(f"预算已设置为{budget_desc}")
83
-
84
- if not ack_parts:
85
- return ""
86
-
87
- return "好的," + ",".join(ack_parts) + "。"
88
 
89
- def _generate_persona_enhanced_plan(self, user_message: str, session_state: dict) -> str:
90
- """生成最终旅行计划"""
91
-
92
- # 获取基本信息
93
- destination = session_state.get("destination", {})
94
- duration = session_state.get("duration", {})
95
- budget = session_state.get("budget", {})
96
- persona = session_state.get("persona", {})
97
-
98
- # 构建计划
99
- location = destination.get('name', '目的地')
100
- days = duration.get('days', '几')
101
- budget_info = self._format_budget_info(budget)
102
- persona_name = persona.get('name', '旅行者')
103
-
104
- # 如果AI可用,生成详细计划
105
- if self.ai_model.is_available():
106
- prompt = self._build_prompt(session_state)
107
- return self.ai_model.generate(user_message, prompt)
 
 
 
 
 
 
108
  else:
109
- # 备用计划
110
- return f"""✈️ 为您制定{location}{days}天旅行计划
111
-
112
- 👤 旅行风格:{persona_name}
113
- 💰 预算:{budget_info}
114
-
115
- 🎯 主要景点:{destination.get('highlights', '经典景点等您探索')}
116
-
117
- 📍 建议路线:根据您的{persona_name}风格,为您推荐最适合的行程安排。
118
-
119
- 如需详细规划,请稍后重试或告诉我具体需求!"""
120
-
121
- def _build_prompt(self, session_state: dict) -> str:
122
- """构建AI提示词"""
123
- destination = session_state.get("destination", {})
124
- duration = session_state.get("duration", {})
125
- budget = session_state.get("budget", {})
126
- persona = session_state.get("persona", {})
127
-
128
- location = destination.get('name', '目的地')
129
- days = duration.get('days', '几')
130
- budget_info = self._format_budget_info(budget)
131
-
132
- # 基础prompt
133
- prompt = f"请为{location}制定{days}天旅行计划,预算{budget_info}。"
134
-
135
- # 添加persona风格
136
- persona_key = persona.get('key')
137
- if persona_key in self.personas:
138
- persona_config = self.personas[persona_key]
139
- template = persona_config.get('prompt_template', '')
140
- if template:
141
- try:
142
- prompt = template.format(
143
- location=location,
144
- days=days,
145
- budget=budget_info,
146
- date="近期",
147
- user_tags="",
148
- commercial_preference="适中",
149
- group_description="个人",
150
- tags=""
151
- )
152
- except:
153
- # 如果模板格式化失败,使用基础prompt
154
- pass
155
-
156
- return prompt
157
-
158
- def _format_budget_info(self, budget: dict) -> str:
159
- """格式化预算信息"""
160
- if not budget:
161
- return "中等预算"
162
-
163
- if budget.get('amount') and budget.get('currency'):
164
- return f"{budget['amount']}{budget['currency']}"
165
-
166
- if budget.get('description'):
167
- return budget['description']
168
-
169
- if budget.get('type'):
170
- type_map = {
171
- 'economy': '经济型',
172
- 'comfortable': '舒适型',
173
- 'luxury': '豪华型'
174
- }
175
- return type_map.get(budget['type'], budget['type'])
176
-
177
- return "中等预算"
178
-
179
- def _build_generic_prompt(self, session_state: dict, knowledge_context: str = "") -> str:
180
- """构建通用prompt(当没有persona时使用)"""
181
- destination = session_state.get("destination", {})
182
- duration = session_state.get("duration", {})
183
- budget = session_state.get("budget", {})
184
-
185
- location = destination.get('name', '目的地')
186
- days = duration.get('days', '几')
187
- budget_info = self._format_budget_info(budget)
188
-
189
- prompt = f"""你是一个专业的旅游助手。请为用户生成一个详细的旅行计划。
190
-
191
- 【基本信息】
192
- - 目的地:{location}
193
- - 旅行天数:{days}天
194
- - 预算:{budget_info}
195
-
196
- 【要求】
197
- - 提供具体的景点推荐和路线安排
198
- - 包含交通、住宿、餐饮建议
199
- - 确保所有推荐都在预算范围内
200
- - 提供实用的旅行贴士"""
201
-
202
- if knowledge_context:
203
- prompt += f"\n\n【背景信息】\n{knowledge_context}"
204
-
205
- prompt += "\n\n请生成一份实用、详细的旅行计划。"
206
-
207
- return prompt
208
-
209
- def _build_search_query(self, session_state: dict) -> str:
210
- """构建知识库搜索查询"""
211
- destination = session_state.get("destination", {})
212
- persona = session_state.get("persona", {})
213
- budget = session_state.get("budget", {})
214
-
215
- query_parts = []
216
-
217
- # 添加目的地
218
- if destination.get('name'):
219
- query_parts.append(destination['name'])
220
-
221
- # 添加persona类型
222
- if persona.get('key'):
223
- query_parts.append(persona['key'])
224
-
225
- # 添加预算类型
226
- if budget.get('type'):
227
- query_parts.append(budget['type'])
228
-
229
- return " ".join(query_parts) if query_parts else "欧洲旅行"
230
-
231
- def _generate_fallback_plan(self, session_state: dict, knowledge_context: str = "") -> str:
232
- """AI不可用时的备用计划生成(现在包含预算和persona信息)"""
233
- destination = session_state.get("destination", {})
234
- duration = session_state.get("duration", {})
235
- budget = session_state.get("budget", {})
236
- persona = session_state.get("persona", {})
237
-
238
- location = destination.get('name', '目的地')
239
- days = duration.get('days', '几')
240
- budget_info = self._format_budget_info(budget)
241
- persona_name = persona.get('name', '旅行者')
242
-
243
- plan = f"为您推荐 {location} {days}天旅行计划:\n\n"
244
-
245
- # 用户信息概述
246
- plan += f"👤 旅行者类型:{persona_name}\n"
247
- plan += f"💰 预算范围:{budget_info}\n\n"
248
-
249
- # 优先使用知识库信息
250
- if knowledge_context and "相关旅游知识" in knowledge_context:
251
- plan += f"📚 {knowledge_context}\n\n"
252
-
253
- # 使用cities.json中的基础信息
254
- highlights = destination.get('highlights', '精彩景点等待您的探索')
255
- plan += f"🎯 主要景点:{highlights}\n\n"
256
-
257
- best_season = destination.get('best_season', '全年适宜')
258
- plan += f"🌤️ 最佳旅行时间:{best_season}\n\n"
259
-
260
- avg_budget = destination.get('avg_daily_budget', 100)
261
- plan += f"💰 日均预算参考:约{avg_budget}欧元/天\n\n"
262
-
263
- # 根据persona调整建议风格
264
- if persona.get('key') == 'planner':
265
- plan += "📋 建议制定详细的每日行程表,包含具体时间安排和备选方案。\n\n"
266
- elif persona.get('key') == 'social':
267
- plan += "📸 推荐寻找热门打卡点,准备好相机记录美好时光!\n\n"
268
- elif persona.get('key') == 'experiential':
269
- plan += "🎨 建议深入当地社区,寻找地道的文化体验和小众景点。\n\n"
270
-
271
- # 预算相关建议
272
- if budget.get('type') == 'economy':
273
- plan += "💡 经济型贴士:考虑使用公共交通,选择青旅或民宿,寻找当地平价美食。\n\n"
274
- elif budget.get('type') == 'luxury':
275
- plan += "✨ 豪华体验:可以考虑五星酒店、私人导览和米其林餐厅。\n\n"
276
-
277
- plan += "如需更详细的个性化规划,请告诉我您的具体需求!"
278
- return plan
279
-
280
- def _format_knowledge_context(self, knowledge_items: list) -> str:
281
- """格式化知识库内容作为上下文"""
282
- if not knowledge_items:
283
- return "基于一般旅游建议"
284
-
285
- try:
286
- # 只使用最相关的一条知识
287
- item = knowledge_items[0]
288
- knowledge = item.get('knowledge', {})
289
- travel_knowledge = knowledge.get('travel_knowledge', {})
290
-
291
- context_parts = []
292
-
293
- # 1. 获取目的地信息
294
- destination_info = travel_knowledge.get('destination_info', {})
295
- if destination_info:
296
- primary_destinations = destination_info.get('primary_destinations', [])
297
- if primary_destinations:
298
- context_parts.append(f"目的地: {', '.join(primary_destinations)}")
299
-
300
- recommended_duration = destination_info.get('recommended_duration')
301
- if recommended_duration:
302
- context_parts.append(f"推荐天数: {recommended_duration}天")
303
-
304
- # 2. 获取预算信息
305
- budget_analysis = travel_knowledge.get('budget_analysis', {})
306
- if budget_analysis:
307
- total_budget = budget_analysis.get('total_budget_range')
308
- if total_budget:
309
- context_parts.append(f"预算参考: {total_budget}")
310
-
311
- daily_average = budget_analysis.get('daily_average')
312
- if daily_average:
313
- context_parts.append(f"日均预算: {daily_average}")
314
-
315
- # 3. 获取专业见解
316
- professional_insights = travel_knowledge.get('professional_insights', {})
317
- if professional_insights:
318
- common_mistakes = professional_insights.get('common_mistakes', [])
319
- if common_mistakes:
320
- context_parts.append(f"避免误区: {', '.join(common_mistakes[:2])}")
321
-
322
- insider_secrets = professional_insights.get('insider_secrets', [])
323
- if insider_secrets:
324
- context_parts.append(f"内行贴士: {', '.join(insider_secrets[:2])}")
325
-
326
- seasonal_info = professional_insights.get('seasonal_considerations', {})
327
- if seasonal_info:
328
- best_months = seasonal_info.get('best_months', [])
329
- if best_months:
330
- context_parts.append(f"最佳时间: {', '.join(best_months)}")
331
-
332
- # 4. 拼接结果
333
- if context_parts:
334
- return "相关旅游知识:\n- " + "\n- ".join(context_parts)
335
  else:
336
- return "基于专业旅游建议"
337
-
338
- except Exception as e:
339
- log.error(f"❌ 格式化知识库上下文失败: {e}", exc_info=True)
340
- return "基于一般旅游建议"
 
1
  import json
 
2
  from .ai_model import AIModel
3
+ from .session_manager import SessionState # 假设 session_state 是一个类
4
  from utils.logger import log
5
+ import re
6
 
7
  class ResponseGenerator:
8
+ def __init__(self, ai_model: AIModel):
9
  self.ai_model = ai_model
 
10
  self.personas = self._load_personas()
11
 
12
+ def _load_personas(self) -> dict:
13
+
14
+ with open("./config/personas.json", 'r', encoding='utf-8') as f:
 
15
  data = json.load(f)
16
+ log.info(f"✅ 成功加载 {len(data.get('personas', {}))} 个 persona。")
17
  return data.get('personas', {})
18
+
19
+ def generate(self, session_state: SessionState) -> str:
20
+ """
21
+ 根据当前会话状态,生成一个完全符合选定 Persona 风格的智能回复。
22
+ """
23
  try:
24
+ persona_key = session_state.persona or "planner" # 如果未选择,则默认为 planner
25
+ persona_config = self.personas.get(persona_key, self.personas.get("planner"))
 
 
 
 
26
 
27
+ # 1. 检查信息完整性,并使用 persona 的风格来提问
28
+ next_question = self._get_next_question(session_state, persona_config)
29
  if next_question:
30
+ # 如果是第一个问题(目的地未知),直接返回问题
31
+ if not session_state.destination:
32
+ return next_question
33
+ # 否则,先确认已知信息,再提问
34
+ else:
35
+ ack = self._generate_acknowledgement(session_state, persona_config)
36
+ return f"{ack} {next_question}"
37
+
38
+ # 2. 如果所有信息都齐全,则生成最终的旅行计划
39
+ log.info(f"✅ 所有信息已集齐,开始为 persona '{persona_key}' 生成最终计划。")
40
+ plan = self._generate_persona_enhanced_plan(session_state, persona_config)
41
+ return plan
42
 
 
 
 
 
 
 
 
 
43
  except Exception as e:
44
+ log.error(f"❌ 在响应生成过程中发生未知错误: {e}", exc_info=True)
45
+ return "抱歉,我的思绪好像出了一点小问题,我们能重新开始吗?"
46
+
47
+ def _get_next_question(self, session_state: SessionState, persona_config: dict) -> str:
48
+ """根据缺失信息,生成符合 persona 语气的下一个问题。"""
49
+
50
+ persona_tone_style = f"语气({persona_config.get('tone', ['友好'])[0]})"
51
+
52
+ if not session_state.destination:
53
+ # 根据不同 persona 定制提问方式
54
+ if persona_config.get('name', '').startswith("高效"):
55
+ return "您好,为了开始规划,请首先提供您的目的地城市。"
56
+ elif persona_config.get('name', '').startswith("社交"):
57
+ return "哈喽!准备好开启一场超棒的旅行了吗?快告诉我你们想去哪里呀?目的地是哪里呀?🌍"
58
+ else: # 默认或体验型
59
+ return "您好!很高兴能为您服务。请问这次您渴望探索哪个目的地呢?"
60
+
61
+ if not session_state.duration:
62
+ dest_name = session_state.destination
63
+ if persona_config.get('name', '').startswith("高效"):
64
+ return f"目的地已确认为 {dest_name}。请提供计划的旅行总天数。"
65
+ elif persona_config.get('name', '').startswith("社交"):
66
+ return f"哇!{dest_name}!听起来就超赞的!你们打算一起玩几天呀?🥳"
67
+ else:
68
+ return f"{dest_name},一个充满故事的地方。您希望在这片土地上沉浸几天呢?"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
+ if not session_state.budget:
71
+ if persona_config.get('name', '').startswith("高效"):
72
+ return "为了进行精确的预算分配,请明确您的预算等级(例如:经济型、舒适型、豪华型)或大致的总金额。"
73
+ elif persona_config.get('name', '').startswith("社交"):
74
+ return "最后一个问题!这次出去玩的预算大概是多少呀?是小资轻奢还是尽情嗨皮呢?💰"
75
+ else:
76
+ return "为了让体验更贴合您的期望,可以分享一下这次旅行的预算范围吗?"
77
+
78
+ return "" # 所有信息都已收集完毕
79
+
80
+ def _generate_acknowledgement(self, session_state: SessionState, persona_config: dict) -> str:
81
+ """对刚刚更新的信息,生成一个符合 persona 语气的确认。"""
82
+ # 这个函数可以在 session_state 更新后被调用,以提供即时反馈。
83
+ # 在当前简化流程中,它被整合到提问环节。
84
+ dest = session_state.destination
85
+ days = session_state.duration
86
+
87
+ if persona_config.get('name', '').startswith("高效"):
88
+ if days:
89
+ return f"已记录:行程共计 {days} 天。"
90
+ return f"收到。目的地:{dest}。"
91
+ elif persona_config.get('name', '').startswith("社交"):
92
+ if days:
93
+ return f"收到啦!{days} 天的行程,一定能玩得超开心!"
94
+ return f"好嘞!目的地 {dest} 已锁定!"
95
  else:
96
+ if days:
97
+ return f"明白了,一段为期 {days} 天的旅程。"
98
+ return f"好的,我们将围绕 {dest} 展开探索。"
99
+
100
+ def _generate_persona_enhanced_plan(self, session_state: SessionState, persona_config: dict) -> str:
101
+ """调用AI模型,使用 persona 模板生成最终的旅行计划。"""
102
+ prompt = self._build_prompt(session_state, persona_config)
103
+
104
+ log.info(f"🧠 正在使用 persona '{persona_config.get('name')}' 的模板生成计划...")
105
+ # log.debug(f"最终生成的Prompt:\n---\n{prompt}\n---") # 如果需要调试,可以取消此行注释
106
+
107
+ # 实际调用AI模型
108
+ # plan = self.ai_model.generate(prompt)
109
+ # return plan
110
+
111
+ # --- 当前用于演示的临时返回 ---
112
+ # 它会清晰地展示我们是如何根据persona构建prompt的
113
+ return f" **计划生成预览** ✨\n\n我将使用下面的指令为您生成计划:\n\n```prompt\n{prompt}\n```"
114
+
115
+
116
+ def _build_prompt(self, session_state: SessionState, persona_config: dict) -> str:
117
+ """
118
+ 【核心升级】
119
+ 一个更健壮、更灵活的 prompt 构建方法。
120
+ 它会安全地填充模板,即使 session_state 中的信息不完整。
121
+ """
122
+ template = persona_config.get('prompt_template')
123
+ if not template:
124
+ log.warning(f"Persona '{persona_config.get('name')}' 缺少 'prompt_template',将使用通用模板。")
125
+ # 此处可以返回一个通用的后备 prompt
126
+ return f"请为我规划一个去 {session_state.destination} 的 {session_state.duration} 天行程。"
127
+
128
+ # 1. 从 session_state 收集所有可能用到的信息
129
+ context = {
130
+ "location": session_state.destination,
131
+ "days": session_state.duration,
132
+ "budget": session_state.budget,
133
+ "date": session_state.get_specific_info('date', '近期'), # 假设 session_state 有此方法
134
+ "user_tags": ", ".join(session_state.get_specific_info('tags', [])),
135
+ "commercial_preference": session_state.get_specific_info('commercial', '适中'),
136
+ "group_description": session_state.get_specific_info('group', '个人'),
137
+ "tags": ", ".join(session_state.get_specific_info('tags', []))
138
+ }
139
+
140
+ # 2. 使用正则表达式查找模板中所有的 {placeholder}
141
+ placeholders = re.findall(r'\{(\w+)\}', template)
142
+
143
+ # 3. 用 context 中的值替换模板中的占位符
144
+ # 这种方法比 .format(**context) 更安全,因为它只替换存在的占位符,
145
+ # 并且如果 context 中缺少某个键,它不会抛出 KeyError。
146
+ for key in placeholders:
147
+ value = context.get(key)
148
+ if value is not None:
149
+ template = template.replace(f'{{{key}}}', str(value))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  else:
151
+ # 如果 context 中没有这个值,可以选择留空或使用默认值
152
+ template = template.replace(f'{{{key}}}', '(未提供)')
153
+
154
+ return template