Spaces:
Sleeping
Sleeping
幻觉修复 温度调整
Browse files- modules/ai_model.py +2 -2
- modules/info_extractor.py +53 -55
- modules/response_generator.py +81 -139
modules/ai_model.py
CHANGED
|
@@ -140,7 +140,7 @@ class AIModel:
|
|
| 140 |
else: # text
|
| 141 |
return input_type, None, raw_input
|
| 142 |
|
| 143 |
-
def run_inference(self, input_type: str, formatted_input: Union[str, Image.Image], prompt: str,temperature: float = 0.
|
| 144 |
|
| 145 |
try:
|
| 146 |
# 截断过长的 prompt
|
|
@@ -189,7 +189,7 @@ class AIModel:
|
|
| 189 |
)
|
| 190 |
generated_tokens = outputs[0][input_len:]
|
| 191 |
decoded = self.processor.tokenizer.decode(generated_tokens, skip_special_tokens=True).strip()
|
| 192 |
-
|
| 193 |
return decoded if decoded else "我理解了您的问题,请告诉我更多具体信息。"
|
| 194 |
|
| 195 |
except RuntimeError as e:
|
|
|
|
| 140 |
else: # text
|
| 141 |
return input_type, None, raw_input
|
| 142 |
|
| 143 |
+
def run_inference(self, input_type: str, formatted_input: Union[str, Image.Image], prompt: str,temperature: float = 0.5) -> str:
|
| 144 |
|
| 145 |
try:
|
| 146 |
# 截断过长的 prompt
|
|
|
|
| 189 |
)
|
| 190 |
generated_tokens = outputs[0][input_len:]
|
| 191 |
decoded = self.processor.tokenizer.decode(generated_tokens, skip_special_tokens=True).strip()
|
| 192 |
+
|
| 193 |
return decoded if decoded else "我理解了您的问题,请告诉我更多具体信息。"
|
| 194 |
|
| 195 |
except RuntimeError as e:
|
modules/info_extractor.py
CHANGED
|
@@ -4,9 +4,59 @@ from utils.logger import log
|
|
| 4 |
from .ai_model import AIModel
|
| 5 |
|
| 6 |
class InfoExtractor:
|
| 7 |
-
def __init__(self, ai_model):
|
| 8 |
self.ai_model = ai_model
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
def _build_prompt_template(self) -> str:
|
| 12 |
# --- 重点更新:使用更严格的指令和结构化示例 ---
|
|
@@ -62,56 +112,4 @@ class InfoExtractor:
|
|
| 62 |
**你的输出 (必须是纯JSON):**
|
| 63 |
"""
|
| 64 |
|
| 65 |
-
|
| 66 |
-
log.info(f"🧠 使用LLM开始提取信息,消息: '{message}'")
|
| 67 |
-
prompt = self.prompt_template.format(user_message=message)
|
| 68 |
-
raw_response = self.ai_model.generate(prompt)
|
| 69 |
-
|
| 70 |
-
if not raw_response:
|
| 71 |
-
log.error("❌ LLM模型没有返回任何内容。")
|
| 72 |
-
return {}
|
| 73 |
-
|
| 74 |
-
json_str = ""
|
| 75 |
-
try:
|
| 76 |
-
match = re.search(r'```json\s*(\{.*?\})\s*```', raw_response, re.DOTALL)
|
| 77 |
-
if match:
|
| 78 |
-
json_str = match.group(1)
|
| 79 |
-
else:
|
| 80 |
-
start_index = raw_response.find('{')
|
| 81 |
-
end_index = raw_response.rfind('}')
|
| 82 |
-
if start_index != -1 and end_index != -1 and end_index > start_index:
|
| 83 |
-
json_str = raw_response[start_index:end_index + 1]
|
| 84 |
-
else:
|
| 85 |
-
raise json.JSONDecodeError("在LLM的返回中未找到有效的JSON对象。", raw_response, 0)
|
| 86 |
-
|
| 87 |
-
extracted_data = json.loads(json_str)
|
| 88 |
-
log.info(f"✅ LLM成功提取并解析JSON: {extracted_data}")
|
| 89 |
-
except json.JSONDecodeError as e:
|
| 90 |
-
log.error(f"❌ 无法解析LLM返回的JSON: '{raw_response}'. 错误: {e}")
|
| 91 |
-
return {}
|
| 92 |
-
|
| 93 |
-
# --- 重点更新:使用更健壮、更安全的逻辑来清理数据 ---
|
| 94 |
-
final_info = {}
|
| 95 |
-
|
| 96 |
-
# 安全地处理 'destination'
|
| 97 |
-
destination_data = extracted_data.get("destination")
|
| 98 |
-
if isinstance(destination_data, dict) and destination_data.get("name"):
|
| 99 |
-
final_info["destination"] = {"name": destination_data["name"]}
|
| 100 |
-
|
| 101 |
-
# 安全地处理 'duration'
|
| 102 |
-
duration_data = extracted_data.get("duration")
|
| 103 |
-
if isinstance(duration_data, dict) and duration_data.get("days"):
|
| 104 |
-
try:
|
| 105 |
-
final_info["duration"] = {"days": int(duration_data["days"])}
|
| 106 |
-
except (ValueError, TypeError):
|
| 107 |
-
log.warning(f"⚠️ 无法将duration days '{duration_data.get('days')}' 转换为整数。")
|
| 108 |
-
|
| 109 |
-
# 安全地处理 'budget'
|
| 110 |
-
budget_data = extracted_data.get("budget")
|
| 111 |
-
if isinstance(budget_data, dict):
|
| 112 |
-
# 只要budget对象里有任何非null的值,就把它加进来
|
| 113 |
-
if any(v is not None for v in budget_data.values()):
|
| 114 |
-
final_info["budget"] = budget_data
|
| 115 |
-
|
| 116 |
-
log.info(f"📊 LLM最终提取结果 (安全处理后): {list(final_info.keys())}")
|
| 117 |
-
return final_info
|
|
|
|
| 4 |
from .ai_model import AIModel
|
| 5 |
|
| 6 |
class InfoExtractor:
|
| 7 |
+
def __init__(self, ai_model: AIModel):
|
| 8 |
self.ai_model = ai_model
|
| 9 |
+
# 预定义的提取结构,用于验证和规范化
|
| 10 |
+
self.extraction_schema = {
|
| 11 |
+
"destination": {"type": dict, "fields": {"name": str, "country": str}},
|
| 12 |
+
"duration": {"type": dict, "fields": {"days": int, "description": str}},
|
| 13 |
+
"budget": {"type": dict, "fields": {"type": str, "amount": int, "currency": str, "description": str}}
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
def extract(self, user_message: str) -> dict:
|
| 17 |
+
"""从用户消息中提取结构化信息,确保使用确定性解码。"""
|
| 18 |
+
prompt = self._build_extraction_prompt(user_message)
|
| 19 |
+
|
| 20 |
+
# --- 核心修复:强制使用确定性解码以杜绝幻觉 ---
|
| 21 |
+
# 确保调用AI模型时,使用类似 do_sample=False 或 temperature=0 的参数
|
| 22 |
+
# 这里我们模拟这个调用,并强调其重要性
|
| 23 |
+
log.info("🧠 使用LLM开始提取信息 (模式: 确定性)")
|
| 24 |
+
raw_response = self.ai_model.generate(
|
| 25 |
+
prompt,
|
| 26 |
+
do_sample=False, # 关键:关闭采样,获取最可能的结果
|
| 27 |
+
temperature=0.0 # 关键:将温度设为0,减少随机性
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
extracted_json = json.loads(raw_response)
|
| 32 |
+
log.info(f"✅ LLM成功提取并解析JSON: {extracted_json}")
|
| 33 |
+
# 使用新的验证方法
|
| 34 |
+
validated_data = self._validate_and_normalize(extracted_json)
|
| 35 |
+
log.info(f"📊 LLM最终提取结果 (安全处理后): {validated_data}")
|
| 36 |
+
return validated_data
|
| 37 |
+
except (json.JSONDecodeError, TypeError) as e:
|
| 38 |
+
log.error(f"❌ 解析或验证LLM提取的JSON失败: {e}", exc_info=True)
|
| 39 |
+
return {} # 返回一个空字典而不是列表
|
| 40 |
+
|
| 41 |
+
def _validate_and_normalize(self, data: dict) -> dict:
|
| 42 |
+
"""
|
| 43 |
+
根据预定义schema验证并规范化提取的数据。
|
| 44 |
+
这取代了之前返回列表的逻辑,只返回符合结构的键值对。
|
| 45 |
+
"""
|
| 46 |
+
if not isinstance(data, dict):
|
| 47 |
+
return {}
|
| 48 |
+
|
| 49 |
+
validated_output = {}
|
| 50 |
+
for key, schema in self.extraction_schema.items():
|
| 51 |
+
if key in data and isinstance(data[key], schema["type"]):
|
| 52 |
+
# 这里可以添加更深层次的字段类型验证
|
| 53 |
+
# 为简化,我们暂时只验证第一层
|
| 54 |
+
validated_output[key] = data[key]
|
| 55 |
+
|
| 56 |
+
if not validated_output:
|
| 57 |
+
log.warning(f"⚠️ 提取的数据 {data} 未通过验证,未发现任何有效字段。")
|
| 58 |
+
|
| 59 |
+
return validated_output
|
| 60 |
|
| 61 |
def _build_prompt_template(self) -> str:
|
| 62 |
# --- 重点更新:使用更严格的指令和结构化示例 ---
|
|
|
|
| 112 |
**你的输出 (必须是纯JSON):**
|
| 113 |
"""
|
| 114 |
|
| 115 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/response_generator.py
CHANGED
|
@@ -10,51 +10,36 @@ class ResponseGenerator:
|
|
| 10 |
self.personas = self._load_personas()
|
| 11 |
|
| 12 |
def _load_personas(self):
|
| 13 |
-
|
| 14 |
personas_path = "./config/personas.json"
|
| 15 |
-
|
| 16 |
with open(personas_path, 'r', encoding='utf-8') as f:
|
| 17 |
data = json.load(f)
|
| 18 |
log.info(f"✅ 成功加载 {len(data.get('personas', {}))} 个persona配置。")
|
| 19 |
return data.get('personas', {})
|
| 20 |
|
| 21 |
def _get_current_persona_config(self, session_state: dict) -> dict:
|
| 22 |
-
"""
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
return {
|
| 28 |
-
"name": "旅行助手",
|
| 29 |
-
"tone": ["专业", "友好"],
|
| 30 |
-
"style": "中立"
|
| 31 |
-
}
|
| 32 |
|
| 33 |
def generate(self, user_message: str, session_state: dict, extracted_info: dict) -> str:
|
| 34 |
-
"""生成智能响应,整个过程融入Persona风格"""
|
| 35 |
try:
|
| 36 |
response_parts = []
|
| 37 |
-
|
| 38 |
-
# 1. 生成带有人格风格的确认反馈
|
| 39 |
acknowledgement = self._generate_acknowledgement(extracted_info, session_state)
|
| 40 |
if acknowledgement:
|
| 41 |
response_parts.append(acknowledgement)
|
| 42 |
|
| 43 |
-
# 2. 检查信息完整性,用人格化的方式询问缺失信息
|
| 44 |
next_question = self._get_next_question(session_state)
|
| 45 |
-
|
| 46 |
-
# 只有在有下一步问题时才加入回复列表
|
| 47 |
if next_question:
|
| 48 |
-
# 如果已有确认信息,为了避免语气生硬,加个连接词
|
| 49 |
if response_parts:
|
| 50 |
-
|
| 51 |
else:
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
# 3. 如果所有信息都齐全,并且没有追问,则生成最终计划
|
| 55 |
if not next_question:
|
| 56 |
plan = self._generate_persona_enhanced_plan(user_message, session_state)
|
| 57 |
-
# 如果已有确认信息,用换行符分隔,使计划更突出
|
| 58 |
if response_parts:
|
| 59 |
response_parts.append("\n\n" + plan)
|
| 60 |
else:
|
|
@@ -67,82 +52,72 @@ class ResponseGenerator:
|
|
| 67 |
return "抱歉,我在处理您的请求时遇到了问题,请稍后再试。"
|
| 68 |
|
| 69 |
def _get_next_question(self, session_state: dict) -> str:
|
| 70 |
-
"""根据Persona
|
| 71 |
persona_config = self._get_current_persona_config(session_state)
|
| 72 |
-
persona_style = persona_config.get("style", "")
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
#
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
| 85 |
return "您好!为了高效地开始规划,请首先明确您的目的地城市。"
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
if "
|
| 90 |
-
return f"{destination_name}超棒的!打算和小伙伴们在那玩几天呀?"
|
| 91 |
-
if "体验" in persona_style:
|
| 92 |
-
return f"感知到了,{destination_name}。你希望在这片土地上沉浸多少个日夜?"
|
| 93 |
return f"目的地已锁定:{destination_name}。请提供计划的旅行天数。"
|
| 94 |
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
if "
|
| 98 |
-
return f"太棒啦,{days}天的行程!这次出去玩,预算大概是多少呀?是经济实惠,还是想来个轻奢体验呢?"
|
| 99 |
-
if "体验" in persona_style:
|
| 100 |
-
return f"{days}天的探索之旅,听起来很不错。对于这次旅行的开销,你有什么样的构想?"
|
| 101 |
return f"已记录:行程共{days}天。请明确您的预算范围(例如:经济型、舒适型,或具体金额)。"
|
| 102 |
|
| 103 |
return "" # 所有信息已收集完毕
|
| 104 |
|
| 105 |
def _generate_acknowledgement(self, extracted_info: dict, session_state: dict) -> str:
|
| 106 |
-
|
| 107 |
-
if not extracted_info:
|
| 108 |
-
return ""
|
| 109 |
-
|
| 110 |
persona_config = self._get_current_persona_config(session_state)
|
| 111 |
persona_style = persona_config.get("style", "")
|
| 112 |
-
|
| 113 |
ack_parts = []
|
| 114 |
if "destination" in extracted_info:
|
| 115 |
name = extracted_info['destination'].get('name', '目的地')
|
| 116 |
if "社交" in persona_style: ack_parts.append(f"目的地锁定{name}!已经开始期待啦!💖")
|
| 117 |
elif "体验" in persona_style: ack_parts.append(f"我感知到了,{name},一个充满故事的地方")
|
| 118 |
else: ack_parts.append(f"确认:目的地已记录为{name}")
|
| 119 |
-
|
| 120 |
if "duration" in extracted_info:
|
| 121 |
days = extracted_info['duration'].get('days', '几')
|
| 122 |
if "社交" in persona_style: ack_parts.append(f"玩{days}天,时间超充裕的")
|
| 123 |
elif "体验" in persona_style: ack_parts.append(f"{days}个日夜,足够深入探索了")
|
| 124 |
else: ack_parts.append(f"行程时长已设定为{days}天")
|
| 125 |
-
|
| 126 |
if "budget" in extracted_info:
|
| 127 |
-
budget_desc = self._format_budget_info(extracted_info
|
| 128 |
if "社交" in persona_style: ack_parts.append(f"{budget_desc}的预算,妥妥的")
|
| 129 |
elif "体验" in persona_style: ack_parts.append(f"了解,{budget_desc}的投入,追求的是价值而非价格")
|
| 130 |
else: ack_parts.append(f"预算已明确为{budget_desc}")
|
| 131 |
-
|
| 132 |
return ",".join(ack_parts) + "。" if ack_parts else ""
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
def _generate_persona_enhanced_plan(self, user_message: str, session_state: dict) -> str:
|
| 137 |
persona_config = self._get_current_persona_config(session_state)
|
| 138 |
-
destination = session_state.get("destination", {})
|
| 139 |
-
duration = session_state.get("duration", {})
|
| 140 |
-
budget = session_state.get("budget", {})
|
| 141 |
-
|
| 142 |
-
location = destination.get('name', '目的地')
|
| 143 |
-
days = duration.get('days', '几')
|
| 144 |
-
budget_info = self._format_budget_info(budget)
|
| 145 |
-
|
| 146 |
if self.ai_model.is_available():
|
| 147 |
prompt = self._build_prompt(session_state, persona_config)
|
| 148 |
log.info(f"🚀 使用Persona '{persona_config.get('name')}' 构建的Prompt进行生成。")
|
|
@@ -152,83 +127,50 @@ class ResponseGenerator:
|
|
| 152 |
return self._generate_fallback_plan(session_state)
|
| 153 |
|
| 154 |
def _build_prompt(self, session_state: dict, persona_config: dict) -> str:
|
| 155 |
-
destination = session_state.get("destination", {})
|
| 156 |
-
duration = session_state.get("duration", {})
|
| 157 |
-
budget = session_state.get("budget", {})
|
| 158 |
-
|
| 159 |
-
location = destination.get('name', '目的地')
|
| 160 |
-
days = duration.get('days', '几')
|
| 161 |
-
budget_info = self._format_budget_info(budget)
|
| 162 |
-
|
| 163 |
template = persona_config.get('prompt_template')
|
| 164 |
-
if template:
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
if
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
budget = session_state.get("budget", {})
|
| 197 |
-
location = destination.get('name', '目的地')
|
| 198 |
-
days = duration.get('days', '几')
|
| 199 |
-
budget_info = self._format_budget_info(budget)
|
| 200 |
-
prompt = f"""你是一个专业的旅游助手。请为用户生成一个详细的旅行计划。
|
| 201 |
-
【基本信息】
|
| 202 |
-
- 目的地:{location}
|
| 203 |
-
- 旅行天数:{days}天
|
| 204 |
-
- 预算:{budget_info}
|
| 205 |
-
【要求】
|
| 206 |
-
- 提供具体的景点推荐和路线安排
|
| 207 |
-
- 包含交通、住宿、餐饮建议
|
| 208 |
-
- 确保所有推荐都在预算范围内
|
| 209 |
-
- 提供实用的旅行贴士"""
|
| 210 |
-
if knowledge_context:
|
| 211 |
-
prompt += f"\n\n【背景信息】\n{knowledge_context}"
|
| 212 |
-
prompt += "\n\n请生成一份实用、详细的旅行计划。"
|
| 213 |
-
return prompt
|
| 214 |
-
|
| 215 |
-
def _generate_fallback_plan(self, session_state: dict, knowledge_context: str = "") -> str:
|
| 216 |
-
# (此方法保持您原有的实现)
|
| 217 |
-
destination = session_state.get("destination", {})
|
| 218 |
-
duration = session_state.get("duration", {})
|
| 219 |
-
budget = session_state.get("budget", {})
|
| 220 |
persona_config = self._get_current_persona_config(session_state)
|
| 221 |
-
location =
|
| 222 |
-
days =
|
| 223 |
-
budget_info = self._format_budget_info(budget)
|
| 224 |
persona_name = persona_config.get('name', '旅行者')
|
| 225 |
plan = f"为您推荐 {location} {days}天旅行计划:\n\n"
|
| 226 |
plan += f"👤 旅行者类型:{persona_name}\n"
|
| 227 |
plan += f"💰 预算范围:{budget_info}\n\n"
|
| 228 |
-
|
| 229 |
-
highlights = destination.get('highlights', '精彩景点等待您的探索')
|
| 230 |
plan += f"🎯 主要景点:{highlights}\n\n"
|
| 231 |
-
persona_key =
|
| 232 |
if persona_key == 'planner': plan += "📋 建议制定详细的每日行程表。\n"
|
| 233 |
elif persona_key == 'social': plan += "📸 推荐寻找热门打卡点!\n"
|
| 234 |
elif persona_key == 'experiential': plan += "🎨 建议深入当地社区,寻找地道体验。\n"
|
|
|
|
| 10 |
self.personas = self._load_personas()
|
| 11 |
|
| 12 |
def _load_personas(self):
|
|
|
|
| 13 |
personas_path = "./config/personas.json"
|
| 14 |
+
|
| 15 |
with open(personas_path, 'r', encoding='utf-8') as f:
|
| 16 |
data = json.load(f)
|
| 17 |
log.info(f"✅ 成功加载 {len(data.get('personas', {}))} 个persona配置。")
|
| 18 |
return data.get('personas', {})
|
| 19 |
|
| 20 |
def _get_current_persona_config(self, session_state: dict) -> dict:
|
| 21 |
+
persona_key = session_state.get("persona", {}).get("key") if isinstance(session_state.get("persona"), dict) else None
|
| 22 |
+
return self.personas.get(persona_key, {
|
| 23 |
+
"name": "旅行助手", "style": "中立",
|
| 24 |
+
"tone": ["专业", "友好"], "prompt_template": self._build_generic_prompt(session_state)
|
| 25 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
def generate(self, user_message: str, session_state: dict, extracted_info: dict) -> str:
|
|
|
|
| 28 |
try:
|
| 29 |
response_parts = []
|
|
|
|
|
|
|
| 30 |
acknowledgement = self._generate_acknowledgement(extracted_info, session_state)
|
| 31 |
if acknowledgement:
|
| 32 |
response_parts.append(acknowledgement)
|
| 33 |
|
|
|
|
| 34 |
next_question = self._get_next_question(session_state)
|
|
|
|
|
|
|
| 35 |
if next_question:
|
|
|
|
| 36 |
if response_parts:
|
| 37 |
+
response_parts.append("那么," + next_question[0].lower() + next_question[1:])
|
| 38 |
else:
|
| 39 |
+
response_parts.append(next_question)
|
| 40 |
+
|
|
|
|
| 41 |
if not next_question:
|
| 42 |
plan = self._generate_persona_enhanced_plan(user_message, session_state)
|
|
|
|
| 43 |
if response_parts:
|
| 44 |
response_parts.append("\n\n" + plan)
|
| 45 |
else:
|
|
|
|
| 52 |
return "抱歉,我在处理您的请求时遇到了问题,请稍后再试。"
|
| 53 |
|
| 54 |
def _get_next_question(self, session_state: dict) -> str:
|
| 55 |
+
"""根据Persona风格,健壮地获取下一个问题,绝不崩溃。"""
|
| 56 |
persona_config = self._get_current_persona_config(session_state)
|
| 57 |
+
persona_style = persona_config.get("style", "中立")
|
| 58 |
+
|
| 59 |
+
# --- 核心修复:借鉴session_manager的健壮性检查逻辑 ---
|
| 60 |
+
destination_info = session_state.get("destination") # 可能为 None
|
| 61 |
+
duration_info = session_state.get("duration") # 可能为 None
|
| 62 |
+
budget_info = session_state.get("budget") # 可能为 None
|
| 63 |
+
|
| 64 |
+
# 只有当info是字典时才尝试获取深层数据,否则使用安全默认值
|
| 65 |
+
destination_name = destination_info.get('name', '那里') if isinstance(destination_info, dict) else '那里'
|
| 66 |
+
days = duration_info.get('days', '几') if isinstance(duration_info, dict) else '几'
|
| 67 |
+
|
| 68 |
+
# 依次检查核心信息是否缺失
|
| 69 |
+
if not destination_info:
|
| 70 |
+
if "社交" in persona_style: return "哈喽!准备好去哪里嗨皮了吗?告诉我想去哪个城市,我们来一场刷爆朋友圈的旅行吧!✨"
|
| 71 |
+
if "体验" in persona_style: return "你好,旅行者。为了开启一段独特的深度体验,你心中的目的地是哪里?"
|
| 72 |
return "您好!为了高效地开始规划,请首先明确您的目的地城市。"
|
| 73 |
|
| 74 |
+
if not duration_info:
|
| 75 |
+
if "社交" in persona_style: return f"{destination_name}超棒的!打算和小伙伴们在那玩几天呀?"
|
| 76 |
+
if "体验" in persona_style: return f"感知到了,{destination_name}。你希望在这片土地上沉浸多少个日夜?"
|
|
|
|
|
|
|
|
|
|
| 77 |
return f"目的地已锁定:{destination_name}。请提供计划的旅行天数。"
|
| 78 |
|
| 79 |
+
if not budget_info:
|
| 80 |
+
if "社交" in persona_style: return f"太棒啦,{days}天的行程!这次出去玩,预算大概是多少呀?是经济实惠,还是想来个轻奢体验呢?"
|
| 81 |
+
if "体验" in persona_style: return f"{days}天的探索之旅,听起来很不错。对于这次旅行的开销,你有什么样的构想?"
|
|
|
|
|
|
|
|
|
|
| 82 |
return f"已记录:行程共{days}天。请明确您的预算范围(例如:经济型、舒适型,或具体金额)。"
|
| 83 |
|
| 84 |
return "" # 所有信息已收集完毕
|
| 85 |
|
| 86 |
def _generate_acknowledgement(self, extracted_info: dict, session_state: dict) -> str:
|
| 87 |
+
# ... (此部分及以下方法与上一版健壮版本相同,无需修改) ...
|
| 88 |
+
if not extracted_info: return ""
|
|
|
|
|
|
|
| 89 |
persona_config = self._get_current_persona_config(session_state)
|
| 90 |
persona_style = persona_config.get("style", "")
|
|
|
|
| 91 |
ack_parts = []
|
| 92 |
if "destination" in extracted_info:
|
| 93 |
name = extracted_info['destination'].get('name', '目的地')
|
| 94 |
if "社交" in persona_style: ack_parts.append(f"目的地锁定{name}!已经开始期待啦!💖")
|
| 95 |
elif "体验" in persona_style: ack_parts.append(f"我感知到了,{name},一个充满故事的地方")
|
| 96 |
else: ack_parts.append(f"确认:目的地已记录为{name}")
|
|
|
|
| 97 |
if "duration" in extracted_info:
|
| 98 |
days = extracted_info['duration'].get('days', '几')
|
| 99 |
if "社交" in persona_style: ack_parts.append(f"玩{days}天,时间超充裕的")
|
| 100 |
elif "体验" in persona_style: ack_parts.append(f"{days}个日夜,足够深入探索了")
|
| 101 |
else: ack_parts.append(f"行程时长已设定为{days}天")
|
|
|
|
| 102 |
if "budget" in extracted_info:
|
| 103 |
+
budget_desc = self._format_budget_info(extracted_info.get('budget'))
|
| 104 |
if "社交" in persona_style: ack_parts.append(f"{budget_desc}的预算,妥妥的")
|
| 105 |
elif "体验" in persona_style: ack_parts.append(f"了解,{budget_desc}的投入,追求的是价值而非价格")
|
| 106 |
else: ack_parts.append(f"预算已明确为{budget_desc}")
|
|
|
|
| 107 |
return ",".join(ack_parts) + "。" if ack_parts else ""
|
| 108 |
|
| 109 |
+
def _format_budget_info(self, budget: dict) -> str:
|
| 110 |
+
if not budget or not isinstance(budget, dict): return "未指定"
|
| 111 |
+
if budget.get('amount') and budget.get('currency'): return f"{budget['amount']}{budget['currency']}"
|
| 112 |
+
if budget.get('description'): return budget['description']
|
| 113 |
+
if budget.get('type'):
|
| 114 |
+
type_map = {'economy': '经济型', 'comfortable': '舒适型', 'luxury': '豪华型'}
|
| 115 |
+
return type_map.get(budget['type'], budget['type'])
|
| 116 |
+
return "未指定"
|
| 117 |
+
|
| 118 |
+
# --- 以下方法保持不变 ---
|
| 119 |
def _generate_persona_enhanced_plan(self, user_message: str, session_state: dict) -> str:
|
| 120 |
persona_config = self._get_current_persona_config(session_state)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
if self.ai_model.is_available():
|
| 122 |
prompt = self._build_prompt(session_state, persona_config)
|
| 123 |
log.info(f"🚀 使用Persona '{persona_config.get('name')}' 构建的Prompt进行生成。")
|
|
|
|
| 127 |
return self._generate_fallback_plan(session_state)
|
| 128 |
|
| 129 |
def _build_prompt(self, session_state: dict, persona_config: dict) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
template = persona_config.get('prompt_template')
|
| 131 |
+
if not template:
|
| 132 |
+
return self._build_generic_prompt(session_state)
|
| 133 |
+
try:
|
| 134 |
+
format_args = {
|
| 135 |
+
"location": self._safe_get_session_value(session_state, "destination", "name", "未指定"),
|
| 136 |
+
"days": self._safe_get_session_value(session_state, "duration", "days", "未指定"),
|
| 137 |
+
"budget": self._format_budget_info(session_state.get("budget")),
|
| 138 |
+
"date": session_state.get('date', '近期'),
|
| 139 |
+
"user_tags": ", ".join(session_state.get('user_tags', [])),
|
| 140 |
+
"commercial_preference": session_state.get('commercial_preference', '适中'),
|
| 141 |
+
"group_description": session_state.get('group_description', '个人'),
|
| 142 |
+
"tags": ", ".join(session_state.get('tags', []))
|
| 143 |
+
}
|
| 144 |
+
return template.format(**format_args)
|
| 145 |
+
except KeyError as e:
|
| 146 |
+
log.warning(f"Persona模板格式化失败,缺少键: {e}。将使用通用模板。")
|
| 147 |
+
return self._build_generic_prompt(session_state)
|
| 148 |
+
|
| 149 |
+
def _safe_get_session_value(self, session, key1, key2, default):
|
| 150 |
+
"""安全地从嵌套的session字典中取值"""
|
| 151 |
+
level1 = session.get(key1)
|
| 152 |
+
if isinstance(level1, dict):
|
| 153 |
+
return level1.get(key2, default)
|
| 154 |
+
return default
|
| 155 |
+
|
| 156 |
+
def _build_generic_prompt(self, session_state: dict) -> str:
|
| 157 |
+
location = self._safe_get_session_value(session_state, "destination", "name", "目的地")
|
| 158 |
+
days = self._safe_get_session_value(session_state, "duration", "days", "几")
|
| 159 |
+
budget_info = self._format_budget_info(session_state.get("budget"))
|
| 160 |
+
return f"你是一个专业的旅游助手。请为用户生成一个详细的旅行计划。\n【基本信息】\n- 目的地:{location}\n- 旅行天数:{days}天\n- 预算:{budget_info}\n【要求】\n- 提供具体的景点推荐和路线安排\n- 包含交通、住宿、餐饮建议\n- 确保所有推荐都在预算范围内\n- 提供实用的旅行贴士\n\n请生成一份实用、详细的旅行计划。"
|
| 161 |
+
|
| 162 |
+
def _generate_fallback_plan(self, session_state: dict) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
persona_config = self._get_current_persona_config(session_state)
|
| 164 |
+
location = self._safe_get_session_value(session_state, "destination", "name", "目的地")
|
| 165 |
+
days = self._safe_get_session_value(session_state, "duration", "days", "几")
|
| 166 |
+
budget_info = self._format_budget_info(session_state.get("budget"))
|
| 167 |
persona_name = persona_config.get('name', '旅行者')
|
| 168 |
plan = f"为您推荐 {location} {days}天旅行计划:\n\n"
|
| 169 |
plan += f"👤 旅行者类型:{persona_name}\n"
|
| 170 |
plan += f"💰 预算范围:{budget_info}\n\n"
|
| 171 |
+
highlights = self._safe_get_session_value(session_state, "destination", "highlights", "精彩景点等待您的探索")
|
|
|
|
| 172 |
plan += f"🎯 主要景点:{highlights}\n\n"
|
| 173 |
+
persona_key = self._safe_get_session_value(session_state, "persona", "key", None)
|
| 174 |
if persona_key == 'planner': plan += "📋 建议制定详细的每日行程表。\n"
|
| 175 |
elif persona_key == 'social': plan += "📸 推荐寻找热门打卡点!\n"
|
| 176 |
elif persona_key == 'experiential': plan += "🎨 建议深入当地社区,寻找地道体验。\n"
|