2025-02-02 23:01:14 +08:00
|
|
|
|
import uuid
|
2025-07-08 16:59:38 +08:00
|
|
|
|
import re
|
2025-02-02 23:01:14 +08:00
|
|
|
|
from typing import List, Dict
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Message:
|
2025-05-07 18:06:13 +08:00
|
|
|
|
def __init__(
|
2026-03-11 10:13:13 +08:00
|
|
|
|
self,
|
|
|
|
|
|
role: str,
|
|
|
|
|
|
content: str = None,
|
|
|
|
|
|
uniq_id: str = None,
|
|
|
|
|
|
tool_calls=None,
|
|
|
|
|
|
tool_call_id=None,
|
|
|
|
|
|
is_temporary=False,
|
2025-05-07 18:06:13 +08:00
|
|
|
|
):
|
2025-02-02 23:01:14 +08:00
|
|
|
|
self.uniq_id = uniq_id if uniq_id is not None else str(uuid.uuid4())
|
|
|
|
|
|
self.role = role
|
|
|
|
|
|
self.content = content
|
2025-03-15 11:48:14 +08:00
|
|
|
|
self.tool_calls = tool_calls
|
|
|
|
|
|
self.tool_call_id = tool_call_id
|
2026-03-11 10:13:13 +08:00
|
|
|
|
self.is_temporary = is_temporary # 标记临时消息(如工具调用提醒)
|
2025-02-02 23:01:14 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Dialogue:
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.dialogue: List[Message] = []
|
|
|
|
|
|
# 获取当前时间
|
2025-05-07 18:06:13 +08:00
|
|
|
|
self.current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
2025-02-02 23:01:14 +08:00
|
|
|
|
|
|
|
|
|
|
def put(self, message: Message):
|
|
|
|
|
|
self.dialogue.append(message)
|
|
|
|
|
|
|
2025-03-15 11:48:14 +08:00
|
|
|
|
def getMessages(self, m, dialogue):
|
|
|
|
|
|
if m.tool_calls is not None:
|
|
|
|
|
|
dialogue.append({"role": m.role, "tool_calls": m.tool_calls})
|
|
|
|
|
|
elif m.role == "tool":
|
2025-05-07 18:06:13 +08:00
|
|
|
|
dialogue.append(
|
2025-05-09 11:39:32 +08:00
|
|
|
|
{
|
|
|
|
|
|
"role": m.role,
|
|
|
|
|
|
"tool_call_id": (
|
|
|
|
|
|
str(uuid.uuid4()) if m.tool_call_id is None else m.tool_call_id
|
|
|
|
|
|
),
|
|
|
|
|
|
"content": m.content,
|
|
|
|
|
|
}
|
2025-05-07 18:06:13 +08:00
|
|
|
|
)
|
2025-03-15 11:48:14 +08:00
|
|
|
|
else:
|
|
|
|
|
|
dialogue.append({"role": m.role, "content": m.content})
|
|
|
|
|
|
|
2025-02-02 23:01:14 +08:00
|
|
|
|
def get_llm_dialogue(self) -> List[Dict[str, str]]:
|
2025-07-08 11:25:54 +08:00
|
|
|
|
# 直接调用get_llm_dialogue_with_memory,传入None作为memory_str
|
|
|
|
|
|
# 这样确保说话人功能在所有调用路径下都生效
|
2025-07-11 10:15:14 +08:00
|
|
|
|
return self.get_llm_dialogue_with_memory(None, None)
|
2025-03-03 15:00:04 +08:00
|
|
|
|
|
2025-03-22 20:39:52 +08:00
|
|
|
|
def update_system_message(self, new_content: str):
|
|
|
|
|
|
"""更新或添加系统消息"""
|
|
|
|
|
|
# 查找第一个系统消息
|
|
|
|
|
|
system_msg = next((msg for msg in self.dialogue if msg.role == "system"), None)
|
|
|
|
|
|
if system_msg:
|
|
|
|
|
|
system_msg.content = new_content
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.put(Message(role="system", content=new_content))
|
|
|
|
|
|
|
2026-04-21 11:36:06 +08:00
|
|
|
|
def _ensure_tool_calls_complete(self, messages: List[Message]) -> List[Message]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
确保所有 tool_calls 都有对应的 tool 响应
|
|
|
|
|
|
修复被打断导致的悬空 tool_calls,防止大模型 API 报 400 错误
|
|
|
|
|
|
"""
|
|
|
|
|
|
pending_tool_calls = set()
|
|
|
|
|
|
result = []
|
|
|
|
|
|
|
|
|
|
|
|
for msg in messages:
|
|
|
|
|
|
result.append(msg)
|
|
|
|
|
|
|
|
|
|
|
|
if msg.role == "assistant" and msg.tool_calls:
|
|
|
|
|
|
for tc in msg.tool_calls:
|
|
|
|
|
|
tc_id = tc.get("id") if isinstance(tc, dict) else getattr(tc, "id", None)
|
|
|
|
|
|
if tc_id:
|
|
|
|
|
|
pending_tool_calls.add(tc_id)
|
|
|
|
|
|
|
|
|
|
|
|
elif msg.role == "tool" and msg.tool_call_id:
|
|
|
|
|
|
pending_tool_calls.discard(msg.tool_call_id)
|
|
|
|
|
|
|
|
|
|
|
|
for missing_id in pending_tool_calls:
|
|
|
|
|
|
dummy_tool_msg = Message(
|
|
|
|
|
|
role="tool",
|
|
|
|
|
|
content='{"status": "interrupted", "message": "动作已取消/被打断"}',
|
|
|
|
|
|
tool_call_id=missing_id
|
|
|
|
|
|
)
|
|
|
|
|
|
result.append(dummy_tool_msg)
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
2025-05-07 18:06:13 +08:00
|
|
|
|
def get_llm_dialogue_with_memory(
|
2026-03-11 10:13:13 +08:00
|
|
|
|
self, memory_str: str = None, voiceprint_config: dict = None
|
2025-05-07 18:06:13 +08:00
|
|
|
|
) -> List[Dict[str, str]]:
|
2025-07-08 11:25:54 +08:00
|
|
|
|
# 构建对话
|
2025-03-03 15:00:04 +08:00
|
|
|
|
dialogue = []
|
2025-05-07 18:06:13 +08:00
|
|
|
|
|
2025-03-03 15:00:04 +08:00
|
|
|
|
# 添加系统提示和记忆
|
|
|
|
|
|
system_message = next(
|
|
|
|
|
|
(msg for msg in self.dialogue if msg.role == "system"), None
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if system_message:
|
2026-04-21 15:01:57 +08:00
|
|
|
|
# 以 <context> 为分界点,拆分静态 system prompt 和动态上下文
|
|
|
|
|
|
# 静态部分(规则、身份等)保持不变,可命中前缀缓存
|
2026-04-27 11:08:25 +08:00
|
|
|
|
# 动态部分(时间、天气、记忆等)作为第二条 system 消息,保持 system 权威性
|
2026-04-21 15:01:57 +08:00
|
|
|
|
full_prompt = system_message.content
|
|
|
|
|
|
context_match = re.search(r"<context>", full_prompt)
|
|
|
|
|
|
if context_match:
|
|
|
|
|
|
static_part = full_prompt[:context_match.start()]
|
|
|
|
|
|
dynamic_part = full_prompt[context_match.start():]
|
|
|
|
|
|
else:
|
|
|
|
|
|
static_part = full_prompt
|
|
|
|
|
|
dynamic_part = ""
|
2025-07-11 10:15:14 +08:00
|
|
|
|
|
2026-04-27 11:08:25 +08:00
|
|
|
|
# 第一段:静态 system prompt(前缀缓存可命中)
|
2026-04-21 15:01:57 +08:00
|
|
|
|
dialogue.append({"role": "system", "content": static_part})
|
2025-03-03 15:00:04 +08:00
|
|
|
|
|
2026-04-27 11:08:25 +08:00
|
|
|
|
# 第二段:few-shot 示例(会话内不变,也是缓存前缀的一部分)
|
2026-04-21 11:36:06 +08:00
|
|
|
|
non_system_messages = [m for m in self.dialogue if m.role != "system"]
|
2026-04-27 11:08:25 +08:00
|
|
|
|
fewshot_messages = [m for m in non_system_messages if m.is_temporary]
|
|
|
|
|
|
complete_fewshot = self._ensure_tool_calls_complete(fewshot_messages)
|
|
|
|
|
|
for m in complete_fewshot:
|
2026-04-21 11:36:06 +08:00
|
|
|
|
self.getMessages(m, dialogue)
|
2025-03-03 15:00:04 +08:00
|
|
|
|
|
2026-04-27 11:08:25 +08:00
|
|
|
|
# 第三段:动态上下文 system prompt(时间、记忆、说话人等)
|
|
|
|
|
|
# 保持 system 角色以确保模型权威性,不降级为 user
|
2026-04-21 15:01:57 +08:00
|
|
|
|
if system_message and dynamic_part:
|
|
|
|
|
|
# 替换时间占位符
|
|
|
|
|
|
dynamic_part = dynamic_part.replace(
|
|
|
|
|
|
"{{current_time}}", datetime.now().strftime("%H:%M")
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 填充记忆
|
|
|
|
|
|
if memory_str is not None:
|
|
|
|
|
|
dynamic_part = re.sub(
|
|
|
|
|
|
r"<memory>.*?</memory>",
|
|
|
|
|
|
f"<memory>\n{memory_str}\n</memory>",
|
|
|
|
|
|
dynamic_part,
|
|
|
|
|
|
flags=re.DOTALL,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 追加说话人信息
|
|
|
|
|
|
try:
|
|
|
|
|
|
speakers = voiceprint_config.get("speakers", [])
|
|
|
|
|
|
if speakers:
|
|
|
|
|
|
dynamic_part += "\n<speakers_info>"
|
|
|
|
|
|
for speaker_str in speakers:
|
|
|
|
|
|
try:
|
|
|
|
|
|
parts = speaker_str.split(",", 2)
|
|
|
|
|
|
if len(parts) >= 2:
|
|
|
|
|
|
name = parts[1].strip()
|
|
|
|
|
|
description = (
|
|
|
|
|
|
parts[2].strip() if len(parts) >= 3 else ""
|
|
|
|
|
|
)
|
|
|
|
|
|
dynamic_part += f"\n- {name}:{description}"
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
dynamic_part += "\n</speakers_info>"
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-04-27 11:08:25 +08:00
|
|
|
|
dialogue.append({"role": "system", "content": dynamic_part})
|
|
|
|
|
|
|
|
|
|
|
|
# 第四段:实际对话历史(不含 few-shot)
|
|
|
|
|
|
actual_messages = [m for m in non_system_messages if not m.is_temporary]
|
|
|
|
|
|
complete_actual = self._ensure_tool_calls_complete(actual_messages)
|
|
|
|
|
|
for m in complete_actual:
|
|
|
|
|
|
self.getMessages(m, dialogue)
|
2026-04-21 15:01:57 +08:00
|
|
|
|
|
2025-03-03 15:00:04 +08:00
|
|
|
|
return dialogue
|