diff --git a/scripts/replyer_action_stats.py b/scripts/replyer_action_stats.py new file mode 100644 index 00000000..f155d98e --- /dev/null +++ b/scripts/replyer_action_stats.py @@ -0,0 +1,304 @@ +""" +统计和展示 replyer 动作选择记录 + +用法: + python scripts/replyer_action_stats.py +""" + +import json +import os +import sys +from collections import Counter, defaultdict +from datetime import datetime +from typing import Dict, List, Any +from pathlib import Path + +# Add project root to Python path +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, project_root) + +try: + from src.common.database.database_model import ChatStreams + from src.chat.message_receive.chat_stream import get_chat_manager +except ImportError: + ChatStreams = None + get_chat_manager = None + + +def get_chat_name(chat_id: str) -> str: + """根据 chat_id 获取聊天名称""" + try: + if ChatStreams: + chat_stream = ChatStreams.get_or_none(ChatStreams.stream_id == chat_id) + if chat_stream: + if chat_stream.group_name: + return f"{chat_stream.group_name}" + elif chat_stream.user_nickname: + return f"{chat_stream.user_nickname}的私聊" + + if get_chat_manager: + chat_manager = get_chat_manager() + stream_name = chat_manager.get_stream_name(chat_id) + if stream_name: + return stream_name + + return f"未知聊天 ({chat_id[:8]}...)" + except Exception: + return f"查询失败 ({chat_id[:8]}...)" + + +def load_records(temp_dir: str = "data/temp") -> List[Dict[str, Any]]: + """加载所有 replyer 动作记录""" + records = [] + temp_path = Path(temp_dir) + + if not temp_path.exists(): + print(f"目录不存在: {temp_dir}") + return records + + # 查找所有 replyer_action_*.json 文件 + pattern = "replyer_action_*.json" + for file_path in temp_path.glob(pattern): + try: + with open(file_path, "r", encoding="utf-8") as f: + data = json.load(f) + records.append(data) + except Exception as e: + print(f"读取文件失败 {file_path}: {e}") + + # 按时间戳排序 + records.sort(key=lambda x: x.get("timestamp", "")) + return records + + +def format_timestamp(ts: str) -> str: + """格式化时间戳""" + try: + dt = datetime.fromisoformat(ts) + return dt.strftime("%Y-%m-%d %H:%M:%S") + except Exception: + return ts + + +def calculate_time_distribution(records: List[Dict[str, Any]]) -> Dict[str, int]: + """计算时间分布""" + now = datetime.now() + distribution = { + "今天": 0, + "昨天": 0, + "3天内": 0, + "7天内": 0, + "30天内": 0, + "更早": 0, + } + + for record in records: + try: + ts = record.get("timestamp", "") + if not ts: + continue + dt = datetime.fromisoformat(ts) + diff = (now - dt).days + + if diff == 0: + distribution["今天"] += 1 + elif diff == 1: + distribution["昨天"] += 1 + elif diff < 3: + distribution["3天内"] += 1 + elif diff < 7: + distribution["7天内"] += 1 + elif diff < 30: + distribution["30天内"] += 1 + else: + distribution["更早"] += 1 + except Exception: + pass + + return distribution + + +def print_statistics(records: List[Dict[str, Any]]): + """打印统计信息""" + if not records: + print("没有找到任何记录") + return + + print("=" * 80) + print("Replyer 动作选择记录统计") + print("=" * 80) + print() + + # 总记录数 + total_count = len(records) + print(f"📊 总记录数: {total_count}") + print() + + # 时间范围 + timestamps = [r.get("timestamp", "") for r in records if r.get("timestamp")] + if timestamps: + first_time = format_timestamp(min(timestamps)) + last_time = format_timestamp(max(timestamps)) + print(f"📅 时间范围: {first_time} ~ {last_time}") + print() + + # 按 think_level 统计 + think_levels = [r.get("think_level", 0) for r in records] + think_level_counter = Counter(think_levels) + print("🧠 思考深度分布:") + for level in sorted(think_level_counter.keys()): + count = think_level_counter[level] + percentage = (count / total_count) * 100 + level_name = {0: "不需要思考", 1: "简单思考", 2: "深度思考"}.get(level, f"未知({level})") + print(f" Level {level} ({level_name}): {count} 次 ({percentage:.1f}%)") + print() + + # 按 chat_id 统计(总体) + chat_counter = Counter([r.get("chat_id", "未知") for r in records]) + print(f"💬 聊天分布 (共 {len(chat_counter)} 个聊天):") + # 只显示前10个 + for chat_id, count in chat_counter.most_common(10): + chat_name = get_chat_name(chat_id) + percentage = (count / total_count) * 100 + print(f" {chat_name}: {count} 次 ({percentage:.1f}%)") + if len(chat_counter) > 10: + print(f" ... 还有 {len(chat_counter) - 10} 个聊天") + print() + + # 每个 chat_id 的详细统计 + print("=" * 80) + print("每个聊天的详细统计") + print("=" * 80) + print() + + # 按 chat_id 分组记录 + records_by_chat = defaultdict(list) + for record in records: + chat_id = record.get("chat_id", "未知") + records_by_chat[chat_id].append(record) + + # 按记录数排序 + sorted_chats = sorted(records_by_chat.items(), key=lambda x: len(x[1]), reverse=True) + + for chat_id, chat_records in sorted_chats: + chat_name = get_chat_name(chat_id) + chat_count = len(chat_records) + chat_percentage = (chat_count / total_count) * 100 + + print(f"📱 {chat_name} ({chat_id[:8]}...)") + print(f" 总记录数: {chat_count} ({chat_percentage:.1f}%)") + + # 该聊天的 think_level 分布 + chat_think_levels = [r.get("think_level", 0) for r in chat_records] + chat_think_counter = Counter(chat_think_levels) + print(" 思考深度分布:") + for level in sorted(chat_think_counter.keys()): + level_count = chat_think_counter[level] + level_percentage = (level_count / chat_count) * 100 + level_name = {0: "不需要思考", 1: "简单思考", 2: "深度思考"}.get(level, f"未知({level})") + print(f" Level {level} ({level_name}): {level_count} 次 ({level_percentage:.1f}%)") + + # 该聊天的时间范围 + chat_timestamps = [r.get("timestamp", "") for r in chat_records if r.get("timestamp")] + if chat_timestamps: + first_time = format_timestamp(min(chat_timestamps)) + last_time = format_timestamp(max(chat_timestamps)) + print(f" 时间范围: {first_time} ~ {last_time}") + + # 该聊天的时间分布 + chat_time_dist = calculate_time_distribution(chat_records) + print(" 时间分布:") + for period, count in chat_time_dist.items(): + if count > 0: + period_percentage = (count / chat_count) * 100 + print(f" {period}: {count} 次 ({period_percentage:.1f}%)") + + # 显示该聊天最近的一条理由示例 + if chat_records: + latest_record = chat_records[-1] + reason = latest_record.get("reason", "无理由") + if len(reason) > 120: + reason = reason[:120] + "..." + timestamp = format_timestamp(latest_record.get("timestamp", "")) + think_level = latest_record.get("think_level", 0) + print(f" 最新记录 [{timestamp}] (Level {think_level}): {reason}") + + print() + + # 时间分布 + time_dist = calculate_time_distribution(records) + print("⏰ 时间分布:") + for period, count in time_dist.items(): + if count > 0: + percentage = (count / total_count) * 100 + print(f" {period}: {count} 次 ({percentage:.1f}%)") + print() + + # 显示一些示例理由 + print("📝 示例理由 (最近5条):") + recent_records = records[-5:] + for i, record in enumerate(recent_records, 1): + reason = record.get("reason", "无理由") + think_level = record.get("think_level", 0) + timestamp = format_timestamp(record.get("timestamp", "")) + chat_id = record.get("chat_id", "未知") + chat_name = get_chat_name(chat_id) + + # 截断过长的理由 + if len(reason) > 100: + reason = reason[:100] + "..." + + print(f" {i}. [{timestamp}] {chat_name} (Level {think_level})") + print(f" {reason}") + print() + + # 按 think_level 分组显示理由示例 + print("=" * 80) + print("按思考深度分类的示例理由") + print("=" * 80) + print() + + for level in [0, 1, 2]: + level_records = [r for r in records if r.get("think_level") == level] + if not level_records: + continue + + level_name = {0: "不需要思考", 1: "简单思考", 2: "深度思考"}.get(level, f"未知({level})") + print(f"Level {level} ({level_name}) - 共 {len(level_records)} 条:") + + # 显示3个示例(选择最近的) + examples = level_records[-3:] if len(level_records) >= 3 else level_records + for i, record in enumerate(examples, 1): + reason = record.get("reason", "无理由") + if len(reason) > 150: + reason = reason[:150] + "..." + timestamp = format_timestamp(record.get("timestamp", "")) + chat_id = record.get("chat_id", "未知") + chat_name = get_chat_name(chat_id) + print(f" {i}. [{timestamp}] {chat_name}") + print(f" {reason}") + print() + + # 统计信息汇总 + print("=" * 80) + print("统计汇总") + print("=" * 80) + print(f"总记录数: {total_count}") + print(f"涉及聊天数: {len(chat_counter)}") + if chat_counter: + avg_count = total_count / len(chat_counter) + print(f"平均每个聊天记录数: {avg_count:.1f}") + else: + print("平均每个聊天记录数: N/A") + print() + + +def main(): + """主函数""" + records = load_records() + print_statistics(records) + + +if __name__ == "__main__": + main() + diff --git a/src/express/expression_learner.py b/src/bw_learner/expression_learner.py similarity index 66% rename from src/express/expression_learner.py rename to src/bw_learner/expression_learner.py index 58b2cd6d..71866dea 100644 --- a/src/express/expression_learner.py +++ b/src/bw_learner/expression_learner.py @@ -3,19 +3,18 @@ import json import os import re import asyncio -from typing import List, Optional, Tuple -import traceback +from typing import List, Optional, Tuple, Any, Dict from src.common.logger import get_logger from src.common.database.database_model import Expression from src.llm_models.utils_model import LLMRequest from src.config.config import model_config, global_config from src.chat.utils.chat_message_builder import ( - get_raw_msg_by_timestamp_with_chat_inclusive, build_anonymous_messages, ) from src.chat.utils.prompt_builder import Prompt, global_prompt_manager from src.chat.message_receive.chat_stream import get_chat_manager -from src.express.express_utils import filter_message_content +from src.bw_learner.learner_utils import filter_message_content, is_bot_message, build_context_paragraph, contains_bot_self_name +from src.bw_learner.jargon_miner import miner_manager from json_repair import repair_json @@ -26,33 +25,52 @@ logger = get_logger("expressor") def init_prompt() -> None: learn_style_prompt = """{chat_str} - -请从上面这段群聊中概括除了人名为"SELF"之外的人的语言风格。 -每一行消息前面的方括号中的数字(如 [1]、[2])是该行消息的唯一编号,请在输出中引用这些编号来标注“表达方式的来源行”。 +你的名字是{bot_name},现在请你完成两个提取任务 +任务1:请从上面这段群聊中用户的语言风格和说话方式 1. 只考虑文字,不要考虑表情包和图片 -2. 不要涉及具体的人名,但是可以涉及具体名词 -3. 思考有没有特殊的梗,一并总结成语言风格 -4. 例子仅供参考,请严格根据群聊内容总结!!! +2. 不要总结SELF的发言 +3. 不要涉及具体的人名,也不要涉及具体名词 +4. 思考有没有特殊的梗,一并总结成语言风格 +5. 例子仅供参考,请严格根据群聊内容总结!!! 注意:总结成如下格式的规律,总结的内容要详细,但具有概括性: -例如:当"AAAAA"时,可以"BBBBB", AAAAA代表某个具体的场景,不超过20个字。BBBBB代表对应的语言风格,特定句式或表达方式,不超过20个字。 +例如:当"AAAAA"时,可以"BBBBB", AAAAA代表某个场景,不超过20个字。BBBBB代表对应的语言风格,特定句式或表达方式,不超过20个字。 +表达方式在3-5个左右,不要超过10个 + + +任务2:请从上面这段聊天内容中提取"可能是黑话"的候选项(黑话/俚语/网络缩写/口头禅)。 +- 必须为对话中真实出现过的短词或短语 +- 必须是你无法理解含义的词语,没有明确含义的词语,请不要选择有明确含义,或者含义清晰的词语 +- 排除:人名、@、表情包/图片中的内容、纯标点、常规功能词(如的、了、呢、啊等) +- 每个词条长度建议 2-8 个字符(不强制),尽量短小 +- 请你提取出可能的黑话,最多30个黑话,请尽量提取所有 + +黑话必须为以下几种类型: +- 由字母构成的,汉语拼音首字母的简写词,例如:nb、yyds、xswl +- 英文词语的缩写,用英文字母概括一个词汇或含义,例如:CPU、GPU、API +- 中文词语的缩写,用几个汉字概括一个词汇或含义,例如:社死、内卷 + +输出要求: +将表达方式,语言风格和黑话以 JSON 数组输出,每个元素为一个对象,结构如下(注意字段名): +注意请不要输出重复内容,请对表达方式和黑话进行去重。 -请严格以 JSON 数组的形式输出结果,每个元素为一个对象,结构如下(注意字段名): [ {{"situation": "AAAAA", "style": "BBBBB", "source_id": "3"}}, {{"situation": "CCCC", "style": "DDDD", "source_id": "7"}} {{"situation": "对某件事表示十分惊叹", "style": "使用 我嘞个xxxx", "source_id": "[消息编号]"}}, {{"situation": "表示讽刺的赞同,不讲道理", "style": "对对对", "source_id": "[消息编号]"}}, {{"situation": "当涉及游戏相关时,夸赞,略带戏谑意味", "style": "使用 这么强!", "source_id": "[消息编号]"}}, + {{"content": "词条", "source_id": "12"}}, + {{"content": "词条2", "source_id": "5"}} ] -请注意: -- 不要总结你自己(SELF)的发言,尽量保证总结内容的逻辑性 -- 请只针对最重要的若干条表达方式进行总结,避免输出太多重复或相似的条目 - 其中: +表达方式条目: - situation:表示“在什么情境下”的简短概括(不超过20个字) - style:表示对应的语言风格或常用表达(不超过20个字) - source_id:该表达方式对应的“来源行编号”,即上方聊天记录中方括号里的数字(例如 [3]),请只输出数字本身,不要包含方括号 +黑话jargon条目: +- content:表示黑话的内容 +- source_id:该黑话对应的“来源行编号”,即上方聊天记录中方括号里的数字(例如 [3]),请只输出数字本身,不要包含方括号 现在请你输出 JSON: """ @@ -69,110 +87,107 @@ class ExpressionLearner: self.summary_model: LLMRequest = LLMRequest( model_set=model_config.model_task_config.utils_small, request_type="expression.summary" ) - self.embedding_model: LLMRequest = LLMRequest( - model_set=model_config.model_task_config.embedding, request_type="expression.embedding" - ) self.chat_id = chat_id self.chat_stream = get_chat_manager().get_stream(chat_id) self.chat_name = get_chat_manager().get_stream_name(chat_id) or chat_id - # 维护每个chat的上次学习时间 - self.last_learning_time: float = time.time() - # 学习锁,防止并发执行学习任务 self._learning_lock = asyncio.Lock() - # 学习参数 - _, self.enable_learning, self.learning_intensity = global_config.expression.get_expression_config_for_chat( - self.chat_id - ) - # 防止除以零:如果学习强度为0或负数,使用最小值0.0001 - if self.learning_intensity <= 0: - logger.warning(f"学习强度为 {self.learning_intensity},已自动调整为 0.0001 以避免除以零错误") - self.learning_intensity = 0.0000001 - self.min_messages_for_learning = 15 / self.learning_intensity # 触发学习所需的最少消息数 - self.min_learning_interval = 120 / self.learning_intensity - - def should_trigger_learning(self) -> bool: - """ - 检查是否应该触发学习 - - Args: - chat_id: 聊天流ID - - Returns: - bool: 是否应该触发学习 - """ - # 检查是否允许学习 - if not self.enable_learning: - return False - - # 检查时间间隔 - time_diff = time.time() - self.last_learning_time - if time_diff < self.min_learning_interval: - return False - - # 检查消息数量(只检查指定聊天流的消息) - recent_messages = get_raw_msg_by_timestamp_with_chat_inclusive( - chat_id=self.chat_id, - timestamp_start=self.last_learning_time, - timestamp_end=time.time(), - ) - - if not recent_messages or len(recent_messages) < self.min_messages_for_learning: - return False - - return True - - async def trigger_learning_for_chat(self): - """ - 为指定聊天流触发学习 - - Args: - chat_id: 聊天流ID - - Returns: - bool: 是否成功触发学习 - """ - # 使用异步锁防止并发执行 - async with self._learning_lock: - # 在锁内检查,避免并发触发 - # 如果锁被持有,其他协程会等待,但等待期间条件可能已变化,所以需要再次检查 - if not self.should_trigger_learning(): - return - - # 保存学习开始前的时间戳,用于获取消息范围 - learning_start_timestamp = time.time() - previous_learning_time = self.last_learning_time - - # 立即更新学习时间,防止并发触发 - self.last_learning_time = learning_start_timestamp - - try: - logger.info(f"在聊天流 {self.chat_name} 学习表达方式") - # 学习语言风格,传递学习开始前的时间戳 - learnt_style = await self.learn_and_store(num=25, timestamp_start=previous_learning_time) - - if learnt_style: - logger.info(f"聊天流 {self.chat_name} 表达学习完成") - else: - logger.warning(f"聊天流 {self.chat_name} 表达学习未获得有效结果") - - except Exception as e: - logger.error(f"为聊天流 {self.chat_name} 触发学习失败: {e}") - traceback.print_exc() - # 即使失败也保持时间戳更新,避免频繁重试 - return - - async def learn_and_store(self, num: int = 10, timestamp_start: Optional[float] = None) -> List[Tuple[str, str, str]]: + async def learn_and_store( + self, + messages: List[Any], + ) -> List[Tuple[str, str, str]]: """ 学习并存储表达方式 Args: + messages: 外部传入的消息列表(必需) num: 学习数量 timestamp_start: 学习开始的时间戳,如果为None则使用self.last_learning_time """ - learnt_expressions = await self.learn_expression(num, timestamp_start=timestamp_start) + if not messages: + return None + + random_msg = messages + + # 学习用(开启行编号,便于溯源) + random_msg_str: str = await build_anonymous_messages(random_msg, show_ids=True) + + prompt: str = await global_prompt_manager.format_prompt( + "learn_style_prompt", + bot_name=global_config.bot.nickname, + chat_str=random_msg_str, + ) + + # print(f"random_msg_str:{random_msg_str}") + # logger.info(f"学习{type_str}的prompt: {prompt}") + + try: + response, _ = await self.express_learn_model.generate_response_async(prompt, temperature=0.3) + except Exception as e: + logger.error(f"学习表达方式失败,模型生成出错: {e}") + return None + + # 解析 LLM 返回的表达方式列表和黑话列表(包含来源行编号) + expressions: List[Tuple[str, str, str]] + jargon_entries: List[Tuple[str, str]] # (content, source_id) + expressions, jargon_entries = self.parse_expression_response(response) + expressions = self._filter_self_reference_styles(expressions) + + # 检查表达方式数量,如果超过10个则放弃本次表达学习 + if len(expressions) > 10: + logger.info(f"表达方式提取数量超过10个(实际{len(expressions)}个),放弃本次表达学习") + expressions = [] + + # 检查黑话数量,如果超过30个则放弃本次黑话学习 + if len(jargon_entries) > 30: + logger.info(f"黑话提取数量超过30个(实际{len(jargon_entries)}个),放弃本次黑话学习") + jargon_entries = [] + + # 处理黑话条目,路由到 jargon_miner(即使没有表达方式也要处理黑话) + if jargon_entries: + await self._process_jargon_entries(jargon_entries, random_msg) + + # 如果没有表达方式,直接返回 + if not expressions: + logger.info("过滤后没有可用的表达方式(style 与机器人名称重复)") + return [] + + logger.info(f"学习的prompt: {prompt}") + logger.info(f"学习的expressions: {expressions}") + logger.info(f"学习的jargon_entries: {jargon_entries}") + logger.info(f"学习的response: {response}") + + # 直接根据 source_id 在 random_msg 中溯源,获取 context + filtered_expressions: List[Tuple[str, str, str]] = [] # (situation, style, context) + + for situation, style, source_id in expressions: + source_id_str = (source_id or "").strip() + if not source_id_str.isdigit(): + # 无效的来源行编号,跳过 + continue + + line_index = int(source_id_str) - 1 # build_anonymous_messages 的编号从 1 开始 + if line_index < 0 or line_index >= len(random_msg): + # 超出范围,跳过 + continue + + # 当前行的原始内容 + current_msg = random_msg[line_index] + + # 过滤掉从bot自己发言中提取到的表达方式 + if is_bot_message(current_msg): + continue + + context = filter_message_content(current_msg.processed_plain_text or "") + if not context: + continue + + filtered_expressions.append((situation, style, context)) + + + learnt_expressions = filtered_expressions if learnt_expressions is None: logger.info("没有学习到表达风格") @@ -205,93 +220,24 @@ class ExpressionLearner: return learnt_expressions - async def learn_expression(self, num: int = 10, timestamp_start: Optional[float] = None) -> Optional[List[Tuple[str, str, str]]]: - """从指定聊天流学习表达方式 - - Args: - num: 学习数量 - timestamp_start: 学习开始的时间戳,如果为None则使用self.last_learning_time + def parse_expression_response(self, response: str) -> Tuple[List[Tuple[str, str, str]], List[Tuple[str, str]]]: """ - current_time = time.time() - - # 使用传入的时间戳,如果没有则使用self.last_learning_time - start_timestamp = timestamp_start if timestamp_start is not None else self.last_learning_time - - # 获取上次学习之后的消息 - random_msg = get_raw_msg_by_timestamp_with_chat_inclusive( - chat_id=self.chat_id, - timestamp_start=start_timestamp, - timestamp_end=current_time, - limit=num, - ) - # print(random_msg) - if not random_msg or random_msg == []: - return None - - # 学习用(开启行编号,便于溯源) - random_msg_str: str = await build_anonymous_messages(random_msg, show_ids=True) - - prompt: str = await global_prompt_manager.format_prompt( - "learn_style_prompt", - chat_str=random_msg_str, - ) - - # print(f"random_msg_str:{random_msg_str}") - # logger.info(f"学习{type_str}的prompt: {prompt}") - - try: - response, _ = await self.express_learn_model.generate_response_async(prompt, temperature=0.3) - except Exception as e: - logger.error(f"学习表达方式失败,模型生成出错: {e}") - return None - - # 解析 LLM 返回的表达方式列表(包含来源行编号) - expressions: List[Tuple[str, str, str]] = self.parse_expression_response(response) - expressions = self._filter_self_reference_styles(expressions) - if not expressions: - logger.info("过滤后没有可用的表达方式(style 与机器人名称重复)") - return None - # logger.debug(f"学习{type_str}的response: {response}") - - # 直接根据 source_id 在 random_msg 中溯源,获取 context - filtered_expressions: List[Tuple[str, str, str]] = [] # (situation, style, context) - - for situation, style, source_id in expressions: - source_id_str = (source_id or "").strip() - if not source_id_str.isdigit(): - # 无效的来源行编号,跳过 - continue - - line_index = int(source_id_str) - 1 # build_anonymous_messages 的编号从 1 开始 - if line_index < 0 or line_index >= len(random_msg): - # 超出范围,跳过 - continue - - # 当前行的原始内容 - current_msg = random_msg[line_index] - context = filter_message_content(current_msg.processed_plain_text or "") - if not context: - continue - - filtered_expressions.append((situation, style, context)) - - if not filtered_expressions: - return None - - return filtered_expressions - - def parse_expression_response(self, response: str) -> List[Tuple[str, str, str]]: - """ - 解析 LLM 返回的表达风格总结 JSON,提取 (situation, style, source_id) 元组列表。 + 解析 LLM 返回的表达风格总结和黑话 JSON,提取两个列表。 期望的 JSON 结构: [ - {"situation": "AAAAA", "style": "BBBBB", "source_id": "3"}, + {"situation": "AAAAA", "style": "BBBBB", "source_id": "3"}, // 表达方式 + {"content": "词条", "source_id": "12"}, // 黑话 ... ] + + Returns: + Tuple[List[Tuple[str, str, str]], List[Tuple[str, str]]]: + 第一个列表是表达方式 (situation, style, source_id) + 第二个列表是黑话 (content, source_id) """ if not response: - return [] + return [], [] raw = response.strip() @@ -307,7 +253,8 @@ class ExpressionLearner: raw = raw.strip() parsed = None - expressions: List[Tuple[str, str, str]] = [] + expressions: List[Tuple[str, str, str]] = [] # (situation, style, source_id) + jargon_entries: List[Tuple[str, str]] = [] # (content, source_id) try: # 优先尝试直接解析 @@ -356,9 +303,9 @@ class ExpressionLearner: if in_string: # 在字符串值内部,将中文引号替换为转义的英文引号 - if char == '"': # 中文左引号 + if char == '"': # 中文左引号 U+201C result.append('\\"') - elif char == '"': # 中文右引号 + elif char == '"': # 中文右引号 U+201D result.append('\\"') else: result.append(char) @@ -399,15 +346,23 @@ class ExpressionLearner: for item in parsed_list: if not isinstance(item, dict): continue + + # 检查是否是表达方式条目(有 situation 和 style) situation = str(item.get("situation", "")).strip() style = str(item.get("style", "")).strip() source_id = str(item.get("source_id", "")).strip() - if not situation or not style or not source_id: - # 三个字段必须同时存在 - continue - expressions.append((situation, style, source_id)) + + if situation and style and source_id: + # 表达方式条目 + expressions.append((situation, style, source_id)) + elif item.get("content"): + # 黑话条目(有 content 字段) + content = str(item.get("content", "")).strip() + source_id = str(item.get("source_id", "")).strip() + if content and source_id: + jargon_entries.append((content, source_id)) - return expressions + return expressions, jargon_entries def _filter_self_reference_styles(self, expressions: List[Tuple[str, str, str]]) -> List[Tuple[str, str, str]]: """ @@ -545,6 +500,66 @@ class ExpressionLearner: logger.error(f"概括表达情境失败: {e}") return None + async def _process_jargon_entries(self, jargon_entries: List[Tuple[str, str]], messages: List[Any]) -> None: + """ + 处理从 expression learner 提取的黑话条目,路由到 jargon_miner + + Args: + jargon_entries: 黑话条目列表,每个元素是 (content, source_id) + messages: 消息列表,用于构建上下文 + """ + if not jargon_entries or not messages: + return + + # 获取 jargon_miner 实例 + jargon_miner = miner_manager.get_miner(self.chat_id) + + # 构建黑话条目格式,与 jargon_miner.run_once 中的格式一致 + entries: List[Dict[str, List[str]]] = [] + + for content, source_id in jargon_entries: + content = content.strip() + if not content: + continue + + # 检查是否包含机器人名称 + if contains_bot_self_name(content): + logger.info(f"跳过包含机器人昵称/别名的黑话: {content}") + continue + + # 解析 source_id + source_id_str = (source_id or "").strip() + if not source_id_str.isdigit(): + logger.warning(f"黑话条目 source_id 无效: content={content}, source_id={source_id_str}") + continue + + # build_anonymous_messages 的编号从 1 开始 + line_index = int(source_id_str) - 1 + if line_index < 0 or line_index >= len(messages): + logger.warning(f"黑话条目 source_id 超出范围: content={content}, source_id={source_id_str}") + continue + + # 检查是否是机器人自己的消息 + target_msg = messages[line_index] + if is_bot_message(target_msg): + logger.info(f"跳过引用机器人自身消息的黑话: content={content}, source_id={source_id_str}") + continue + + # 构建上下文段落 + context_paragraph = build_context_paragraph(messages, line_index) + if not context_paragraph: + logger.warning(f"黑话条目上下文为空: content={content}, source_id={source_id_str}") + continue + + entries.append({"content": content, "raw_content": [context_paragraph]}) + + if not entries: + return + + # 调用 jargon_miner 处理这些条目 + await jargon_miner.process_extracted_entries(entries) + + init_prompt() diff --git a/src/express/expression_reflector.py b/src/bw_learner/expression_reflector.py similarity index 100% rename from src/express/expression_reflector.py rename to src/bw_learner/expression_reflector.py diff --git a/src/express/expression_selector.py b/src/bw_learner/expression_selector.py similarity index 66% rename from src/express/expression_selector.py rename to src/bw_learner/expression_selector.py index 8d586d5d..931c5eb5 100644 --- a/src/express/expression_selector.py +++ b/src/bw_learner/expression_selector.py @@ -10,7 +10,7 @@ from src.config.config import global_config, model_config from src.common.logger import get_logger from src.common.database.database_model import Expression from src.chat.utils.prompt_builder import Prompt, global_prompt_manager -from src.express.express_utils import weighted_sample +from src.bw_learner.learner_utils import weighted_sample logger = get_logger("expression_selector") @@ -111,6 +111,65 @@ class ExpressionSelector: return group_chat_ids return [chat_id] + def _select_expressions_simple(self, chat_id: str, max_num: int) -> Tuple[List[Dict[str, Any]], List[int]]: + """ + 简单模式:只选择 count > 1 的项目,要求至少有10个才进行选择,随机选5个,不进行LLM选择 + + Args: + chat_id: 聊天流ID + max_num: 最大选择数量(此参数在此模式下不使用,固定选择5个) + + Returns: + Tuple[List[Dict[str, Any]], List[int]]: 选中的表达方式列表和ID列表 + """ + try: + # 支持多chat_id合并抽选 + related_chat_ids = self.get_related_chat_ids(chat_id) + + # 查询所有相关chat_id的表达方式,排除 rejected=1 的,且只选择 count > 1 的 + style_query = Expression.select().where( + (Expression.chat_id.in_(related_chat_ids)) + & (~Expression.rejected) + & (Expression.count > 1) + ) + + style_exprs = [ + { + "id": expr.id, + "situation": expr.situation, + "style": expr.style, + "last_active_time": expr.last_active_time, + "source_id": expr.chat_id, + "create_date": expr.create_date if expr.create_date is not None else expr.last_active_time, + "count": expr.count if getattr(expr, "count", None) is not None else 1, + "checked": expr.checked if getattr(expr, "checked", None) is not None else False, + } + for expr in style_query + ] + + # 要求至少有10个 count > 1 的表达方式才进行选择 + min_required = 10 + if len(style_exprs) < min_required: + logger.info(f"聊天流 {chat_id} count > 1 的表达方式不足 {min_required} 个(实际 {len(style_exprs)} 个),不进行选择") + return [], [] + + # 固定选择5个 + select_count = 5 + import random + selected_style = random.sample(style_exprs, select_count) + + # 更新last_active_time + if selected_style: + self.update_expressions_last_active_time(selected_style) + + selected_ids = [expr["id"] for expr in selected_style] + logger.debug(f"think_level=0: 从 {len(style_exprs)} 个 count>1 的表达方式中随机选择了 {len(selected_style)} 个") + return selected_style, selected_ids + + except Exception as e: + logger.error(f"简单模式选择表达方式失败: {e}") + return [], [] + def _random_expressions(self, chat_id: str, total_num: int) -> List[Dict[str, Any]]: """ 随机选择表达方式 @@ -164,6 +223,7 @@ class ExpressionSelector: max_num: int = 10, target_message: Optional[str] = None, reply_reason: Optional[str] = None, + think_level: int = 1, ) -> Tuple[List[Dict[str, Any]], List[int]]: """ 选择适合的表达方式(使用classic模式:随机选择+LLM选择) @@ -174,6 +234,7 @@ class ExpressionSelector: max_num: 最大选择数量 target_message: 目标消息内容 reply_reason: planner给出的回复理由 + think_level: 思考级别,0/1 Returns: Tuple[List[Dict[str, Any]], List[int]]: 选中的表达方式列表和ID列表 @@ -184,8 +245,8 @@ class ExpressionSelector: return [], [] # 使用classic模式(随机选择+LLM选择) - logger.debug(f"使用classic模式为聊天流 {chat_id} 选择表达方式") - return await self._select_expressions_classic(chat_id, chat_info, max_num, target_message, reply_reason) + logger.debug(f"使用classic模式为聊天流 {chat_id} 选择表达方式,think_level={think_level}") + return await self._select_expressions_classic(chat_id, chat_info, max_num, target_message, reply_reason, think_level) async def _select_expressions_classic( self, @@ -194,6 +255,7 @@ class ExpressionSelector: max_num: int = 10, target_message: Optional[str] = None, reply_reason: Optional[str] = None, + think_level: int = 1, ) -> Tuple[List[Dict[str, Any]], List[int]]: """ classic模式:随机选择+LLM选择 @@ -204,24 +266,80 @@ class ExpressionSelector: max_num: 最大选择数量 target_message: 目标消息内容 reply_reason: planner给出的回复理由 + think_level: 思考级别,0/1 Returns: Tuple[List[Dict[str, Any]], List[int]]: 选中的表达方式列表和ID列表 """ try: - # 1. 使用随机抽样选择表达方式 - style_exprs = self._random_expressions(chat_id, 20) + # think_level == 0: 只选择 count > 1 的项目,随机选10个,不进行LLM选择 + if think_level == 0: + return self._select_expressions_simple(chat_id, max_num) + + # think_level == 1: 先选高count,再从所有表达方式中随机抽样 + # 1. 获取所有表达方式并分离 count > 1 和 count <= 1 的 + related_chat_ids = self.get_related_chat_ids(chat_id) + style_query = Expression.select().where( + (Expression.chat_id.in_(related_chat_ids)) & (~Expression.rejected) + ) + + all_style_exprs = [ + { + "id": expr.id, + "situation": expr.situation, + "style": expr.style, + "last_active_time": expr.last_active_time, + "source_id": expr.chat_id, + "create_date": expr.create_date if expr.create_date is not None else expr.last_active_time, + "count": expr.count if getattr(expr, "count", None) is not None else 1, + "checked": expr.checked if getattr(expr, "checked", None) is not None else False, + } + for expr in style_query + ] - if len(style_exprs) < 10: - logger.info(f"聊天流 {chat_id} 表达方式正在积累中") + # 分离 count > 1 和 count <= 1 的表达方式 + high_count_exprs = [expr for expr in all_style_exprs if (expr.get("count", 1) or 1) > 1] + + # 根据 think_level 设置要求(仅支持 0/1,0 已在上方返回) + min_high_count = 10 + min_total_count = 10 + select_high_count = 5 + select_random_count = 5 + + # 检查数量要求 + if len(high_count_exprs) < min_high_count: + logger.info(f"聊天流 {chat_id} count > 1 的表达方式不足 {min_high_count} 个(实际 {len(high_count_exprs)} 个),不进行选择") return [], [] + + if len(all_style_exprs) < min_total_count: + logger.info(f"聊天流 {chat_id} 总表达方式不足 {min_total_count} 个(实际 {len(all_style_exprs)} 个),不进行选择") + return [], [] + + # 先选取高count的表达方式 + selected_high = weighted_sample(high_count_exprs, min(len(high_count_exprs), select_high_count)) + + # 然后从所有表达方式中随机抽样(使用加权抽样) + remaining_num = select_random_count + selected_random = weighted_sample(all_style_exprs, min(len(all_style_exprs), remaining_num)) + + # 合并候选池(去重,避免重复) + candidate_exprs = selected_high.copy() + candidate_ids = {expr["id"] for expr in candidate_exprs} + for expr in selected_random: + if expr["id"] not in candidate_ids: + candidate_exprs.append(expr) + candidate_ids.add(expr["id"]) + + # 打乱顺序,避免高count的都在前面 + import random + random.shuffle(candidate_exprs) # 2. 构建所有表达方式的索引和情境列表 all_expressions: List[Dict[str, Any]] = [] all_situations: List[str] = [] # 添加style表达方式 - for expr in style_exprs: + for expr in candidate_exprs: expr = expr.copy() all_expressions.append(expr) all_situations.append(f"{len(all_expressions)}.当 {expr['situation']} 时,使用 {expr['style']}") @@ -233,7 +351,7 @@ class ExpressionSelector: all_situations_str = "\n".join(all_situations) if target_message: - target_message_str = f",现在你想要对这条消息进行回复:“{target_message}”" + target_message_str = f",现在你想要对这条消息进行回复:\"{target_message}\"" target_message_extra_block = "4.考虑你要回复的目标消息" else: target_message_str = "" @@ -262,7 +380,8 @@ class ExpressionSelector: # 4. 调用LLM content, (reasoning_content, model_name, _) = await self.llm_model.generate_response_async(prompt=prompt) - # print(prompt) + print(prompt) + print(content) if not content: logger.warning("LLM返回空结果") diff --git a/src/jargon/jargon_explainer.py b/src/bw_learner/jargon_explainer.py similarity index 96% rename from src/jargon/jargon_explainer.py rename to src/bw_learner/jargon_explainer.py index 28122008..ac62fa5f 100644 --- a/src/jargon/jargon_explainer.py +++ b/src/bw_learner/jargon_explainer.py @@ -7,8 +7,8 @@ from src.common.database.database_model import Jargon from src.llm_models.utils_model import LLMRequest from src.config.config import model_config, global_config from src.chat.utils.prompt_builder import Prompt, global_prompt_manager -from src.jargon.jargon_miner import search_jargon -from src.jargon.jargon_utils import is_bot_message, contains_bot_self_name, parse_chat_id_list, chat_id_list_contains +from src.bw_learner.jargon_miner import search_jargon +from src.bw_learner.learner_utils import is_bot_message, contains_bot_self_name, parse_chat_id_list, chat_id_list_contains logger = get_logger("jargon") @@ -82,7 +82,7 @@ class JargonExplainer: query = Jargon.select().where((Jargon.meaning.is_null(False)) & (Jargon.meaning != "")) # 根据all_global配置决定查询逻辑 - if global_config.jargon.all_global: + if global_config.expression.all_global_jargon: # 开启all_global:只查询is_global=True的记录 query = query.where(Jargon.is_global) else: @@ -107,7 +107,7 @@ class JargonExplainer: continue # 检查chat_id(如果all_global=False) - if not global_config.jargon.all_global: + if not global_config.expression.all_global_jargon: if jargon.is_global: # 全局黑话,包含 pass @@ -181,7 +181,7 @@ class JargonExplainer: content = entry["content"] # 根据是否开启全局黑话,决定查询方式 - if global_config.jargon.all_global: + if global_config.expression.all_global_jargon: # 开启全局黑话:查询所有is_global=True的记录 results = search_jargon( keyword=content, @@ -265,7 +265,7 @@ def match_jargon_from_text(chat_text: str, chat_id: str) -> List[str]: return [] query = Jargon.select().where((Jargon.meaning.is_null(False)) & (Jargon.meaning != "")) - if global_config.jargon.all_global: + if global_config.expression.all_global_jargon: query = query.where(Jargon.is_global) query = query.order_by(Jargon.count.desc()) @@ -277,7 +277,7 @@ def match_jargon_from_text(chat_text: str, chat_id: str) -> List[str]: if not content: continue - if not global_config.jargon.all_global and not jargon.is_global: + if not global_config.expression.all_global_jargon and not jargon.is_global: chat_id_list = parse_chat_id_list(jargon.chat_id) if not chat_id_list_contains(chat_id_list, chat_id): continue diff --git a/src/jargon/jargon_miner.py b/src/bw_learner/jargon_miner.py similarity index 76% rename from src/jargon/jargon_miner.py rename to src/bw_learner/jargon_miner.py index 7cda4e02..275d9cbd 100644 --- a/src/jargon/jargon_miner.py +++ b/src/bw_learner/jargon_miner.py @@ -1,6 +1,7 @@ import time import json import asyncio +import random from collections import OrderedDict from typing import List, Dict, Optional, Any from json_repair import repair_json @@ -16,7 +17,7 @@ from src.chat.utils.chat_message_builder import ( get_raw_msg_by_timestamp_with_chat_inclusive, ) from src.chat.utils.prompt_builder import Prompt, global_prompt_manager -from src.jargon.jargon_utils import ( +from src.bw_learner.learner_utils import ( is_bot_message, build_context_paragraph, contains_bot_self_name, @@ -29,6 +30,29 @@ from src.jargon.jargon_utils import ( logger = get_logger("jargon") +def _is_single_char_jargon(content: str) -> bool: + """ + 判断是否是单字黑话(单个汉字、英文或数字) + + Args: + content: 词条内容 + + Returns: + bool: 如果是单字黑话返回True,否则返回False + """ + if not content or len(content) != 1: + return False + + char = content[0] + # 判断是否是单个汉字、单个英文字母或单个数字 + return ( + '\u4e00' <= char <= '\u9fff' or # 汉字 + 'a' <= char <= 'z' or # 小写字母 + 'A' <= char <= 'Z' or # 大写字母 + '0' <= char <= '9' # 数字 + ) + + def _init_prompt() -> None: prompt_str = """ **聊天内容,其中的{bot_name}的发言内容是你自己的发言,[msg_id] 是消息ID** @@ -36,11 +60,9 @@ def _init_prompt() -> None: 请从上面这段聊天内容中提取"可能是黑话"的候选项(黑话/俚语/网络缩写/口头禅)。 - 必须为对话中真实出现过的短词或短语 -- 必须是你无法理解含义的词语,没有明确含义的词语 -- 请不要选择有明确含义,或者含义清晰的词语 +- 必须是你无法理解含义的词语,没有明确含义的词语,请不要选择有明确含义,或者含义清晰的词语 - 排除:人名、@、表情包/图片中的内容、纯标点、常规功能词(如的、了、呢、啊等) - 每个词条长度建议 2-8 个字符(不强制),尽量短小 -- 合并重复项,去重 黑话必须为以下几种类型: - 由字母构成的,汉语拼音首字母的简写词,例如:nb、yyds、xswl @@ -67,12 +89,14 @@ def _init_inference_prompts() -> None: {content} **词条出现的上下文。其中的{bot_name}的发言内容是你自己的发言** {raw_content_list} +{previous_meaning_section} 请根据上下文,推断"{content}"这个词条的含义。 - 如果这是一个黑话、俚语或网络用语,请推断其含义 - 如果含义明确(常规词汇),也请说明 - {bot_name} 的发言内容可能包含错误,请不要参考其发言内容 - 如果上下文信息不足,无法推断含义,请设置 no_info 为 true +{previous_meaning_instruction} 以 JSON 格式输出: {{ @@ -166,10 +190,6 @@ def _should_infer_meaning(jargon_obj: Jargon) -> bool: class JargonMiner: def __init__(self, chat_id: str) -> None: self.chat_id = chat_id - self.last_learning_time: float = time.time() - # 频率控制,可按需调整 - self.min_messages_for_learning: int = 30 - self.min_learning_interval: float = 60 self.llm = LLMRequest( model_set=model_config.model_task_config.utils, @@ -200,6 +220,10 @@ class JargonMiner: if not key: return + # 单字黑话(单个汉字、英文或数字)不记录到缓存 + if _is_single_char_jargon(key): + return + if key in self.cache: self.cache.move_to_end(key) else: @@ -272,13 +296,37 @@ class JargonMiner: logger.warning(f"jargon {content} 没有raw_content,跳过推断") return + # 获取当前count和上一次的meaning + current_count = jargon_obj.count or 0 + previous_meaning = jargon_obj.meaning or "" + + # 当count为24, 60时,随机移除一半的raw_content项目 + if current_count in [24, 60] and len(raw_content_list) > 1: + # 计算要保留的数量(至少保留1个) + keep_count = max(1, len(raw_content_list) // 2) + raw_content_list = random.sample(raw_content_list, keep_count) + logger.info(f"jargon {content} count={current_count},随机移除后剩余 {len(raw_content_list)} 个raw_content项目") + # 步骤1: 基于raw_content和content推断 raw_content_text = "\n".join(raw_content_list) + + # 当count为24, 60, 100时,在prompt中放入上一次推断出的meaning作为参考 + previous_meaning_section = "" + previous_meaning_instruction = "" + if current_count in [24, 60, 100] and previous_meaning: + previous_meaning_section = f""" +**上一次推断的含义(仅供参考)** +{previous_meaning} +""" + previous_meaning_instruction = "- 请参考上一次推断的含义,结合新的上下文信息,给出更准确或更新的推断结果" + prompt1 = await global_prompt_manager.format_prompt( "jargon_inference_with_context_prompt", content=content, bot_name=global_config.bot.nickname, raw_content_list=raw_content_text, + previous_meaning_section=previous_meaning_section, + previous_meaning_instruction=previous_meaning_instruction, ) response1, _ = await self.llm_inference.generate_response_async(prompt1, temperature=0.3) @@ -430,45 +478,16 @@ class JargonMiner: traceback.print_exc() - def should_trigger(self) -> bool: - # 冷却时间检查 - if time.time() - self.last_learning_time < self.min_learning_interval: - return False - - # 拉取最近消息数量是否足够 - recent_messages = get_raw_msg_by_timestamp_with_chat_inclusive( - chat_id=self.chat_id, - timestamp_start=self.last_learning_time, - timestamp_end=time.time(), - ) - return bool(recent_messages and len(recent_messages) >= self.min_messages_for_learning) - - async def run_once(self) -> None: + async def run_once(self, messages: List[Any]) -> None: + """ + 运行一次黑话提取 + + Args: + messages: 外部传入的消息列表(必需) + """ # 使用异步锁防止并发执行 async with self._extraction_lock: try: - # 在锁内检查,避免并发触发 - if not self.should_trigger(): - return - - chat_stream = get_chat_manager().get_stream(self.chat_id) - if not chat_stream: - return - - # 记录本次提取的时间窗口,避免重复提取 - extraction_start_time = self.last_learning_time - extraction_end_time = time.time() - - # 立即更新学习时间,防止并发触发 - self.last_learning_time = extraction_end_time - - # 拉取学习窗口内的消息 - messages = get_raw_msg_by_timestamp_with_chat_inclusive( - chat_id=self.chat_id, - timestamp_start=extraction_start_time, - timestamp_end=extraction_end_time, - limit=20, - ) if not messages: return @@ -608,7 +627,7 @@ class JargonMiner: # 查找匹配的记录 matched_obj = None for obj in query: - if global_config.jargon.all_global: + if global_config.expression.all_global_jargon: # 开启all_global:所有content匹配的记录都可以 matched_obj = obj break @@ -648,7 +667,7 @@ class JargonMiner: obj.chat_id = json.dumps(updated_chat_id_list, ensure_ascii=False) # 开启all_global时,确保记录标记为is_global=True - if global_config.jargon.all_global: + if global_config.expression.all_global_jargon: obj.is_global = True # 关闭all_global时,保持原有is_global不变(不修改) @@ -664,7 +683,7 @@ class JargonMiner: updated += 1 else: # 没找到匹配记录,创建新记录 - if global_config.jargon.all_global: + if global_config.expression.all_global_jargon: # 开启all_global:新记录默认为is_global=True is_global_new = True else: @@ -704,6 +723,145 @@ class JargonMiner: logger.error(f"JargonMiner 运行失败: {e}") # 即使失败也保持时间戳更新,避免频繁重试 + async def process_extracted_entries(self, entries: List[Dict[str, List[str]]]) -> None: + """ + 处理已提取的黑话条目(从 expression_learner 路由过来的) + + Args: + entries: 黑话条目列表,每个元素格式为 {"content": "...", "raw_content": [...]} + """ + if not entries: + return + + try: + # 去重并合并raw_content(按 content 聚合) + merged_entries: OrderedDict[str, Dict[str, List[str]]] = OrderedDict() + for entry in entries: + content_key = entry["content"] + raw_list = entry.get("raw_content", []) or [] + if content_key in merged_entries: + merged_entries[content_key]["raw_content"].extend(raw_list) + else: + merged_entries[content_key] = { + "content": content_key, + "raw_content": list(raw_list), + } + + uniq_entries = [] + for merged_entry in merged_entries.values(): + raw_content_list = merged_entry["raw_content"] + if raw_content_list: + merged_entry["raw_content"] = list(dict.fromkeys(raw_content_list)) + uniq_entries.append(merged_entry) + + saved = 0 + updated = 0 + for entry in uniq_entries: + content = entry["content"] + raw_content_list = entry["raw_content"] # 已经是列表 + + try: + # 查询所有content匹配的记录 + query = Jargon.select().where(Jargon.content == content) + + # 查找匹配的记录 + matched_obj = None + for obj in query: + if global_config.expression.all_global_jargon: + # 开启all_global:所有content匹配的记录都可以 + matched_obj = obj + break + else: + # 关闭all_global:需要检查chat_id列表是否包含目标chat_id + chat_id_list = parse_chat_id_list(obj.chat_id) + if chat_id_list_contains(chat_id_list, self.chat_id): + matched_obj = obj + break + + if matched_obj: + obj = matched_obj + try: + obj.count = (obj.count or 0) + 1 + except Exception: + obj.count = 1 + + # 合并raw_content列表:读取现有列表,追加新值,去重 + existing_raw_content = [] + if obj.raw_content: + try: + existing_raw_content = ( + json.loads(obj.raw_content) if isinstance(obj.raw_content, str) else obj.raw_content + ) + if not isinstance(existing_raw_content, list): + existing_raw_content = [existing_raw_content] if existing_raw_content else [] + except (json.JSONDecodeError, TypeError): + existing_raw_content = [obj.raw_content] if obj.raw_content else [] + + # 合并并去重 + merged_list = list(dict.fromkeys(existing_raw_content + raw_content_list)) + obj.raw_content = json.dumps(merged_list, ensure_ascii=False) + + # 更新chat_id列表:增加当前chat_id的计数 + chat_id_list = parse_chat_id_list(obj.chat_id) + updated_chat_id_list = update_chat_id_list(chat_id_list, self.chat_id, increment=1) + obj.chat_id = json.dumps(updated_chat_id_list, ensure_ascii=False) + + # 开启all_global时,确保记录标记为is_global=True + if global_config.expression.all_global_jargon: + obj.is_global = True + # 关闭all_global时,保持原有is_global不变(不修改) + + obj.save() + + # 检查是否需要推断(达到阈值且超过上次判定值) + if _should_infer_meaning(obj): + # 异步触发推断,不阻塞主流程 + # 重新加载对象以确保数据最新 + jargon_id = obj.id + asyncio.create_task(self._infer_meaning_by_id(jargon_id)) + + updated += 1 + else: + # 没找到匹配记录,创建新记录 + if global_config.expression.all_global_jargon: + # 开启all_global:新记录默认为is_global=True + is_global_new = True + else: + # 关闭all_global:新记录is_global=False + is_global_new = False + + # 使用新格式创建chat_id列表:[[chat_id, count]] + chat_id_list = [[self.chat_id, 1]] + chat_id_json = json.dumps(chat_id_list, ensure_ascii=False) + + Jargon.create( + content=content, + raw_content=json.dumps(raw_content_list, ensure_ascii=False), + chat_id=chat_id_json, + is_global=is_global_new, + count=1, + ) + saved += 1 + except Exception as e: + logger.error(f"保存jargon失败: chat_id={self.chat_id}, content={content}, err={e}") + continue + finally: + self._add_to_cache(content) + + # 固定输出提取的jargon结果,格式化为可读形式(只要有提取结果就输出) + if uniq_entries: + # 收集所有提取的jargon内容 + jargon_list = [entry["content"] for entry in uniq_entries] + jargon_str = ",".join(jargon_list) + + # 输出格式化的结果(使用logger.info会自动应用jargon模块的颜色) + logger.info(f"[{self.stream_name}]疑似黑话: {jargon_str}") + + if saved or updated: + logger.info(f"jargon写入: 新增 {saved} 条,更新 {updated} 条,chat_id={self.chat_id}") + except Exception as e: + logger.error(f"处理已提取的黑话条目失败: {e}") + class JargonMinerManager: def __init__(self) -> None: @@ -718,9 +876,6 @@ class JargonMinerManager: miner_manager = JargonMinerManager() -async def extract_and_store_jargon(chat_id: str) -> None: - miner = miner_manager.get_miner(chat_id) - await miner.run_once() def search_jargon( @@ -770,7 +925,7 @@ def search_jargon( query = query.where(search_condition) # 根据all_global配置决定查询逻辑 - if global_config.jargon.all_global: + if global_config.expression.all_global_jargon: # 开启all_global:所有记录都是全局的,查询所有is_global=True的记录(无视chat_id) query = query.where(Jargon.is_global) # 注意:对于all_global=False的情况,chat_id过滤在Python层面进行,以便兼容新旧格式 @@ -787,7 +942,7 @@ def search_jargon( results = [] for jargon in query: # 如果提供了chat_id且all_global=False,需要检查chat_id列表是否包含目标chat_id - if chat_id and not global_config.jargon.all_global: + if chat_id and not global_config.expression.all_global_jargon: chat_id_list = parse_chat_id_list(jargon.chat_id) # 如果记录是is_global=True,或者chat_id列表包含目标chat_id,则包含 if not jargon.is_global and not chat_id_list_contains(chat_id_list, chat_id): diff --git a/src/jargon/jargon_utils.py b/src/bw_learner/learner_utils.py similarity index 53% rename from src/jargon/jargon_utils.py rename to src/bw_learner/learner_utils.py index f42d807b..e67f8ea4 100644 --- a/src/jargon/jargon_utils.py +++ b/src/bw_learner/learner_utils.py @@ -1,5 +1,9 @@ +import re +import difflib +import random import json -from typing import List, Dict, Optional, Any +from datetime import datetime +from typing import Optional, List, Dict, Any from src.common.logger import get_logger from src.config.config import global_config @@ -9,7 +13,147 @@ from src.chat.utils.chat_message_builder import ( from src.chat.utils.utils import parse_platform_accounts -logger = get_logger("jargon") +logger = get_logger("learner_utils") + + +def filter_message_content(content: Optional[str]) -> str: + """ + 过滤消息内容,移除回复、@、图片等格式 + + Args: + content: 原始消息内容 + + Returns: + str: 过滤后的内容 + """ + if not content: + return "" + + # 移除以[回复开头、]结尾的部分,包括后面的",说:"部分 + content = re.sub(r"\[回复.*?\],说:\s*", "", content) + # 移除@<...>格式的内容 + content = re.sub(r"@<[^>]*>", "", content) + # 移除[picid:...]格式的图片ID + content = re.sub(r"\[picid:[^\]]*\]", "", content) + # 移除[表情包:...]格式的内容 + content = re.sub(r"\[表情包:[^\]]*\]", "", content) + + return content.strip() + + +def calculate_similarity(text1: str, text2: str) -> float: + """ + 计算两个文本的相似度,返回0-1之间的值 + 使用SequenceMatcher计算相似度 + + Args: + text1: 第一个文本 + text2: 第二个文本 + + Returns: + float: 相似度值,范围0-1 + """ + return difflib.SequenceMatcher(None, text1, text2).ratio() + + +def format_create_date(timestamp: float) -> str: + """ + 将时间戳格式化为可读的日期字符串 + + Args: + timestamp: 时间戳 + + Returns: + str: 格式化后的日期字符串 + """ + try: + return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, OSError): + return "未知时间" + + +def _compute_weights(population: List[Dict]) -> List[float]: + """ + 根据表达的count计算权重,范围限定在1~5之间。 + count越高,权重越高,但最多为基础权重的5倍。 + 如果表达已checked,权重会再乘以3倍。 + """ + if not population: + return [] + + counts = [] + checked_flags = [] + for item in population: + count = item.get("count", 1) + try: + count_value = float(count) + except (TypeError, ValueError): + count_value = 1.0 + counts.append(max(count_value, 0.0)) + # 获取checked状态 + checked = item.get("checked", False) + checked_flags.append(bool(checked)) + + min_count = min(counts) + max_count = max(counts) + + if max_count == min_count: + base_weights = [1.0 for _ in counts] + else: + base_weights = [] + for count_value in counts: + # 线性映射到[1,5]区间 + normalized = (count_value - min_count) / (max_count - min_count) + base_weights.append(1.0 + normalized * 4.0) # 1~5 + + # 如果checked,权重乘以3 + weights = [] + for base_weight, checked in zip(base_weights, checked_flags, strict=False): + if checked: + weights.append(base_weight * 3.0) + else: + weights.append(base_weight) + return weights + + +def weighted_sample(population: List[Dict], k: int) -> List[Dict]: + """ + 随机抽样函数 + + Args: + population: 总体数据列表 + k: 需要抽取的数量 + + Returns: + List[Dict]: 抽取的数据列表 + """ + if not population or k <= 0: + return [] + + if len(population) <= k: + return population.copy() + + selected: List[Dict] = [] + population_copy = population.copy() + + for _ in range(min(k, len(population_copy))): + weights = _compute_weights(population_copy) + total_weight = sum(weights) + if total_weight <= 0: + # 回退到均匀随机 + idx = random.randint(0, len(population_copy) - 1) + selected.append(population_copy.pop(idx)) + continue + + threshold = random.uniform(0, total_weight) + cumulative = 0.0 + for idx, weight in enumerate(weights): + cumulative += weight + if threshold <= cumulative: + selected.append(population_copy.pop(idx)) + break + + return selected def parse_chat_id_list(chat_id_value: Any) -> List[List[Any]]: @@ -62,25 +206,37 @@ def update_chat_id_list(chat_id_list: List[List[Any]], target_chat_id: str, incr Returns: List[List[Any]]: 更新后的chat_id列表 """ - # 查找是否已存在该chat_id - found = False - for item in chat_id_list: - if isinstance(item, list) and len(item) >= 1 and str(item[0]) == str(target_chat_id): - # 找到匹配的chat_id,增加计数 - if len(item) >= 2: - item[1] = (item[1] if isinstance(item[1], (int, float)) else 0) + increment - else: - item.append(increment) - found = True - break - - if not found: + item = _find_chat_id_item(chat_id_list, target_chat_id) + if item is not None: + # 找到匹配的chat_id,增加计数 + if len(item) >= 2: + item[1] = (item[1] if isinstance(item[1], (int, float)) else 0) + increment + else: + item.append(increment) + else: # 未找到,添加新条目 chat_id_list.append([target_chat_id, increment]) return chat_id_list +def _find_chat_id_item(chat_id_list: List[List[Any]], target_chat_id: str) -> Optional[List[Any]]: + """ + 在chat_id列表中查找匹配的项(辅助函数) + + Args: + chat_id_list: chat_id列表,格式为 [[chat_id, count], ...] + target_chat_id: 要查找的chat_id + + Returns: + 如果找到则返回匹配的项,否则返回None + """ + for item in chat_id_list: + if isinstance(item, list) and len(item) >= 1 and str(item[0]) == str(target_chat_id): + return item + return None + + def chat_id_list_contains(chat_id_list: List[List[Any]], target_chat_id: str) -> bool: """ 检查chat_id列表中是否包含指定的chat_id @@ -92,10 +248,7 @@ def chat_id_list_contains(chat_id_list: List[List[Any]], target_chat_id: str) -> Returns: bool: 如果包含则返回True """ - for item in chat_id_list: - if isinstance(item, list) and len(item) >= 1 and str(item[0]) == str(target_chat_id): - return True - return False + return _find_chat_id_item(chat_id_list, target_chat_id) is not None def contains_bot_self_name(content: str) -> bool: @@ -115,7 +268,7 @@ def contains_bot_self_name(content: str) -> bool: candidates = [name for name in [nickname, *alias_names] if name] - return any(name in target for name in candidates if target) + return any(name in target for name in candidates) def build_context_paragraph(messages: List[Any], center_index: int) -> Optional[str]: diff --git a/src/bw_learner/message_recorder.py b/src/bw_learner/message_recorder.py new file mode 100644 index 00000000..c49da951 --- /dev/null +++ b/src/bw_learner/message_recorder.py @@ -0,0 +1,217 @@ +import time +import asyncio +from typing import List, Any +from src.common.logger import get_logger +from src.config.config import global_config +from src.chat.message_receive.chat_stream import get_chat_manager +from src.chat.utils.chat_message_builder import get_raw_msg_by_timestamp_with_chat_inclusive +from src.bw_learner.expression_learner import expression_learner_manager +from src.bw_learner.jargon_miner import miner_manager + +logger = get_logger("bw_learner") + + +class MessageRecorder: + """ + 统一的消息记录器,负责管理时间窗口和消息提取,并将消息分发给 expression_learner 和 jargon_miner + """ + + def __init__(self, chat_id: str) -> None: + self.chat_id = chat_id + self.chat_stream = get_chat_manager().get_stream(chat_id) + self.chat_name = get_chat_manager().get_stream_name(chat_id) or chat_id + + # 维护每个chat的上次提取时间 + self.last_extraction_time: float = time.time() + + # 提取锁,防止并发执行 + self._extraction_lock = asyncio.Lock() + + # 获取 expression 和 jargon 的配置参数 + self._init_parameters() + + # 获取 expression_learner 和 jargon_miner 实例 + self.expression_learner = expression_learner_manager.get_expression_learner(chat_id) + self.jargon_miner = miner_manager.get_miner(chat_id) + + def _init_parameters(self) -> None: + """初始化提取参数""" + # 获取 expression 配置 + _, self.enable_expression_learning, self.enable_jargon_learning = ( + global_config.expression.get_expression_config_for_chat(self.chat_id) + ) + self.min_messages_for_extraction = 30 + self.min_extraction_interval = 60 + + logger.debug( + f"MessageRecorder 初始化: chat_id={self.chat_id}, " + f"min_messages={self.min_messages_for_extraction}, " + f"min_interval={self.min_extraction_interval}" + ) + + def should_trigger_extraction(self) -> bool: + """ + 检查是否应该触发消息提取 + + Returns: + bool: 是否应该触发提取 + """ + # 检查时间间隔 + time_diff = time.time() - self.last_extraction_time + if time_diff < self.min_extraction_interval: + return False + + # 检查消息数量 + recent_messages = get_raw_msg_by_timestamp_with_chat_inclusive( + chat_id=self.chat_id, + timestamp_start=self.last_extraction_time, + timestamp_end=time.time(), + ) + + if not recent_messages or len(recent_messages) < self.min_messages_for_extraction: + return False + + return True + + async def extract_and_distribute(self) -> None: + """ + 提取消息并分发给 expression_learner 和 jargon_miner + """ + # 使用异步锁防止并发执行 + async with self._extraction_lock: + # 在锁内检查,避免并发触发 + if not self.should_trigger_extraction(): + return + + # 检查 chat_stream 是否存在 + if not self.chat_stream: + return + + # 记录本次提取的时间窗口,避免重复提取 + extraction_start_time = self.last_extraction_time + extraction_end_time = time.time() + + # 立即更新提取时间,防止并发触发 + self.last_extraction_time = extraction_end_time + + try: + logger.info(f"在聊天流 {self.chat_name} 开始统一消息提取和分发") + + # 拉取提取窗口内的消息 + messages = get_raw_msg_by_timestamp_with_chat_inclusive( + chat_id=self.chat_id, + timestamp_start=extraction_start_time, + timestamp_end=extraction_end_time, + ) + + if not messages: + logger.debug(f"聊天流 {self.chat_name} 没有新消息,跳过提取") + return + + # 按时间排序,确保顺序一致 + messages = sorted(messages, key=lambda msg: msg.time or 0) + + logger.info( + f"聊天流 {self.chat_name} 提取到 {len(messages)} 条消息," + f"时间窗口: {extraction_start_time:.2f} - {extraction_end_time:.2f}" + ) + + + # 分别触发 expression_learner 和 jargon_miner 的处理 + # 传递提取的消息,避免它们重复获取 + # 触发 expression 学习(如果启用) + if self.enable_expression_learning: + asyncio.create_task( + self._trigger_expression_learning(extraction_start_time, extraction_end_time, messages) + ) + + # 触发 jargon 提取(如果启用),传递消息 + # if self.enable_jargon_learning: + # asyncio.create_task( + # self._trigger_jargon_extraction(extraction_start_time, extraction_end_time, messages) + # ) + + except Exception as e: + logger.error(f"为聊天流 {self.chat_name} 提取和分发消息失败: {e}") + import traceback + traceback.print_exc() + # 即使失败也保持时间戳更新,避免频繁重试 + + async def _trigger_expression_learning( + self, + timestamp_start: float, + timestamp_end: float, + messages: List[Any] + ) -> None: + """ + 触发 expression 学习,使用指定的消息列表 + + Args: + timestamp_start: 开始时间戳 + timestamp_end: 结束时间戳 + messages: 消息列表 + """ + try: + # 传递消息给 ExpressionLearner(必需参数) + learnt_style = await self.expression_learner.learn_and_store(messages=messages) + + if learnt_style: + logger.info(f"聊天流 {self.chat_name} 表达学习完成") + else: + logger.debug(f"聊天流 {self.chat_name} 表达学习未获得有效结果") + except Exception as e: + logger.error(f"为聊天流 {self.chat_name} 触发表达学习失败: {e}") + import traceback + traceback.print_exc() + + async def _trigger_jargon_extraction( + self, + timestamp_start: float, + timestamp_end: float, + messages: List[Any] + ) -> None: + """ + 触发 jargon 提取,使用指定的消息列表 + + Args: + timestamp_start: 开始时间戳 + timestamp_end: 结束时间戳 + messages: 消息列表 + """ + try: + # 传递消息给 JargonMiner,避免它重复获取 + await self.jargon_miner.run_once(messages=messages) + + except Exception as e: + logger.error(f"为聊天流 {self.chat_name} 触发黑话提取失败: {e}") + import traceback + traceback.print_exc() + + +class MessageRecorderManager: + """MessageRecorder 管理器""" + + def __init__(self) -> None: + self._recorders: dict[str, MessageRecorder] = {} + + def get_recorder(self, chat_id: str) -> MessageRecorder: + """获取或创建指定 chat_id 的 MessageRecorder""" + if chat_id not in self._recorders: + self._recorders[chat_id] = MessageRecorder(chat_id) + return self._recorders[chat_id] + + +# 全局管理器实例 +recorder_manager = MessageRecorderManager() + + +async def extract_and_distribute_messages(chat_id: str) -> None: + """ + 统一的消息提取和分发入口函数 + + Args: + chat_id: 聊天流ID + """ + recorder = recorder_manager.get_recorder(chat_id) + await recorder.extract_and_distribute() + diff --git a/src/express/reflect_tracker.py b/src/bw_learner/reflect_tracker.py similarity index 100% rename from src/express/reflect_tracker.py rename to src/bw_learner/reflect_tracker.py diff --git a/src/chat/brain_chat/brain_chat.py b/src/chat/brain_chat/brain_chat.py index 9945b496..ae05e5dd 100644 --- a/src/chat/brain_chat/brain_chat.py +++ b/src/chat/brain_chat/brain_chat.py @@ -16,7 +16,8 @@ from src.chat.brain_chat.brain_planner import BrainPlanner from src.chat.planner_actions.action_modifier import ActionModifier from src.chat.planner_actions.action_manager import ActionManager from src.chat.heart_flow.hfc_utils import CycleDetail -from src.express.expression_learner import expression_learner_manager +from src.bw_learner.expression_learner import expression_learner_manager +from src.bw_learner.message_recorder import extract_and_distribute_messages from src.person_info.person_info import Person from src.plugin_system.base.component_types import EventType, ActionInfo from src.plugin_system.core import events_manager @@ -252,7 +253,7 @@ class BrainChatting: # ReflectTracker Check # 在每次回复前检查一次上下文,看是否有反思问题得到了解答 # ------------------------------------------------------------------------- - from src.express.reflect_tracker import reflect_tracker_manager + from src.bw_learner.reflect_tracker import reflect_tracker_manager tracker = reflect_tracker_manager.get_tracker(self.stream_id) if tracker: @@ -265,13 +266,15 @@ class BrainChatting: # Expression Reflection Check # 检查是否需要提问表达反思 # ------------------------------------------------------------------------- - from src.express.expression_reflector import expression_reflector_manager + from src.bw_learner.expression_reflector import expression_reflector_manager reflector = expression_reflector_manager.get_or_create_reflector(self.stream_id) asyncio.create_task(reflector.check_and_ask()) async with global_prompt_manager.async_message_scope(self.chat_stream.context.get_template_name()): - asyncio.create_task(self.expression_learner.trigger_learning_for_chat()) + # 通过 MessageRecorder 统一提取消息并分发给 expression_learner 和 jargon_miner + # 在 replyer 执行时触发,统一管理时间窗口,避免重复获取消息 + asyncio.create_task(extract_and_distribute_messages(self.stream_id)) cycle_timers, thinking_id = self.start_cycle() logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次思考") @@ -308,7 +311,6 @@ class BrainChatting: current_available_actions=available_actions, chat_content_block=chat_content_block, message_id_list=message_id_list, - interest=global_config.personality.interest, prompt_key="brain_planner_prompt_react", ) continue_flag, modified_message = await events_manager.handle_mai_events( diff --git a/src/chat/brain_chat/brain_planner.py b/src/chat/brain_chat/brain_planner.py index df143ead..7da5dc29 100644 --- a/src/chat/brain_chat/brain_planner.py +++ b/src/chat/brain_chat/brain_planner.py @@ -40,7 +40,6 @@ def init_prompt(): """ {time_block} {name_block} -你的兴趣是:{interest} {chat_context_description},以下是具体的聊天内容 **聊天内容** @@ -305,7 +304,6 @@ class BrainPlanner: current_available_actions=filtered_actions, chat_content_block=chat_content_block, message_id_list=message_id_list, - interest=global_config.personality.interest, prompt_key=prompt_key, ) diff --git a/src/chat/frequency_control/frequency_control.py b/src/chat/frequency_control/frequency_control.py deleted file mode 100644 index 897a92cc..00000000 --- a/src/chat/frequency_control/frequency_control.py +++ /dev/null @@ -1,163 +0,0 @@ -from datetime import datetime -import time -import asyncio -from typing import Dict - -from src.chat.utils.chat_message_builder import ( - get_raw_msg_by_timestamp_with_chat, - build_readable_messages, -) -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager -from src.config.config import global_config, model_config -from src.llm_models.utils_model import LLMRequest -from src.common.logger import get_logger -from src.plugin_system.apis import frequency_api - - -def init_prompt(): - Prompt( - """{name_block} -{time_block} -你现在正在聊天,请根据下面的聊天记录判断是否有用户觉得你的发言过于频繁或者发言过少 -{message_str} - -如果用户觉得你的发言过于频繁,请输出"过于频繁",否则输出"正常" -如果用户觉得你的发言过少,请输出"过少",否则输出"正常" -**你只能输出以下三个词之一,不要输出任何其他文字、解释或标点:** -- 正常 -- 过于频繁 -- 过少 -""", - "frequency_adjust_prompt", - ) - - -logger = get_logger("frequency_control") - - -class FrequencyControl: - """简化的频率控制类,仅管理不同chat_id的频率值""" - - def __init__(self, chat_id: str): - self.chat_id = chat_id - # 发言频率调整值 - self.talk_frequency_adjust: float = 1.0 - - self.last_frequency_adjust_time: float = 0.0 - self.frequency_model = LLMRequest( - model_set=model_config.model_task_config.utils_small, request_type="frequency.adjust" - ) - # 频率调整锁,防止并发执行 - self._adjust_lock = asyncio.Lock() - - def get_talk_frequency_adjust(self) -> float: - """获取发言频率调整值""" - return self.talk_frequency_adjust - - def set_talk_frequency_adjust(self, value: float) -> None: - """设置发言频率调整值""" - self.talk_frequency_adjust = max(0.1, min(5.0, value)) - - async def trigger_frequency_adjust(self) -> None: - # 使用异步锁防止并发执行 - async with self._adjust_lock: - # 在锁内检查,避免并发触发 - current_time = time.time() - previous_adjust_time = self.last_frequency_adjust_time - - msg_list = get_raw_msg_by_timestamp_with_chat( - chat_id=self.chat_id, - timestamp_start=previous_adjust_time, - timestamp_end=current_time, - ) - - if current_time - previous_adjust_time < 160 or len(msg_list) <= 20: - return - - # 立即更新调整时间,防止并发触发 - self.last_frequency_adjust_time = current_time - - try: - new_msg_list = get_raw_msg_by_timestamp_with_chat( - chat_id=self.chat_id, - timestamp_start=previous_adjust_time, - timestamp_end=current_time, - limit=20, - limit_mode="latest", - ) - - message_str = build_readable_messages( - new_msg_list, - replace_bot_name=True, - timestamp_mode="relative", - read_mark=0.0, - show_actions=False, - ) - time_block = f"当前时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - bot_name = global_config.bot.nickname - bot_nickname = ( - f",也有人叫你{','.join(global_config.bot.alias_names)}" if global_config.bot.alias_names else "" - ) - name_block = f"你的名字是{bot_name}{bot_nickname},请注意哪些是你自己的发言。" - - prompt = await global_prompt_manager.format_prompt( - "frequency_adjust_prompt", - name_block=name_block, - time_block=time_block, - message_str=message_str, - ) - response, (reasoning_content, _, _) = await self.frequency_model.generate_response_async( - prompt, - ) - - # logger.info(f"频率调整 prompt: {prompt}") - # logger.info(f"频率调整 response: {response}") - - if global_config.debug.show_prompt: - logger.info(f"频率调整 prompt: {prompt}") - logger.info(f"频率调整 response: {response}") - logger.info(f"频率调整 reasoning_content: {reasoning_content}") - - final_value_by_api = frequency_api.get_current_talk_value(self.chat_id) - - # LLM依然输出过多内容时取消本次调整。合法最多4个字,但有的模型可能会输出一些markdown换行符等,需要长度宽限 - if len(response) < 20: - if "过于频繁" in response: - logger.info(f"频率调整: 过于频繁,调整值到{final_value_by_api}") - self.talk_frequency_adjust = max(0.1, min(1.5, self.talk_frequency_adjust * 0.8)) - elif "过少" in response: - logger.info(f"频率调整: 过少,调整值到{final_value_by_api}") - self.talk_frequency_adjust = max(0.1, min(1.5, self.talk_frequency_adjust * 1.2)) - except Exception as e: - logger.error(f"频率调整失败: {e}") - # 即使失败也保持时间戳更新,避免频繁重试 - - -class FrequencyControlManager: - """频率控制管理器,管理多个聊天流的频率控制实例""" - - def __init__(self): - self.frequency_control_dict: Dict[str, FrequencyControl] = {} - - def get_or_create_frequency_control(self, chat_id: str) -> FrequencyControl: - """获取或创建指定聊天流的频率控制实例""" - if chat_id not in self.frequency_control_dict: - self.frequency_control_dict[chat_id] = FrequencyControl(chat_id) - return self.frequency_control_dict[chat_id] - - def remove_frequency_control(self, chat_id: str) -> bool: - """移除指定聊天流的频率控制实例""" - if chat_id in self.frequency_control_dict: - del self.frequency_control_dict[chat_id] - return True - return False - - def get_all_chat_ids(self) -> list[str]: - """获取所有有频率控制的聊天ID""" - return list(self.frequency_control_dict.keys()) - - -init_prompt() - -# 创建全局实例 -frequency_control_manager = FrequencyControlManager() diff --git a/src/chat/heart_flow/frequency_control.py b/src/chat/heart_flow/frequency_control.py new file mode 100644 index 00000000..d3be7801 --- /dev/null +++ b/src/chat/heart_flow/frequency_control.py @@ -0,0 +1,50 @@ +from typing import Dict + +from src.common.logger import get_logger + +logger = get_logger("frequency_control") + + +class FrequencyControl: + """简化的频率控制类,仅管理不同chat_id的频率值""" + + def __init__(self, chat_id: str): + self.chat_id = chat_id + # 发言频率调整值 + self.talk_frequency_adjust: float = 1.0 + + def get_talk_frequency_adjust(self) -> float: + """获取发言频率调整值""" + return self.talk_frequency_adjust + + def set_talk_frequency_adjust(self, value: float) -> None: + """设置发言频率调整值""" + self.talk_frequency_adjust = max(0.1, min(5.0, value)) + + +class FrequencyControlManager: + """频率控制管理器,管理多个聊天流的频率控制实例""" + + def __init__(self): + self.frequency_control_dict: Dict[str, FrequencyControl] = {} + + def get_or_create_frequency_control(self, chat_id: str) -> FrequencyControl: + """获取或创建指定聊天流的频率控制实例""" + if chat_id not in self.frequency_control_dict: + self.frequency_control_dict[chat_id] = FrequencyControl(chat_id) + return self.frequency_control_dict[chat_id] + + def remove_frequency_control(self, chat_id: str) -> bool: + """移除指定聊天流的频率控制实例""" + if chat_id in self.frequency_control_dict: + del self.frequency_control_dict[chat_id] + return True + return False + + def get_all_chat_ids(self) -> list[str]: + """获取所有有频率控制的聊天ID""" + return list(self.frequency_control_dict.keys()) + + +# 创建全局实例 +frequency_control_manager = FrequencyControlManager() diff --git a/src/chat/heart_flow/heartFC_chat.py b/src/chat/heart_flow/heartFC_chat.py index 4a54ae6f..725960bd 100644 --- a/src/chat/heart_flow/heartFC_chat.py +++ b/src/chat/heart_flow/heartFC_chat.py @@ -16,11 +16,11 @@ from src.chat.planner_actions.planner import ActionPlanner from src.chat.planner_actions.action_modifier import ActionModifier from src.chat.planner_actions.action_manager import ActionManager from src.chat.heart_flow.hfc_utils import CycleDetail -from src.express.expression_learner import expression_learner_manager -from src.chat.frequency_control.frequency_control import frequency_control_manager -from src.express.reflect_tracker import reflect_tracker_manager -from src.express.expression_reflector import expression_reflector_manager -from src.jargon import extract_and_store_jargon +from src.bw_learner.expression_learner import expression_learner_manager +from src.chat.heart_flow.frequency_control import frequency_control_manager +from src.bw_learner.reflect_tracker import reflect_tracker_manager +from src.bw_learner.expression_reflector import expression_reflector_manager +from src.bw_learner.message_recorder import extract_and_distribute_messages from src.person_info.person_info import Person from src.plugin_system.base.component_types import EventType, ActionInfo from src.plugin_system.core import events_manager @@ -29,6 +29,7 @@ from src.chat.utils.chat_message_builder import ( build_readable_messages_with_id, get_raw_msg_before_timestamp_with_chat, ) +from src.chat.utils.utils import record_replyer_action_temp from src.hippo_memorizer.chat_history_summarizer import ChatHistorySummarizer if TYPE_CHECKING: @@ -99,7 +100,6 @@ class HeartFChatting: self._current_cycle_detail: CycleDetail = None # type: ignore self.last_read_time = time.time() - 2 - self.no_reply_until_call = False self.is_mute = False @@ -207,23 +207,6 @@ class HeartFChatting: if len(recent_messages_list) >= threshold: # for message in recent_messages_list: # print(message.processed_plain_text) - # !处理no_reply_until_call逻辑 - if self.no_reply_until_call: - for message in recent_messages_list: - if ( - message.is_mentioned - or message.is_at - or len(recent_messages_list) >= 8 - or time.time() - self.last_read_time > 600 - ): - self.no_reply_until_call = False - self.last_read_time = time.time() - break - # 没有提到,继续保持沉默 - if self.no_reply_until_call: - # logger.info(f"{self.log_prefix} 没有提到,继续保持沉默") - await asyncio.sleep(1) - return True self.last_read_time = time.time() @@ -328,15 +311,12 @@ class HeartFChatting: start_time = time.time() async with global_prompt_manager.async_message_scope(self.chat_stream.context.get_template_name()): - asyncio.create_task(self.expression_learner.trigger_learning_for_chat()) - asyncio.create_task( - frequency_control_manager.get_or_create_frequency_control(self.stream_id).trigger_frequency_adjust() - ) + # 通过 MessageRecorder 统一提取消息并分发给 expression_learner 和 jargon_miner + # 在 replyer 执行时触发,统一管理时间窗口,避免重复获取消息 + asyncio.create_task(extract_and_distribute_messages(self.stream_id)) # 添加curious检测任务 - 检测聊天记录中的矛盾、冲突或需要提问的内容 # asyncio.create_task(check_and_make_question(self.stream_id)) - # 添加jargon提取任务 - 提取聊天中的黑话/俚语并入库(内部自行取消息并带冷却) - asyncio.create_task(extract_and_store_jargon(self.stream_id)) # 添加聊天内容概括任务 - 累积、打包和压缩聊天记录 # 注意:后台循环已在start()中启动,这里作为额外触发点,在有思考时立即处理 # asyncio.create_task(self.chat_history_summarizer.process()) @@ -377,7 +357,6 @@ class HeartFChatting: current_available_actions=available_actions, chat_content_block=chat_content_block, message_id_list=message_id_list, - interest=global_config.personality.interest, ) continue_flag, modified_message = await events_manager.handle_mai_events( EventType.ON_PLAN, None, prompt_info[0], None, self.chat_stream.stream_id @@ -618,31 +597,6 @@ class HeartFChatting: return {"action_type": "no_reply", "success": True, "result": "选择不回复", "command": ""} - elif action_planner_info.action_type == "no_reply_until_call": - # 直接当场执行no_reply_until_call逻辑 - logger.info(f"{self.log_prefix} 保持沉默,直到有人直接叫的名字") - reason = action_planner_info.reasoning or "选择不回复" - - # 增加连续 no_reply 计数 - self.consecutive_no_reply_count += 1 - self.no_reply_until_call = True - await database_api.store_action_info( - chat_stream=self.chat_stream, - action_build_into_prompt=False, - action_prompt_display=reason, - action_done=True, - thinking_id=thinking_id, - action_data={}, - action_name="no_reply_until_call", - action_reasoning=reason, - ) - return { - "action_type": "no_reply_until_call", - "success": True, - "result": "保持沉默,直到有人直接叫的名字", - "command": "", - } - elif action_planner_info.action_type == "reply": # 直接当场执行reply逻辑 self.questioned = False @@ -651,8 +605,16 @@ class HeartFChatting: self.consecutive_no_reply_count = 0 reason = action_planner_info.reasoning or "" + think_level = action_planner_info.action_data.get("think_level", 1) # 使用 action_reasoning(planner 的整体思考理由)作为 reply_reason planner_reasoning = action_planner_info.action_reasoning or reason + + record_replyer_action_temp( + chat_id=self.stream_id, + reason=reason, + think_level=think_level, + ) + await database_api.store_action_info( chat_stream=self.chat_stream, action_build_into_prompt=False, @@ -674,6 +636,7 @@ class HeartFChatting: request_type="replyer", from_plugin=False, reply_time_point=action_planner_info.action_data.get("loop_start_time", time.time()), + think_level=think_level, ) if not success or not llm_response or not llm_response.reply_set: diff --git a/src/chat/message_receive/bot.py b/src/chat/message_receive/bot.py index 1f5c7012..14c49d04 100644 --- a/src/chat/message_receive/bot.py +++ b/src/chat/message_receive/bot.py @@ -7,7 +7,6 @@ from maim_message import UserInfo, Seg, GroupInfo from src.common.logger import get_logger from src.config.config import global_config -from src.mood.mood_manager import mood_manager # 导入情绪管理器 from src.chat.message_receive.chat_stream import get_chat_manager from src.chat.message_receive.message import MessageRecv from src.chat.message_receive.storage import MessageStorage @@ -73,7 +72,6 @@ class ChatBot: def __init__(self): self.bot = None # bot 实例引用 self._started = False - self.mood_manager = mood_manager # 获取情绪管理器单例 self.heartflow_message_receiver = HeartFCMessageReceiver() # 新增 async def _ensure_started(self): diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index f8bab0bb..298791f6 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -36,7 +36,6 @@ def init_prompt(): """ {time_block} {name_block} -你的兴趣是:{interest} {chat_context_description},以下是具体的聊天内容 **聊天内容** {chat_content_block} @@ -46,9 +45,10 @@ reply 动作描述: 1.你可以选择呼叫了你的名字,但是你没有做出回应的消息进行回复 2.你可以自然的顺着正在进行的聊天内容进行回复或自然的提出一个问题 -3.不要回复你自己发送的消息 +3.不要选择回复你自己发送的消息 4.不要单独对表情包进行回复 -{{"action":"reply", "target_message_id":"消息id(m+数字)", "reason":"原因"}} +5.think_level表示思考深度,0表示该回复不需要思考和回忆,1表示该回复需要进行回忆和思考 +{{"action":"reply", "think_level":数值等级(0或1), "target_messamge_id":"消息id(m+数字)"}} no_reply 动作描述: @@ -56,28 +56,25 @@ no_reply 控制聊天频率,不要太过频繁的发言 {{"action":"no_reply"}} -{no_reply_until_call_block} - {action_options_text} - **你之前的action执行和思考记录** {actions_before_now_block} 请选择**可选的**且符合使用条件的action,并说明触发action的消息id(消息id格式:m+数字) -不要回复你自己发送的消息 先输出你的简短的选择思考理由,再输出你选择的action,理由不要分点,精简。 **动作选择要求** 请你根据聊天内容,用户的最新消息和以下标准选择合适的动作: {plan_style} {moderation_prompt} +target_message_id为必填,表示触发消息的id 请选择所有符合使用要求的action,动作用json格式输出,用```json包裹,如果输出多个json,每个json都要单独一行放在同一个```json代码块内,你可以重复使用同一个动作或不同动作: **示例** // 理由文本(简短) ```json -{{"action":"动作名", "target_message_id":"m123", "reason":"原因"}} -{{"action":"动作名", "target_message_id":"m456", "reason":"原因"}} +{{"action":"动作名", "target_message_id":"m123", .....}} +{{"action":"动作名", "target_message_id":"m456", .....}} ```""", "planner_prompt", ) @@ -112,8 +109,8 @@ no_reply **示例** // 理由文本(简短) ```json -{{"action":"动作名", "target_message_id":"m123", "reason":"原因"}} -{{"action":"动作名", "target_message_id":"m456", "reason":"原因"}} +{{"action":"动作名", "target_message_id":"m123"}} +{{"action":"动作名", "target_message_id":"m456"}} ```""", "planner_prompt_mentioned", ) @@ -124,7 +121,7 @@ no_reply 动作描述:{action_description} 使用条件{parallel_text}: {action_require} -{{"action":"{action_name}",{action_parameters}, "target_message_id":"消息id(m+数字)", "reason":"原因"}} +{{"action":"{action_name}",{action_parameters}, "target_message_id":"消息id(m+数字)"}} """, "action_prompt", ) @@ -218,11 +215,14 @@ class ActionPlanner: try: action = action_json.get("action", "no_reply") - original_reasoning = action_json.get("reason", "未提供原因") - reasoning = self._replace_message_ids_with_text(original_reasoning, message_id_list) - if reasoning is None: - reasoning = original_reasoning - action_data = {key: value for key, value in action_json.items() if key not in ["action", "reason"]} + # 使用 extracted_reasoning(整体推理文本)作为 reasoning + if extracted_reasoning: + reasoning = self._replace_message_ids_with_text(extracted_reasoning, message_id_list) + if reasoning is None: + reasoning = extracted_reasoning + else: + reasoning = "未提供原因" + action_data = {key: value for key, value in action_json.items() if key not in ["action"]} # 非no_reply动作需要target_message_id target_message = None @@ -248,7 +248,7 @@ class ActionPlanner: # 验证action是否可用 available_action_names = [action_name for action_name, _ in current_available_actions] - internal_action_names = ["no_reply", "reply", "wait_time", "no_reply_until_call"] + internal_action_names = ["no_reply", "reply", "wait_time"] if action not in internal_action_names and action not in available_action_names: logger.warning( @@ -304,7 +304,6 @@ class ActionPlanner: self, available_actions: Dict[str, ActionInfo], loop_start_time: float = 0.0, - is_mentioned: bool = False, ) -> List[ActionPlannerInfo]: # sourcery skip: use-named-expression """ @@ -345,11 +344,6 @@ class ActionPlanner: logger.debug(f"{self.log_prefix}过滤后有{len(filtered_actions)}个可用动作") - # 如果是提及时且没有可用动作,直接返回空列表,不调用LLM以节省token - if is_mentioned and not filtered_actions: - logger.info(f"{self.log_prefix}提及时没有可用动作,跳过plan调用") - return [] - # 构建包含所有动作的提示词 prompt, message_id_list = await self.build_planner_prompt( is_group_chat=is_group_chat, @@ -357,8 +351,6 @@ class ActionPlanner: current_available_actions=filtered_actions, chat_content_block=chat_content_block, message_id_list=message_id_list, - interest=global_config.personality.interest, - is_mentioned=is_mentioned, ) # 调用LLM获取决策 @@ -430,32 +422,6 @@ class ActionPlanner: return plan_log_str - def _has_consecutive_no_reply(self, min_count: int = 3) -> bool: - """ - 检查是否有连续min_count次以上的no_reply - - Args: - min_count: 需要连续的最少次数,默认3 - - Returns: - 如果有连续min_count次以上no_reply返回True,否则返回False - """ - consecutive_count = 0 - - # 从后往前遍历plan_log,检查最新的连续记录 - for _reasoning, _timestamp, content in reversed(self.plan_log): - if isinstance(content, list) and all(isinstance(action, ActionPlannerInfo) for action in content): - # 检查所有action是否都是no_reply - if all(action.action_type == "no_reply" for action in content): - consecutive_count += 1 - if consecutive_count >= min_count: - return True - else: - # 如果遇到非no_reply的action,重置计数 - break - - return False - async def build_planner_prompt( self, is_group_chat: bool, @@ -464,7 +430,6 @@ class ActionPlanner: message_id_list: List[Tuple[str, "DatabaseMessages"]], chat_content_block: str = "", interest: str = "", - is_mentioned: bool = False, ) -> tuple[str, List[Tuple[str, "DatabaseMessages"]]]: """构建 Planner LLM 的提示词 (获取模板并填充数据)""" try: @@ -485,47 +450,18 @@ class ActionPlanner: ) name_block = f"你的名字是{bot_name}{bot_nickname},请注意哪些是你自己的发言。" - # 根据是否是提及时选择不同的模板 - if is_mentioned: - # 提及时使用简化版提示词,不需要reply、no_reply、no_reply_until_call - planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt_mentioned") - prompt = planner_prompt_template.format( - time_block=time_block, - chat_context_description=chat_context_description, - chat_content_block=chat_content_block, - actions_before_now_block=actions_before_now_block, - action_options_text=action_options_block, - moderation_prompt=moderation_prompt_block, - name_block=name_block, - interest=interest, - plan_style=global_config.personality.plan_style, - ) - else: - # 正常流程使用完整版提示词 - # 检查是否有连续3次以上no_reply,如果有则添加no_reply_until_call选项 - no_reply_until_call_block = "" - if self._has_consecutive_no_reply(min_count=3): - no_reply_until_call_block = """no_reply_until_call -动作描述: -保持沉默,直到有人直接叫你的名字 -当前话题不感兴趣时使用,或有人不喜欢你的发言时使用 -当你频繁选择no_reply时使用,表示话题暂时与你无关 -{{"action":"no_reply_until_call"}} -""" - - planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt") - prompt = planner_prompt_template.format( - time_block=time_block, - chat_context_description=chat_context_description, - chat_content_block=chat_content_block, - actions_before_now_block=actions_before_now_block, - action_options_text=action_options_block, - no_reply_until_call_block=no_reply_until_call_block, - moderation_prompt=moderation_prompt_block, - name_block=name_block, - interest=interest, - plan_style=global_config.personality.plan_style, - ) + planner_prompt_template = await global_prompt_manager.get_prompt_async("planner_prompt") + prompt = planner_prompt_template.format( + time_block=time_block, + chat_context_description=chat_context_description, + chat_content_block=chat_content_block, + actions_before_now_block=actions_before_now_block, + action_options_text=action_options_block, + moderation_prompt=moderation_prompt_block, + name_block=name_block, + interest=interest, + plan_style=global_config.personality.plan_style, + ) return prompt, message_id_list except Exception as e: diff --git a/src/chat/replyer/group_generator.py b/src/chat/replyer/group_generator.py index a37925c2..57f64dba 100644 --- a/src/chat/replyer/group_generator.py +++ b/src/chat/replyer/group_generator.py @@ -18,13 +18,12 @@ from src.chat.message_receive.uni_message_sender import UniversalMessageSender from src.chat.utils.timer_calculator import Timer # <--- Import Timer from src.chat.utils.utils import get_chat_type_and_target_info from src.chat.utils.prompt_builder import global_prompt_manager -from src.mood.mood_manager import mood_manager from src.chat.utils.chat_message_builder import ( build_readable_messages, get_raw_msg_before_timestamp_with_chat, replace_user_references, ) -from src.express.expression_selector import expression_selector +from src.bw_learner.expression_selector import expression_selector from src.plugin_system.apis.message_api import translate_pid_to_description # from src.memory_system.memory_activator import MemoryActivator @@ -36,7 +35,7 @@ from src.chat.replyer.prompt.lpmm_prompt import init_lpmm_prompt from src.chat.replyer.prompt.replyer_prompt import init_replyer_prompt from src.chat.replyer.prompt.rewrite_prompt import init_rewrite_prompt from src.memory_system.memory_retrieval import init_memory_retrieval_prompt, build_memory_retrieval_prompt -from src.jargon.jargon_explainer import explain_jargon_in_context +from src.bw_learner.jargon_explainer import explain_jargon_in_context init_lpmm_prompt() init_replyer_prompt() @@ -73,6 +72,7 @@ class DefaultReplyer: stream_id: Optional[str] = None, reply_message: Optional[DatabaseMessages] = None, reply_time_point: Optional[float] = time.time(), + think_level: int = 1, ) -> Tuple[bool, LLMGenerationDataModel]: # sourcery skip: merge-nested-ifs """ @@ -107,6 +107,7 @@ class DefaultReplyer: reply_message=reply_message, reply_reason=reply_reason, reply_time_point=reply_time_point, + think_level=think_level, ) llm_response.prompt = prompt llm_response.selected_expressions = selected_expressions @@ -135,10 +136,9 @@ class DefaultReplyer: content, reasoning_content, model_name, tool_call = await self.llm_generate_content(prompt) # logger.debug(f"replyer生成内容: {content}") - logger.info(f"replyer生成内容: {content}") - if global_config.debug.show_replyer_reasoning: - logger.info(f"replyer生成推理:\n{reasoning_content}") - logger.info(f"replyer生成模型: {model_name}") + logger.info(f"模型: [{model_name}][思考等级:{think_level}]生成内容: {content}") + if global_config.debug.show_replyer_reasoning and reasoning_content: + logger.info(f"模型: [{model_name}][思考等级:{think_level}]生成推理:\n{reasoning_content}") llm_response.content = content llm_response.reasoning = reasoning_content @@ -228,7 +228,7 @@ class DefaultReplyer: return False, llm_response async def build_expression_habits( - self, chat_history: str, target: str, reply_reason: str = "" + self, chat_history: str, target: str, reply_reason: str = "", think_level: int = 1 ) -> Tuple[str, List[int]]: # sourcery skip: for-append-to-extend """构建表达习惯块 @@ -237,6 +237,7 @@ class DefaultReplyer: chat_history: 聊天历史记录 target: 目标消息内容 reply_reason: planner给出的回复理由 + think_level: 思考级别,0/1/2 Returns: str: 表达习惯信息字符串 @@ -249,7 +250,7 @@ class DefaultReplyer: # 使用从处理器传来的选中表达方式 # 使用模型预测选择表达方式 selected_expressions, selected_ids = await expression_selector.select_suitable_expressions( - self.chat_stream.stream_id, chat_history, max_num=8, target_message=target, reply_reason=reply_reason + self.chat_stream.stream_id, chat_history, max_num=8, target_message=target, reply_reason=reply_reason, think_level=think_level ) if selected_expressions: @@ -272,12 +273,6 @@ class DefaultReplyer: return f"{expression_habits_title}\n{expression_habits_block}", selected_ids - async def build_mood_state_prompt(self) -> str: - """构建情绪状态提示""" - if not global_config.mood.enable_mood: - return "" - mood_state = await mood_manager.get_mood_by_chat_id(self.chat_stream.stream_id).get_mood() - return f"你现在的心情是:{mood_state}" async def build_tool_info(self, chat_history: str, sender: str, target: str, enable_tool: bool = True) -> str: """构建工具信息块 @@ -705,6 +700,7 @@ class DefaultReplyer: chosen_actions: Optional[List[ActionPlannerInfo]] = None, enable_tool: bool = True, reply_time_point: Optional[float] = time.time(), + think_level: int = 1, ) -> Tuple[str, List[int]]: """ 构建回复器上下文 @@ -792,7 +788,7 @@ class DefaultReplyer: # 并行执行八个构建任务(包括黑话解释) task_results = await asyncio.gather( self._time_and_run_task( - self.build_expression_habits(chat_talking_prompt_short, target, reply_reason), "expression_habits" + self.build_expression_habits(chat_talking_prompt_short, target, reply_reason, think_level=think_level), "expression_habits" ), self._time_and_run_task( self.build_tool_info(chat_talking_prompt_short, sender, target, enable_tool=enable_tool), "tool_info" @@ -800,10 +796,9 @@ class DefaultReplyer: self._time_and_run_task(self.get_prompt_info(chat_talking_prompt_short, sender, target), "prompt_info"), self._time_and_run_task(self.build_actions_prompt(available_actions, chosen_actions), "actions_info"), self._time_and_run_task(self.build_personality_prompt(), "personality_prompt"), - self._time_and_run_task(self.build_mood_state_prompt(), "mood_state_prompt"), self._time_and_run_task( build_memory_retrieval_prompt( - chat_talking_prompt_short, sender, target, self.chat_stream, self.tool_executor + chat_talking_prompt_short, sender, target, self.chat_stream, think_level=think_level ), "memory_retrieval", ), @@ -821,7 +816,6 @@ class DefaultReplyer: "prompt_info": "获取知识", "actions_info": "动作信息", "personality_prompt": "人格信息", - "mood_state_prompt": "情绪状态", "memory_retrieval": "记忆检索", "jargon_explanation": "黑话解释", } @@ -851,14 +845,8 @@ class DefaultReplyer: personality_prompt: str = results_dict["personality_prompt"] memory_retrieval: str = results_dict["memory_retrieval"] keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target) - mood_state_prompt: str = results_dict["mood_state_prompt"] jargon_explanation: str = results_dict.get("jargon_explanation") or "" - - # 从 chosen_actions 中提取 planner 的整体思考理由 - planner_reasoning = "" - if global_config.chat.include_planner_reasoning and reply_reason: - # 如果没有 chosen_actions,使用 reply_reason 作为备选 - planner_reasoning = f"你的想法是:{reply_reason}" + planner_reasoning = f"你的想法是:{reply_reason}" if extra_info: extra_info_block = f"以下是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策\n{extra_info}\n以上是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策" @@ -893,14 +881,20 @@ class DefaultReplyer: chat_prompt_content = self.get_chat_prompt_for_chat(chat_id) chat_prompt_block = f"{chat_prompt_content}\n" if chat_prompt_content else "" - # 固定使用群聊回复模板 + # 根据think_level选择不同的回复模板 + # think_level=0: 轻量回复(简短平淡) + # think_level=1: 中等回复(日常口语化) + if think_level == 0: + prompt_name = "replyer_prompt_0" + else: # think_level == 1 或默认 + prompt_name = "replyer_prompt" + return await global_prompt_manager.format_prompt( - "replyer_prompt", + prompt_name, expression_habits_block=expression_habits_block, tool_info_block=tool_info, bot_name=global_config.bot.nickname, knowledge_prompt=prompt_info, - mood_state=mood_state_prompt, # relation_info_block=relation_info, extra_info_block=extra_info_block, jargon_explanation=jargon_explanation, @@ -926,8 +920,6 @@ class DefaultReplyer: ) -> str: # sourcery skip: merge-else-if-into-elif, remove-redundant-if chat_stream = self.chat_stream chat_id = chat_stream.stream_id - is_group_chat = bool(chat_stream.group_info) - sender, target = self._parse_reply_target(reply_to) target = replace_user_references(target, chat_stream.platform, replace_bot_name=True) @@ -967,58 +959,30 @@ class DefaultReplyer: if sender and target: # 使用预先分析的内容类型结果 - if is_group_chat: - if sender: - if has_only_pics and not has_text: - # 只包含图片 - reply_target_block = ( - f"现在{sender}发送的图片:{pic_part}。引起了你的注意,你想要在群里发言或者回复这条消息。" - ) - elif has_text and pic_part: - # 既有图片又有文字 - reply_target_block = f"现在{sender}发送了图片:{pic_part},并说:{text_part}。引起了你的注意,你想要在群里发言或者回复这条消息。" - else: - # 只包含文字 - reply_target_block = ( - f"现在{sender}说的:{text_part}。引起了你的注意,你想要在群里发言或者回复这条消息。" - ) - elif target: - reply_target_block = f"现在{target}引起了你的注意,你想要在群里发言或者回复这条消息。" + if sender: + if has_only_pics and not has_text: + # 只包含图片 + reply_target_block = ( + f"现在{sender}发送的图片:{pic_part}。引起了你的注意,你想要在群里发言或者回复这条消息。" + ) + elif has_text and pic_part: + # 既有图片又有文字 + reply_target_block = f"现在{sender}发送了图片:{pic_part},并说:{text_part}。引起了你的注意,你想要在群里发言或者回复这条消息。" else: - reply_target_block = "现在,你想要在群里发言或者回复消息。" - else: # private chat - if sender: - if has_only_pics and not has_text: - # 只包含图片 - reply_target_block = f"现在{sender}发送的图片:{pic_part}。引起了你的注意,针对这条消息回复。" - elif has_text and pic_part: - # 既有图片又有文字 - reply_target_block = ( - f"现在{sender}发送了图片:{pic_part},并说:{text_part}。引起了你的注意,针对这条消息回复。" - ) - else: - # 只包含文字 - reply_target_block = f"现在{sender}说的:{text_part}。引起了你的注意,针对这条消息回复。" - elif target: - reply_target_block = f"现在{target}引起了你的注意,针对这条消息回复。" - else: - reply_target_block = "现在,你想要回复。" + # 只包含文字 + reply_target_block = ( + f"现在{sender}说的:{text_part}。引起了你的注意,你想要在群里发言或者回复这条消息。" + ) + elif target: + reply_target_block = f"现在{target}引起了你的注意,你想要在群里发言或者回复这条消息。" + else: + reply_target_block = "现在,你想要在群里发言或者回复消息。" else: reply_target_block = "" - if is_group_chat: - chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1") - chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2") - else: - chat_target_name = "对方" - if self.chat_target_info: - chat_target_name = self.chat_target_info.person_name or self.chat_target_info.user_nickname or "对方" - chat_target_1 = await global_prompt_manager.format_prompt( - "chat_target_private1", sender_name=chat_target_name - ) - chat_target_2 = await global_prompt_manager.format_prompt( - "chat_target_private2", sender_name=chat_target_name - ) + + chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1") + chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2") template_name = "default_expressor_prompt" @@ -1090,7 +1054,7 @@ class DefaultReplyer: # 移除 content 前后的换行符和空格 content = content.strip() - logger.info(f"使用 {model_name} 生成回复内容: {content}") + # logger.info(f"使用 {model_name} 生成回复内容: {content}") return content, reasoning_content, model_name, tool_calls async def get_prompt_info(self, message: str, sender: str, target: str): diff --git a/src/chat/replyer/private_generator.py b/src/chat/replyer/private_generator.py index 726ce4b2..9ff340d9 100644 --- a/src/chat/replyer/private_generator.py +++ b/src/chat/replyer/private_generator.py @@ -23,9 +23,8 @@ from src.chat.utils.chat_message_builder import ( get_raw_msg_before_timestamp_with_chat, replace_user_references, ) -from src.express.expression_selector import expression_selector +from src.bw_learner.expression_selector import expression_selector from src.plugin_system.apis.message_api import translate_pid_to_description -from src.mood.mood_manager import mood_manager # from src.memory_system.memory_activator import MemoryActivator @@ -34,13 +33,13 @@ from src.plugin_system.base.component_types import ActionInfo, EventType from src.plugin_system.apis import llm_api from src.chat.replyer.prompt.lpmm_prompt import init_lpmm_prompt -from src.chat.replyer.prompt.replyer_prompt import init_replyer_prompt +from src.chat.replyer.prompt.replyer_private_prompt import init_replyer_private_prompt from src.chat.replyer.prompt.rewrite_prompt import init_rewrite_prompt from src.memory_system.memory_retrieval import init_memory_retrieval_prompt, build_memory_retrieval_prompt -from src.jargon.jargon_explainer import explain_jargon_in_context +from src.bw_learner.jargon_explainer import explain_jargon_in_context init_lpmm_prompt() -init_replyer_prompt() +init_replyer_private_prompt() init_rewrite_prompt() init_memory_retrieval_prompt() @@ -72,6 +71,7 @@ class PrivateReplyer: chosen_actions: Optional[List[ActionPlannerInfo]] = None, enable_tool: bool = True, from_plugin: bool = True, + think_level: int = 1, stream_id: Optional[str] = None, reply_message: Optional[DatabaseMessages] = None, reply_time_point: Optional[float] = time.time(), @@ -287,12 +287,6 @@ class PrivateReplyer: return f"{expression_habits_title}\n{expression_habits_block}", selected_ids - async def build_mood_state_prompt(self) -> str: - """构建情绪状态提示""" - if not global_config.mood.enable_mood: - return "" - mood_state = await mood_manager.get_mood_by_chat_id(self.chat_stream.stream_id).get_mood() - return f"你现在的心情是:{mood_state}" async def build_tool_info(self, chat_history: str, sender: str, target: str, enable_tool: bool = True) -> str: """构建工具信息块 @@ -721,7 +715,6 @@ class PrivateReplyer: self._time_and_run_task(self.get_prompt_info(chat_talking_prompt_short, sender, target), "prompt_info"), self._time_and_run_task(self.build_actions_prompt(available_actions, chosen_actions), "actions_info"), self._time_and_run_task(self.build_personality_prompt(), "personality_prompt"), - self._time_and_run_task(self.build_mood_state_prompt(), "mood_state_prompt"), self._time_and_run_task( build_memory_retrieval_prompt( chat_talking_prompt_short, sender, target, self.chat_stream, self.tool_executor @@ -742,7 +735,6 @@ class PrivateReplyer: "prompt_info": "获取知识", "actions_info": "动作信息", "personality_prompt": "人格信息", - "mood_state_prompt": "情绪状态", "memory_retrieval": "记忆检索", "jargon_explanation": "黑话解释", } @@ -770,16 +762,10 @@ class PrivateReplyer: prompt_info: str = results_dict["prompt_info"] # 直接使用格式化后的结果 actions_info: str = results_dict["actions_info"] personality_prompt: str = results_dict["personality_prompt"] - mood_state_prompt: str = results_dict["mood_state_prompt"] memory_retrieval: str = results_dict["memory_retrieval"] keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target) jargon_explanation: str = results_dict.get("jargon_explanation") or "" - - # 从 chosen_actions 中提取 planner 的整体思考理由 - planner_reasoning = "" - if global_config.chat.include_planner_reasoning and reply_reason: - # 如果没有 chosen_actions,使用 reply_reason 作为备选 - planner_reasoning = f"你的想法是:{reply_reason}" + planner_reasoning = f"你的想法是:{reply_reason}" if extra_info: extra_info_block = f"以下是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策\n{extra_info}\n以上是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策" @@ -814,7 +800,6 @@ class PrivateReplyer: expression_habits_block=expression_habits_block, tool_info_block=tool_info, knowledge_prompt=prompt_info, - mood_state=mood_state_prompt, relation_info_block=relation_info, extra_info_block=extra_info_block, identity=personality_prompt, @@ -837,7 +822,6 @@ class PrivateReplyer: expression_habits_block=expression_habits_block, tool_info_block=tool_info, knowledge_prompt=prompt_info, - mood_state=mood_state_prompt, relation_info_block=relation_info, extra_info_block=extra_info_block, identity=personality_prompt, @@ -904,59 +888,35 @@ class PrivateReplyer: ) if sender and target: - # 使用预先分析的内容类型结果 - if is_group_chat: - if sender: - if has_only_pics and not has_text: - # 只包含图片 - reply_target_block = ( - f"现在{sender}发送的图片:{pic_part}。引起了你的注意,你想要在群里发言或者回复这条消息。" - ) - elif has_text and pic_part: - # 既有图片又有文字 - reply_target_block = f"现在{sender}发送了图片:{pic_part},并说:{text_part}。引起了你的注意,你想要在群里发言或者回复这条消息。" - else: - # 只包含文字 - reply_target_block = ( - f"现在{sender}说的:{text_part}。引起了你的注意,你想要在群里发言或者回复这条消息。" - ) - elif target: - reply_target_block = f"现在{target}引起了你的注意,你想要在群里发言或者回复这条消息。" + if sender: + if has_only_pics and not has_text: + # 只包含图片 + reply_target_block = f"现在{sender}发送的图片:{pic_part}。引起了你的注意,针对这条消息回复。" + elif has_text and pic_part: + # 既有图片又有文字 + reply_target_block = ( + f"现在{sender}发送了图片:{pic_part},并说:{text_part}。引起了你的注意,针对这条消息回复。" + ) else: - reply_target_block = "现在,你想要在群里发言或者回复消息。" - else: # private chat - if sender: - if has_only_pics and not has_text: - # 只包含图片 - reply_target_block = f"现在{sender}发送的图片:{pic_part}。引起了你的注意,针对这条消息回复。" - elif has_text and pic_part: - # 既有图片又有文字 - reply_target_block = ( - f"现在{sender}发送了图片:{pic_part},并说:{text_part}。引起了你的注意,针对这条消息回复。" - ) - else: - # 只包含文字 - reply_target_block = f"现在{sender}说的:{text_part}。引起了你的注意,针对这条消息回复。" - elif target: - reply_target_block = f"现在{target}引起了你的注意,针对这条消息回复。" - else: - reply_target_block = "现在,你想要回复。" + # 只包含文字 + reply_target_block = f"现在{sender}说的:{text_part}。引起了你的注意,针对这条消息回复。" + elif target: + reply_target_block = f"现在{target}引起了你的注意,针对这条消息回复。" + else: + reply_target_block = "现在,你想要回复。" else: reply_target_block = "" - if is_group_chat: - chat_target_1 = await global_prompt_manager.get_prompt_async("chat_target_group1") - chat_target_2 = await global_prompt_manager.get_prompt_async("chat_target_group2") - else: - chat_target_name = "对方" - if self.chat_target_info: - chat_target_name = self.chat_target_info.person_name or self.chat_target_info.user_nickname or "对方" - chat_target_1 = await global_prompt_manager.format_prompt( - "chat_target_private1", sender_name=chat_target_name - ) - chat_target_2 = await global_prompt_manager.format_prompt( - "chat_target_private2", sender_name=chat_target_name - ) + + chat_target_name = "对方" + if self.chat_target_info: + chat_target_name = self.chat_target_info.person_name or self.chat_target_info.user_nickname or "对方" + chat_target_1 = await global_prompt_manager.format_prompt( + "chat_target_private1", sender_name=chat_target_name + ) + chat_target_2 = await global_prompt_manager.format_prompt( + "chat_target_private2", sender_name=chat_target_name + ) template_name = "default_expressor_prompt" diff --git a/src/chat/replyer/prompt/replyer_private_prompt.py b/src/chat/replyer/prompt/replyer_private_prompt.py new file mode 100644 index 00000000..c251d1a1 --- /dev/null +++ b/src/chat/replyer/prompt/replyer_private_prompt.py @@ -0,0 +1,40 @@ +from src.chat.utils.prompt_builder import Prompt + +def init_replyer_private_prompt(): + Prompt( + """{knowledge_prompt}{tool_info_block}{extra_info_block} + {expression_habits_block}{memory_retrieval}{jargon_explanation} + + 你正在和{sender_name}聊天,这是你们之前聊的内容: + {time_block} + {dialogue_prompt} + + {reply_target_block}。 + {planner_reasoning} + {identity} + {chat_prompt}你正在和{sender_name}聊天,现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些, + 尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理。 + {reply_style} + 请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 + {moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", + "private_replyer_prompt", + ) + + Prompt( + """{knowledge_prompt}{tool_info_block}{extra_info_block} +{expression_habits_block}{memory_retrieval}{jargon_explanation} + +你正在和{sender_name}聊天,这是你们之前聊的内容: +{time_block} +{dialogue_prompt} + +你现在想补充说明你刚刚自己的发言内容:{target},原因是{reason} +请你根据聊天内容,组织一条新回复。注意,{target} 是刚刚你自己的发言,你要在这基础上进一步发言,请按照你自己的角度来继续进行回复。注意保持上下文的连贯性。 +{identity} +{chat_prompt}尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。 +{reply_style} +请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 +{moderation_prompt}不要输出多余内容(包括冒号和引号,括号,表情包,at或 @等 )。 +""", + "private_replyer_self_prompt", + ) \ No newline at end of file diff --git a/src/chat/replyer/prompt/replyer_prompt.py b/src/chat/replyer/prompt/replyer_prompt.py index c1c89124..b9a6addf 100644 --- a/src/chat/replyer/prompt/replyer_prompt.py +++ b/src/chat/replyer/prompt/replyer_prompt.py @@ -3,9 +3,6 @@ from src.chat.utils.prompt_builder import Prompt def init_replyer_prompt(): - Prompt("正在群里聊天", "chat_target_group2") - Prompt("和{sender_name}聊天", "chat_target_private2") - Prompt( """{knowledge_prompt}{tool_info_block}{extra_info_block} {expression_habits_block}{memory_retrieval}{jargon_explanation} @@ -18,49 +15,31 @@ def init_replyer_prompt(): {reply_target_block}。 {planner_reasoning} {identity} -{chat_prompt}你正在群里聊天,现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些,{mood_state} +{chat_prompt}你正在群里聊天,现在请你读读之前的聊天记录,然后给出日常且口语化的回复, 尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理。 {reply_style} -请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出一句回复内容就好。 -不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。 +请注意不要输出多余内容(包括不必要的前后缀,冒号,括号,表情包,at或 @等 ),只输出发言内容就好。 现在,你说:""", - "replyer_prompt", + "replyer_prompt_0", ) - + Prompt( """{knowledge_prompt}{tool_info_block}{extra_info_block} {expression_habits_block}{memory_retrieval}{jargon_explanation} -你正在和{sender_name}聊天,这是你们之前聊的内容: +你正在qq群里聊天,下面是群里正在聊的内容,其中包含聊天记录和聊天中的图片 +其中标注 {bot_name}(你) 的发言是你自己的发言,请注意区分: {time_block} {dialogue_prompt} {reply_target_block}。 {planner_reasoning} {identity} -{chat_prompt}你正在和{sender_name}聊天,现在请你读读之前的聊天记录,然后给出日常且口语化的回复,平淡一些,{mood_state} -尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理。 +{chat_prompt}你正在群里聊天,现在请你读读之前的聊天记录,把握当前的话题,然后给出回复, +{keywords_reaction_prompt}请注意把握聊天内容。 {reply_style} -请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 -{moderation_prompt}不要输出多余内容(包括前后缀,冒号和引号,括号,表情包,at或 @等 )。""", - "private_replyer_prompt", +请注意不要输出多余内容(包括不必要的前后缀,冒号,括号,at或 @等 ),只输出发言内容就好。 +现在,你说:""", + "replyer_prompt", ) - Prompt( - """{knowledge_prompt}{tool_info_block}{extra_info_block} -{expression_habits_block}{memory_retrieval}{jargon_explanation} - -你正在和{sender_name}聊天,这是你们之前聊的内容: -{time_block} -{dialogue_prompt} - -你现在想补充说明你刚刚自己的发言内容:{target},原因是{reason} -请你根据聊天内容,组织一条新回复。注意,{target} 是刚刚你自己的发言,你要在这基础上进一步发言,请按照你自己的角度来继续进行回复。注意保持上下文的连贯性。{mood_state} -{identity} -{chat_prompt}尽量简短一些。{keywords_reaction_prompt}请注意把握聊天内容,不要回复的太有条理,可以有个性。 -{reply_style} -请注意不要输出多余内容(包括前后缀,冒号和引号,括号,表情等),只输出回复内容。 -{moderation_prompt}不要输出多余内容(包括冒号和引号,括号,表情包,at或 @等 )。 -""", - "private_replyer_self_prompt", - ) diff --git a/src/chat/utils/utils.py b/src/chat/utils/utils.py index 0464b734..67089fe6 100644 --- a/src/chat/utils/utils.py +++ b/src/chat/utils/utils.py @@ -4,6 +4,8 @@ import time import jieba import json import ast +import os +from datetime import datetime from typing import Optional, Tuple, List, TYPE_CHECKING @@ -641,6 +643,42 @@ def get_chat_type_and_target_info(chat_id: str) -> Tuple[bool, Optional["TargetP return is_group_chat, chat_target_info +def record_replyer_action_temp(chat_id: str, reason: str, think_level: int) -> None: + """ + 临时记录replyer动作被选择的信息(仅群聊) + + Args: + chat_id: 聊天ID + reason: 选择理由 + think_level: 思考深度等级 + """ + try: + # 确保data/temp目录存在 + temp_dir = "data/temp" + os.makedirs(temp_dir, exist_ok=True) + + # 创建记录数据 + record_data = { + "chat_id": chat_id, + "reason": reason, + "think_level": think_level, + "timestamp": datetime.now().isoformat(), + } + + # 生成文件名(使用时间戳避免冲突) + timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S_%f") + filename = f"replyer_action_{timestamp_str}.json" + filepath = os.path.join(temp_dir, filename) + + # 写入文件 + with open(filepath, "w", encoding="utf-8") as f: + json.dump(record_data, f, ensure_ascii=False, indent=2) + + logger.debug(f"已记录replyer动作选择: chat_id={chat_id}, think_level={think_level}") + except Exception as e: + logger.warning(f"记录replyer动作选择失败: {e}") + + def assign_message_ids(messages: List[DatabaseMessages]) -> List[Tuple[str, DatabaseMessages]]: """ 为消息列表中的每个消息分配唯一的简短随机ID diff --git a/src/common/database/database_model.py b/src/common/database/database_model.py index 5e916317..02a581cd 100644 --- a/src/common/database/database_model.py +++ b/src/common/database/database_model.py @@ -592,22 +592,41 @@ def _fix_table_constraints(table_name, model, constraints_to_fix): db.execute_sql(f"CREATE TABLE {backup_table} AS SELECT * FROM {table_name}") logger.info(f"已创建备份表 '{backup_table}'") - # 2. 删除原表 + # 2. 获取原始行数(在删除表之前) + original_count = db.execute_sql(f"SELECT COUNT(*) FROM {backup_table}").fetchone()[0] + logger.info(f"备份表 '{backup_table}' 包含 {original_count} 行数据") + + # 3. 删除原表 db.execute_sql(f"DROP TABLE {table_name}") logger.info(f"已删除原表 '{table_name}'") - # 3. 重新创建表(使用当前模型定义) + # 4. 重新创建表(使用当前模型定义) db.create_tables([model]) logger.info(f"已重新创建表 '{table_name}' 使用新的约束") - # 4. 从备份表恢复数据 - # 获取字段列表 + # 5. 从备份表恢复数据 + # 获取字段列表,排除主键字段(让数据库自动生成新的主键) fields = list(model._meta.fields.keys()) - fields_str = ", ".join(fields) - - # 对于需要从 NOT NULL 改为 NULL 的字段,直接复制数据 - # 对于需要从 NULL 改为 NOT NULL 的字段,需要处理 NULL 值 - insert_sql = f"INSERT INTO {table_name} ({fields_str}) SELECT {fields_str} FROM {backup_table}" + # Peewee 默认使用 'id' 作为主键字段名 + # 尝试获取主键字段名,如果获取失败则默认使用 'id' + primary_key_name = 'id' # 默认值 + try: + if hasattr(model._meta, 'primary_key') and model._meta.primary_key: + if hasattr(model._meta.primary_key, 'name'): + primary_key_name = model._meta.primary_key.name + elif isinstance(model._meta.primary_key, str): + primary_key_name = model._meta.primary_key + except Exception: + pass # 如果获取失败,使用默认值 'id' + + # 如果字段列表包含主键,则排除它 + if primary_key_name in fields: + fields_without_pk = [f for f in fields if f != primary_key_name] + logger.info(f"排除主键字段 '{primary_key_name}',让数据库自动生成新的主键") + else: + fields_without_pk = fields + + fields_str = ", ".join(fields_without_pk) # 检查是否有字段需要从 NULL 改为 NOT NULL null_to_notnull_fields = [ @@ -620,7 +639,7 @@ def _fix_table_constraints(table_name, model, constraints_to_fix): # 构建更复杂的 SELECT 语句来处理 NULL 值 select_fields = [] - for field_name in fields: + for field_name in fields_without_pk: if field_name in null_to_notnull_fields: field_obj = model._meta.fields[field_name] # 根据字段类型设置默认值 @@ -641,12 +660,13 @@ def _fix_table_constraints(table_name, model, constraints_to_fix): select_str = ", ".join(select_fields) insert_sql = f"INSERT INTO {table_name} ({fields_str}) SELECT {select_str} FROM {backup_table}" + else: + # 没有需要处理 NULL 的字段,直接复制数据(排除主键) + insert_sql = f"INSERT INTO {table_name} ({fields_str}) SELECT {fields_str} FROM {backup_table}" db.execute_sql(insert_sql) logger.info(f"已从备份表恢复数据到 '{table_name}'") - # 5. 验证数据完整性 - original_count = db.execute_sql(f"SELECT COUNT(*) FROM {backup_table}").fetchone()[0] new_count = db.execute_sql(f"SELECT COUNT(*) FROM {table_name}").fetchone()[0] if original_count == new_count: diff --git a/src/config/config.py b/src/config/config.py index 061fa07d..44125df4 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -31,10 +31,8 @@ from src.config.official_configs import ( RelationshipConfig, ToolConfig, VoiceConfig, - MoodConfig, MemoryConfig, DebugConfig, - JargonConfig, DreamConfig, ) @@ -58,7 +56,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template") # 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码 # 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/ -MMC_VERSION = "0.11.7-snapshot.1" +MMC_VERSION = "0.12.0-snapshot.1" def get_key_comment(toml_table, key): @@ -355,9 +353,7 @@ class Config(ConfigBase): tool: ToolConfig memory: MemoryConfig debug: DebugConfig - mood: MoodConfig voice: VoiceConfig - jargon: JargonConfig dream: DreamConfig diff --git a/src/config/official_configs.py b/src/config/official_configs.py index ca0fcd7b..e9115a1b 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -45,9 +45,6 @@ class PersonalityConfig(ConfigBase): reply_style: str = "" """表达风格""" - interest: str = "" - """兴趣""" - plan_style: str = "" """说话规则,行为风格""" @@ -79,12 +76,6 @@ class ChatConfig(ConfigBase): max_context_size: int = 18 """上下文长度""" - interest_rate_mode: Literal["fast", "accurate"] = "fast" - """兴趣值计算模式,fast为快速计算,accurate为精确计算""" - - planner_size: float = 1.5 - """副规划器大小,越小,麦麦的动作执行能力越精细,但是消耗更多token,调大可以缓解429类错误""" - mentioned_bot_reply: bool = True """是否启用提及必回复""" @@ -117,9 +108,6 @@ class ChatConfig(ConfigBase): 时间区间支持跨夜,例如 "23:00-02:00"。 """ - include_planner_reasoning: bool = False - """是否将planner推理加入replyer,默认关闭(不加入)""" - def _parse_stream_config_to_chat_id(self, stream_config_str: str) -> Optional[str]: """与 ChatStream.get_stream_id 一致地从 "platform:id:type" 生成 chat_id。""" try: @@ -284,20 +272,20 @@ class ExpressionConfig(ConfigBase): learning_list: list[list] = field(default_factory=lambda: []) """ 表达学习配置列表,支持按聊天流配置 - 格式: [["chat_stream_id", "use_expression", "enable_learning", learning_intensity], ...] + 格式: [["chat_stream_id", "use_expression", "enable_learning", "enable_jargon_learning"], ...] 示例: [ - ["", "enable", "enable", 1.0], # 全局配置:使用表达,启用学习,学习强度1.0 - ["qq:1919810:private", "enable", "enable", 1.5], # 特定私聊配置:使用表达,启用学习,学习强度1.5 - ["qq:114514:private", "enable", "disable", 0.5], # 特定私聊配置:使用表达,禁用学习,学习强度0.5 + ["", "enable", "enable", "enable"], # 全局配置:使用表达,启用学习,启用jargon学习 + ["qq:1919810:private", "enable", "enable", "enable"], # 特定私聊配置:使用表达,启用学习,启用jargon学习 + ["qq:114514:private", "enable", "disable", "disable"], # 特定私聊配置:使用表达,禁用学习,禁用jargon学习 ] 说明: - 第一位: chat_stream_id,空字符串表示全局配置 - 第二位: 是否使用学到的表达 ("enable"/"disable") - 第三位: 是否学习表达 ("enable"/"disable") - - 第四位: 学习强度(浮点数),影响学习频率,最短学习时间间隔 = 300/学习强度(秒) + - 第四位: 是否启用jargon学习 ("enable"/"disable") """ expression_groups: list[list[str]] = field(default_factory=list) @@ -320,6 +308,9 @@ class ExpressionConfig(ConfigBase): 如果列表为空,则所有聊天流都可以进行表达反思(前提是 reflect = true) """ + all_global_jargon: bool = False + """是否将所有新增的jargon项目默认为全局(is_global=True),chat_id记录第一次存储时的id。注意,此功能关闭后,已经记录的全局黑话不会改变,需要手动删除""" + def _parse_stream_config_to_chat_id(self, stream_config_str: str) -> Optional[str]: """ 解析流配置字符串并生成对应的 chat_id @@ -355,7 +346,7 @@ class ExpressionConfig(ConfigBase): except (ValueError, IndexError): return None - def get_expression_config_for_chat(self, chat_stream_id: Optional[str] = None) -> tuple[bool, bool, int]: + def get_expression_config_for_chat(self, chat_stream_id: Optional[str] = None) -> tuple[bool, bool, bool]: """ 根据聊天流ID获取表达配置 @@ -363,35 +354,27 @@ class ExpressionConfig(ConfigBase): chat_stream_id: 聊天流ID,格式为哈希值 Returns: - tuple: (是否使用表达, 是否学习表达, 学习间隔) + tuple: (是否使用表达, 是否学习表达, 是否启用jargon学习) """ if not self.learning_list: - # 如果没有配置,使用默认值:启用表达,启用学习,学习强度1.0(对应300秒间隔) - return True, True, 1.0 + # 如果没有配置,使用默认值:启用表达,启用学习,启用jargon学习 + return True, True, True # 优先检查聊天流特定的配置 if chat_stream_id: specific_expression_config = self._get_stream_specific_config(chat_stream_id) if specific_expression_config is not None: - use_expression, enable_learning, learning_intensity = specific_expression_config - # 防止学习强度为0,自动转换为0.0001 - if learning_intensity == 0: - learning_intensity = 0.0000001 - return use_expression, enable_learning, learning_intensity + return specific_expression_config # 检查全局配置(第一个元素为空字符串的配置) global_expression_config = self._get_global_config() if global_expression_config is not None: - use_expression, enable_learning, learning_intensity = global_expression_config - # 防止学习强度为0,自动转换为0.0001 - if learning_intensity == 0: - learning_intensity = 0.0000001 - return use_expression, enable_learning, learning_intensity + return global_expression_config - # 如果都没有匹配,返回默认值:启用表达,启用学习,学习强度1.0(对应300秒间隔) - return True, True, 1.0 + # 如果都没有匹配,返回默认值:启用表达,启用学习,启用jargon学习 + return True, True, True - def _get_stream_specific_config(self, chat_stream_id: str) -> Optional[tuple[bool, bool, int]]: + def _get_stream_specific_config(self, chat_stream_id: str) -> Optional[tuple[bool, bool, bool]]: """ 获取特定聊天流的表达配置 @@ -399,7 +382,7 @@ class ExpressionConfig(ConfigBase): chat_stream_id: 聊天流ID(哈希值) Returns: - tuple: (是否使用表达, 是否学习表达, 学习间隔),如果没有配置则返回 None + tuple: (是否使用表达, 是否学习表达, 是否启用jargon学习),如果没有配置则返回 None """ for config_item in self.learning_list: if not config_item or len(config_item) < 4: @@ -424,22 +407,19 @@ class ExpressionConfig(ConfigBase): try: use_expression: bool = config_item[1].lower() == "enable" enable_learning: bool = config_item[2].lower() == "enable" - learning_intensity: float = float(config_item[3]) - # 防止学习强度为0,自动转换为0.0001 - if learning_intensity == 0: - learning_intensity = 0.0000001 - return use_expression, enable_learning, learning_intensity # type: ignore + enable_jargon_learning: bool = config_item[3].lower() == "enable" + return use_expression, enable_learning, enable_jargon_learning # type: ignore except (ValueError, IndexError): continue return None - def _get_global_config(self) -> Optional[tuple[bool, bool, int]]: + def _get_global_config(self) -> Optional[tuple[bool, bool, bool]]: """ 获取全局表达配置 Returns: - tuple: (是否使用表达, 是否学习表达, 学习间隔),如果没有配置则返回 None + tuple: (是否使用表达, 是否学习表达, 是否启用jargon学习),如果没有配置则返回 None """ for config_item in self.learning_list: if not config_item or len(config_item) < 4: @@ -450,11 +430,8 @@ class ExpressionConfig(ConfigBase): try: use_expression: bool = config_item[1].lower() == "enable" enable_learning: bool = config_item[2].lower() == "enable" - learning_intensity = float(config_item[3]) - # 防止学习强度为0,自动转换为0.0001 - if learning_intensity == 0: - learning_intensity = 0.0000001 - return use_expression, enable_learning, learning_intensity # type: ignore + enable_jargon_learning: bool = config_item[3].lower() == "enable" + return use_expression, enable_learning, enable_jargon_learning # type: ignore except (ValueError, IndexError): continue @@ -469,20 +446,6 @@ class ToolConfig(ConfigBase): """是否在聊天中启用工具""" -@dataclass -class MoodConfig(ConfigBase): - """情绪配置类""" - - enable_mood: bool = True - """是否启用情绪系统""" - - mood_update_threshold: float = 1 - """情绪更新阈值,越高,更新越慢""" - - emotion_style: str = "情绪较为稳定,但遭遇特定事件的时候起伏较大" - """情感特征,影响情绪的变化情况""" - - @dataclass class VoiceConfig(ConfigBase): """语音识别配置类""" @@ -746,14 +709,6 @@ class LPMMKnowledgeConfig(ConfigBase): """嵌入向量维度,应该与模型的输出维度一致""" -@dataclass -class JargonConfig(ConfigBase): - """Jargon配置类""" - - all_global: bool = False - """是否将所有新增的jargon项目默认为全局(is_global=True),chat_id记录第一次存储时的id""" - - @dataclass class DreamConfig(ConfigBase): """Dream配置类""" diff --git a/src/dream/tools/__init__.py b/src/dream/tools/__init__.py index 488675a6..ef3b8be3 100644 --- a/src/dream/tools/__init__.py +++ b/src/dream/tools/__init__.py @@ -8,3 +8,4 @@ dream agent 工具实现模块。 + diff --git a/src/dream/tools/create_chat_history_tool.py b/src/dream/tools/create_chat_history_tool.py index 43535c29..dde4ffaf 100644 --- a/src/dream/tools/create_chat_history_tool.py +++ b/src/dream/tools/create_chat_history_tool.py @@ -64,3 +64,4 @@ def make_create_chat_history(chat_id: str): + diff --git a/src/dream/tools/delete_chat_history_tool.py b/src/dream/tools/delete_chat_history_tool.py index 02cedf94..e7b9755d 100644 --- a/src/dream/tools/delete_chat_history_tool.py +++ b/src/dream/tools/delete_chat_history_tool.py @@ -27,3 +27,4 @@ def make_delete_chat_history(chat_id: str): # chat_id 目前未直接使用, + diff --git a/src/dream/tools/delete_jargon_tool.py b/src/dream/tools/delete_jargon_tool.py index 2cf38e56..0ff1c218 100644 --- a/src/dream/tools/delete_jargon_tool.py +++ b/src/dream/tools/delete_jargon_tool.py @@ -27,3 +27,4 @@ def make_delete_jargon(chat_id: str): # chat_id 目前未直接使用,预留 + diff --git a/src/dream/tools/finish_maintenance_tool.py b/src/dream/tools/finish_maintenance_tool.py index dbb9f15f..66f8c99e 100644 --- a/src/dream/tools/finish_maintenance_tool.py +++ b/src/dream/tools/finish_maintenance_tool.py @@ -18,3 +18,4 @@ def make_finish_maintenance(chat_id: str): # chat_id 目前未直接使用, + diff --git a/src/dream/tools/get_chat_history_detail_tool.py b/src/dream/tools/get_chat_history_detail_tool.py index b8b0da54..4f9e16b7 100644 --- a/src/dream/tools/get_chat_history_detail_tool.py +++ b/src/dream/tools/get_chat_history_detail_tool.py @@ -53,3 +53,4 @@ def make_get_chat_history_detail(chat_id: str): # chat_id 目前未直接使用 + diff --git a/src/dream/tools/search_chat_history_tool.py b/src/dream/tools/search_chat_history_tool.py index 0c7d1da6..105f6676 100644 --- a/src/dream/tools/search_chat_history_tool.py +++ b/src/dream/tools/search_chat_history_tool.py @@ -224,3 +224,4 @@ def make_search_chat_history(chat_id: str): + diff --git a/src/dream/tools/search_jargon_tool.py b/src/dream/tools/search_jargon_tool.py index d20f170b..0429a6a7 100644 --- a/src/dream/tools/search_jargon_tool.py +++ b/src/dream/tools/search_jargon_tool.py @@ -4,7 +4,7 @@ from src.common.logger import get_logger from src.common.database.database_model import Jargon from src.config.config import global_config from src.chat.utils.utils import parse_keywords_string -from src.jargon.jargon_utils import parse_chat_id_list, chat_id_list_contains +from src.bw_learner.learner_utils import parse_chat_id_list, chat_id_list_contains logger = get_logger("dream_agent") @@ -24,7 +24,7 @@ def make_search_jargon(chat_id: str): query = Jargon.select().where(Jargon.is_jargon) # 根据 all_global 配置决定 chat_id 作用域 - if global_config.jargon.all_global: + if global_config.expression.all_global_jargon: # 开启全局黑话:只看 is_global=True 的记录,不区分 chat_id query = query.where(Jargon.is_global) else: @@ -63,7 +63,7 @@ def make_search_jargon(chat_id: str): if any_matched: filtered_keyword.append(r) - if global_config.jargon.all_global: + if global_config.expression.all_global_jargon: # 全局黑话模式:不再做 chat_id 过滤,直接使用关键词过滤结果 records = filtered_keyword else: @@ -80,7 +80,7 @@ def make_search_jargon(chat_id: str): if not records: scope_note = ( "(当前为全局黑话模式,仅统计 is_global=True 的条目)" - if global_config.jargon.all_global + if global_config.expression.all_global_jargon else "(当前为按 chat_id 作用域模式,仅统计全局黑话或与当前 chat_id 相关的条目)" ) return f"未找到包含关键词'{keyword}'的 Jargon 记录{scope_note}" diff --git a/src/dream/tools/update_chat_history_tool.py b/src/dream/tools/update_chat_history_tool.py index 2070facf..8c0caf63 100644 --- a/src/dream/tools/update_chat_history_tool.py +++ b/src/dream/tools/update_chat_history_tool.py @@ -53,3 +53,4 @@ def make_update_chat_history(chat_id: str): # chat_id 目前未直接使用, + diff --git a/src/dream/tools/update_jargon_tool.py b/src/dream/tools/update_jargon_tool.py index 9ebfe7b9..b50504ae 100644 --- a/src/dream/tools/update_jargon_tool.py +++ b/src/dream/tools/update_jargon_tool.py @@ -53,3 +53,4 @@ def make_update_jargon(chat_id: str): # chat_id 目前未直接使用,预留 + diff --git a/src/express/express_utils.py b/src/express/express_utils.py deleted file mode 100644 index b0702e30..00000000 --- a/src/express/express_utils.py +++ /dev/null @@ -1,145 +0,0 @@ -import re -import difflib -import random -from datetime import datetime -from typing import Optional, List, Dict - - -def filter_message_content(content: Optional[str]) -> str: - """ - 过滤消息内容,移除回复、@、图片等格式 - - Args: - content: 原始消息内容 - - Returns: - str: 过滤后的内容 - """ - if not content: - return "" - - # 移除以[回复开头、]结尾的部分,包括后面的",说:"部分 - content = re.sub(r"\[回复.*?\],说:\s*", "", content) - # 移除@<...>格式的内容 - content = re.sub(r"@<[^>]*>", "", content) - # 移除[picid:...]格式的图片ID - content = re.sub(r"\[picid:[^\]]*\]", "", content) - # 移除[表情包:...]格式的内容 - content = re.sub(r"\[表情包:[^\]]*\]", "", content) - - return content.strip() - - -def calculate_similarity(text1: str, text2: str) -> float: - """ - 计算两个文本的相似度,返回0-1之间的值 - 使用SequenceMatcher计算相似度 - - Args: - text1: 第一个文本 - text2: 第二个文本 - - Returns: - float: 相似度值,范围0-1 - """ - return difflib.SequenceMatcher(None, text1, text2).ratio() - - -def format_create_date(timestamp: float) -> str: - """ - 将时间戳格式化为可读的日期字符串 - - Args: - timestamp: 时间戳 - - Returns: - str: 格式化后的日期字符串 - """ - try: - return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") - except (ValueError, OSError): - return "未知时间" - - -def _compute_weights(population: List[Dict]) -> List[float]: - """ - 根据表达的count计算权重,范围限定在1~5之间。 - count越高,权重越高,但最多为基础权重的5倍。 - 如果表达已checked,权重会再乘以3倍。 - """ - if not population: - return [] - - counts = [] - checked_flags = [] - for item in population: - count = item.get("count", 1) - try: - count_value = float(count) - except (TypeError, ValueError): - count_value = 1.0 - counts.append(max(count_value, 0.0)) - # 获取checked状态 - checked = item.get("checked", False) - checked_flags.append(bool(checked)) - - min_count = min(counts) - max_count = max(counts) - - if max_count == min_count: - base_weights = [1.0 for _ in counts] - else: - base_weights = [] - for count_value in counts: - # 线性映射到[1,5]区间 - normalized = (count_value - min_count) / (max_count - min_count) - base_weights.append(1.0 + normalized * 4.0) # 1~3 - - # 如果checked,权重乘以3 - weights = [] - for base_weight, checked in zip(base_weights, checked_flags, strict=False): - if checked: - weights.append(base_weight * 3.0) - else: - weights.append(base_weight) - return weights - - -def weighted_sample(population: List[Dict], k: int) -> List[Dict]: - """ - 随机抽样函数 - - Args: - population: 总体数据列表 - k: 需要抽取的数量 - - Returns: - List[Dict]: 抽取的数据列表 - """ - if not population or k <= 0: - return [] - - if len(population) <= k: - return population.copy() - - selected: List[Dict] = [] - population_copy = population.copy() - - for _ in range(min(k, len(population_copy))): - weights = _compute_weights(population_copy) - total_weight = sum(weights) - if total_weight <= 0: - # 回退到均匀随机 - idx = random.randint(0, len(population_copy) - 1) - selected.append(population_copy.pop(idx)) - continue - - threshold = random.uniform(0, total_weight) - cumulative = 0.0 - for idx, weight in enumerate(weights): - cumulative += weight - if threshold <= cumulative: - selected.append(population_copy.pop(idx)) - break - - return selected diff --git a/src/jargon/__init__.py b/src/jargon/__init__.py deleted file mode 100644 index 37b61644..00000000 --- a/src/jargon/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .jargon_miner import extract_and_store_jargon - -__all__ = [ - "extract_and_store_jargon", -] diff --git a/src/main.py b/src/main.py index 8482ae5a..d5426f5c 100644 --- a/src/main.py +++ b/src/main.py @@ -13,7 +13,6 @@ from src.config.config import global_config from src.chat.message_receive.bot import chat_bot from src.common.logger import get_logger from src.common.server import get_global_server, Server -from src.mood.mood_manager import mood_manager from src.chat.knowledge import lpmm_start_up from rich.traceback import install @@ -114,10 +113,6 @@ class MainSystem: get_emoji_manager().initialize() logger.info("表情包管理器初始化成功") - # 启动情绪管理器 - if global_config.mood.enable_mood: - await mood_manager.start() - logger.info("情绪管理器初始化成功") # 初始化聊天管理器 await get_chat_manager()._initialize() diff --git a/src/memory_system/memory_retrieval.py b/src/memory_system/memory_retrieval.py index aa20ce0f..248e3062 100644 --- a/src/memory_system/memory_retrieval.py +++ b/src/memory_system/memory_retrieval.py @@ -11,7 +11,7 @@ from src.common.database.database_model import ThinkingBack from src.memory_system.retrieval_tools import get_tool_registry, init_all_tools from src.memory_system.memory_utils import parse_questions_json from src.llm_models.payload_content.message import MessageBuilder, RoleType, Message -from src.jargon.jargon_explainer import match_jargon_from_text, retrieve_concepts_with_jargon +from src.bw_learner.jargon_explainer import match_jargon_from_text, retrieve_concepts_with_jargon logger = get_logger("memory_retrieval") @@ -972,6 +972,7 @@ async def _process_single_question( context: str, initial_info: str = "", initial_jargon_concepts: Optional[List[str]] = None, + max_iterations: Optional[int] = None, ) -> Optional[str]: """处理单个问题的查询 @@ -996,10 +997,14 @@ async def _process_single_question( jargon_concepts_for_agent = initial_jargon_concepts if global_config.memory.enable_jargon_detection else None + # 如果未指定max_iterations,使用配置的默认值 + if max_iterations is None: + max_iterations = global_config.memory.max_agent_iterations + found_answer, answer, thinking_steps, is_timeout = await _react_agent_solve_question( question=question, chat_id=chat_id, - max_iterations=global_config.memory.max_agent_iterations, + max_iterations=max_iterations, timeout=global_config.memory.agent_timeout_seconds, initial_info=question_initial_info, initial_jargon_concepts=jargon_concepts_for_agent, @@ -1029,7 +1034,7 @@ async def build_memory_retrieval_prompt( sender: str, target: str, chat_stream, - tool_executor, + think_level: int = 1, ) -> str: """构建记忆检索提示 使用两段式查询:第一步生成问题,第二步使用ReAct Agent查询答案 @@ -1117,9 +1122,14 @@ async def build_memory_retrieval_prompt( return "" # 第二步:并行处理所有问题(使用配置的最大迭代次数和超时时间) - max_iterations = global_config.memory.max_agent_iterations + base_max_iterations = global_config.memory.max_agent_iterations + # 根据think_level调整迭代次数:think_level=1时不变,think_level=0时减半 + if think_level == 0: + max_iterations = max(1, base_max_iterations // 2) # 至少为1 + else: + max_iterations = base_max_iterations timeout_seconds = global_config.memory.agent_timeout_seconds - logger.debug(f"问题数量: {len(questions)},设置最大迭代次数: {max_iterations},超时时间: {timeout_seconds}秒") + logger.debug(f"问题数量: {len(questions)},think_level={think_level},设置最大迭代次数: {max_iterations}(基础值: {base_max_iterations}),超时时间: {timeout_seconds}秒") # 并行处理所有问题,将概念检索结果作为初始信息传递 question_tasks = [ @@ -1129,6 +1139,7 @@ async def build_memory_retrieval_prompt( context=message, initial_info=initial_info, initial_jargon_concepts=concepts if enable_jargon_detection else None, + max_iterations=max_iterations, ) for question in questions ] diff --git a/src/mood/mood_manager.py b/src/mood/mood_manager.py deleted file mode 100644 index 7cc64801..00000000 --- a/src/mood/mood_manager.py +++ /dev/null @@ -1,230 +0,0 @@ -import time - -from src.common.logger import get_logger -from src.config.config import global_config, model_config -from src.chat.message_receive.chat_stream import get_chat_manager -from src.chat.utils.prompt_builder import Prompt, global_prompt_manager -from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_by_timestamp_with_chat_inclusive -from src.llm_models.utils_model import LLMRequest -from src.manager.async_task_manager import AsyncTask, async_task_manager - - -logger = get_logger("mood") - - -def init_prompt(): - Prompt( - """ -{chat_talking_prompt} -以上是群里正在进行的聊天记录 - -{identity_block} -你先前的情绪状态是:{mood_state} -你的情绪特点是:{emotion_style} - -现在,请你根据先前的情绪状态和现在的聊天内容,总结推断你现在的情绪状态,用简短的词句来描述情绪状态 -请只输出新的情绪状态,不要输出其他内容: -""", - "get_mood_prompt", - ) - - Prompt( - """ -{chat_talking_prompt} -以上是群里最近的聊天记录 - -{identity_block} -你之前的情绪状态是:{mood_state} - -距离你上次关注群里消息已经过去了一段时间,你冷静了下来,请你输出一句话或几个词来描述你现在的情绪状态 -你的情绪特点是:{emotion_style} -请只输出新的情绪状态,不要输出其他内容: -""", - "regress_mood_prompt", - ) - - -class ChatMood: - def __init__(self, chat_id: str): - self.chat_id: str = chat_id - - chat_manager = get_chat_manager() - self.chat_stream = chat_manager.get_stream(self.chat_id) - - if not self.chat_stream: - raise ValueError(f"Chat stream for chat_id {chat_id} not found") - - self.log_prefix = f"[{self.chat_stream.group_info.group_name if self.chat_stream.group_info else self.chat_stream.user_info.user_nickname}]" - - self.mood_state: str = "感觉很平静" - - self.regression_count: int = 0 - - self.mood_model = LLMRequest(model_set=model_config.model_task_config.utils, request_type="mood") - - self.last_change_time: float = 0 - - async def get_mood(self) -> str: - self.regression_count = 0 - - current_time = time.time() - - logger.info(f"{self.log_prefix} 获取情绪状态") - message_list_before_now = get_raw_msg_by_timestamp_with_chat_inclusive( - chat_id=self.chat_id, - timestamp_start=self.last_change_time, - timestamp_end=current_time, - limit=int(global_config.chat.max_context_size / 3), - limit_mode="last", - ) - - chat_talking_prompt = build_readable_messages( - message_list_before_now, - replace_bot_name=True, - timestamp_mode="normal_no_YMD", - read_mark=0.0, - truncate=True, - show_actions=True, - ) - - bot_name = global_config.bot.nickname - if global_config.bot.alias_names: - bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}" - else: - bot_nickname = "" - - identity_block = f"你的名字是{bot_name}{bot_nickname}" - - prompt = await global_prompt_manager.format_prompt( - "get_mood_prompt", - chat_talking_prompt=chat_talking_prompt, - identity_block=identity_block, - mood_state=self.mood_state, - emotion_style=global_config.mood.emotion_style, - ) - - response, (reasoning_content, _, _) = await self.mood_model.generate_response_async( - prompt=prompt, temperature=0.7 - ) - if global_config.debug.show_prompt: - logger.info(f"{self.log_prefix} prompt: {prompt}") - logger.info(f"{self.log_prefix} response: {response}") - logger.info(f"{self.log_prefix} reasoning_content: {reasoning_content}") - - logger.info(f"{self.log_prefix} 情绪状态更新为: {response}") - - self.mood_state = response - - self.last_change_time = current_time - - return response - - async def regress_mood(self): - message_time = time.time() - message_list_before_now = get_raw_msg_by_timestamp_with_chat_inclusive( - chat_id=self.chat_id, - timestamp_start=self.last_change_time, - timestamp_end=message_time, - limit=15, - limit_mode="last", - ) - - chat_talking_prompt = build_readable_messages( - message_list_before_now, - replace_bot_name=True, - timestamp_mode="normal_no_YMD", - read_mark=0.0, - truncate=True, - show_actions=True, - ) - - bot_name = global_config.bot.nickname - if global_config.bot.alias_names: - bot_nickname = f",也有人叫你{','.join(global_config.bot.alias_names)}" - else: - bot_nickname = "" - - identity_block = f"你的名字是{bot_name}{bot_nickname}" - - prompt = await global_prompt_manager.format_prompt( - "regress_mood_prompt", - chat_talking_prompt=chat_talking_prompt, - identity_block=identity_block, - mood_state=self.mood_state, - emotion_style=global_config.mood.emotion_style, - ) - - response, (reasoning_content, _, _) = await self.mood_model.generate_response_async( - prompt=prompt, temperature=0.7 - ) - - if global_config.debug.show_prompt: - logger.info(f"{self.log_prefix} prompt: {prompt}") - logger.info(f"{self.log_prefix} response: {response}") - logger.info(f"{self.log_prefix} reasoning_content: {reasoning_content}") - - logger.info(f"{self.log_prefix} 情绪状态转变为: {response}") - - self.mood_state = response - - self.regression_count += 1 - - -class MoodRegressionTask(AsyncTask): - def __init__(self, mood_manager: "MoodManager"): - super().__init__(task_name="MoodRegressionTask", run_interval=45) - self.mood_manager = mood_manager - - async def run(self): - logger.debug("开始情绪回归任务...") - now = time.time() - for mood in self.mood_manager.mood_list: - if mood.last_change_time == 0: - continue - - if now - mood.last_change_time > 200: - if mood.regression_count >= 2: - continue - - logger.debug(f"{mood.log_prefix} 开始情绪回归, 第 {mood.regression_count + 1} 次") - await mood.regress_mood() - - -class MoodManager: - def __init__(self): - self.mood_list: list[ChatMood] = [] - """当前情绪状态""" - self.task_started: bool = False - - async def start(self): - """启动情绪回归后台任务""" - if self.task_started: - return - - task = MoodRegressionTask(self) - await async_task_manager.add_task(task) - self.task_started = True - logger.info("情绪回归任务已启动") - - def get_mood_by_chat_id(self, chat_id: str) -> ChatMood: - for mood in self.mood_list: - if mood.chat_id == chat_id: - return mood - - new_mood = ChatMood(chat_id) - self.mood_list.append(new_mood) - return new_mood - - def reset_mood_by_chat_id(self, chat_id: str): - for mood in self.mood_list: - if mood.chat_id == chat_id: - mood.mood_state = "感觉很平静" - mood.regression_count = 0 - return - self.mood_list.append(ChatMood(chat_id)) - - -init_prompt() - -mood_manager = MoodManager() -"""全局情绪管理器""" diff --git a/src/plugin_system/apis/__init__.py b/src/plugin_system/apis/__init__.py index 730ee907..a872bfc7 100644 --- a/src/plugin_system/apis/__init__.py +++ b/src/plugin_system/apis/__init__.py @@ -19,7 +19,6 @@ from src.plugin_system.apis import ( send_api, tool_api, frequency_api, - mood_api, auto_talk_api, ) from .logging_api import get_logger @@ -42,6 +41,5 @@ __all__ = [ "register_plugin", "tool_api", "frequency_api", - "mood_api", "auto_talk_api", ] diff --git a/src/plugin_system/apis/frequency_api.py b/src/plugin_system/apis/frequency_api.py index bc906186..9cde1d90 100644 --- a/src/plugin_system/apis/frequency_api.py +++ b/src/plugin_system/apis/frequency_api.py @@ -1,5 +1,5 @@ from src.common.logger import get_logger -from src.chat.frequency_control.frequency_control import frequency_control_manager +from src.chat.heart_flow.frequency_control import frequency_control_manager from src.config.config import global_config logger = get_logger("frequency_api") diff --git a/src/plugin_system/apis/generator_api.py b/src/plugin_system/apis/generator_api.py index 29d0b66d..cdac2791 100644 --- a/src/plugin_system/apis/generator_api.py +++ b/src/plugin_system/apis/generator_api.py @@ -81,6 +81,7 @@ async def generate_reply( chat_id: Optional[str] = None, action_data: Optional[Dict[str, Any]] = None, reply_message: Optional["DatabaseMessages"] = None, + think_level: int = 1, extra_info: str = "", reply_reason: str = "", available_actions: Optional[Dict[str, ActionInfo]] = None, @@ -136,6 +137,7 @@ async def generate_reply( enable_tool=enable_tool, reply_message=reply_message, reply_reason=reply_reason, + think_level=think_level, from_plugin=from_plugin, stream_id=chat_stream.stream_id if chat_stream else chat_id, reply_time_point=reply_time_point, diff --git a/src/plugin_system/apis/mood_api.py b/src/plugin_system/apis/mood_api.py deleted file mode 100644 index d5300e9f..00000000 --- a/src/plugin_system/apis/mood_api.py +++ /dev/null @@ -1,13 +0,0 @@ -import asyncio -from typing import Optional - -from src.common.logger import get_logger -from src.mood.mood_manager import mood_manager - -logger = get_logger("mood_api") - - -async def get_mood_by_chat_id(chat_id: str) -> Optional[float]: - chat_mood = mood_manager.get_mood_by_chat_id(chat_id) - mood = asyncio.create_task(chat_mood.get_mood()) - return mood diff --git a/src/webui/config_routes.py b/src/webui/config_routes.py index 63803d5e..25127fe5 100644 --- a/src/webui/config_routes.py +++ b/src/webui/config_routes.py @@ -29,9 +29,7 @@ from src.config.official_configs import ( ToolConfig, MemoryConfig, DebugConfig, - MoodConfig, VoiceConfig, - JargonConfig, ) from src.config.api_ada_configs import ( ModelTaskConfig, @@ -104,7 +102,6 @@ async def get_config_section_schema(section_name: str): - tool: ToolConfig - memory: MemoryConfig - debug: DebugConfig - - mood: MoodConfig - voice: VoiceConfig - jargon: JargonConfig - model_task_config: ModelTaskConfig @@ -130,9 +127,7 @@ async def get_config_section_schema(section_name: str): "tool": ToolConfig, "memory": MemoryConfig, "debug": DebugConfig, - "mood": MoodConfig, "voice": VoiceConfig, - "jargon": JargonConfig, "model_task_config": ModelTaskConfig, "api_provider": APIProvider, "model_info": ModelInfo, diff --git a/src/webui/config_schema.py b/src/webui/config_schema.py index d160000a..5c5763fa 100644 --- a/src/webui/config_schema.py +++ b/src/webui/config_schema.py @@ -296,7 +296,6 @@ class ConfigSchemaGenerator: "plan_style", "visual_style", "private_plan_style", - "emotion_style", "reaction", "filtration_prompt", ]: diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 8ff9ca0b..a381104b 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "7.0.2" +version = "7.1.5" #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- # 如果你想要修改配置文件,请递增version的值 @@ -27,16 +27,14 @@ personality = "是一个女大学生,现在在读大二,会刷贴吧。" # 描述麦麦说话的表达风格,表达习惯,如要修改,可以酌情新增内容 reply_style = "请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景。可以参考贴吧,知乎和微博的回复风格。" -# 麦麦的兴趣,会影响麦麦对什么话题进行回复 -interest = "对技术相关话题,游戏和动漫相关话题感兴趣,也对日常话题感兴趣,不喜欢太过沉重严肃的话题" - # 麦麦的说话规则,行为风格: plan_style = """ 1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用 2.如果相同的内容已经被执行,请不要重复执行 -3.请控制你的发言频率,不要太过频繁的发言 -4.如果有人对你感到厌烦,请减少回复 -5.如果有人对你进行攻击,或者情绪激动,请你以合适的方法应对""" +3.你对技术相关话题,游戏和动漫相关话题感兴趣,也对日常话题感兴趣,不喜欢太过沉重严肃的话题 +4.请控制你的发言频率,不要太过频繁的发言 +5.如果有人对你感到厌烦,请减少回复 +6.如果有人对你进行攻击,或者情绪激动,请你以合适的方法应对""" # 麦麦识图规则,不建议修改 visual_style = "请用中文描述这张图片的内容。如果有文字,请把文字描述概括出来,请留意其主题,直观感受,输出为一段平文本,最多30字,请注意不要分点,就输出一段文本" @@ -60,16 +58,14 @@ state_probability = 0.3 [expression] # 表达学习配置 learning_list = [ # 表达学习配置列表,支持按聊天流配置 - ["", "enable", "enable", "1.0"], # 全局配置:使用表达,启用学习,学习强度1.0 - ["qq:1919810:group", "enable", "enable", "1.5"], # 特定群聊配置:使用表达,启用学习,学习强度1.5 - ["qq:114514:private", "enable", "disable", "0.5"], # 特定私聊配置:使用表达,禁用学习,学习强度0.5 + ["", "enable", "enable", "enable"], # 全局配置:使用表达,启用学习,启用jargon学习 + ["qq:1919810:group", "enable", "enable", "enable"], # 特定群聊配置:使用表达,启用学习,启用jargon学习 + ["qq:114514:private", "enable", "disable", "disable"], # 特定私聊配置:使用表达,禁用学习,禁用jargon学习 # 格式说明: # 第一位: chat_stream_id,空字符串表示全局配置 # 第二位: 是否使用学到的表达 ("enable"/"disable") # 第三位: 是否学习表达 ("enable"/"disable") - # 第四位: 学习强度(浮点数),影响学习频率,最短学习时间间隔 = 300/学习强度(秒) - # 学习强度越高,学习越频繁;学习强度越低,学习越少 - # 如果学习强度设置为0会自动转换为0.0001以避免除以零错误 + # 第四位: 是否启用jargon学习 ("enable"/"disable") ] expression_groups = [ @@ -85,12 +81,14 @@ reflect = false # 是否启用表达反思(Bot主动向管理员询问表达 reflect_operator_id = "" # 表达反思操作员ID,格式:platform:id:type (例如 "qq:123456:private" 或 "qq:654321:group") allow_reflect = [] # 允许进行表达反思的聊天流ID列表,格式:["qq:123456:private", "qq:654321:group", ...],只有在此列表中的聊天流才会提出问题并跟踪。如果列表为空,则所有聊天流都可以进行表达反思(前提是 reflect = true) +all_global_jargon = true # 是否开启全局黑话模式,注意,此功能关闭后,已经记录的全局黑话不会改变,需要手动删除 + [chat] # 麦麦的聊天设置 -talk_value = 1 # 聊天频率,越小越沉默,范围0-1,如果设置为0会自动转换为0.0001以避免除以零错误 +talk_value = 1 # 聊天频率,越小越沉默,范围0-1 mentioned_bot_reply = true # 是否启用提及必回复 max_context_size = 30 # 上下文长度 -planner_smooth = 2 # 规划器平滑,增大数值会减小planner负荷,略微降低反应速度,推荐1-5,0为关闭,必须大于等于0 +planner_smooth = 3 # 规划器平滑,增大数值会减小planner负荷,略微降低反应速度,推荐1-5,0为关闭,必须大于等于0 enable_talk_value_rules = true # 是否启用动态发言频率规则 @@ -106,10 +104,8 @@ talk_value_rules = [ { target = "qq:114514:private", time = "00:00-23:59", value = 0.3 }, ] -include_planner_reasoning = false # 是否将planner推理加入replyer,默认关闭(不加入) - [memory] -max_agent_iterations = 2 # 记忆思考深度(最低为1) +max_agent_iterations = 3 # 记忆思考深度(最低为1) agent_timeout_seconds = 45.0 # 最长回忆时间(秒) enable_jargon_detection = true # 记忆检索过程中是否启用黑话识别 global_memory = false # 是否允许记忆检索进行全局查询 @@ -131,17 +127,9 @@ dream_time_ranges = [ ] # dream_time_ranges = [] -[jargon] -all_global = true # 是否开启全局黑话模式,注意,此功能关闭后,已经记录的全局黑话不会改变,需要手动删除 - [tool] enable_tool = true # 是否启用工具 -[mood] -enable_mood = false # 是否启用情绪系统 -mood_update_threshold = 1 # 情绪更新阈值,越高,更新越慢 -# 情感特征,影响情绪的变化情况 -emotion_style = "情绪较为稳定,但遭遇特定事件的时候起伏较大" [emoji] emoji_chance = 0.4 # 麦麦激活表情包动作的概率 diff --git a/webui/dist/assets/charts-DbiuC1q1.js b/webui/dist/assets/charts-DbiuC1q1.js new file mode 100644 index 00000000..1548c853 Binary files /dev/null and b/webui/dist/assets/charts-DbiuC1q1.js differ diff --git a/webui/dist/assets/charts-Dhri-zxi.js b/webui/dist/assets/charts-Dhri-zxi.js deleted file mode 100644 index 28ac978e..00000000 Binary files a/webui/dist/assets/charts-Dhri-zxi.js and /dev/null differ diff --git a/webui/dist/assets/codemirror-BHeANvwm.js b/webui/dist/assets/codemirror-BEE0n9kQ.js similarity index 86% rename from webui/dist/assets/codemirror-BHeANvwm.js rename to webui/dist/assets/codemirror-BEE0n9kQ.js index e36f34ed..2ead8a87 100644 Binary files a/webui/dist/assets/codemirror-BHeANvwm.js and b/webui/dist/assets/codemirror-BEE0n9kQ.js differ diff --git a/webui/dist/assets/dnd-Dyi3CnuX.js b/webui/dist/assets/dnd-CHfCzWUK.js similarity index 99% rename from webui/dist/assets/dnd-Dyi3CnuX.js rename to webui/dist/assets/dnd-CHfCzWUK.js index e0429073..48ee198e 100644 Binary files a/webui/dist/assets/dnd-Dyi3CnuX.js and b/webui/dist/assets/dnd-CHfCzWUK.js differ diff --git a/webui/dist/assets/icons-BusT0Ku_.js b/webui/dist/assets/icons-CwAZotQh.js similarity index 97% rename from webui/dist/assets/icons-BusT0Ku_.js rename to webui/dist/assets/icons-CwAZotQh.js index 37e077ee..f0275a38 100644 Binary files a/webui/dist/assets/icons-BusT0Ku_.js and b/webui/dist/assets/icons-CwAZotQh.js differ diff --git a/webui/dist/assets/index-BCi5Y59M.css b/webui/dist/assets/index-BCi5Y59M.css new file mode 100644 index 00000000..1694b5fa Binary files /dev/null and b/webui/dist/assets/index-BCi5Y59M.css differ diff --git a/webui/dist/assets/index-DrsALSzA.css b/webui/dist/assets/index-DrsALSzA.css deleted file mode 100644 index 20dd8da0..00000000 Binary files a/webui/dist/assets/index-DrsALSzA.css and /dev/null differ diff --git a/webui/dist/assets/index-DHIER1DT.js b/webui/dist/assets/index-oWnJmocA.js similarity index 56% rename from webui/dist/assets/index-DHIER1DT.js rename to webui/dist/assets/index-oWnJmocA.js index fdeaa7e3..5035a104 100644 Binary files a/webui/dist/assets/index-DHIER1DT.js and b/webui/dist/assets/index-oWnJmocA.js differ diff --git a/webui/dist/assets/markdown-A1ShuLvG.js b/webui/dist/assets/markdown-kUhwkcQP.js similarity index 89% rename from webui/dist/assets/markdown-A1ShuLvG.js rename to webui/dist/assets/markdown-kUhwkcQP.js index 92fb2e43..1d787d15 100644 Binary files a/webui/dist/assets/markdown-A1ShuLvG.js and b/webui/dist/assets/markdown-kUhwkcQP.js differ diff --git a/webui/dist/assets/misc-CKjrIrIJ.js b/webui/dist/assets/misc-CKjrIrIJ.js new file mode 100644 index 00000000..65e52572 Binary files /dev/null and b/webui/dist/assets/misc-CKjrIrIJ.js differ diff --git a/webui/dist/assets/misc-DyBU7ISD.js b/webui/dist/assets/misc-DyBU7ISD.js deleted file mode 100644 index 922264e9..00000000 Binary files a/webui/dist/assets/misc-DyBU7ISD.js and /dev/null differ diff --git a/webui/dist/assets/radix-core-C3XKqQJw.js b/webui/dist/assets/radix-core-9dEfQl-6.js similarity index 99% rename from webui/dist/assets/radix-core-C3XKqQJw.js rename to webui/dist/assets/radix-core-9dEfQl-6.js index d16e79af..b91c3865 100644 Binary files a/webui/dist/assets/radix-core-C3XKqQJw.js and b/webui/dist/assets/radix-core-9dEfQl-6.js differ diff --git a/webui/dist/assets/radix-extra-DnIxMvW0.js b/webui/dist/assets/radix-extra-DDK-u9dm.js similarity index 99% rename from webui/dist/assets/radix-extra-DnIxMvW0.js rename to webui/dist/assets/radix-extra-DDK-u9dm.js index c88d08e4..513ee94e 100644 Binary files a/webui/dist/assets/radix-extra-DnIxMvW0.js and b/webui/dist/assets/radix-extra-DDK-u9dm.js differ diff --git a/webui/dist/assets/react-vendor-Dtc2IqVY.js b/webui/dist/assets/react-vendor-BmxF9s7Q.js similarity index 98% rename from webui/dist/assets/react-vendor-Dtc2IqVY.js rename to webui/dist/assets/react-vendor-BmxF9s7Q.js index 087b3f14..97721520 100644 Binary files a/webui/dist/assets/react-vendor-Dtc2IqVY.js and b/webui/dist/assets/react-vendor-BmxF9s7Q.js differ diff --git a/webui/dist/assets/reactflow-B3n3_Vkw.js b/webui/dist/assets/reactflow-B3n3_Vkw.js deleted file mode 100644 index d4b54604..00000000 Binary files a/webui/dist/assets/reactflow-B3n3_Vkw.js and /dev/null differ diff --git a/webui/dist/assets/reactflow-DLoXAt4c.js b/webui/dist/assets/reactflow-DLoXAt4c.js new file mode 100644 index 00000000..ef5f8568 Binary files /dev/null and b/webui/dist/assets/reactflow-DLoXAt4c.js differ diff --git a/webui/dist/assets/router-Bz250laD.js b/webui/dist/assets/router-Bz250laD.js new file mode 100644 index 00000000..936e5305 Binary files /dev/null and b/webui/dist/assets/router-Bz250laD.js differ diff --git a/webui/dist/assets/router-CWhjJi2n.js b/webui/dist/assets/router-CWhjJi2n.js deleted file mode 100644 index 3159c04f..00000000 Binary files a/webui/dist/assets/router-CWhjJi2n.js and /dev/null differ diff --git a/webui/dist/assets/uppy-BMZiFQyG.js b/webui/dist/assets/uppy-BMZiFQyG.js new file mode 100644 index 00000000..0743af8a Binary files /dev/null and b/webui/dist/assets/uppy-BMZiFQyG.js differ diff --git a/webui/dist/assets/uppy-DUr9_tfX.js b/webui/dist/assets/uppy-DUr9_tfX.js deleted file mode 100644 index db56de90..00000000 Binary files a/webui/dist/assets/uppy-DUr9_tfX.js and /dev/null differ diff --git a/webui/dist/assets/utils-CCeOswSm.js b/webui/dist/assets/utils-BXc2jIuz.js similarity index 99% rename from webui/dist/assets/utils-CCeOswSm.js rename to webui/dist/assets/utils-BXc2jIuz.js index 9ffa25cc..347ebdbc 100644 Binary files a/webui/dist/assets/utils-CCeOswSm.js and b/webui/dist/assets/utils-BXc2jIuz.js differ diff --git a/webui/dist/index.html b/webui/dist/index.html index 105aa76d..2c686515 100644 Binary files a/webui/dist/index.html and b/webui/dist/index.html differ