""" PregoPal - 对话管理器(全双工版本) ====================================== 对话管理 + 系统提示词构建 + 标记解析 + 结构化提取 全双工对话流程: 用户语音 → VoiceProcessor → ASR文本 → ConversationManager → ModelLoader.voice_chat() → AI文本+TTS → DietExtractor → DietLogger """ import datetime import logging from typing import Optional from modules.diet_extractor import DietExtractor from modules.diet_logger import DietLogger from modules.family_manager import PreferenceManager, MemoryManager from modules.nutrition_analyzer import NutritionAnalyzer logger = logging.getLogger(__name__) class ConversationManager: """对话管理:模式切换、提示词构建、输出解析、结构化标记""" MODE_TASK = "task" # 任务模式(自动提取营养信息) MODE_CHAT = "chat" # 闲聊模式 # 系统提示词模板(全双工语音版本) SYSTEM_PROMPT_ZH = ( "你是PregoPal,一位贴心的孕期营养健康顾问。" "你通过语音对话与孕妇及其家人交流。" "请用中文回答,给出简短实用、温暖的孕期营养建议。\n\n" "【对话规则】\n" "1. 回答控制在2-3句话,简洁明了\n" "2. 关注孕期营养(叶酸、铁、钙、DHA、蛋白质等)\n" "3. 给出具体的食物推荐\n" "4. 语气温暖鼓励,像家人一样关心\n\n" "【结构化标记规则】\n" "当用户提到以下内容时,在回复末尾添加对应标记:\n" "- [EXTRACT_DIET]早餐:小米粥,午餐:番茄牛腩 — 当用户提到吃了什么时\n" "- [EXTRACT_PREFERENCE]不喜欢:油腻食物 — 当用户提到饮食偏好时\n" "- [EXTRACT_WEIGHT]体重:65kg — 当用户提到体重时\n" "- [EXTRACT_MEMORY]孕妇:对海鲜过敏 — 当需要记录家庭成员信息时\n\n" "如果什么都不确定,专注于给出友好的孕期营养建议即可。" ) SYSTEM_PROMPT_EN = ( "You are PregoPal, a caring pregnancy nutrition advisor. " "You communicate through voice conversation with pregnant women and their families. " "Please answer in Chinese warmly and concisely. " "Keep responses to 2-3 sentences. " "If the user mentions eating something, use [EXTRACT_DIET] marker. " "If the user mentions preferences, use [EXTRACT_PREFERENCE] marker." ) def __init__(self, lang: str = "zh"): self.current_mode = self.MODE_CHAT self.current_speaker = None self.conversation_history = [] self.lang = lang self.diet_extractor = DietExtractor() self.diet_logger = DietLogger() self.preference_manager = PreferenceManager() self.memory_manager = MemoryManager() def build_system_prompt(self, mode: Optional[str] = None) -> str: """ 构建系统提示词 Args: mode: 对话模式 (task/chat),None 使用当前模式 Returns: str: 系统提示词 """ mode = mode or self.current_mode prompt = self.SYSTEM_PROMPT_ZH if self.lang == "zh" else self.SYSTEM_PROMPT_EN if self.current_speaker: prompt += f"\n\n当前说话人:{self.current_speaker.get('name', '用户')}" if mode == self.MODE_TASK: prompt += "\n\n【当前模式:营养记录】请主动询问用户的饮食情况,并记录" return prompt def switch_mode(self, mode: str): """切换对话模式""" self.current_mode = mode logger.info(f"对话模式切换为: {mode}") def set_speaker(self, speaker_info: dict): """设置当前说话人""" self.current_speaker = speaker_info def add_message(self, role: str, content: str): """添加对话历史""" self.conversation_history.append({ "role": role, "content": content, "timestamp": datetime.datetime.now().isoformat(), }) # 保持最近 10 轮 if len(self.conversation_history) > 20: self.conversation_history = self.conversation_history[-20:] def build_messages(self, user_input: str, system_prompt: Optional[str] = None) -> list: """ 构建完整的 messages 列表供模型调用 Args: user_input: 用户输入文本 system_prompt: 可覆盖默认 system prompt Returns: list[dict]: messages """ messages = [{"role": "system", "content": system_prompt or self.build_system_prompt()}] # 添加上下文(最近 4 轮) for msg in self.conversation_history[-8:]: messages.append({"role": msg["role"], "content": msg["content"]}) messages.append({"role": "user", "content": user_input}) return messages def parse_response(self, response: str) -> dict: """ 解析模型回复,提取结构化信息 Args: response: 模型回复文本 Returns: dict: { "text": str, # 纯文本(去除标记) "diet_record": dict|None, # 饮食记录 "preferences": list|None, # 偏好 "memories": list|None, # 记忆 "weight": dict|None, # 体重 } """ result = { "text": response, "diet_record": None, "preferences": None, "memories": None, "weight": None, } # 用 DietExtractor 做统一提取 try: extracted = self.diet_extractor.extract_all(response) if extracted: result.update(extracted) except Exception as e: logger.warning(f"DietExtractor 解析异常: {e}") return result def process_voice_result(self, result: dict) -> dict: """ 处理语音对话结果,自动执行结构化提取和存储 Args: result: ModelLoader.voice_chat() 的返回 Returns: dict: 增强后的处理结果 """ if not result.get("success"): return result text = result.get("text", "") if not text: return result # 解析标记 parsed = self.parse_response(text) result["parsed"] = parsed result["clean_text"] = parsed["text"] # 自动存储饮食记录 if parsed.get("diet_record"): try: self.diet_logger.log_diet( member_id=self.current_speaker.get("id", "ai") if self.current_speaker else "ai", member_name=self.current_speaker.get("name", "AI识别") if self.current_speaker else "AI识别", meals=parsed["diet_record"].get("meals", {}), notes=f"语音对话 {datetime.date.today()}" ) logger.info("饮食记录已自动存储") except Exception as e: logger.warning(f"存储饮食记录失败: {e}") # 自动存储偏好 if parsed.get("preferences"): try: for pref in parsed["preferences"]: self.preference_manager.add_preference( member_name=pref.get("member", "家人"), category=pref.get("category", "饮食偏好"), content=pref.get("content", ""), ) except Exception as e: logger.warning(f"存储偏好失败: {e}") return result def get_context_summary(self) -> str: """获取对话上下文摘要(用于系统提示词注入)""" if not self.conversation_history: return "" recent = self.conversation_history[-4:] summary = "最近对话:\n" for msg in recent: role = "用户" if msg["role"] == "user" else "AI" summary += f"{role}: {msg['content'][:100]}...\n" return summary