diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index c8278dac..15d3a1a6 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -368,37 +368,37 @@ class DefaultReplyer: return f"{expression_habits_title}\n{expression_habits_block}", selected_ids - async def build_memory_block(self, chat_history: List[DatabaseMessages], target: str) -> str: - """构建记忆块 + # async def build_memory_block(self, chat_history: List[DatabaseMessages], target: str) -> str: + # """构建记忆块 - Args: - chat_history: 聊天历史记录 - target: 目标消息内容 + # Args: + # chat_history: 聊天历史记录 + # target: 目标消息内容 - Returns: - str: 记忆信息字符串 - """ + # Returns: + # str: 记忆信息字符串 + # """ - if not global_config.memory.enable_memory: - return "" + # if not global_config.memory.enable_memory: + # return "" - instant_memory = None + # instant_memory = None - running_memories = await self.memory_activator.activate_memory_with_chat_history( - target_message=target, chat_history=chat_history - ) - if not running_memories: - return "" + # running_memories = await self.memory_activator.activate_memory_with_chat_history( + # target_message=target, chat_history=chat_history + # ) + # if not running_memories: + # return "" - memory_str = "以下是当前在聊天中,你回忆起的记忆:\n" - for running_memory in running_memories: - keywords, content = running_memory - memory_str += f"- {keywords}:{content}\n" + # memory_str = "以下是当前在聊天中,你回忆起的记忆:\n" + # for running_memory in running_memories: + # keywords, content = running_memory + # memory_str += f"- {keywords}:{content}\n" - if instant_memory: - memory_str += f"- {instant_memory}\n" + # if instant_memory: + # memory_str += f"- {instant_memory}\n" - return memory_str + # return memory_str async def build_tool_info(self, chat_history: str, sender: str, target: str, enable_tool: bool = True) -> str: """构建工具信息块 diff --git a/src/common/data_models/message_data_model.py b/src/common/data_models/message_data_model.py index 70b970df..a3d5751f 100644 --- a/src/common/data_models/message_data_model.py +++ b/src/common/data_models/message_data_model.py @@ -1,4 +1,4 @@ -from typing import Optional, TYPE_CHECKING, List, Tuple, Union, Dict +from typing import Optional, TYPE_CHECKING, List, Tuple, Union, Dict, Any from dataclasses import dataclass, field from enum import Enum @@ -50,10 +50,65 @@ class ReplyContentType(Enum): return self.value +@dataclass +class ForwardNode(BaseDataModel): + user_id: Optional[str] = None + user_nickname: Optional[str] = None + content: Union[List["ReplyContent"], str] = field(default_factory=list) + + @classmethod + def construct_as_id_reference(cls, message_id: str) -> "ForwardNode": + return cls(user_id="", user_nickname="", content=message_id) + + @classmethod + def construct_as_created_node( + cls, user_id: str, user_nickname: str, content: List["ReplyContent"] + ) -> "ForwardNode": + return cls(user_id=user_id, user_nickname=user_nickname, content=content) + + @dataclass class ReplyContent(BaseDataModel): content_type: ReplyContentType | str - content: Union[str, Dict, List["ReplyContent"]] # 支持嵌套的 ReplyContent + content: Union[str, Dict, List[ForwardNode], List["ReplyContent"]] # 支持嵌套的 ReplyContent + + @classmethod + def construct_as_text(cls, text: str): + return cls(content_type=ReplyContentType.TEXT, content=text) + + @classmethod + def construct_as_image(cls, image_base64: str): + return cls(content_type=ReplyContentType.IMAGE, content=image_base64) + + @classmethod + def construct_as_voice(cls, voice_base64: str): + return cls(content_type=ReplyContentType.VOICE, content=voice_base64) + + @classmethod + def construct_as_emoji(cls, emoji_str: str): + return cls(content_type=ReplyContentType.EMOJI, content=emoji_str) + + @classmethod + def construct_as_command(cls, command_arg: Dict): + return cls(content_type=ReplyContentType.COMMAND, content=command_arg) + + @classmethod + def construct_as_hybrid(cls, hybrid_content: List[Tuple[ReplyContentType | str, str]]): + hybrid_content_list: List[ReplyContent] = [] + for content_type, content in hybrid_content: + assert content_type not in [ + ReplyContentType.HYBRID, + ReplyContentType.FORWARD, + ReplyContentType.VOICE, + ReplyContentType.COMMAND, + ], "混合内容的每个项不能是混合、转发、语音或命令类型" + assert isinstance(content, str), "混合内容的每个项必须是字符串" + hybrid_content_list.append(ReplyContent(content_type=content_type, content=content)) + return cls(content_type=ReplyContentType.HYBRID, content=hybrid_content_list) + + @classmethod + def construct_as_forward(cls, forward_nodes: List[ForwardNode]): + return cls(content_type=ReplyContentType.FORWARD, content=forward_nodes) def __post_init__(self): if isinstance(self.content_type, ReplyContentType): @@ -82,36 +137,70 @@ class ReplySetModel(BaseDataModel): return len(self.reply_data) def add_text_content(self, text: str): - """添加文本内容""" + """ + 添加文本内容 + Args: + text: 文本内容 + """ self.reply_data.append(ReplyContent(content_type=ReplyContentType.TEXT, content=text)) def add_image_content(self, image_base64: str): - """添加图片内容,base64编码的图片数据""" + """ + 添加图片内容,base64编码的图片数据 + Args: + image_base64: base64编码的图片数据 + """ self.reply_data.append(ReplyContent(content_type=ReplyContentType.IMAGE, content=image_base64)) def add_voice_content(self, voice_base64: str): - """添加语音内容,base64编码的音频数据""" + """ + 添加语音内容,base64编码的音频数据 + Args: + voice_base64: base64编码的音频数据 + """ self.reply_data.append(ReplyContent(content_type=ReplyContentType.VOICE, content=voice_base64)) - def add_hybrid_content(self, hybrid_content: List[Tuple[ReplyContentType, str]]): + def add_hybrid_content_by_raw(self, hybrid_content: List[Tuple[ReplyContentType | str, str]]): """ - 添加混合型内容,可以包含多种类型的内容 - - 实际解析时只关注最外层,没有递归嵌套处理 + 添加混合型内容,可以包含text, image, emoji的任意组合 + Args: + hybrid_content: 元组 (类型, 消息内容) 构成的列表,如[(ReplyContentType.TEXT, "Hello"), (ReplyContentType.IMAGE, " Tuple[Seg, bool]: + """ + 把 ReplyContent 转换为 Seg 结构 (Forward 中仅递归一次) + Args: + reply_content: ReplyContent 对象 + Returns: + Tuple[Seg, bool]: 转换后的 Seg 结构和是否需要typing的标志 + """ + content_type = reply_content.content_type + if content_type == ReplyContentType.TEXT: + text_data: str = reply_content.content # type: ignore + return Seg(type="text", data=text_data), True + elif content_type == ReplyContentType.IMAGE: + return Seg(type="image", data=reply_content.content), False # type: ignore + elif content_type == ReplyContentType.EMOJI: + return Seg(type="emoji", data=reply_content.content), False # type: ignore + elif content_type == ReplyContentType.COMMAND: + return Seg(type="command", data=reply_content.content), False # type: ignore + elif content_type == ReplyContentType.VOICE: + return Seg(type="voice", data=reply_content.content), False # type: ignore + elif content_type == ReplyContentType.HYBRID: + hybrid_message_list_data: List[ReplyContent] = reply_content.content # type: ignore + assert isinstance(hybrid_message_list_data, list), "混合类型内容必须是列表" + sub_seg_list: List[Seg] = [] + for sub_content in hybrid_message_list_data: + sub_content_type = sub_content.content_type + sub_content_data = sub_content.content + + if sub_content_type == ReplyContentType.TEXT: + sub_seg_list.append(Seg(type="text", data=sub_content_data)) # type: ignore + elif sub_content_type == ReplyContentType.IMAGE: + sub_seg_list.append(Seg(type="image", data=sub_content_data)) # type: ignore + elif sub_content_type == ReplyContentType.EMOJI: + sub_seg_list.append(Seg(type="emoji", data=sub_content_data)) # type: ignore + else: + logger.warning(f"[SendAPI] 混合类型中不支持的子内容类型: {repr(sub_content_type)}") + continue + return Seg(type="seglist", data=sub_seg_list), True + elif content_type == ReplyContentType.FORWARD: + forward_message_list_data: List["ForwardNode"] = reply_content.content # type: ignore + assert isinstance(forward_message_list_data, list), "转发类型内容必须是列表" + forward_message_list: List[MessageBase] = [] + for forward_node in forward_message_list_data: + message_segment = Seg(type="id", data=forward_node.content) # type: ignore + user_info: Optional[UserInfo] = None + if forward_node.user_id and forward_node.user_nickname: + assert isinstance(forward_node.content, list), "转发节点内容必须是列表" + user_info = UserInfo(user_id=forward_node.user_id, user_nickname=forward_node.user_nickname) + single_node_content: List[Seg] = [] + for sub_content in forward_node.content: + if sub_content.content_type != ReplyContentType.FORWARD: + sub_seg, _ = _parse_content_to_seg(sub_content) + single_node_content.append(sub_seg) + message_segment = Seg(type="seglist", data=single_node_content) + forward_message_list.append( + MessageBase(message_segment=message_segment, message_info=BaseMessageInfo(user_info=user_info)) + ) + return Seg(type="forward", data=forward_message_list), False # type: ignore + else: + message_type_in_str = content_type.value if isinstance(content_type, ReplyContentType) else str(content_type) + return Seg(type=message_type_in_str, data=reply_content.content), True # type: ignore