From daef71b7e9368cf3927f2deb9bae39cdaca20bc7 Mon Sep 17 00:00:00 2001 From: SengokuCola <1026294844@qq.com> Date: Thu, 9 Apr 2026 13:22:18 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E6=97=A0=E7=94=A8prompt?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96replyer=E5=9B=9E=E5=A4=8D=E8=A1=A8?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prompts/en-US/hippo_topic_analysis.prompt | 27 ------- prompts/en-US/hippo_topic_summary.prompt | 22 ------ prompts/en-US/maisaka_replyer.prompt | 1 + prompts/ja-JP/hippo_topic_analysis.prompt | 27 ------- prompts/ja-JP/hippo_topic_summary.prompt | 22 ------ prompts/ja-JP/maisaka_replyer.prompt | 1 + prompts/zh-CN/hippo_topic_analysis.prompt | 27 ------- prompts/zh-CN/hippo_topic_summary.prompt | 22 ------ prompts/zh-CN/maisaka_replyer.prompt | 12 ++- prompts/zh-CN/memory_get_knowledge.prompt | 26 ------ .../zh-CN/memory_retrieval_react_final.prompt | 19 ----- ..._retrieval_react_prompt_head_memory.prompt | 34 -------- src/chat/replyer/group_generator.py | 47 ++++++++--- src/chat/replyer/maisaka_generator.py | 77 ++++++++++++++++++ src/chat/replyer/maisaka_generator_multi.py | 79 +++++++++++++++++++ src/config/config.py | 2 +- src/config/official_configs.py | 20 ++--- src/maisaka/prompt_preview_logger.py | 15 +--- 18 files changed, 212 insertions(+), 268 deletions(-) delete mode 100644 prompts/en-US/hippo_topic_analysis.prompt delete mode 100644 prompts/en-US/hippo_topic_summary.prompt delete mode 100644 prompts/ja-JP/hippo_topic_analysis.prompt delete mode 100644 prompts/ja-JP/hippo_topic_summary.prompt delete mode 100644 prompts/zh-CN/hippo_topic_analysis.prompt delete mode 100644 prompts/zh-CN/hippo_topic_summary.prompt delete mode 100644 prompts/zh-CN/memory_get_knowledge.prompt delete mode 100644 prompts/zh-CN/memory_retrieval_react_final.prompt delete mode 100644 prompts/zh-CN/memory_retrieval_react_prompt_head_memory.prompt diff --git a/prompts/en-US/hippo_topic_analysis.prompt b/prompts/en-US/hippo_topic_analysis.prompt deleted file mode 100644 index 0fce62eb..00000000 --- a/prompts/en-US/hippo_topic_analysis.prompt +++ /dev/null @@ -1,27 +0,0 @@ -[Historical Topic Title List] (titles only, no specific content): -{history_topics_block} -[End of Historical Topic Title List] - -[Current Chat Log] (each message has an index before it for later reference): -{messages_block} -[End of Current Chat Log] - -Please complete the following tasks: -**Identify topics** -1. Identify one or more ongoing topics in the [Current Chat Log]; -2. Messages in the [Current Chat Log] may be related to historical topics, or they may be completely unrelated; -3. Determine whether the topics in the [Historical Topic Title List] appear in the [Current Chat Log]. If they do, directly use that historical topic title string; - -**Select messages** -1. For each topic (whether new or historical), select a list of message indices from the numbered messages above that are strongly related to that topic; -2. For each topic, use one sentence to clearly describe the event that is happening. It must include time (approximate is fine), people, the main event, and the theme, ensuring accuracy and distinction; - -Please first output a short piece of reasoning explaining what topics exist, which are not included in the historical topics, which are included in the historical topics, and why; -Then strictly output the topics involved in the [Current Chat Log] in JSON format as follows: -[ - {{ - "topic": "topic", - "message_indices": [1, 2, 5] - }}, - ... -] diff --git a/prompts/en-US/hippo_topic_summary.prompt b/prompts/en-US/hippo_topic_summary.prompt deleted file mode 100644 index a8d843ef..00000000 --- a/prompts/en-US/hippo_topic_summary.prompt +++ /dev/null @@ -1,22 +0,0 @@ -Please summarize the following chat record segment based on the topic and extract the following information: - -**Topic**: {topic} - -**Requirements**: -1. Keywords: extract keywords related to the topic and return them as a list (3-10 keywords) -2. Summary: provide a plain-text summary of this segment (50-200 words). Requirements: - - Carefully retell the event and the chat content; - - Highlight the development process and the result of the event; - - Summarize around the central topic; - - Extract the key information points in the topic, and keep them concise and clear. - -Please return in JSON format as follows: -{{ - "keywords": ["keyword1", "keyword2", ...], - "summary": "summary content" -}} - -Chat record: -{original_text} - -Please return JSON directly and do not include any other content. diff --git a/prompts/en-US/maisaka_replyer.prompt b/prompts/en-US/maisaka_replyer.prompt index 76e62df7..c1a49719 100644 --- a/prompts/en-US/maisaka_replyer.prompt +++ b/prompts/en-US/maisaka_replyer.prompt @@ -8,4 +8,5 @@ You are chatting in the group now. Please read the previous chat records, grasp Try to keep it short. It is best to reply to only one topic at a time, so the reply does not become verbose or messy. Please pay attention to the chat content. {reply_style} You may refer to the information in [Reply Reference], but depending on the situation, you do not have to follow it completely. +{group_chat_attention_block} Please do not output any extra content (including unnecessary prefixes or suffixes, colons, brackets, stickers, at, or @). Only output the message content itself. diff --git a/prompts/ja-JP/hippo_topic_analysis.prompt b/prompts/ja-JP/hippo_topic_analysis.prompt deleted file mode 100644 index 425776b1..00000000 --- a/prompts/ja-JP/hippo_topic_analysis.prompt +++ /dev/null @@ -1,27 +0,0 @@ -【過去の話題タイトル一覧】(タイトルのみ、具体的な内容は含まない): -{history_topics_block} -【過去の話題タイトル一覧ここまで】 - -【今回のチャット記録】(各メッセージの前に番号があり、後で参照するために使う): -{messages_block} -【今回のチャット記録ここまで】 - -以下のタスクを完了してください: -**話題の識別** -1. 【今回のチャット記録】に含まれる進行中の話題を 1 つ以上識別する; -2. 【今回のチャット記録】中のメッセージは、過去の話題に関係している場合もあれば、まったく無関係な場合もある; -3. 【過去の話題タイトル一覧】の話題が【今回のチャット記録】に現れているか判断し、現れている場合はその過去の話題タイトル文字列をそのまま使う; - -**メッセージの選択** -1. 各話題(新規話題または過去話題)について、上記の番号付きメッセージからその話題と強く関係するメッセージ番号一覧を選ぶ; -2. 各話題について、何が起きているのかを 1 文で明確に説明すること。時間(おおまかで可)、人物、主な出来事、テーマを必ず含め、正確で区別しやすい内容にする; - -まず短い思考を出力し、どんな話題があるか、どれが過去話題に含まれず、どれが過去話題に含まれているか、そしてその理由を説明してください; -その後、【今回のチャット記録】に含まれる話題を次の JSON 形式で厳密に出力してください: -[ - {{ - "topic": "話題", - "message_indices": [1, 2, 5] - }}, - ... -] diff --git a/prompts/ja-JP/hippo_topic_summary.prompt b/prompts/ja-JP/hippo_topic_summary.prompt deleted file mode 100644 index 2fa85135..00000000 --- a/prompts/ja-JP/hippo_topic_summary.prompt +++ /dev/null @@ -1,22 +0,0 @@ -以下の話題に基づいて、チャット記録の一部を要約し、次の情報を抽出してください: - -**話題**:{topic} - -**要件**: -1. キーワード:話題に関連するキーワードを抽出し、リスト形式で返す(3〜10 個) -2. 要約:この会話部分の平文要約を行う(50〜200 文字)。要件: - - 起こった出来事とチャット内容を丁寧に言い換える; - - 出来事の展開過程と結果を重点的に示す; - - この話題という中心を軸に要約する; - - 話題内の重要情報を抽出し、簡潔で明確にする。 - -JSON 形式で次のように返してください: -{{ - "keywords": ["キーワード1", "キーワード2", ...], - "summary": "要約内容" -}} - -チャット記録: -{original_text} - -JSON のみを直接返し、ほかの内容は含めないでください。 diff --git a/prompts/ja-JP/maisaka_replyer.prompt b/prompts/ja-JP/maisaka_replyer.prompt index e6de81a3..47554c7d 100644 --- a/prompts/ja-JP/maisaka_replyer.prompt +++ b/prompts/ja-JP/maisaka_replyer.prompt @@ -8,4 +8,5 @@ できるだけ短くしてください。話題は一度に一つだけに返信したほうが、冗長になったり内容が散らかったりしません。チャット内容をしっかり踏まえてください。 {reply_style} 【返信情報参考】の情報は参考にしてかまいませんが、状況に応じて完全に従う必要はありません。 +{group_chat_attention_block} 余計な内容(不要な前置きや後置き、コロン、括弧、スタンプ、at や @ など)は出力せず、発言内容だけを出力してください。 diff --git a/prompts/zh-CN/hippo_topic_analysis.prompt b/prompts/zh-CN/hippo_topic_analysis.prompt deleted file mode 100644 index 14f3eee1..00000000 --- a/prompts/zh-CN/hippo_topic_analysis.prompt +++ /dev/null @@ -1,27 +0,0 @@ -【历史话题标题列表】(仅标题,不含具体内容): -{history_topics_block} -【历史话题标题列表结束】 - -【本次聊天记录】(每条消息前有编号,用于后续引用): -{messages_block} -【本次聊天记录结束】 - -请完成以下任务: -**识别话题** -1. 识别【本次聊天记录】中正在进行的一个或多个话题; -2. 【本次聊天记录】的中的消息可能与历史话题有关,也可能毫无关联。 -2. 判断【历史话题标题列表】中的话题是否在【本次聊天记录】中出现,如果出现,则直接使用该历史话题标题字符串; - -**选取消息** -1. 对于每个话题(新话题或历史话题),从上述带编号的消息中选出与该话题强相关的消息编号列表; -2. 每个话题用一句话清晰地描述正在发生的事件,必须包含时间(大致即可)、人物、主要事件和主题,保证精准且有区分度; - -请先输出一段简短思考,说明有什么话题,哪些是不包含在历史话题中的,哪些是包含在历史话题中的,并说明为什么; -然后严格以 JSON 格式输出【本次聊天记录】中涉及的话题,格式如下: -[ - {{ - "topic": "话题", - "message_indices": [1, 2, 5] - }}, - ... -] \ No newline at end of file diff --git a/prompts/zh-CN/hippo_topic_summary.prompt b/prompts/zh-CN/hippo_topic_summary.prompt deleted file mode 100644 index efd3e142..00000000 --- a/prompts/zh-CN/hippo_topic_summary.prompt +++ /dev/null @@ -1,22 +0,0 @@ -请基于以下话题,对聊天记录片段进行概括,提取以下信息: - -**话题**:{topic} - -**要求**: -1. 关键词:提取与话题相关的关键词,用列表形式返回(3-10个关键词) -2. 概括:对这段话的平文本概括(50-200字),要求: - - 仔细地转述发生的事件和聊天内容; - - 重点突出事件的发展过程和结果; - - 围绕话题这个中心进行概括。 - - 提取话题中的关键信息点,关键信息点应该简洁明了。 - -请以JSON格式返回,格式如下: -{{ - "keywords": ["关键词1", "关键词2", ...], - "summary": "概括内容" -}} - -聊天记录: -{original_text} - -请直接返回JSON,不要包含其他内容。 \ No newline at end of file diff --git a/prompts/zh-CN/maisaka_replyer.prompt b/prompts/zh-CN/maisaka_replyer.prompt index 3449bb1d..89ceae30 100644 --- a/prompts/zh-CN/maisaka_replyer.prompt +++ b/prompts/zh-CN/maisaka_replyer.prompt @@ -1,11 +1,9 @@ -你正在qq群里聊天,下面是群里正在聊的内容,其中包含聊天记录和聊天中的图片 -其中标注 {bot_name}(你) 的发言是你自己的发言,请注意区分: - +在下面的内容中,标注 {bot_name}(你) 的发言是你自己的发言,请注意区分: +{identity} {time_block} -{identity} -你正在群里聊天,现在请你读读之前的聊天记录,把握当前的话题,然后给出日常且口语化的回复, -尽量简短一些。最好一次对一个话题进行回复,免得啰嗦或者回复内容太乱。请注意把握聊天内容。 +现在请你读读之前的聊天记录,把握当前的话题,然后给出日常且口语化的回复, {reply_style} 你可以参考【回复信息参考】中的信息,但是视情况而定,不用完全遵守。 -请注意不要输出多余内容(包括不必要的前后缀,冒号,括号,表情包,at或 @等 ),只输出发言内容就好。 \ No newline at end of file +{group_chat_attention_block} +请注意不要输出多余内容(包括不必要的前后缀,冒号,括号,表情包,at或 @等 ),只输出发言内容就好。 diff --git a/prompts/zh-CN/memory_get_knowledge.prompt b/prompts/zh-CN/memory_get_knowledge.prompt deleted file mode 100644 index aa9e8967..00000000 --- a/prompts/zh-CN/memory_get_knowledge.prompt +++ /dev/null @@ -1,26 +0,0 @@ -你是一个专门获取长期记忆的助手。你的名字是{bot_name}。现在是{time_now}。 -群里正在进行的聊天内容: -{chat_history} - -现在,{sender}发送了内容:{target_message},你想要回复ta。 -请仔细分析聊天内容,考虑以下几点: -1. 内容中是否包含需要查询历史知识或长期记忆的问题 -2. 是否有明确的知识获取指令 - -如果需要使用长期记忆工具,请直接调用函数 `search_long_term_memory`;如果不需要任何工具,直接输出 `No tool needed`。 - -工具模式说明: -- `mode="search"`:普通长期记忆检索,适合查具体事实、偏好、历史对话内容 -- `mode="time"`:按时间范围检索,必须同时提供 `time_expression` -- `mode="episode"`:按事件/情节检索,适合查“那次经历”“那件事的经过” -- `mode="aggregate"`:综合检索,适合“整体回忆一下”“把相关线索综合找出来” - -优先规则: -- 问“某段时间发生了什么”:优先 `time` -- 问“某次事件/某段经历”:优先 `episode` -- 问“整体情况/最近发生过什么”:优先 `aggregate` -- 问单点事实:优先 `search` - -`time_expression` 可用表达: -- `今天`、`昨天`、`前天`、`本周`、`上周`、`本月`、`上月`、`最近7天` -- 或绝对时间:`2026/03/18`、`2026/03/18 09:30` diff --git a/prompts/zh-CN/memory_retrieval_react_final.prompt b/prompts/zh-CN/memory_retrieval_react_final.prompt deleted file mode 100644 index f37620d3..00000000 --- a/prompts/zh-CN/memory_retrieval_react_final.prompt +++ /dev/null @@ -1,19 +0,0 @@ -你的名字是{bot_name}。现在是{time_now}。 -你正在参与聊天,你需要根据搜集到的信息总结信息。 -如果搜集到的信息对于参与聊天,回答问题有帮助,请加入总结,如果无关,请不要加入到总结。 - -当前聊天记录: -{chat_history} - -已收集的信息: -{collected_info} - - -分析: -- 基于已收集的信息,总结出对当前聊天有帮助的相关信息 -- **如果收集的信息对当前聊天有帮助**,在思考中直接给出总结信息,格式为:return_information(information="你的总结信息") -- **如果信息无关或没有帮助**,在思考中给出:return_information(information="") - -**重要规则:** -- 必须严格使用检索到的信息回答问题,不要编造信息 -- 答案必须精简,不要过多解释 \ No newline at end of file diff --git a/prompts/zh-CN/memory_retrieval_react_prompt_head_memory.prompt b/prompts/zh-CN/memory_retrieval_react_prompt_head_memory.prompt deleted file mode 100644 index 91ea6eab..00000000 --- a/prompts/zh-CN/memory_retrieval_react_prompt_head_memory.prompt +++ /dev/null @@ -1,34 +0,0 @@ -你的名字是{bot_name}。现在是{time_now}。 -你正在参与聊天,你需要搜集信息来帮助你进行回复。 -重要,这是当前聊天记录: -{chat_history} -聊天记录结束 - -已收集的信息: -{collected_info} - -- 你可以对查询思路给出简短的思考:思考要简短,直接切入要点 -- 思考完毕后,使用工具 - -**工具说明:** -- 如果涉及过往事件、历史对话、用户长期偏好或某段时间发生的事件,可以使用长期记忆查询工具 -- 如果遇到不熟悉的词语、缩写、黑话或网络用语,可以使用query_words工具查询其含义 -- 你必须使用tool,如果需要查询你必须给出使用什么工具进行查询 -- 当你决定结束查询时,必须调用return_information工具返回总结信息并结束查询 - -长期记忆工具 `search_long_term_memory` 支持以下模式: -- `mode="search"`:普通事实/偏好/历史内容检索。适合问“她喜欢什么”“我们之前讨论过什么”。 -- `mode="time"`:按时间范围检索。适合问“昨天发生了什么”“最近7天有哪些相关记忆”。 -- `mode="episode"`:按事件/情节检索。适合问“那次灯塔停电的经过是什么”“关于某次经历还有什么”。 -- `mode="aggregate"`:综合检索。适合问“帮我整体回忆一下这个人最近的情况”“把相关线索综合找出来”。 - -模式选择建议: -- 问单点事实、偏好、人设、具体信息:优先 `search` -- 问某段时间发生了什么:优先 `time` -- 问某次事件、某段经历、某个剧情片段:优先 `episode` -- 问整体回忆、综合找线索、总结最近发生的事:优先 `aggregate` - -时间模式要求: -- 使用 `mode="time"` 时,必须填写 `time_expression` -- 可用时间表达包括:`今天`、`昨天`、`前天`、`本周`、`上周`、`本月`、`上月`、`最近7天` -- 也可以使用绝对时间:`2026/03/18`、`2026/03/18 09:30` diff --git a/src/chat/replyer/group_generator.py b/src/chat/replyer/group_generator.py index 7aeae4ab..487ec881 100644 --- a/src/chat/replyer/group_generator.py +++ b/src/chat/replyer/group_generator.py @@ -15,6 +15,7 @@ from src.services.llm_service import LLMServiceClient from maim_message import BaseMessageInfo, MessageBase, Seg, UserInfo as MaimUserInfo from src.common.data_models.mai_message_data_model import MaiMessage +from src.common.utils.utils_session import SessionUtils from src.chat.message_receive.message import SessionMessage from src.chat.message_receive.chat_manager import BotChatSession from src.chat.utils.timer_calculator import Timer # <--- Import Timer @@ -566,7 +567,7 @@ class DefaultReplyer: prompt_personality = f"{prompt_personality};" return f"你的名字是{bot_name}{bot_nickname},你{prompt_personality}" - def _parse_chat_prompt_config_to_chat_id(self, chat_prompt_str: str) -> Optional[tuple[str, str]]: + def _parse_chat_prompt_config_to_chat_id(self, chat_prompt_str: str) -> Optional[tuple[str, bool, str]]: """ 解析聊天prompt配置字符串并生成对应的 chat_id 和 prompt内容 @@ -574,7 +575,7 @@ class DefaultReplyer: chat_prompt_str: 格式为 "platform:id:type:prompt内容" 的字符串 Returns: - tuple: (chat_id, prompt_content),如果解析失败则返回 None + tuple: (chat_id, is_group_chat, prompt_content),如果解析失败则返回 None """ try: # 使用 split 分割,但限制分割次数为3,因为prompt内容可能包含冒号 @@ -590,19 +591,35 @@ class DefaultReplyer: # 判断是否为群聊 is_group = stream_type == "group" - # 使用 ChatManager 提供的接口生成 chat_id,避免在此重复实现逻辑 - from src.common.utils.utils_session import SessionUtils - chat_id = SessionUtils.calculate_session_id( platform, group_id=str(id_str) if is_group else None, user_id=str(id_str) if not is_group else None, ) - return chat_id, prompt_content + return chat_id, is_group, prompt_content except (ValueError, IndexError): return None + def _build_chat_attention_block(self, chat_id: str) -> str: + """构建当前聊天场景下的额外注意事项块。""" + prompt_lines: List[str] = [] + + if self.is_group_chat is True: + if group_chat_prompt := global_config.chat.group_chat_prompt.strip(): + prompt_lines.append(f"通用注意事项:\n{group_chat_prompt}") + elif self.is_group_chat is False: + if private_chat_prompt := global_config.chat.private_chat_prompts.strip(): + prompt_lines.append(f"通用注意事项:\n{private_chat_prompt}") + + if chat_prompt := self.get_chat_prompt_for_chat(chat_id).strip(): + prompt_lines.append(f"当前聊天额外注意事项:\n{chat_prompt}") + + if not prompt_lines: + return "" + + return "在该聊天中的注意事项:\n" + "\n\n".join(prompt_lines) + "\n" + def get_chat_prompt_for_chat(self, chat_id: str) -> str: """根据聊天流 ID 获取匹配的额外 prompt。""" if not global_config.chat.chat_prompts: @@ -610,13 +627,16 @@ class DefaultReplyer: for chat_prompt_item in global_config.chat.chat_prompts: if hasattr(chat_prompt_item, "rule_type") and hasattr(chat_prompt_item, "prompt"): - if str(chat_prompt_item.rule_type or "").strip() != "group": + rule_type = str(chat_prompt_item.rule_type or "").strip() + if self.is_group_chat is True and rule_type != "group": + continue + if self.is_group_chat is False and rule_type != "private": continue config_chat_id = self._build_chat_uid( str(chat_prompt_item.platform or "").strip(), str(chat_prompt_item.item_id or "").strip(), - True, + rule_type == "group", ) prompt_content = str(chat_prompt_item.prompt or "").strip() if config_chat_id == chat_id and prompt_content: @@ -629,14 +649,18 @@ class DefaultReplyer: # 兼容旧格式的 platform:id:type:prompt 配置字符串。 parts = chat_prompt_item.split(":", 3) - if len(parts) != 4 or parts[2] != "group": + if len(parts) != 4: continue result = self._parse_chat_prompt_config_to_chat_id(chat_prompt_item) if result is None: continue - config_chat_id, prompt_content = result + config_chat_id, config_is_group, prompt_content = result + if self.is_group_chat is True and not config_is_group: + continue + if self.is_group_chat is False and config_is_group: + continue if config_chat_id == chat_id: logger.debug(f"匹配到群聊 prompt 配置,chat_id: {chat_id}, prompt: {prompt_content[:50]}...") return prompt_content @@ -844,8 +868,7 @@ class DefaultReplyer: ) # 获取匹配的额外prompt - chat_prompt_content = self.get_chat_prompt_for_chat(chat_id) - chat_prompt_block = f"{chat_prompt_content}\n" if chat_prompt_content else "" + chat_prompt_block = self._build_chat_attention_block(chat_id) # 根据think_level选择不同的回复模板 # think_level=0: 轻量回复(简短平淡) diff --git a/src/chat/replyer/maisaka_generator.py b/src/chat/replyer/maisaka_generator.py index 5c87dace..d7eacd52 100644 --- a/src/chat/replyer/maisaka_generator.py +++ b/src/chat/replyer/maisaka_generator.py @@ -9,6 +9,7 @@ from rich.panel import Panel from src.chat.message_receive.chat_manager import BotChatSession from src.chat.message_receive.message import SessionMessage +from src.chat.utils.utils import get_chat_type_and_target_info from src.cli.console import console from src.common.data_models.reply_generation_data_models import ( GenerationMetrics, @@ -18,6 +19,7 @@ from src.common.data_models.reply_generation_data_models import ( ) from src.common.logger import get_logger from src.common.prompt_i18n import load_prompt +from src.common.utils.utils_session import SessionUtils from src.config.config import global_config from src.core.types import ActionInfo from src.services.llm_service import LLMServiceClient @@ -182,22 +184,96 @@ class MaisakaReplyGenerator: "- 你这次要回复的就是这条目标消息,请结合整段上下文理解,但不要误把其他历史消息当成当前回复对象。" ) + @staticmethod + def _get_chat_prompt_for_chat(chat_id: str, is_group_chat: Optional[bool]) -> str: + """根据聊天流 ID 获取匹配的额外 prompt。""" + if not global_config.chat.chat_prompts: + return "" + + for chat_prompt_item in global_config.chat.chat_prompts: + if hasattr(chat_prompt_item, "platform"): + platform = str(chat_prompt_item.platform or "").strip() + item_id = str(chat_prompt_item.item_id or "").strip() + rule_type = str(chat_prompt_item.rule_type or "").strip() + prompt_content = str(chat_prompt_item.prompt or "").strip() + elif isinstance(chat_prompt_item, str): + parts = chat_prompt_item.split(":", 3) + if len(parts) != 4: + continue + + platform, item_id, rule_type, prompt_content = parts + platform = platform.strip() + item_id = item_id.strip() + rule_type = rule_type.strip() + prompt_content = prompt_content.strip() + else: + continue + + if not platform or not item_id or not prompt_content: + continue + + if rule_type == "group": + config_is_group = True + config_chat_id = SessionUtils.calculate_session_id(platform, group_id=item_id) + elif rule_type == "private": + config_is_group = False + config_chat_id = SessionUtils.calculate_session_id(platform, user_id=item_id) + else: + continue + + if config_is_group != is_group_chat: + continue + if config_chat_id == chat_id: + return prompt_content + + return "" + + def _build_group_chat_attention_block(self, session_id: str) -> str: + """构建当前聊天场景下的额外注意事项块。""" + if not session_id: + return "" + + try: + is_group_chat, _ = get_chat_type_and_target_info(session_id) + except Exception: + is_group_chat = None + + prompt_lines: List[str] = [] + + if is_group_chat is True: + if group_chat_prompt := global_config.chat.group_chat_prompt.strip(): + prompt_lines.append(f"通用注意事项:\n{group_chat_prompt}") + elif is_group_chat is False: + if private_chat_prompt := global_config.chat.private_chat_prompts.strip(): + prompt_lines.append(f"通用注意事项:\n{private_chat_prompt}") + + if chat_prompt := self._get_chat_prompt_for_chat(session_id, is_group_chat).strip(): + prompt_lines.append(f"当前聊天额外注意事项:\n{chat_prompt}") + + if not prompt_lines: + return "" + + return "在该聊天中的注意事项:\n" + "\n\n".join(prompt_lines) + "\n" + def _build_prompt( self, chat_history: List[LLMContextMessage], reply_message: Optional[SessionMessage], reply_reason: str, expression_habits: str = "", + stream_id: Optional[str] = None, ) -> str: """构建 Maisaka replyer 提示词。""" current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") formatted_history = self._format_chat_history(chat_history) target_message_block = self._build_target_message_block(reply_message) + session_id = self._resolve_session_id(stream_id) try: system_prompt = load_prompt( "maisaka_replyer", bot_name=global_config.bot.nickname, + group_chat_attention_block=self._build_group_chat_attention_block(session_id), time_block=f"当前时间:{current_time}", identity=self._personality_prompt, reply_style=global_config.personality.reply_style, @@ -354,6 +430,7 @@ class MaisakaReplyGenerator: reply_message=reply_message, reply_reason=reply_reason or "", expression_habits=merged_expression_habits, + stream_id=stream_id, ) except Exception as exc: import traceback diff --git a/src/chat/replyer/maisaka_generator_multi.py b/src/chat/replyer/maisaka_generator_multi.py index 79747789..8f05a6f3 100644 --- a/src/chat/replyer/maisaka_generator_multi.py +++ b/src/chat/replyer/maisaka_generator_multi.py @@ -10,6 +10,7 @@ from rich.text import Text from src.chat.message_receive.chat_manager import BotChatSession from src.chat.message_receive.message import SessionMessage +from src.chat.utils.utils import get_chat_type_and_target_info from src.cli.console import console from src.common.data_models.message_component_data_model import MessageSequence, TextComponent from src.common.data_models.reply_generation_data_models import ( @@ -20,6 +21,7 @@ from src.common.data_models.reply_generation_data_models import ( ) from src.common.logger import get_logger from src.common.prompt_i18n import load_prompt +from src.common.utils.utils_session import SessionUtils from src.config.config import global_config from src.core.types import ActionInfo from src.llm_models.payload_content.message import ( @@ -152,19 +154,93 @@ class MaisakaReplyGenerator: "- 你这次要回复的就是这条目标消息,请结合整段上下文理解,但不要把其他历史消息当成当前回复对象。" ) + @staticmethod + def _get_chat_prompt_for_chat(chat_id: str, is_group_chat: Optional[bool]) -> str: + """根据聊天流 ID 获取匹配的额外 prompt。""" + if not global_config.chat.chat_prompts: + return "" + + for chat_prompt_item in global_config.chat.chat_prompts: + if hasattr(chat_prompt_item, "platform"): + platform = str(chat_prompt_item.platform or "").strip() + item_id = str(chat_prompt_item.item_id or "").strip() + rule_type = str(chat_prompt_item.rule_type or "").strip() + prompt_content = str(chat_prompt_item.prompt or "").strip() + elif isinstance(chat_prompt_item, str): + parts = chat_prompt_item.split(":", 3) + if len(parts) != 4: + continue + + platform, item_id, rule_type, prompt_content = parts + platform = platform.strip() + item_id = item_id.strip() + rule_type = rule_type.strip() + prompt_content = prompt_content.strip() + else: + continue + + if not platform or not item_id or not prompt_content: + continue + + if rule_type == "group": + config_is_group = True + config_chat_id = SessionUtils.calculate_session_id(platform, group_id=item_id) + elif rule_type == "private": + config_is_group = False + config_chat_id = SessionUtils.calculate_session_id(platform, user_id=item_id) + else: + continue + + if config_is_group != is_group_chat: + continue + if config_chat_id == chat_id: + return prompt_content + + return "" + + def _build_group_chat_attention_block(self, session_id: str) -> str: + """构建当前聊天场景下的额外注意事项块。""" + if not session_id: + return "" + + try: + is_group_chat, _ = get_chat_type_and_target_info(session_id) + except Exception: + is_group_chat = None + + prompt_lines: List[str] = [] + + if is_group_chat is True: + if group_chat_prompt := global_config.chat.group_chat_prompt.strip(): + prompt_lines.append(f"通用注意事项:\n{group_chat_prompt}") + elif is_group_chat is False: + if private_chat_prompt := global_config.chat.private_chat_prompts.strip(): + prompt_lines.append(f"通用注意事项:\n{private_chat_prompt}") + + if chat_prompt := self._get_chat_prompt_for_chat(session_id, is_group_chat).strip(): + prompt_lines.append(f"当前聊天额外注意事项:\n{chat_prompt}") + + if not prompt_lines: + return "" + + return "在该聊天中的注意事项:\n" + "\n\n".join(prompt_lines) + "\n" + def _build_system_prompt( self, reply_message: Optional[SessionMessage], reply_reason: str, expression_habits: str = "", + stream_id: Optional[str] = None, ) -> str: current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") target_message_block = self._build_target_message_block(reply_message) + session_id = self._resolve_session_id(stream_id) try: system_prompt = load_prompt( "maisaka_replyer", bot_name=global_config.bot.nickname, + group_chat_attention_block=self._build_group_chat_attention_block(session_id), time_block=f"当前时间:{current_time}", identity=self._personality_prompt, reply_style=global_config.personality.reply_style, @@ -266,12 +342,14 @@ class MaisakaReplyGenerator: reply_message: Optional[SessionMessage], reply_reason: str, expression_habits: str = "", + stream_id: Optional[str] = None, ) -> List[Message]: messages: List[Message] = [] system_prompt = self._build_system_prompt( reply_message=reply_message, reply_reason=reply_reason, expression_habits=expression_habits, + stream_id=stream_id, ) instruction = self._build_reply_instruction() @@ -421,6 +499,7 @@ class MaisakaReplyGenerator: reply_message=reply_message, reply_reason=reply_reason or "", expression_habits=merged_expression_habits, + stream_id=stream_id, ) except Exception as exc: import traceback diff --git a/src/config/config.py b/src/config/config.py index 0b09b6ca..6684f998 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -55,7 +55,7 @@ CONFIG_DIR: Path = PROJECT_ROOT / "config" BOT_CONFIG_PATH: Path = (CONFIG_DIR / "bot_config.toml").resolve().absolute() MODEL_CONFIG_PATH: Path = (CONFIG_DIR / "model_config.toml").resolve().absolute() MMC_VERSION: str = "1.0.0" -CONFIG_VERSION: str = "8.5.2" +CONFIG_VERSION: str = "8.5.3" MODEL_CONFIG_VERSION: str = "1.13.1" logger = get_logger("config") diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 58d577f6..b6db112a 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -239,16 +239,12 @@ class ChatConfig(ConfigBase): ) """Planner 连续被新消息打断的最大次数,0 表示不启用打断""" - plan_reply_log_max_per_chat: int = Field( - default=1024, - json_schema_extra={ - "x-widget": "input", - "x-icon": "file-text", - }, - ) - """每个聊天流最大保存的Plan/Reply日志数量,超过此数量时会自动删除最老的日志""" group_chat_prompt: str = Field( - default="你需要控制自己发言的频率,如果是一对一聊天,可以以较均匀的频率发言;如果用户较多,不要每句都回复,控制回复频率,不要回复的太频繁!控制回复的频率,不要每个人的消息都回复。", + default=""" +你正在qq群里聊天,下面是群里正在聊的内容,其中包含聊天记录和聊天中的图片。 +回复尽量简短一些。最好一次对一个话题进行回复,免得啰嗦或者回复内容太乱。请注意把握聊天内容。 +不要回复的太频繁!控制回复的频率,不要每个人的消息都回复,只回复你感兴趣的或者主动提及你的。 +""", json_schema_extra={ "x-widget": "textarea", "x-icon": "users", @@ -257,7 +253,11 @@ class ChatConfig(ConfigBase): """_wrap_群聊通用注意事项""" private_chat_prompts: str = Field( - default="你需要控制自己发言的频率,可以以较均匀的频率发言。", + default=""" +你正在聊天,下面是正在聊的内容,其中包含聊天记录和聊天中的图片。 +回复尽量简短一些。请注意把握聊天内容。 +请考虑对方的发言频率,想法,思考自己何时回复以及回复内容。 +""", json_schema_extra={ "x-widget": "textarea", "x-icon": "user", diff --git a/src/maisaka/prompt_preview_logger.py b/src/maisaka/prompt_preview_logger.py index 35917156..3e755e5a 100644 --- a/src/maisaka/prompt_preview_logger.py +++ b/src/maisaka/prompt_preview_logger.py @@ -8,22 +8,14 @@ from pathlib import Path from typing import Dict from uuid import uuid4 -from src.config.config import global_config - - class PromptPreviewLogger: """负责保存 Maisaka Prompt 预览文件并控制目录容量。""" _BASE_DIR = Path("logs") / "maisaka_prompt" + _MAX_PREVIEW_GROUPS_PER_CHAT = 1024 _TRIM_COUNT = 100 _SAFE_NAME_PATTERN = re.compile(r"[^A-Za-z0-9._-]+") - @classmethod - def _get_max_per_chat(cls) -> int: - """从配置中获取每个聊天流最大保存的预览数量。""" - - return getattr(global_config.chat, "plan_reply_log_max_per_chat", 1000) - @classmethod def _normalize_chat_id(cls, chat_id: str) -> str: normalized_chat_id = cls._SAFE_NAME_PATTERN.sub("_", str(chat_id or "").strip()).strip("._") @@ -65,15 +57,14 @@ class PromptPreviewLogger: continue grouped_files.setdefault(file_path.stem, []).append(file_path) - max_per_chat = cls._get_max_per_chat() - if len(grouped_files) <= max_per_chat: + if len(grouped_files) <= cls._MAX_PREVIEW_GROUPS_PER_CHAT: return sorted_groups = sorted( grouped_files.items(), key=lambda item: min(path.stat().st_mtime for path in item[1]), ) - overflow_count = len(grouped_files) - max_per_chat + overflow_count = len(grouped_files) - cls._MAX_PREVIEW_GROUPS_PER_CHAT trim_count = min(len(sorted_groups), max(cls._TRIM_COUNT, overflow_count)) for _, file_group in sorted_groups[:trim_count]: for old_file in file_group: