PregoPal / core /conversation_manager.py
J.B-Lin
全双工语音对话实现
edca135
Raw
History Blame Contribute Delete
8.41 kB
"""
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