Spaces:
Runtime error
Runtime error
| """ | |
| 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 | |